8000 Feature: Added Monthly Food and Housing Expenses from A Time of War by IllianiBird · Pull Request #6863 · MegaMek/mekhq · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Feature: Added Monthly Food and Housing Expenses from A Time of War #6863

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
May 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.\
<br>\
<br>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.\
<br>\
<br>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.\
<br>\
<br>These values have been extrapolated from those found in <i>A Time of War</i> 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.
Expand Down
3 changes: 3 additions & 0 deletions MekHQ/resources/mekhq/resources/Finances.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -124,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
Expand Down
26 changes: 26 additions & 0 deletions MekHQ/src/mekhq/campaign/CampaignOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1071,6 +1073,8 @@ public CampaignOptions() {
sellUnits = false;
sellParts = false;
payForRecruitment = false;
payForFood = false;
payForHousing = false;
useLoanLimits = false;
usePercentageMaint = false;
infantryDontCount = false;
Expand Down Expand Up @@ -3329,6 +3333,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;
}
Expand Down Expand Up @@ -5308,6 +5328,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);
Expand Down Expand Up @@ -6241,6 +6263,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")) {
Expand Down
138 changes: 138 additions & 0 deletions MekHQ/src/mekhq/campaign/finances/Accountant.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,12 @@
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.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

Expand All @@ -48,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;

Expand All @@ -57,6 +61,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();
}
Expand Down Expand Up @@ -119,6 +130,133 @@ public Money getOverheadExpenses() {
}
}

/**
* Calculates the total monthly expenses for food and housing for all active personnel in the campaign.
*
* <p>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:</p>
*
* <ul>
* <li>Prisoners or dependents have specific food and housing rates.</li>
* <li>Officers have higher food and housing rates than enlisted personnel.</li>
* <li>Crew members of non-DropShip large vessels live aboard their vessel and are exempt from housing charges.</li>
* </ul>
*
* <p>If neither food nor housing expenses are enabled in the campaign options, this method returns zero.</p>
*
* @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();
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<Person> 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;

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.
*
* <p>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.</p>
*
* @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.
*
Expand Down
53 changes: 38 additions & 15 deletions MekHQ/src/mekhq/campaign/finances/Finances.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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"),
Expand All @@ -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);
}
}
Expand All @@ -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,
Expand All @@ -367,9 +371,9 @@ public void newDay(final Campaign campaign, final LocalDate yesterday, final Loc
"</font>"));
}
} 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,
Expand Down Expand Up @@ -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()));

Expand All @@ -439,7 +443,7 @@ public void newDay(final Campaign campaign, final LocalDate yesterday, final Loc
resourceMap.getString("Payroll.text"),
"</font>"));

if (campaign.getCampaignOptions().isUseLoyaltyModifiers()) {
if (campaignOptions.isUseLoyaltyModifiers()) {
for (Person person : campaign.getPersonnel()) {
if (person.getStatus().isDepartedUnit()) {
continue;
Expand All @@ -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"),
Expand All @@ -478,6 +482,25 @@ public void newDay(final Campaign campaign, final LocalDate yesterday, final Loc
"</font>"));
}
}

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("<font color='" +
MekHQ.getMHQOptions().getFontColorNegativeHexColor() +
"'>" +
resourceMap.getString("InsufficientFunds.text"),
resourceMap.getString("HousingAndFoodCosts.text"),
"</font>"));
}
}
}

List<Loan> newLoans = new ArrayList<>();
Expand Down
Loading
Loading
0