From 088fa07553de3b2169a8cbb657c5c970e69e0da3 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sat, 3 May 2025 16:26:34 -0500 Subject: [PATCH 1/4] Feature: Added Food and Housing Expenses from A Time of War --- .../CampaignOptionsDialog.properties | 15 + .../mekhq/resources/Finances.properties | 2 + MekHQ/src/mekhq/campaign/Campaign.java | 32 + MekHQ/src/mekhq/campaign/CampaignOptions.java | 26 + .../mekhq/campaign/finances/Accountant.java | 129 +++ .../src/mekhq/campaign/finances/Finances.java | 53 +- .../campaignOptions/contents/FinancesTab.java | 21 + .../campaign/finances/AccountantTest.java | 822 ++++++++++++++++++ 8 files changed, 1085 insertions(+), 15 deletions(-) create mode 100644 MekHQ/unittests/mekhq/campaign/finances/AccountantTest.java diff --git a/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties b/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties index 512529bc21..5aaec42854 100644 --- a/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties +++ b/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties @@ -1287,6 +1287,21 @@ lblPayForTransportBox.text=Transport lblPayForTransportBox.tooltip=Pay for excess transportation needs. lblPayForRecruitmentBox.text=Recruitment (FM:Mr) lblPayForRecruitmentBox.tooltip=Pay a two-month salary to new recruits. +lblPayForFoodBox.text=Food (A Time of War) \u26A0 \uD83C\uDF1F +lblPayForFoodBox.tooltip=Each month funds are deducted based on the number of active (or in-unit students) personnel in\ + \ your campaign and their status or rank.\ +
\ +
Each prisoner and dependent consume 120 C-Bills of food per month. Consumption for Enlisted personnel is 240\ + \ C-Bills/month. While Officers (including Warrant Officers) consume 480 C-Bills/month. +lblPayForHousingBox.text=Housing (A Time of War) \u26A0 \uD83C\uDF1F +lblPayForHousingBox.tooltip=Each month, while not in transit, funds are deducted based on the number of active (or\ + \ in-unit students) personnel in your campaign and their status or rank.\ +
\ +
Each prisoner and dependent requires 228 C-Bills of housing per month. Housing for Enlisted personnel is 312\ + \ C-Bills/month. While housing each Officer (including Warrant Officers) requires 780 C-Bills/month.\ +
\ +
These values have been extrapolated from those found in A Time of War and include all necessary utilities\ + \ and services. Crew of JumpShips, WarShips, and Space Stations are exempt from housing costs. # createGeneralOptionsPanel lblUseLoanLimitsBox.text=Available Loans Based on Unit Reputation \u270E lblUseLoanLimitsBox.tooltip=Put limits on interest, collateral, and length. diff --git a/MekHQ/resources/mekhq/resources/Finances.properties b/MekHQ/resources/mekhq/resources/Finances.properties index e5b95b81a7..8281e4a220 100644 --- a/MekHQ/resources/mekhq/resources/Finances.properties +++ b/MekHQ/resources/mekhq/resources/Finances.properties @@ -102,6 +102,8 @@ 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 +FoodAndHousing.title=Monthly Food and/or Housing Costs +FoodAndHousing.text=Your account has been debited for %s in food and/or housing expenses. # Loans Loan.title=loan payment to %s Loan.text=Your account has been debited for %s in loan payment to %s diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index 0cc6d0b30e..d34d03dd68 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -2529,6 +2529,38 @@ public List getActivePersonnel(boolean includePrisoners) { return activePersonnel; } + /** + * Returns a list of active personnel, including students that are assigned to their home (unit) academy. + * + *

The returned list includes:

+ * + * + * @return a list of {@link Person} objects who are either active or in-unit students. + * + * @author Illiani + * @since 0.50.06 + */ + public List getActivePersonnelIncludingInUnitStudents() { + List activePersonnel = new ArrayList<>(); + for (Person person : getPersonnel()) { + PersonnelStatus status = person.getStatus(); + if (status.isStudent()) { + Academy academy = getAcademy(person.getEduAcademyName(), person.getEduAcademyNameInSet()); + if (academy != null && academy.isHomeSchool()) { + activePersonnel.add(person); + continue; + } + } + if (status.isActive()) { + activePersonnel.add(person); + } + } + return activePersonnel; + } + /** * Retrieves a filtered list of personnel who have at least one combat profession. *

diff --git a/MekHQ/src/mekhq/campaign/CampaignOptions.java b/MekHQ/src/mekhq/campaign/CampaignOptions.java index 50bbf5cdde..d875c71621 100644 --- a/MekHQ/src/mekhq/campaign/CampaignOptions.java +++ b/MekHQ/src/mekhq/campaign/CampaignOptions.java @@ -466,6 +466,8 @@ public static String getTechLevelName(final int techLevel) { private boolean sellUnits; private boolean sellParts; private boolean payForRecruitment; + private boolean payForFood; + private boolean payForHousing; private boolean useLoanLimits; private boolean usePercentageMaint; // Unofficial private boolean infantryDontCount; // Unofficial @@ -1071,6 +1073,8 @@ public CampaignOptions() { sellUnits = false; sellParts = false; payForRecruitment = false; + payForFood = false; + payForHousing = false; useLoanLimits = false; usePercentageMaint = false; infantryDontCount = false; @@ -3321,6 +3325,22 @@ public void setPayForRecruitment(final boolean payForRecruitment) { this.payForRecruitment = payForRecruitment; } + public boolean isPayForFood() { + return payForFood; + } + + public void setPayForFood(final boolean payForFood) { + this.payForFood = payForFood; + } + + public boolean isPayForHousing() { + return payForHousing; + } + + public void setPayForHousing(final boolean payForHousing) { + this.payForHousing = payForHousing; + } + public boolean isUseLoanLimits() { return useLoanLimits; } @@ -5292,6 +5312,8 @@ public void writeToXml(final PrintWriter pw, int indent) { MHQXMLUtility.writeSimpleXMLTag(pw, indent, "sellUnits", sellUnits); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "sellParts", sellParts); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "payForRecruitment", payForRecruitment); + MHQXMLUtility.writeSimpleXMLTag(pw, indent, "payForFood", payForFood); + MHQXMLUtility.writeSimpleXMLTag(pw, indent, "payForHousing", payForHousing); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "useLoanLimits", useLoanLimits); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "usePercentageMaint", usePercentageMaint); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "infantryDontCount", infantryDontCount); @@ -6225,6 +6247,10 @@ public static CampaignOptions generateCampaignOptionsFromXml(Node wn, Version ve retVal.sellParts = Boolean.parseBoolean(wn2.getTextContent()); } else if (nodeName.equalsIgnoreCase("payForRecruitment")) { retVal.payForRecruitment = Boolean.parseBoolean(wn2.getTextContent()); + } else if (nodeName.equalsIgnoreCase("payForFood")) { + retVal.payForFood = Boolean.parseBoolean(wn2.getTextContent()); + } else if (nodeName.equalsIgnoreCase("payForHousing")) { + retVal.payForHousing = Boolean.parseBoolean(wn2.getTextContent()); } else if (nodeName.equalsIgnoreCase("useLoanLimits")) { retVal.useLoanLimits = Boolean.parseBoolean(wn2.getTextContent().trim()); } else if (nodeName.equalsIgnoreCase("usePercentageMaint")) { diff --git a/MekHQ/src/mekhq/campaign/finances/Accountant.java b/MekHQ/src/mekhq/campaign/finances/Accountant.java index 1b85dff2b7..6d6d50bf30 100644 --- a/MekHQ/src/mekhq/campaign/finances/Accountant.java +++ b/MekHQ/src/mekhq/campaign/finances/Accountant.java @@ -24,13 +24,20 @@ * * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of * InMediaRes Productions, LLC. + * + * MechWarrior Copyright Microsoft Corporation. MekHQ was created under + * Microsoft's "Game Content Usage Rules" + * and it is not endorsed by or + * affiliated with Microsoft. */ package mekhq.campaign.finances; import static mekhq.campaign.force.Force.FORCE_NONE; +import static mekhq.campaign.personnel.ranks.Rank.RWO_MIN; import java.time.LocalDate; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.UUID; @@ -52,6 +59,13 @@ public record Accountant(Campaign campaign) { private static final MMLogger logger = MMLogger.create(Accountant.class); + final public static int HOUSING_PRISONER_OR_DEPENDENT = 228; + final public static int HOUSING_ENLISTED = 312; + final public static int HOUSING_OFFICER = 780; + final public static int FOOD_PRISONER_OR_DEPENDENT = 120; + final public static int FOOD_ENLISTED = 240; + final public static int FOOD_OFFICER = 480; + public CampaignOptions getCampaignOptions() { return campaign().getCampaignOptions(); } @@ -114,6 +128,121 @@ public Money getOverheadExpenses() { } } + /** + * Calculates the total monthly expenses for food and housing for all active personnel in the campaign. + * + *

The calculation considers both food and housing costs based on the campaign's configuration and + * personnel roles, including officers, enlisted members, prisoners, and dependents. Housing costs are only applied + * if the campaign is located on a planet. Food and housing usage is counted using fixed per-person rates according + * to their role/status:

+ * + *
    + *
  • Prisoners or dependents have specific food and housing rates.
  • + *
  • Officers have higher food and housing rates than enlisted personnel.
  • + *
  • Crew members of non-DropShip large vessels live aboard their vessel and are exempt from housing charges.
  • + *
+ * + *

If neither food nor housing expenses are enabled in the campaign options, this method returns zero.

+ * + * @return a {@link Money} object representing the total monthly food and housing expenses for the campaign + */ + public Money getMonthlyFoodAndHousingExpenses() { + boolean payForFood = getCampaignOptions().isPayForFood(); + boolean payForHousing = getCampaignOptions().isPayForHousing() && campaign.getLocation().isOnPlanet(); + + if (!payForFood && !payForHousing) { + return Money.zero(); + } + + int prisonerOrDependentHousingUsage = 0; + int enlistedHousingUsage = 0; + int officerHousingUsage = 0; + + int prisonerOrDependentFoodUsage = 0; + int enlistedFoodUsage = 0; + int officerFoodUsage = 0; + + // Determine housing and food requirements + List personnel = campaign.getActivePersonnelIncludingInUnitStudents(); + for (Person person : personnel) { + boolean isPrisonerOrDependent = person.getPrisonerStatus().isCurrentPrisoner() || + person.getPrimaryRole().isCivilian(); + boolean isOfficer = person.getRankNumeric() >= RWO_MIN; + + if (payForHousing) { + Unit unit = person.getUnit(); + + // Crew of non-DropShip vessels live on their vessel full time + if (!isNonDropShipLargeVessel(unit)) { + if (isPrisonerOrDependent) { + prisonerOrDependentHousingUsage++; + } else if (isOfficer) { + officerHousingUsage++; + } else { + enlistedHousingUsage++; + } + } + } + + if (payForFood) { + if (isPrisonerOrDependent) { + prisonerOrDependentFoodUsage++; + } else if (isOfficer) { + officerFoodUsage++; + } else { + enlistedFoodUsage++; + } + } + } + + // calculate total costs + int expenses = 0; + if (payForHousing) { + expenses += prisonerOrDependentHousingUsage * HOUSING_PRISONER_OR_DEPENDENT; + expenses += enlistedHousingUsage * HOUSING_ENLISTED; + expenses += officerHousingUsage * HOUSING_OFFICER; + } + + if (payForFood) { + expenses += prisonerOrDependentFoodUsage * FOOD_PRISONER_OR_DEPENDENT; + expenses += enlistedFoodUsage * FOOD_ENLISTED; + expenses += officerFoodUsage * FOOD_OFFICER; + } + + logger.debug("prisonerOrDependentHousingUsage: {}", prisonerOrDependentHousingUsage); + logger.debug("enlistedHousingUsage: {}", enlistedHousingUsage); + logger.debug("officerHousingUsage: {}", officerHousingUsage); + logger.debug("prisonerOrDependentFoodUsage: {}", prisonerOrDependentFoodUsage); + logger.debug("enlistedFoodUsage: {}", enlistedFoodUsage); + logger.debug("officerFoodUsage: {}", officerFoodUsage); + logger.debug("expenses: {}", expenses); + + return Money.of(expenses); + } + + /** + * Determines whether the specified unit is a large vessel that is not a DropShip. + * + *

This method checks if the given {@code unit} is non-null, retrieves its associated {@link Entity}, and + * evaluates whether it qualifies as a large craft but is not a DropShip.

+ * + * @param unit the unit to be tested; may be {@code null} + * + * @return {@code true} if the unit exists, its entity is a large craft, and it is not a DropShip; {@code false} + * otherwise + * + * @author Illiani + * @since 0.50.06 + */ + public boolean isNonDropShipLargeVessel(Unit unit) { + if (unit == null) { + return false; + } + + Entity entity = unit.getEntity(); + return (entity != null) && entity.isLargeCraft() && !entity.isDropShip(); + } + /** * Gets peacetime costs including salaries. * diff --git a/MekHQ/src/mekhq/campaign/finances/Finances.java b/MekHQ/src/mekhq/campaign/finances/Finances.java index 527d985a3e..7d7dc7f30f 100644 --- a/MekHQ/src/mekhq/campaign/finances/Finances.java +++ b/MekHQ/src/mekhq/campaign/finances/Finances.java @@ -52,6 +52,7 @@ import megamek.logging.MMLogger; import mekhq.MekHQ; import mekhq.campaign.Campaign; +import mekhq.campaign.CampaignOptions; import mekhq.campaign.event.LoanDefaultedEvent; import mekhq.campaign.event.TransactionCreditEvent; import mekhq.campaign.event.TransactionDebitEvent; @@ -311,8 +312,11 @@ public void addLoan(Loan loan) { } public void newDay(final Campaign campaign, final LocalDate yesterday, final LocalDate today) { + CampaignOptions campaignOptions = campaign.getCampaignOptions(); + Accountant accountant = campaign.getAccountant(); + // check for a new fiscal year - if (campaign.getCampaignOptions().getFinancialYearDuration().isEndOfFinancialYear(campaign.getLocalDate())) { + if (campaignOptions.getFinancialYearDuration().isEndOfFinancialYear(campaign.getLocalDate())) { // calculate profits Money profits = getProfits(); campaign.addReport(String.format(resourceMap.getString("Profits.finances"), @@ -322,7 +326,7 @@ public void newDay(final Campaign campaign, final LocalDate yesterday, final Loc newFiscalYear(campaign); // pay taxes - if ((campaign.getCampaignOptions().isUseTaxes()) && (!profits.isZero())) { + if ((campaignOptions.isUseTaxes()) && (!profits.isZero())) { payTaxes(campaign, profits); } } @@ -347,10 +351,10 @@ public void newDay(final Campaign campaign, final LocalDate yesterday, final Loc // Handle peacetime operating expenses, payroll, and loan payments if (today.getDayOfMonth() == 1) { - if (campaign.getCampaignOptions().isUsePeacetimeCost()) { - if (!campaign.getCampaignOptions().isShowPeacetimeCost()) { + if (campaignOptions.isUsePeacetimeCost()) { + if (!campaignOptions.isShowPeacetimeCost()) { // Do not include salaries as that will be tracked below - Money peacetimeCost = campaign.getAccountant().getPeacetimeCost(false); + Money peacetimeCost = accountant.getPeacetimeCost(false); if (debit(TransactionType.MAINTENANCE, today, @@ -367,9 +371,9 @@ public void newDay(final Campaign campaign, final LocalDate yesterday, final Loc "")); } } else { - Money sparePartsCost = campaign.getAccountant().getMonthlySpareParts(); - Money ammoCost = campaign.getAccountant().getMonthlyAmmo(); - Money fuelCost = campaign.getAccountant().getMonthlyFuel(); + Money sparePartsCost = accountant.getMonthlySpareParts(); + Money ammoCost = accountant.getMonthlyAmmo(); + Money fuelCost = accountant.getMonthlyFuel(); if (debit(TransactionType.MAINTENANCE, today, @@ -418,16 +422,16 @@ public void newDay(final Campaign campaign, final LocalDate yesterday, final Loc } } - if (campaign.getCampaignOptions().isPayForSalaries()) { + if (campaignOptions.isPayForSalaries()) { - Money payRollCost = campaign.getAccountant().getPayRoll(); + Money payRollCost = accountant.getPayRoll(); if (debit(TransactionType.SALARIES, today, payRollCost, resourceMap.getString("Salaries.title"), - campaign.getAccountant().getPayRollSummary(), - campaign.getCampaignOptions().isTrackTotalEarnings())) { + accountant.getPayRollSummary(), + campaignOptions.isTrackTotalEarnings())) { campaign.addReport(String.format(resourceMap.getString("Salaries.text"), payRollCost.toAmountAndSymbolString())); @@ -439,7 +443,7 @@ public void newDay(final Campaign campaign, final LocalDate yesterday, final Loc resourceMap.getString("Payroll.text"), "")); - if (campaign.getCampaignOptions().isUseLoyaltyModifiers()) { + if (campaignOptions.isUseLoyaltyModifiers()) { for (Person person : campaign.getPersonnel()) { if (person.getStatus().isDepartedUnit()) { continue; @@ -463,8 +467,8 @@ public void newDay(final Campaign campaign, final LocalDate yesterday, final Loc } // Handle overhead expenses - if (campaign.getCampaignOptions().isPayForOverhead()) { - Money overheadCost = campaign.getAccountant().getOverheadExpenses(); + if (campaignOptions.isPayForOverhead()) { + Money overheadCost = accountant.getOverheadExpenses(); if (debit(TransactionType.OVERHEAD, today, overheadCost, resourceMap.getString("Overhead.title"))) { campaign.addReport(String.format(resourceMap.getString("Overhead.text"), @@ -478,6 +482,25 @@ public void newDay(final Campaign campaign, final LocalDate yesterday, final Loc "")); } } + + if (campaignOptions.isPayForFood() || campaignOptions.isPayForHousing()) { + Money foodAndHousingCosts = accountant.getMonthlyFoodAndHousingExpenses(); + + if (debit(TransactionType.OVERHEAD, + today, + foodAndHousingCosts, + resourceMap.getString("FoodAndHousing.title"))) { + campaign.addReport(String.format(resourceMap.getString("FoodAndHousing.text"), + foodAndHousingCosts.toAmountAndSymbolString())); + } else { + campaign.addReport(String.format("" + + resourceMap.getString("InsufficientFunds.text"), + resourceMap.getString("OverheadCosts.text"), + "")); + } + } } List newLoans = new ArrayList<>(); diff --git a/MekHQ/src/mekhq/gui/campaignOptions/contents/FinancesTab.java b/MekHQ/src/mekhq/gui/campaignOptions/contents/FinancesTab.java index 7279010e00..a8ef4a64c0 100644 --- a/MekHQ/src/mekhq/gui/campaignOptions/contents/FinancesTab.java +++ b/MekHQ/src/mekhq/gui/campaignOptions/contents/FinancesTab.java @@ -24,6 +24,11 @@ * * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of * InMediaRes Productions, LLC. + * + * MechWarrior Copyright Microsoft Corporation. MekHQ was created under + * Microsoft's "Game Content Usage Rules" + * and it is not endorsed by or + * affiliated with Microsoft. */ package mekhq.gui.campaignOptions.contents; @@ -87,6 +92,8 @@ public class FinancesTab { private JCheckBox payForMaintainBox; private JCheckBox payForTransportBox; private JCheckBox payForRecruitmentBox; + private JCheckBox payForFoodBox; + private JCheckBox payForHousingBox; private JPanel pnlSales; @@ -194,6 +201,8 @@ private void initializeGeneralOptionsTab() { payForMaintainBox = new JCheckBox(); payForTransportBox = new JCheckBox(); payForRecruitmentBox = new JCheckBox(); + payForFoodBox = new JCheckBox(); + payForHousingBox = new JCheckBox(); // Sales pnlSales = new JPanel(); @@ -284,6 +293,8 @@ private JPanel createPaymentsPanel() { payForMaintainBox = new CampaignOptionsCheckBox("PayForMaintainBox"); payForTransportBox = new CampaignOptionsCheckBox("PayForTransportBox"); payForRecruitmentBox = new CampaignOptionsCheckBox("PayForRecruitmentBox"); + payForFoodBox = new CampaignOptionsCheckBox("PayForFoodBox"); + payForHousingBox = new CampaignOptionsCheckBox("PayForHousingBox"); // Layout the Panel final JPanel panel = new CampaignOptionsStandardPanel("PaymentsPanel", true, "PaymentsPanel"); @@ -314,6 +325,12 @@ private JPanel createPaymentsPanel() { layout.gridx++; panel.add(payForRecruitmentBox, layout); + layout.gridx = 0; + layout.gridy++; + panel.add(payForFoodBox, layout); + layout.gridx++; + panel.add(payForHousingBox, layout); + return panel; } @@ -785,6 +802,8 @@ public void applyCampaignOptionsToCampaign(@Nullable CampaignOptions presetCampa options.setPayForMaintain(payForMaintainBox.isSelected()); options.setPayForTransport(payForTransportBox.isSelected()); options.setPayForRecruitment(payForRecruitmentBox.isSelected()); + options.setPayForFood(payForFoodBox.isSelected()); + options.setPayForHousing(payForHousingBox.isSelected()); options.setSellUnits(sellUnitsBox.isSelected()); options.setSellParts(sellPartsBox.isSelected()); options.setUseTaxes(chkUseTaxes.isSelected()); @@ -851,6 +870,8 @@ public void loadValuesFromCampaignOptions(@Nullable CampaignOptions presetCampai payForMaintainBox.setSelected(options.isPayForMaintain()); payForTransportBox.setSelected(options.isPayForTransport()); payForRecruitmentBox.setSelected(options.isPayForRecruitment()); + payForFoodBox.setSelected(options.isPayForFood()); + payForHousingBox.setSelected(options.isPayForHousing()); sellUnitsBox.setSelected(options.isSellUnits()); sellPartsBox.setSelected(options.isSellParts()); chkUseTaxes.setSelected(options.isUseTaxes()); diff --git a/MekHQ/unittests/mekhq/campaign/finances/AccountantTest.java b/MekHQ/unittests/mekhq/campaign/finances/AccountantTest.java new file mode 100644 index 0000000000..1afabf2618 --- /dev/null +++ b/MekHQ/unittests/mekhq/campaign/finances/AccountantTest.java @@ -0,0 +1,822 @@ +/* + * 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. + * + * MechWarrior Copyright Microsoft Corporation. MekHQ was created under + * Microsoft's "Game Content Usage Rules" + * and it is not endorsed by or + * affiliated with Microsoft. + */ +package mekhq.campaign.finances; + +import static mekhq.campaign.finances.Accountant.FOOD_ENLISTED; +import static mekhq.campaign.finances.Accountant.FOOD_OFFICER; +import static mekhq.campaign.finances.Accountant.FOOD_PRISONER_OR_DEPENDENT; +import static mekhq.campaign.finances.Accountant.HOUSING_ENLISTED; +import static mekhq.campaign.finances.Accountant.HOUSING_OFFICER; +import static mekhq.campaign.finances.Accountant.HOUSING_PRISONER_OR_DEPENDENT; +import static mekhq.campaign.personnel.enums.PersonnelRole.DEPENDENT; +import static mekhq.campaign.personnel.enums.PersonnelRole.MEKWARRIOR; +import static mekhq.campaign.personnel.enums.PersonnelRole.VESSEL_GUNNER; +import static mekhq.campaign.personnel.ranks.Rank.RWO_MIN; +import static mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus.PRISONER; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +import megamek.common.Entity; +import mekhq.campaign.Campaign; +import mekhq.campaign.CampaignOptions; +import mekhq.campaign.CurrentLocation; +import mekhq.campaign.personnel.Person; +import mekhq.campaign.unit.Unit; +import mekhq.campaign.universe.Faction; +import org.junit.jupiter.api.Test; + +public class AccountantTest { + + @Test + void testGetMonthlyFoodAndHousingExpenses_WhenFoodAndHousingDisabled() { + // Setup + Campaign mockCampaign = mock(Campaign.class); + + CampaignOptions mockCampaignOptions = mock(CampaignOptions.class); + when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions); + when(mockCampaignOptions.isPayForFood()).thenReturn(false); + when(mockCampaignOptions.isPayForHousing()).thenReturn(false); + + Accountant accountant = new Accountant(mockCampaign); + + CurrentLocation location = new CurrentLocation(); + when(mockCampaign.getLocation()).thenReturn(location); + + // Act + Money expected = Money.zero(); + Money actual = accountant.getMonthlyFoodAndHousingExpenses(); + + // Assert + assertEquals(expected, actual); + } + + @Test + void testGetMonthlyFoodAndHousingExpenses_WhenNoPersonnel() { + // Setup + Campaign mockCampaign = mock(Campaign.class); + + CampaignOptions mockCampaignOptions = mock(CampaignOptions.class); + when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions); + when(mockCampaignOptions.isPayForFood()).thenReturn(false); + when(mockCampaignOptions.isPayForHousing()).thenReturn(false); + + Accountant accountant = new Accountant(mockCampaign); + + CurrentLocation location = new CurrentLocation(); + when(mockCampaign.getLocation()).thenReturn(location); + + when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(new ArrayList<>()); + + // Act + Money expected = Money.zero(); + Money actual = accountant.getMonthlyFoodAndHousingExpenses(); + + // Assert + assertEquals(expected, actual); + } + + @Test + void testGetMonthlyFoodAndHousingExpenses_WhenOnlyPrisoners() { + // Setup + Campaign mockCampaign = mock(Campaign.class); + + CampaignOptions mockCampaignOptions = mock(CampaignOptions.class); + when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions); + when(mockCampaignOptions.isPayForFood()).thenReturn(true); + when(mockCampaignOptions.isPayForHousing()).thenReturn(true); + + Accountant accountant = new Accountant(mockCampaign); + + CurrentLocation location = new CurrentLocation(); + when(mockCampaign.getLocation()).thenReturn(location); + + Faction faction = new Faction(); + when(mockCampaign.getFaction()).thenReturn(faction); + + Person prisoner = new Person(mockCampaign); + prisoner.setPrisonerStatus(mockCampaign, PRISONER, false); + List prisoners = List.of(prisoner, prisoner, prisoner); + + when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(prisoners); + + // Act + int expensesFood = FOOD_PRISONER_OR_DEPENDENT * prisoners.size(); + int expensesHousing = HOUSING_PRISONER_OR_DEPENDENT * prisoners.size(); + Money expected = Money.of(expensesFood + expensesHousing); + Money actual = accountant.getMonthlyFoodAndHousingExpenses(); + + // Assert + assertEquals(expected, actual); + } + + @Test + void testGetMonthlyHousingExpenses_WhenOnlyPrisoners() { + // Setup + Campaign mockCampaign = mock(Campaign.class); + + CampaignOptions mockCampaignOptions = mock(CampaignOptions.class); + when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions); + when(mockCampaignOptions.isPayForFood()).thenReturn(false); + when(mockCampaignOptions.isPayForHousing()).thenReturn(true); + + Accountant accountant = new Accountant(mockCampaign); + + CurrentLocation location = new CurrentLocation(); + when(mockCampaign.getLocation()).thenReturn(location); + + Faction faction = new Faction(); + when(mockCampaign.getFaction()).thenReturn(faction); + + Person prisoner = new Person(mockCampaign); + prisoner.setPrisonerStatus(mockCampaign, PRISONER, false); + List prisoners = List.of(prisoner, prisoner, prisoner); + + when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(prisoners); + + // Act + int expensesHousing = HOUSING_PRISONER_OR_DEPENDENT * prisoners.size(); + Money expected = Money.of(expensesHousing); + Money actual = accountant.getMonthlyFoodAndHousingExpenses(); + + // Assert + assertEquals(expected, actual); + } + + @Test + void testGetMonthlyFoodExpenses_WhenOnlyPrisoners() { + // Setup + Campaign mockCampaign = mock(Campaign.class); + + CampaignOptions mockCampaignOptions = mock(CampaignOptions.class); + when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions); + when(mockCampaignOptions.isPayForFood()).thenReturn(true); + when(mockCampaignOptions.isPayForHousing()).thenReturn(false); + + Accountant accountant = new Accountant(mockCampaign); + + CurrentLocation location = new CurrentLocation(); + when(mockCampaign.getLocation()).thenReturn(location); + + Faction faction = new Faction(); + when(mockCampaign.getFaction()).thenReturn(faction); + + Person prisoner = new Person(mockCampaign); + prisoner.setPrisonerStatus(mockCampaign, PRISONER, false); + List prisoners = List.of(prisoner, prisoner, prisoner); + + when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(prisoners); + + // Act + int expensesFood = FOOD_PRISONER_OR_DEPENDENT * prisoners.size(); + Money expected = Money.of(expensesFood); + Money actual = accountant.getMonthlyFoodAndHousingExpenses(); + + // Assert + assertEquals(expected, actual); + } + + @Test + void testGetMonthlyFoodAndHousingExpenses_WhenOnlyDependents() { + // Setup + Campaign mockCampaign = mock(Campaign.class); + + CampaignOptions mockCampaignOptions = mock(CampaignOptions.class); + when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions); + when(mockCampaignOptions.isPayForFood()).thenReturn(true); + when(mockCampaignOptions.isPayForHousing()).thenReturn(true); + + Accountant accountant = new Accountant(mockCampaign); + + CurrentLocation location = new CurrentLocation(); + when(mockCampaign.getLocation()).thenReturn(location); + + Faction faction = new Faction(); + when(mockCampaign.getFaction()).thenReturn(faction); + + Person dependent = new Person(mockCampaign); + dependent.setPrimaryRole(mockCampaign, DEPENDENT); + List dependents = List.of(dependent, dependent, dependent); + + when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(dependents); + + // Act + int expensesFood = FOOD_PRISONER_OR_DEPENDENT * dependents.size(); + int expensesHousing = HOUSING_PRISONER_OR_DEPENDENT * dependents.size(); + Money expected = Money.of(expensesFood + expensesHousing); + Money actual = accountant.getMonthlyFoodAndHousingExpenses(); + + // Assert + assertEquals(expected, actual); + } + + @Test + void testGetMonthlyHousingExpenses_WhenOnlyDependents() { + // Setup + Campaign mockCampaign = mock(Campaign.class); + + CampaignOptions mockCampaignOptions = mock(CampaignOptions.class); + when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions); + when(mockCampaignOptions.isPayForFood()).thenReturn(false); + when(mockCampaignOptions.isPayForHousing()).thenReturn(true); + + Accountant accountant = new Accountant(mockCampaign); + + CurrentLocation location = new CurrentLocation(); + when(mockCampaign.getLocation()).thenReturn(location); + + Faction faction = new Faction(); + when(mockCampaign.getFaction()).thenReturn(faction); + + Person dependent = new Person(mockCampaign); + dependent.setPrimaryRole(mockCampaign, DEPENDENT); + List dependents = List.of(dependent, dependent, dependent); + + when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(dependents); + + // Act + int expensesHousing = HOUSING_PRISONER_OR_DEPENDENT * dependents.size(); + Money expected = Money.of(expensesHousing); + Money actual = accountant.getMonthlyFoodAndHousingExpenses(); + + // Assert + assertEquals(expected, actual); + } + + @Test + void testGetMonthlyFoodExpenses_WhenOnlyDependents() { + // Setup + Campaign mockCampaign = mock(Campaign.class); + + CampaignOptions mockCampaignOptions = mock(CampaignOptions.class); + when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions); + when(mockCampaignOptions.isPayForFood()).thenReturn(true); + when(mockCampaignOptions.isPayForHousing()).thenReturn(false); + + Accountant accountant = new Accountant(mockCampaign); + + CurrentLocation location = new CurrentLocation(); + when(mockCampaign.getLocation()).thenReturn(location); + + Faction faction = new Faction(); + when(mockCampaign.getFaction()).thenReturn(faction); + + Person dependent = new Person(mockCampaign); + dependent.setPrimaryRole(mockCampaign, DEPENDENT); + List dependents = List.of(dependent, dependent, dependent); + + when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(dependents); + + // Act + int expensesFood = FOOD_PRISONER_OR_DEPENDENT * dependents.size(); + Money expected = Money.of(expensesFood); + Money actual = accountant.getMonthlyFoodAndHousingExpenses(); + + // Assert + assertEquals(expected, actual); + } + + @Test + void testGetMonthlyFoodAndHousingExpenses_WhenOnlyEnlisted() { + // Setup + Campaign mockCampaign = mock(Campaign.class); + + CampaignOptions mockCampaignOptions = mock(CampaignOptions.class); + when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions); + when(mockCampaignOptions.isPayForFood()).thenReturn(true); + when(mockCampaignOptions.isPayForHousing()).thenReturn(true); + + Accountant accountant = new Accountant(mockCampaign); + + CurrentLocation location = new CurrentLocation(); + when(mockCampaign.getLocation()).thenReturn(location); + + Faction faction = new Faction(); + when(mockCampaign.getFaction()).thenReturn(faction); + + Person enlisted = new Person(mockCampaign); + enlisted.setPrimaryRole(mockCampaign, MEKWARRIOR); + enlisted.setRank(RWO_MIN - 1); + List enlistedPersonnel = List.of(enlisted, enlisted, enlisted); + + when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(enlistedPersonnel); + + // Act + int expensesFood = FOOD_ENLISTED * enlistedPersonnel.size(); + int expensesHousing = HOUSING_ENLISTED * enlistedPersonnel.size(); + Money expected = Money.of(expensesFood + expensesHousing); + Money actual = accountant.getMonthlyFoodAndHousingExpenses(); + + // Assert + assertEquals(expected, actual); + } + + @Test + void testGetMonthlyHousingExpenses_WhenOnlyEnlisted() { + // Setup + Campaign mockCampaign = mock(Campaign.class); + + CampaignOptions mockCampaignOptions = mock(CampaignOptions.class); + when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions); + when(mockCampaignOptions.isPayForFood()).thenReturn(false); + when(mockCampaignOptions.isPayForHousing()).thenReturn(true); + + Accountant accountant = new Accountant(mockCampaign); + + CurrentLocation location = new CurrentLocation(); + when(mockCampaign.getLocation()).thenReturn(location); + + Faction faction = new Faction(); + when(mockCampaign.getFaction()).thenReturn(faction); + + Person enlisted = new Person(mockCampaign); + enlisted.setPrimaryRole(mockCampaign, MEKWARRIOR); + enlisted.setRank(RWO_MIN - 1); + List enlistedPersonnel = List.of(enlisted, enlisted, enlisted); + + when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(enlistedPersonnel); + + // Act + int expensesHousing = HOUSING_ENLISTED * enlistedPersonnel.size(); + Money expected = Money.of(expensesHousing); + Money actual = accountant.getMonthlyFoodAndHousingExpenses(); + + // Assert + assertEquals(expected, actual); + } + + @Test + void testGetMonthlyFoodExpenses_WhenOnlyEnlisted() { + // Setup + Campaign mockCampaign = mock(Campaign.class); + + CampaignOptions mockCampaignOptions = mock(CampaignOptions.class); + when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions); + when(mockCampaignOptions.isPayForFood()).thenReturn(true); + when(mockCampaignOptions.isPayForHousing()).thenReturn(false); + + Accountant accountant = new Accountant(mockCampaign); + + CurrentLocation location = new CurrentLocation(); + when(mockCampaign.getLocation()).thenReturn(location); + + Faction faction = new Faction(); + when(mockCampaign.getFaction()).thenReturn(faction); + + Person enlisted = new Person(mockCampaign); + enlisted.setPrimaryRole(mockCampaign, MEKWARRIOR); + enlisted.setRank(RWO_MIN - 1); + List enlistedPersonnel = List.of(enlisted, enlisted, enlisted); + + when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(enlistedPersonnel); + + // Act + int expensesFood = FOOD_ENLISTED * enlistedPersonnel.size(); + Money expected = Money.of(expensesFood); + Money actual = accountant.getMonthlyFoodAndHousingExpenses(); + + // Assert + assertEquals(expected, actual); + } + + @Test + void testGetMonthlyFoodAndHousingExpenses_WhenOnlyOfficers() { + // Setup + Campaign mockCampaign = mock(Campaign.class); + + CampaignOptions mockCampaignOptions = mock(CampaignOptions.class); + when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions); + when(mockCampaignOptions.isPayForFood()).thenReturn(true); + when(mockCampaignOptions.isPayForHousing()).thenReturn(true); + + Accountant accountant = new Accountant(mockCampaign); + + CurrentLocation location = new CurrentLocation(); + when(mockCampaign.getLocation()).thenReturn(location); + + Faction faction = new Faction(); + when(mockCampaign.getFaction()).thenReturn(faction); + + Person officer = new Person(mockCampaign); + officer.setPrimaryRole(mockCampaign, MEKWARRIOR); + officer.setRank(RWO_MIN + 1); + List officerPersonnel = List.of(officer, officer, officer); + + when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(officerPersonnel); + + // Act + int expensesFood = FOOD_OFFICER * officerPersonnel.size(); + int expensesHousing = HOUSING_OFFICER * officerPersonnel.size(); + Money expected = Money.of(expensesFood + expensesHousing); + Money actual = accountant.getMonthlyFoodAndHousingExpenses(); + + // Assert + assertEquals(expected, actual); + } + + @Test + void testGetMonthlyHousingExpenses_WhenOnlyOfficers() { + // Setup + Campaign mockCampaign = mock(Campaign.class); + + CampaignOptions mockCampaignOptions = mock(CampaignOptions.class); + when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions); + when(mockCampaignOptions.isPayForFood()).thenReturn(false); + when(mockCampaignOptions.isPayForHousing()).thenReturn(true); + + Accountant accountant = new Accountant(mockCampaign); + + CurrentLocation location = new CurrentLocation(); + when(mockCampaign.getLocation()).thenReturn(location); + + Faction faction = new Faction(); + when(mockCampaign.getFaction()).thenReturn(faction); + + Person officer = new Person(mockCampaign); + officer.setPrimaryRole(mockCampaign, MEKWARRIOR); + officer.setRank(RWO_MIN + 1); + List officerPersonnel = List.of(officer, officer, officer); + + when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(officerPersonnel); + + // Act + int expensesHousing = HOUSING_OFFICER * officerPersonnel.size(); + Money expected = Money.of(expensesHousing); + Money actual = accountant.getMonthlyFoodAndHousingExpenses(); + + // Assert + assertEquals(expected, actual); + } + + @Test + void testGetMonthlyFoodExpenses_WhenOnlyOfficers() { + // Setup + Campaign mockCampaign = mock(Campaign.class); + + CampaignOptions mockCampaignOptions = mock(CampaignOptions.class); + when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions); + when(mockCampaignOptions.isPayForFood()).thenReturn(true); + when(mockCampaignOptions.isPayForHousing()).thenReturn(false); + + Accountant accountant = new Accountant(mockCampaign); + + CurrentLocation location = new CurrentLocation(); + when(mockCampaign.getLocation()).thenReturn(location); + + Faction faction = new Faction(); + when(mockCampaign.getFaction()).thenReturn(faction); + + Person officer = new Person(mockCampaign); + officer.setPrimaryRole(mockCampaign, MEKWARRIOR); + officer.setRank(RWO_MIN + 1); + List officerPersonnel = List.of(officer, officer, officer); + + when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(officerPersonnel); + + // Act + int expensesFood = FOOD_OFFICER * officerPersonnel.size(); + Money expected = Money.of(expensesFood); + Money actual = accountant.getMonthlyFoodAndHousingExpenses(); + + // Assert + assertEquals(expected, actual); + } + + @Test + void testGetMonthlyFoodAndHousingExpenses_Mixed() { + // Setup + Campaign mockCampaign = mock(Campaign.class); + + CampaignOptions mockCampaignOptions = mock(CampaignOptions.class); + when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions); + when(mockCampaignOptions.isPayForFood()).thenReturn(true); + when(mockCampaignOptions.isPayForHousing()).thenReturn(true); + + Accountant accountant = new Accountant(mockCampaign); + + CurrentLocation location = new CurrentLocation(); + when(mockCampaign.getLocation()).thenReturn(location); + + Faction faction = new Faction(); + when(mockCampaign.getFaction()).thenReturn(faction); + + Person prisoner = new Person(mockCampaign); + prisoner.setPrisonerStatus(mockCampaign, PRISONER, false); + List prisoners = List.of(prisoner, prisoner, prisoner); + + Person dependent = new Person(mockCampaign); + dependent.setPrimaryRole(mockCampaign, DEPENDENT); + List dependents = List.of(dependent, dependent, dependent); + + Person enlisted = new Person(mockCampaign); + enlisted.setPrimaryRole(mockCampaign, MEKWARRIOR); + enlisted.setRank(RWO_MIN - 1); + List enlistedPersonnel = List.of(enlisted, enlisted, enlisted); + + Person officer = new Person(mockCampaign); + officer.setPrimaryRole(mockCampaign, MEKWARRIOR); + officer.setRank(RWO_MIN + 1); + List officerPersonnel = List.of(officer, officer, officer); + + List allPersonnel = new ArrayList<>(); + allPersonnel.addAll(prisoners); + allPersonnel.addAll(dependents); + allPersonnel.addAll(enlistedPersonnel); + allPersonnel.addAll(officerPersonnel); + + when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(allPersonnel); + + // Act + int expensesFood = FOOD_PRISONER_OR_DEPENDENT * (prisoners.size() + dependents.size()); + expensesFood += FOOD_ENLISTED * enlistedPersonnel.size(); + expensesFood += FOOD_OFFICER * officerPersonnel.size(); + + int expensesHousing = HOUSING_PRISONER_OR_DEPENDENT * (prisoners.size() + dependents.size()); + expensesHousing += HOUSING_ENLISTED * enlistedPersonnel.size(); + expensesHousing += HOUSING_OFFICER * officerPersonnel.size(); + + Money expected = Money.of(expensesFood + expensesHousing); + Money actual = accountant.getMonthlyFoodAndHousingExpenses(); + + // Assert + assertEquals(expected, actual); + } + + @Test + void testGetMonthlyHousingExpenses_Mixed() { + // Setup + Campaign mockCampaign = mock(Campaign.class); + + CampaignOptions mockCampaignOptions = mock(CampaignOptions.class); + when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions); + when(mockCampaignOptions.isPayForFood()).thenReturn(false); + when(mockCampaignOptions.isPayForHousing()).thenReturn(true); + + Accountant accountant = new Accountant(mockCampaign); + + CurrentLocation location = new CurrentLocation(); + when(mockCampaign.getLocation()).thenReturn(location); + + Faction faction = new Faction(); + when(mockCampaign.getFaction()).thenReturn(faction); + + Person prisoner = new Person(mockCampaign); + prisoner.setPrisonerStatus(mockCampaign, PRISONER, false); + List prisoners = List.of(prisoner, prisoner, prisoner); + + Person dependent = new Person(mockCampaign); + dependent.setPrimaryRole(mockCampaign, DEPENDENT); + List dependents = List.of(dependent, dependent, dependent); + + Person enlisted = new Person(mockCampaign); + enlisted.setPrimaryRole(mockCampaign, MEKWARRIOR); + enlisted.setRank(RWO_MIN - 1); + List enlistedPersonnel = List.of(enlisted, enlisted, enlisted); + + Person officer = new Person(mockCampaign); + officer.setPrimaryRole(mockCampaign, MEKWARRIOR); + officer.setRank(RWO_MIN + 1); + List officerPersonnel = List.of(officer, officer, officer); + + List allPersonnel = new ArrayList<>(); + allPersonnel.addAll(prisoners); + allPersonnel.addAll(dependents); + allPersonnel.addAll(enlistedPersonnel); + allPersonnel.addAll(officerPersonnel); + + when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(allPersonnel); + + // Act + int expensesHousing = HOUSING_PRISONER_OR_DEPENDENT * (prisoners.size() + dependents.size()); + expensesHousing += HOUSING_ENLISTED * enlistedPersonnel.size(); + expensesHousing += HOUSING_OFFICER * officerPersonnel.size(); + + Money expected = Money.of(expensesHousing); + Money actual = accountant.getMonthlyFoodAndHousingExpenses(); + + // Assert + assertEquals(expected, actual); + } + + @Test + void testGetMonthlyFoodExpenses_Mixed() { + // Setup + Campaign mockCampaign = mock(Campaign.class); + + CampaignOptions mockCampaignOptions = mock(CampaignOptions.class); + when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions); + when(mockCampaignOptions.isPayForFood()).thenReturn(true); + when(mockCampaignOptions.isPayForHousing()).thenReturn(false); + + Accountant accountant = new Accountant(mockCampaign); + + CurrentLocation location = new CurrentLocation(); + when(mockCampaign.getLocation()).thenReturn(location); + + Faction faction = new Faction(); + when(mockCampaign.getFaction()).thenReturn(faction); + + Person prisoner = new Person(mockCampaign); + prisoner.setPrisonerStatus(mockCampaign, PRISONER, false); + List prisoners = List.of(prisoner, prisoner, prisoner); + + Person dependent = new Person(mockCampaign); + dependent.setPrimaryRole(mockCampaign, DEPENDENT); + List dependents = List.of(dependent, dependent, dependent); + + Person enlisted = new Person(mockCampaign); + enlisted.setPrimaryRole(mockCampaign, MEKWARRIOR); + enlisted.setRank(RWO_MIN - 1); + List enlistedPersonnel = List.of(enlisted, enlisted, enlisted); + + Person officer = new Person(mockCampaign); + officer.setPrimaryRole(mockCampaign, MEKWARRIOR); + officer.setRank(RWO_MIN + 1); + List officerPersonnel = List.of(officer, officer, officer); + + List allPersonnel = new ArrayList<>(); + allPersonnel.addAll(prisoners); + allPersonnel.addAll(dependents); + allPersonnel.addAll(enlistedPersonnel); + allPersonnel.addAll(officerPersonnel); + + when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(allPersonnel); + + // Act + int expensesFood = FOOD_PRISONER_OR_DEPENDENT * (prisoners.size() + dependents.size()); + expensesFood += FOOD_ENLISTED * enlistedPersonnel.size(); + expensesFood += FOOD_OFFICER * officerPersonnel.size(); + + Money expected = Money.of(expensesFood); + Money actual = accountant.getMonthlyFoodAndHousingExpenses(); + + // Assert + assertEquals(expected, actual); + } + + @Test + void testGetMonthlyFoodAndHousingExpenses_Mixed_InTransit() { + // Setup + Campaign mockCampaign = mock(Campaign.class); + + CampaignOptions mockCampaignOptions = mock(CampaignOptions.class); + when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions); + when(mockCampaignOptions.isPayForFood()).thenReturn(true); + when(mockCampaignOptions.isPayForHousing()).thenReturn(true); + + Accountant accountant = new Accountant(mockCampaign); + + CurrentLocation location = new CurrentLocation(); + location.setTransitTime(1); + when(mockCampaign.getLocation()).thenReturn(location); + + Faction faction = new Faction(); + when(mockCampaign.getFaction()).thenReturn(faction); + + Person prisoner = new Person(mockCampaign); + prisoner.setPrisonerStatus(mockCampaign, PRISONER, false); + List prisoners = List.of(prisoner, prisoner, prisoner); + + Person dependent = new Person(mockCampaign); + dependent.setPrimaryRole(mockCampaign, DEPENDENT); + List dependents = List.of(dependent, dependent, dependent); + + Person enlisted = new Person(mockCampaign); + enlisted.setPrimaryRole(mockCampaign, MEKWARRIOR); + enlisted.setRank(RWO_MIN - 1); + List enlistedPersonnel = List.of(enlisted, enlisted, enlisted); + + Person officer = new Person(mockCampaign); + officer.setPrimaryRole(mockCampaign, MEKWARRIOR); + officer.setRank(RWO_MIN + 1); + List officerPersonnel = List.of(officer, officer, officer); + + List allPersonnel = new ArrayList<>(); + allPersonnel.addAll(prisoners); + allPersonnel.addAll(dependents); + allPersonnel.addAll(enlistedPersonnel); + allPersonnel.addAll(officerPersonnel); + + when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(allPersonnel); + + // Act + int expensesFood = FOOD_PRISONER_OR_DEPENDENT * (prisoners.size() + dependents.size()); + expensesFood += FOOD_ENLISTED * enlistedPersonnel.size(); + expensesFood += FOOD_OFFICER * officerPersonnel.size(); + + int expensesHousing = 0; + + Money expected = Money.of(expensesFood + expensesHousing); + Money actual = accountant.getMonthlyFoodAndHousingExpenses(); + + // Assert + assertEquals(expected, actual); + } + + @Test + void testGetMonthlyFoodAndHousingExpenses_Mixed_ExcludingWarShipCrew() { + // Setup + Campaign mockCampaign = mock(Campaign.class); + + CampaignOptions mockCampaignOptions = mock(CampaignOptions.class); + when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions); + when(mockCampaignOptions.isPayForFood()).thenReturn(true); + when(mockCampaignOptions.isPayForHousing()).thenReturn(true); + + Accountant accountant = new Accountant(mockCampaign); + + CurrentLocation location = new CurrentLocation(); + when(mockCampaign.getLocation()).thenReturn(location); + + Faction faction = new Faction(); + when(mockCampaign.getFaction()).thenReturn(faction); + + Person prisoner = new Person(mockCampaign); + prisoner.setPrisonerStatus(mockCampaign, PRISONER, false); + List prisoners = List.of(prisoner, prisoner, prisoner); + + Person dependent = new Person(mockCampaign); + dependent.setPrimaryRole(mockCampaign, DEPENDENT); + List dependents = List.of(dependent, dependent, dependent); + + Person enlisted = new Person(mockCampaign); + enlisted.setPrimaryRole(mockCampaign, MEKWARRIOR); + enlisted.setRank(RWO_MIN - 1); + List enlistedPersonnel = List.of(enlisted, enlisted, enlisted); + + Person officer = new Person(mockCampaign); + officer.setPrimaryRole(mockCampaign, MEKWARRIOR); + officer.setRank(RWO_MIN + 1); + List officerPersonnel = List.of(officer, officer, officer); + + Unit warShip = new Unit(); + Entity mockEntity = mock(Entity.class); + when(mockEntity.isLargeCraft()).thenReturn(true); + when(mockEntity.isDropShip()).thenReturn(false); + warShip.setEntity(mockEntity); + + Person warShipCrew = new Person(mockCampaign); + warShipCrew.setPrimaryRole(mockCampaign, VESSEL_GUNNER); + warShipCrew.setRank(RWO_MIN - 1); + warShipCrew.setUnit(warShip); + List warShipPersonnel = List.of(warShipCrew, warShipCrew, warShipCrew); + + List allPersonnel = new ArrayList<>(); + allPersonnel.addAll(prisoners); + allPersonnel.addAll(dependents); + allPersonnel.addAll(enlistedPersonnel); + allPersonnel.addAll(officerPersonnel); + allPersonnel.addAll(warShipPersonnel); + + when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(allPersonnel); + + // Act + int expensesFood = FOOD_PRISONER_OR_DEPENDENT * (prisoners.size() + dependents.size()); + expensesFood += FOOD_ENLISTED * enlistedPersonnel.size(); + expensesFood += FOOD_OFFICER * officerPersonnel.size(); + expensesFood += FOOD_ENLISTED * warShipPersonnel.size(); + + int expensesHousing = HOUSING_PRISONER_OR_DEPENDENT * (prisoners.size() + dependents.size()); + expensesHousing += HOUSING_ENLISTED * enlistedPersonnel.size(); + expensesHousing += HOUSING_OFFICER * officerPersonnel.size(); + + Money expected = Money.of(expensesFood + expensesHousing); + Money actual = accountant.getMonthlyFoodAndHousingExpenses(); + + // Assert + assertEquals(expected, actual); + } +} From 473833f44055607ced68259f2da822836eec0d59 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sat, 3 May 2025 16:29:58 -0500 Subject: [PATCH 2/4] Added missign 'unable to pay' messaging --- MekHQ/resources/mekhq/resources/Finances.properties | 1 + MekHQ/src/mekhq/campaign/finances/Finances.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/MekHQ/resources/mekhq/resources/Finances.properties b/MekHQ/resources/mekhq/resources/Finances.properties index 8281e4a220..9ca827c928 100644 --- a/MekHQ/resources/mekhq/resources/Finances.properties +++ b/MekHQ/resources/mekhq/resources/Finances.properties @@ -126,6 +126,7 @@ TrainingMunitions.text=for training munitions Fuel.text=for fuel Payroll.text=payroll costs OverheadCosts.text=overhead costs +HousingAndFoodCosts.text=housing and/or food expenses Shares.text=shares AssetPayment.finances=Income from %s AssetPayment.report=Your account has been credited for %s from %s diff --git a/MekHQ/src/mekhq/campaign/finances/Finances.java b/MekHQ/src/mekhq/campaign/finances/Finances.java index 7d7dc7f30f..ffbb603bb6 100644 --- a/MekHQ/src/mekhq/campaign/finances/Finances.java +++ b/MekHQ/src/mekhq/campaign/finances/Finances.java @@ -497,7 +497,7 @@ public void newDay(final Campaign campaign, final LocalDate yesterday, final Loc MekHQ.getMHQOptions().getFontColorNegativeHexColor() + "'>" + resourceMap.getString("InsufficientFunds.text"), - resourceMap.getString("OverheadCosts.text"), + resourceMap.getString("HousingAndFoodCosts.text"), "")); } } From f2c93cfbce410d7e4d4311585493175a625c3a62 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sat, 3 May 2025 16:36:14 -0500 Subject: [PATCH 3/4] Added missing attribution --- MekHQ/src/mekhq/campaign/finances/Accountant.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MekHQ/src/mekhq/campaign/finances/Accountant.java b/MekHQ/src/mekhq/campaign/finances/Accountant.java index 6d6d50bf30..5a71d4423f 100644 --- a/MekHQ/src/mekhq/campaign/finances/Accountant.java +++ b/MekHQ/src/mekhq/campaign/finances/Accountant.java @@ -145,6 +145,8 @@ public Money getOverheadExpenses() { *

If neither food nor housing expenses are enabled in the campaign options, this method returns zero.

* * @return a {@link Money} object representing the total monthly food and housing expenses for the campaign + * @author Illiani + * @since 0.50.06 */ public Money getMonthlyFoodAndHousingExpenses() { boolean payForFood = getCampaignOptions().isPayForFood(); From 72cb604311b8c0bf7aedde90a41f1c11099666d4 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Wed, 14 May 2025 15:48:25 -0500 Subject: [PATCH 4/4] Fixed failing tests --- .../mekhq/campaign/finances/Accountant.java | 14 +++++++- .../education/EducationController.java | 35 ++++++++++++++++++ .../campaign/finances/AccountantTest.java | 36 +++++++++---------- 3 files changed, 66 insertions(+), 19 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/finances/Accountant.java b/MekHQ/src/mekhq/campaign/finances/Accountant.java index c2fa177360..2a642fe2d4 100644 --- a/MekHQ/src/mekhq/campaign/finances/Accountant.java +++ b/MekHQ/src/mekhq/campaign/finances/Accountant.java @@ -36,6 +36,7 @@ import static mekhq.campaign.personnel.ranks.Rank.RWO_MIN; import java.time.LocalDate; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -50,6 +51,7 @@ import mekhq.campaign.finances.enums.TransactionType; import mekhq.campaign.parts.Part; import mekhq.campaign.personnel.Person; +import mekhq.campaign.personnel.education.EducationController; import mekhq.campaign.personnel.enums.PersonnelRole; import mekhq.campaign.unit.Unit; @@ -165,8 +167,18 @@ public Money getMonthlyFoodAndHousingExpenses() { int officerFoodUsage = 0; // Determine housing and food requirements - List personnel = campaign.getActivePersonnelIncludingInUnitStudents(); + List personnel = new ArrayList<>(campaign().getPersonnel()); for (Person person : personnel) { + if (person.getStatus().isDepartedUnit()) { + // No paying for dead people or folks who left the campaign unit + continue; + } + + if (person.getStatus().isStudent() && !EducationController.isBeingHomeSchooled(person)) { + // Tuition includes room and board + continue; + } + boolean isPrisonerOrDependent = person.getPrisonerStatus().isCurrentPrisoner() || person.getPrimaryRole().isCivilian(); boolean isOfficer = person.getRankNumeric() >= RWO_MIN; diff --git a/MekHQ/src/mekhq/campaign/personnel/education/EducationController.java b/MekHQ/src/mekhq/campaign/personnel/education/EducationController.java index f55fd4bc9f..ace571d801 100644 --- a/MekHQ/src/mekhq/campaign/personnel/education/EducationController.java +++ b/MekHQ/src/mekhq/campaign/personnel/education/EducationController.java @@ -78,6 +78,41 @@ private EducationController() { // Just here to remove warning. } + + /** + * Determines whether the specified student is currently being homeschooled. + * + *

This method checks if the student has an associated academy set and retrieves the relevant academy using + * the student's academy name. If an academy is found, it returns {@code true} only if that academy indicates + * homeschooling.

+ * + *

Usage: the primary use of this is to ensure that homeschooled personnel are still being paid their + * salaries, as they will not appear in a list of active personnel.

+ * + * @param student the {@link Person} whose schooling status is to be checked + * + * @return {@code true} if the student is being homeschooled; {@code false} otherwise + * + * @author Illiani + * @since 0.50.06 + */ + public static boolean isBeingHomeSchooled(Person student) { + if (!student.getStatus().isStudent()) { + return false; + } + + if (student.getEduAcademySet() == null) { + return false; + } + + Academy academy = getAcademy(student.getEduAcademySet(), student.getEduAcademyNameInSet()); + if (academy == null) { + return false; + } + + return academy.isHomeSchool(); + } + /** * Checks eligibility for enrollment in an academy. * diff --git a/MekHQ/unittests/mekhq/campaign/finances/AccountantTest.java b/MekHQ/unittests/mekhq/campaign/finances/AccountantTest.java index 1afabf2618..524fb625d8 100644 --- a/MekHQ/unittests/mekhq/campaign/finances/AccountantTest.java +++ b/MekHQ/unittests/mekhq/campaign/finances/AccountantTest.java @@ -99,7 +99,7 @@ void testGetMonthlyFoodAndHousingExpenses_WhenNoPersonnel() { CurrentLocation location = new CurrentLocation(); when(mockCampaign.getLocation()).thenReturn(location); - when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(new ArrayList<>()); + when(mockCampaign.getPersonnel()).thenReturn(new ArrayList<>()); // Act Money expected = Money.zero(); @@ -131,7 +131,7 @@ void testGetMonthlyFoodAndHousingExpenses_WhenOnlyPrisoners() { prisoner.setPrisonerStatus(mockCampaign, PRISONER, false); List prisoners = List.of(prisoner, prisoner, prisoner); - when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(prisoners); + when(mockCampaign.getPersonnel()).thenReturn(prisoners); // Act int expensesFood = FOOD_PRISONER_OR_DEPENDENT * prisoners.size(); @@ -165,7 +165,7 @@ void testGetMonthlyHousingExpenses_WhenOnlyPrisoners() { prisoner.setPrisonerStatus(mockCampaign, PRISONER, false); List prisoners = List.of(prisoner, prisoner, prisoner); - when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(prisoners); + when(mockCampaign.getPersonnel()).thenReturn(prisoners); // Act int expensesHousing = HOUSING_PRISONER_OR_DEPENDENT * prisoners.size(); @@ -198,7 +198,7 @@ void testGetMonthlyFoodExpenses_WhenOnlyPrisoners() { prisoner.setPrisonerStatus(mockCampaign, PRISONER, false); List prisoners = List.of(prisoner, prisoner, prisoner); - when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(prisoners); + when(mockCampaign.getPersonnel()).thenReturn(prisoners); // Act int expensesFood = FOOD_PRISONER_OR_DEPENDENT * prisoners.size(); @@ -231,7 +231,7 @@ void testGetMonthlyFoodAndHousingExpenses_WhenOnlyDependents() { dependent.setPrimaryRole(mockCampaign, DEPENDENT); List dependents = List.of(dependent, dependent, dependent); - when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(dependents); + when(mockCampaign.getPersonnel()).thenReturn(dependents); // Act int expensesFood = FOOD_PRISONER_OR_DEPENDENT * dependents.size(); @@ -265,7 +265,7 @@ void testGetMonthlyHousingExpenses_WhenOnlyDependents() { dependent.setPrimaryRole(mockCampaign, DEPENDENT); List dependents = List.of(dependent, dependent, dependent); - when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(dependents); + when(mockCampaign.getPersonnel()).thenReturn(dependents); // Act int expensesHousing = HOUSING_PRISONER_OR_DEPENDENT * dependents.size(); @@ -298,7 +298,7 @@ void testGetMonthlyFoodExpenses_WhenOnlyDependents() { dependent.setPrimaryRole(mockCampaign, DEPENDENT); List dependents = List.of(dependent, dependent, dependent); - when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(dependents); + when(mockCampaign.getPersonnel()).thenReturn(dependents); // Act int expensesFood = FOOD_PRISONER_OR_DEPENDENT * dependents.size(); @@ -332,7 +332,7 @@ void testGetMonthlyFoodAndHousingExpenses_WhenOnlyEnlisted() { enlisted.setRank(RWO_MIN - 1); List enlistedPersonnel = List.of(enlisted, enlisted, enlisted); - when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(enlistedPersonnel); + when(mockCampaign.getPersonnel()).thenReturn(enlistedPersonnel); // Act int expensesFood = FOOD_ENLISTED * enlistedPersonnel.size(); @@ -367,7 +367,7 @@ void testGetMonthlyHousingExpenses_WhenOnlyEnlisted() { enlisted.setRank(RWO_MIN - 1); List enlistedPersonnel = List.of(enlisted, enlisted, enlisted); - when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(enlistedPersonnel); + when(mockCampaign.getPersonnel()).thenReturn(enlistedPersonnel); // Act int expensesHousing = HOUSING_ENLISTED * enlistedPersonnel.size(); @@ -401,7 +401,7 @@ void testGetMonthlyFoodExpenses_WhenOnlyEnlisted() { enlisted.setRank(RWO_MIN - 1); List enlistedPersonnel = List.of(enlisted, enlisted, enlisted); - when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(enlistedPersonnel); + when(mockCampaign.getPersonnel()).thenReturn(enlistedPersonnel); // Act int expensesFood = FOOD_ENLISTED * enlistedPersonnel.size(); @@ -435,7 +435,7 @@ void testGetMonthlyFoodAndHousingExpenses_WhenOnlyOfficers() { officer.setRank(RWO_MIN + 1); List officerPersonnel = List.of(officer, officer, officer); - when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(officerPersonnel); + when(mockCampaign.getPersonnel()).thenReturn(officerPersonnel); // Act int expensesFood = FOOD_OFFICER * officerPersonnel.size(); @@ -470,7 +470,7 @@ void testGetMonthlyHousingExpenses_WhenOnlyOfficers() { officer.setRank(RWO_MIN + 1); List officerPersonnel = List.of(officer, officer, officer); - when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(officerPersonnel); + when(mockCampaign.getPersonnel()).thenReturn(officerPersonnel); // Act int expensesHousing = HOUSING_OFFICER * officerPersonnel.size(); @@ -504,7 +504,7 @@ void testGetMonthlyFoodExpenses_WhenOnlyOfficers() { officer.setRank(RWO_MIN + 1); List officerPersonnel = List.of(officer, officer, officer); - when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(officerPersonnel); + when(mockCampaign.getPersonnel()).thenReturn(officerPersonnel); // Act int expensesFood = FOOD_OFFICER * officerPersonnel.size(); @@ -557,7 +557,7 @@ void testGetMonthlyFoodAndHousingExpenses_Mixed() { allPersonnel.addAll(enlistedPersonnel); allPersonnel.addAll(officerPersonnel); - when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(allPersonnel); + when(mockCampaign.getPersonnel()).thenReturn(allPersonnel); // Act int expensesFood = FOOD_PRISONER_OR_DEPENDENT * (prisoners.size() + dependents.size()); @@ -617,7 +617,7 @@ void testGetMonthlyHousingExpenses_Mixed() { allPersonnel.addAll(enlistedPersonnel); allPersonnel.addAll(officerPersonnel); - when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(allPersonnel); + when(mockCampaign.getPersonnel()).thenReturn(allPersonnel); // Act int expensesHousing = HOUSING_PRISONER_OR_DEPENDENT * (prisoners.size() + dependents.size()); @@ -673,7 +673,7 @@ void testGetMonthlyFoodExpenses_Mixed() { allPersonnel.addAll(enlistedPersonnel); allPersonnel.addAll(officerPersonnel); - when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(allPersonnel); + when(mockCampaign.getPersonnel()).thenReturn(allPersonnel); // Act int expensesFood = FOOD_PRISONER_OR_DEPENDENT * (prisoners.size() + dependents.size()); @@ -730,7 +730,7 @@ void testGetMonthlyFoodAndHousingExpenses_Mixed_InTransit() { allPersonnel.addAll(enlistedPersonnel); allPersonnel.addAll(officerPersonnel); - when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(allPersonnel); + when(mockCampaign.getPersonnel()).thenReturn(allPersonnel); // Act int expensesFood = FOOD_PRISONER_OR_DEPENDENT * (prisoners.size() + dependents.size()); @@ -801,7 +801,7 @@ void testGetMonthlyFoodAndHousingExpenses_Mixed_ExcludingWarShipCrew() { allPersonnel.addAll(officerPersonnel); allPersonnel.addAll(warShipPersonnel); - when(mockCampaign.getActivePersonnelIncludingInUnitStudents()).thenReturn(allPersonnel); + when(mockCampaign.getPersonnel()).thenReturn(allPersonnel); // Act int expensesFood = FOOD_PRISONER_OR_DEPENDENT * (prisoners.size() + dependents.size());