pirateFactions = new ArrayList<>();
for (Faction activeFaction : factions.getActiveFactions(currentDay)) {
- if (activeFaction.isPirate() &&
- !activeFaction.getShortName().equalsIgnoreCase("PIR")) {
+ if (activeFaction.isPirate() && !activeFaction.getShortName().equalsIgnoreCase("PIR")) {
pirateFactions.add(activeFaction);
}
}
diff --git a/MekHQ/src/mekhq/campaign/CampaignOptions.java b/MekHQ/src/mekhq/campaign/CampaignOptions.java
index f515388fc4a..fb5e517a772 100644
--- a/MekHQ/src/mekhq/campaign/CampaignOptions.java
+++ b/MekHQ/src/mekhq/campaign/CampaignOptions.java
@@ -468,6 +468,7 @@ public static String getTechLevelName(final int techLevel) {
private boolean payForOverhead;
private boolean payForMaintain;
private boolean payForTransport;
+ private boolean trackLeases;
private boolean sellUnits;
private boolean sellParts;
private boolean payForRecruitment;
@@ -1088,6 +1089,7 @@ public CampaignOptions() {
payForOverhead = false;
payForMaintain = false;
payForTransport = false;
+ trackLeases = false;
sellUnits = false;
sellParts = false;
payForRecruitment = false;
@@ -3294,6 +3296,14 @@ public void setPayForTransport(final boolean payForTransport) {
this.payForTransport = payForTransport;
}
+ public boolean isTrackLeases() {
+ return trackLeases;
+ }
+
+ public void setTrackLeases(final boolean trackLeases) {
+ this.trackLeases = trackLeases;
+ }
+
public boolean isSellUnits() {
return sellUnits;
}
@@ -5317,6 +5327,7 @@ public void writeToXml(final PrintWriter pw, int indent) {
MHQXMLUtility.writeSimpleXMLTag(pw, indent, "payForOverhead", payForOverhead);
MHQXMLUtility.writeSimpleXMLTag(pw, indent, "payForMaintain", payForMaintain);
MHQXMLUtility.writeSimpleXMLTag(pw, indent, "payForTransport", payForTransport);
+ MHQXMLUtility.writeSimpleXMLTag(pw, indent, "trackLeases", trackLeases);
MHQXMLUtility.writeSimpleXMLTag(pw, indent, "sellUnits", sellUnits);
MHQXMLUtility.writeSimpleXMLTag(pw, indent, "sellParts", sellParts);
MHQXMLUtility.writeSimpleXMLTag(pw, indent, "payForRecruitment", payForRecruitment);
@@ -6313,6 +6324,8 @@ public static CampaignOptions generateCampaignOptionsFromXml(Node parentNod, Ver
campaignOptions.payForMaintain = Boolean.parseBoolean(nodeContents);
} else if (nodeName.equalsIgnoreCase("payForTransport")) {
campaignOptions.payForTransport = Boolean.parseBoolean(nodeContents);
+ } else if (nodeName.equalsIgnoreCase("trackLeases")) {
+ campaignOptions.trackLeases = Boolean.parseBoolean(nodeContents);
} else if (nodeName.equalsIgnoreCase("sellUnits")) {
campaignOptions.sellUnits = Boolean.parseBoolean(nodeContents);
} else if (nodeName.equalsIgnoreCase("sellParts")) {
@@ -6926,19 +6939,19 @@ public boolean isTrackFactionStanding() {
public void setTrackFactionStanding(boolean trackFactionStanding) {
this.trackFactionStanding = trackFactionStanding;
}
-
+
public boolean isAutoGenerateOpForCallsigns() {
return autoGenerateOpForCallsigns;
}
-
+
public void setAutoGenerateOpForCallsigns(boolean autoGenerateOpForCallsigns) {
this.autoGenerateOpForCallsigns = autoGenerateOpForCallsigns;
}
-
+
public SkillLevel getMinimumCallsignSkillLevel() {
return minimumCallsignSkillLevel;
}
-
+
public void setMinimumCallsignSkillLevel(SkillLevel skillLevel) {
this.minimumCallsignSkillLevel = skillLevel;
}
diff --git a/MekHQ/src/mekhq/campaign/Quartermaster.java b/MekHQ/src/mekhq/campaign/Quartermaster.java
index 429d7116e23..cc833c4516e 100644
--- a/MekHQ/src/mekhq/campaign/Quartermaster.java
+++ b/MekHQ/src/mekhq/campaign/Quartermaster.java
@@ -32,6 +32,7 @@
*/
package mekhq.campaign;
+import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@@ -40,9 +41,11 @@
import megamek.common.Entity;
import megamek.common.annotations.Nullable;
import megamek.common.weapons.infantry.InfantryWeapon;
+import megamek.logging.MMLogger;
import mekhq.MekHQ;
import mekhq.campaign.event.PartArrivedEvent;
import mekhq.campaign.event.PartChangedEvent;
+import mekhq.campaign.finances.Lease;
import mekhq.campaign.finances.Money;
import mekhq.campaign.finances.enums.TransactionType;
import mekhq.campaign.parts.AmmoStorage;
@@ -61,16 +64,17 @@
* Manages machines and materiel for a campaign.
*/
public class Quartermaster {
+ private static final MMLogger LOGGER = MMLogger.create(Quartermaster.class);
+
public enum PartAcquisitionResult {
- PartInherentFailure,
- PlanetSpecificFailure,
- Success
+ PartInherentFailure, PlanetSpecificFailure, Success
}
private final Campaign campaign;
/**
* Initializes a new instance of the Quartermaster class.
+ *
* @param campaign The campaign being managed by the Quartermaster.
*/
public Quartermaster(Campaign campaign) {
@@ -99,17 +103,18 @@ protected Warehouse getWarehouse() {
}
/**
- * Adds a part to the campaign, arriving in a set number of days. By default, the part is treated
- * as not brand new. This method is deprecated in favor of the overloaded method that explicitly
- * accepts a flag to indicate whether the part is brand new.
+ * Adds a part to the campaign, arriving in a set number of days. By default, the part is treated as not brand new.
+ * This method is deprecated in favor of the overloaded method that explicitly accepts a flag to indicate whether
+ * the part is brand new.
*
* This method delegates its behavior to {@link #addPart(Part, int, boolean)} with the
* {@code isBrandNew} flag set to {@code false}.
*
* @param part The part to add to the campaign. Cannot be {@code null}.
* @param transitDays The number of days until the part arrives, or zero if the part is already here.
- * @deprecated Use {@link #addPart(Part, int, boolean)} instead to explicitly indicate whether
- * the part is brand new.
+ *
+ * @deprecated Use {@link #addPart(Part, int, boolean)} instead to explicitly indicate whether the part is brand
+ * new.
*/
@Deprecated
public void addPart(Part part, int transitDays) {
@@ -117,19 +122,19 @@ public void addPart(Part part, int transitDays) {
}
/**
- * Adds a part to the campaign's warehouse, specifying the number of transit days for its arrival
- * and whether the part is considered brand new. The method validates the input and decides whether
- * to skip the addition based on specific conditions, such as test units, spare ammo bins, or
- * missing parts without associated units.
+ * Adds a part to the campaign's warehouse, specifying the number of transit days for its arrival and whether the
+ * part is considered brand new. The method validates the input and decides whether to skip the addition based on
+ * specific conditions, such as test units, spare ammo bins, or missing parts without associated units.
*
* Once validated, the part is marked as new or used, set to arrive in the specified number of
* days, processed for campaign addition, and added to the campaign's warehouse.
*
* @param part The part to add to the campaign. Cannot be {@code null}.
- * @param transitDays The number of days until the part arrives. If the value is negative,
- * it will be adjusted to zero, indicating the part is already here.
- * @param isBrandNew A {@code boolean} indicating whether the part is brand new.
- * {@code true} if the part is new, otherwise {@code false}.
+ * @param transitDays The number of days until the part arrives. If the value is negative, it will be adjusted to
+ * zero, indicating the part is already here.
+ * @param isBrandNew A {@code boolean} indicating whether the part is brand new. {@code true} if the part is new,
+ * otherwise {@code false}.
+ *
* @throws NullPointerException If {@code part} is {@code null}.
*/
public void addPart(Part part, int transitDays, boolean isBrandNew) {
@@ -169,8 +174,9 @@ public void addPart(Part part, int transitDays, boolean isBrandNew) {
/**
* Adds ammo to the campaign.
+ *
* @param ammoType The type of ammo to add.
- * @param shots The number of rounds of ammo to add.
+ * @param shots The number of rounds of ammo to add.
*/
public void addAmmo(AmmoType ammoType, int shots) {
Objects.requireNonNull(ammoType);
@@ -182,9 +188,10 @@ public void addAmmo(AmmoType ammoType, int shots) {
/**
* Adds infantry ammo to the campaign.
- * @param ammoType The type of ammo to add.
+ *
+ * @param ammoType The type of ammo to add.
* @param infantryWeapon The type of infantry weapon using the ammo.
- * @param shots The number of rounds of ammo to add.
+ * @param shots The number of rounds of ammo to add.
*/
public void addAmmo(AmmoType ammoType, InfantryWeapon infantryWeapon, int shots) {
Objects.requireNonNull(ammoType);
@@ -197,10 +204,12 @@ public void addAmmo(AmmoType ammoType, InfantryWeapon infantryWeapon, int shots)
/**
* Removes ammo from the campaign, if available.
- * @param ammoType The type of ammo to remove.
+ *
+ * @param ammoType The type of ammo to remove.
* @param shotsNeeded The number of rounds of ammo needed.
- * @return The number of rounds of ammo removed from the campaign.
- * This value may be less than or equal to {@code shotsNeeded}.
+ *
+ * @return The number of rounds of ammo removed from the campaign. This value may be less than or equal to
+ * {@code shotsNeeded}.
*/
public int removeAmmo(AmmoType ammoType, int shotsNeeded) {
Objects.requireNonNull(ammoType);
@@ -227,7 +236,9 @@ public int removeAmmo(AmmoType ammoType, int shotsNeeded) {
/**
* Remove ammo directly from an AmmoStorage part.
+ *
* @param shotsNeeded The number of shots needed.
+ *
* @return The number of shots removed.
*/
private int removeAmmo(@Nullable AmmoStorage ammoStorage, int shotsNeeded) {
@@ -254,10 +265,12 @@ private int removeAmmo(@Nullable AmmoStorage ammoStorage, int shotsNeeded) {
/**
* Removes compatible ammo from the campaign, if available.
- * @param ammoType The type of ammo to remove.
+ *
+ * @param ammoType The type of ammo to remove.
* @param shotsNeeded The number of rounds of ammo needed.
- * @return The number of rounds of ammo removed from the campaign.
- * This value may be less than or equal to {@code shotsNeeded}.
+ *
+ * @return The number of rounds of ammo removed from the campaign. This value may be less than or equal to
+ * {@code shotsNeeded}.
*/
public int removeCompatibleAmmo(AmmoType ammoType, int shotsNeeded) {
Objects.requireNonNull(ammoType);
@@ -304,19 +317,22 @@ public int removeCompatibleAmmo(AmmoType ammoType, int shotsNeeded) {
/**
* Finds spare ammo of a given type, if any.
+ *
* @param ammoType The AmmoType to search for.
+ *
* @return The matching spare {@code AmmoStorage} part, otherwise {@code null}.
*/
private @Nullable AmmoStorage findSpareAmmo(AmmoType ammoType) {
return (AmmoStorage) getWarehouse().findSparePart(part -> {
- return isAvailableAsSpareAmmo(part)
- && ((AmmoStorage) part).isSameAmmoType(ammoType);
+ return isAvailableAsSpareAmmo(part) && ((AmmoStorage) part).isSameAmmoType(ammoType);
});
}
/**
* Find compatible ammo in the warehouse.
+ *
* @param ammoType The AmmoType to search for compatible types.
+ *
* @return A list of spare {@code AmmoStorage} parts in the warehouse.
*/
private List findCompatibleSpareAmmo(AmmoType ammoType) {
@@ -343,11 +359,13 @@ private List findCompatibleSpareAmmo(AmmoType ammoType) {
}
/**
- * Converts shots from one ammo type to another.
- * NB: it is up to the caller to ensure the ammo types are compatible.
- * @param from The AmmoType for which {@code shots} represents.
+ * Converts shots from one ammo type to another. NB: it is up to the caller to ensure the ammo types are
+ * compatible.
+ *
+ * @param from The AmmoType for which {@code shots} represents.
* @param shots The number of shots of {@code from}.
- * @param to The AmmoType which {@code shots} should be converted to.
+ * @param to The AmmoType which {@code shots} should be converted to.
+ *
* @return The value of {@code shots} when converted to a specific AmmoType.
*/
public static int convertShots(AmmoType from, int shots, AmmoType to) {
@@ -367,11 +385,13 @@ public static int convertShots(AmmoType from, int shots, AmmoType to) {
}
/**
- * Calculates the shots needed when converting from a source ammo to a target ammo.
- * NB: it is up to the caller to ensure the ammo types are compatible.
- * @param target The target ammo type.
+ * Calculates the shots needed when converting from a source ammo to a target ammo. NB: it is up to the caller to
+ * ensure the ammo types are compatible.
+ *
+ * @param target The target ammo type.
* @param shotsNeeded The number of shots needed in the target ammo type.
- * @param source The source ammo type.
+ * @param source The source ammo type.
+ *
* @return The number of shots needed from the source ammo type.
*/
public static int convertShotsNeeded(AmmoType target, int shotsNeeded, AmmoType source) {
@@ -394,7 +414,9 @@ public static int convertShotsNeeded(AmmoType target, int shotsNeeded, AmmoType
/**
* Gets the amount of ammo available of a given type.
+ *
* @param ammoType The type of ammo.
+ *
* @return The number of shots available of the given ammo type.
*/
public int getAmmoAvailable(AmmoType ammoType) {
@@ -405,51 +427,43 @@ public int getAmmoAvailable(AmmoType ammoType) {
// matching ammo. There may be multiple instances of matching
// ammo that have different qualities, so we should return
// all of those counts as viable and not just the first we find.
- return getWarehouse()
- .streamSpareParts()
- .filter(Quartermaster::isAvailableAsSpareAmmo)
- .mapToInt(part -> {
- AmmoStorage spare = (AmmoStorage) part;
- if (spare.isSameAmmoType(ammoType)) {
- return spare.getShots();
- }
- return 0;
- })
- .sum();
+ return getWarehouse().streamSpareParts().filter(Quartermaster::isAvailableAsSpareAmmo).mapToInt(part -> {
+ AmmoStorage spare = (AmmoStorage) part;
+ if (spare.isSameAmmoType(ammoType)) {
+ return spare.getShots();
+ }
+ return 0;
+ }).sum();
} else {
// If we're using ammo by type, stream through all of
// the ammo that matches strictly or is compatible.
- return getWarehouse()
- .streamSpareParts()
- .filter(Quartermaster::isAvailableAsSpareAmmo)
- .mapToInt(part -> {
- AmmoStorage spare = (AmmoStorage) part;
- if (spare.isSameAmmoType(ammoType)) {
- return spare.getShots();
- } else if (spare.isCompatibleAmmo(ammoType)) {
- return convertShots(spare.getType(), spare.getShots(), ammoType);
- }
- return 0;
- })
- .sum();
+ return getWarehouse().streamSpareParts().filter(Quartermaster::isAvailableAsSpareAmmo).mapToInt(part -> {
+ AmmoStorage spare = (AmmoStorage) part;
+ if (spare.isSameAmmoType(ammoType)) {
+ return spare.getShots();
+ } else if (spare.isCompatibleAmmo(ammoType)) {
+ return convertShots(spare.getType(), spare.getShots(), ammoType);
+ }
+ return 0;
+ }).sum();
}
}
/**
- * Gets a value indicating whether or not a given {@code Part}
- * is available for use as spare ammo.
+ * Gets a value indicating whether or not a given {@code Part} is available for use as spare ammo.
+ *
* @param part The part to check if it can be used as spare ammo.
*/
private static boolean isAvailableAsSpareAmmo(@Nullable Part part) {
- return (part instanceof AmmoStorage)
- && part.isPresent()
- && !part.isReservedForRefit();
+ return (part instanceof AmmoStorage) && part.isPresent() && !part.isReservedForRefit();
}
/**
* Gets the amount of ammo available of a given type.
+ *
* @param ammoType The type of ammo.
+ *
* @return The number of shots available of the given ammo type.
*/
public int getAmmoAvailable(AmmoType ammoType, InfantryWeapon weaponType) {
@@ -461,8 +475,10 @@ public int getAmmoAvailable(AmmoType ammoType, InfantryWeapon weaponType) {
/**
* Finds spare infantry ammo of a given type, if any.
- * @param ammoType The {@code AmmoType} to search for.
+ *
+ * @param ammoType The {@code AmmoType} to search for.
* @param weaponType The {@code InfantryWeapon} which carries the ammo.
+ *
* @return The matching spare {@code InfantryAmmoStorage} part, otherwise {@code null}.
*/
private @Nullable InfantryAmmoStorage findSpareAmmo(AmmoType ammoType, InfantryWeapon weaponType) {
@@ -476,11 +492,13 @@ public int getAmmoAvailable(AmmoType ammoType, InfantryWeapon weaponType) {
/**
* Removes infantry ammo from the campaign, if available.
- * @param ammoType The type of ammo to remove.
+ *
+ * @param ammoType The type of ammo to remove.
* @param infantryWeapon The infantry weapon using the ammo.
- * @param shotsNeeded The number of rounds of ammo needed.
- * @return The number of rounds of ammo removed from the campaign.
- * This value may be less than or equal to {@code shotsNeeded}.
+ * @param shotsNeeded The number of rounds of ammo needed.
+ *
+ * @return The number of rounds of ammo removed from the campaign. This value may be less than or equal to
+ * {@code shotsNeeded}.
*/
public int removeAmmo(AmmoType ammoType, InfantryWeapon infantryWeapon, int shotsNeeded) {
Objects.requireNonNull(ammoType);
@@ -498,8 +516,7 @@ public int removeAmmo(AmmoType ammoType, InfantryWeapon infantryWeapon, int shot
}
/**
- * Denotes that a part in-transit has arrived.
- * Should be called when a part goes from 1 daysToArrival to zero.
+ * Denotes that a part in-transit has arrived. Should be called when a part goes from 1 daysToArrival to zero.
*
* @param part The part which has arrived.
*/
@@ -527,8 +544,10 @@ public void arrivePart(Part part) {
/**
* Tries to buy a unit.
- * @param en The entity which represents the unit.
+ *
+ * @param en The entity which represents the unit.
* @param days The number of days until the new unit arrives.
+ *
* @return True if the unit was purchased, otherwise false.
*/
public boolean buyUnit(Entity en, int days) {
@@ -542,8 +561,11 @@ public boolean buyUnit(Entity en, int days) {
if (getCampaignOptions().isPayForUnits()) {
Money cost = new Unit(en, getCampaign()).getBuyCost();
- if (getCampaign().getFinances().debit(TransactionType.UNIT_PURCHASE, getCampaign().getLocalDate(),
- cost, "Purchased " + en.getShortName())) {
+ if (getCampaign().getFinances()
+ .debit(TransactionType.UNIT_PURCHASE,
+ getCampaign().getLocalDate(),
+ cost,
+ "Purchased " + en.getShortName())) {
getCampaign().addNewUnit(en, false, days, quality);
@@ -558,8 +580,36 @@ public boolean buyUnit(Entity en, int days) {
}
}
+ /**
+ * Leases a Unit by creating a new Unit from the entity specified and adding it to the hangar with attached lease.
+ *
+ * @param leaseEntity The unit to lease
+ * @param days The number of days before the lease starts (typically days to arrival)
+ */
+ public boolean createLeasedUnit(Entity leaseEntity, int days) {
+ if (leaseEntity == null) {
+ LOGGER.debug("null unit passed into createLeasedUnit - no unit created");
+ return false;
+ }
+ if (!campaign.getCampaignOptions().isTrackLeases()) {
+ LOGGER.debug("attempted to createLeasedUnit where isTrackLeases is disabled - no unit created");
+ return false;
+ }
+ PartQuality quality = PartQuality.QUALITY_D;
+
+ if (campaign.getCampaignOptions().isUseRandomUnitQualities()) {
+ quality = Unit.getRandomUnitQuality(0);
+ }
+ //We don't want to start the new lease until the unit arrives.
+ LocalDate leaseStart = campaign.getLocalDate().plusDays(days);
+ Unit newUnit = getCampaign().addNewUnit(leaseEntity, false, days, quality, false);
+ newUnit.addLease(new Lease(leaseStart, newUnit));
+ return true;
+ }
+
/**
* Sells a unit.
+ *
* @param unit The unit to sell.
*/
public void sellUnit(Unit unit) {
@@ -567,14 +617,42 @@ public void sellUnit(Unit unit) {
Money sellValue = unit.getSellValue();
- getCampaign().getFinances().credit(TransactionType.UNIT_SALE, getCampaign().getLocalDate(),
- sellValue, "Sale of " + unit.getName());
+ getCampaign().getFinances()
+ .credit(TransactionType.UNIT_SALE, getCampaign().getLocalDate(), sellValue, "Sale of " + unit.getName());
+
+ getCampaign().removeUnit(unit.getId());
+ }
+
+ /**
+ * Disposes of a leased unit. Checks first that the unit exists, and has an attached lease - otherwise throws an
+ * exception.
+ *
+ * @param unit Unit to be removed
+ */
+ public void cancelUnitLease(Unit unit) {
+ if (unit == null) {
+ LOGGER.debug("null unit passed into cancelUnitLease");
+ return;
+ }
+ if (unit.getUnitLease() == null) {
+ LOGGER.debug("Unit {} with no lease passed into cancelUnitLease", unit.getName());
+ return;
+ }
+ LocalDate thisDate = getCampaign().getLocalDate();
+ Money lastMonthLease = unit.getUnitLease().getFinalLeaseCost(thisDate);
+
+ getCampaign().getFinances()
+ .debit(TransactionType.UNIT_CANCEL_LEASE,
+ thisDate,
+ lastMonthLease,
+ "Final Monthly Lease of " + unit.getName());
getCampaign().removeUnit(unit.getId());
}
/**
* Sell all of the parts on hand.
+ *
* @param part The part to sell.
*/
public void sellPart(Part part) {
@@ -593,7 +671,8 @@ public void sellPart(Part part) {
/**
* Sell one or more units of a part.
- * @param part The part to sell.
+ *
+ * @param part The part to sell.
* @param quantity The amount to sell of the part.
*/
public void sellPart(Part part, int quantity) {
@@ -619,14 +698,18 @@ public void sellPart(Part part, int quantity) {
plural = "s";
}
- getCampaign().getFinances().credit(TransactionType.EQUIPMENT_SALE, getCampaign().getLocalDate(),
- cost, "Sale of " + quantity + " " + part.getName() + plural);
+ getCampaign().getFinances()
+ .credit(TransactionType.EQUIPMENT_SALE,
+ getCampaign().getLocalDate(),
+ cost,
+ "Sale of " + quantity + " " + part.getName() + plural);
getWarehouse().removePart(part, quantity);
}
/**
* Sell all of the ammo on hand.
+ *
* @param ammo The ammo to sell.
*/
public void sellAmmo(AmmoStorage ammo) {
@@ -637,7 +720,8 @@ public void sellAmmo(AmmoStorage ammo) {
/**
* Sell one or more shots of ammo.
- * @param ammo The ammo to sell.
+ *
+ * @param ammo The ammo to sell.
* @param shots The number of shots of ammo to sell.
*/
public void sellAmmo(AmmoStorage ammo, int shots) {
@@ -658,14 +742,18 @@ public void sellAmmo(AmmoStorage ammo, int shots) {
Money cost = ammo.getActualValue().multipliedBy(saleProportion);
- getCampaign().getFinances().credit(TransactionType.EQUIPMENT_SALE, getCampaign().getLocalDate(),
- cost, "Sale of " + shots + " " + ammo.getName());
+ getCampaign().getFinances()
+ .credit(TransactionType.EQUIPMENT_SALE,
+ getCampaign().getLocalDate(),
+ cost,
+ "Sale of " + shots + " " + ammo.getName());
getWarehouse().removeAmmo(ammo, shots);
}
/**
* Sell all of the armor on hand.
+ *
* @param armor The armor to sell.
*/
public void sellArmor(Armor armor) {
@@ -676,7 +764,8 @@ public void sellArmor(Armor armor) {
/**
* Sell one or more points of armor
- * @param armor The armor to sell.
+ *
+ * @param armor The armor to sell.
* @param points The number of points of armor to sell.
*/
public void sellArmor(Armor armor, int points) {
@@ -697,14 +786,18 @@ public void sellArmor(Armor armor, int points) {
Money cost = armor.getActualValue().multipliedBy(saleProportion);
- getCampaign().getFinances().credit(TransactionType.EQUIPMENT_SALE, getCampaign().getLocalDate(),
- cost, "Sale of " + points + " " + armor.getName());
+ getCampaign().getFinances()
+ .credit(TransactionType.EQUIPMENT_SALE,
+ getCampaign().getLocalDate(),
+ cost,
+ "Sale of " + points + " " + armor.getName());
getWarehouse().removeArmor(armor, points);
}
/**
* Removes one or more parts from its OmniPod.
+ *
* @param part The omnipodded part.
*/
public void depodPart(Part part) {
@@ -715,7 +808,8 @@ public void depodPart(Part part) {
/**
* Removes one or more parts from its OmniPod.
- * @param part The omnipodded part.
+ *
+ * @param part The omnipodded part.
* @param quantity The number of omnipodded parts to de-pod.
*/
public void depodPart(Part part, int quantity) {
@@ -759,14 +853,18 @@ public void depodPart(Part part, int quantity) {
/**
* Tries to buys a refurbishment for a given part.
+ *
* @param part The part being refurbished.
+ *
* @return True if the refurbishment was purchased, otherwise false.
*/
public boolean buyRefurbishment(Part part) {
if (getCampaignOptions().isPayForParts()) {
- return getCampaign().getFinances().debit(TransactionType.EQUIPMENT_PURCHASE,
- getCampaign().getLocalDate(), part.getActualValue(),
- "Purchase of " + part.getName());
+ return getCampaign().getFinances()
+ .debit(TransactionType.EQUIPMENT_PURCHASE,
+ getCampaign().getLocalDate(),
+ part.getActualValue(),
+ "Purchase of " + part.getName());
} else {
return true;
}
@@ -774,8 +872,10 @@ public boolean buyRefurbishment(Part part) {
/**
* Tries to buy a part arriving in a given number of days.
- * @param part The part to buy.
+ *
+ * @param part The part to buy.
* @param transitDays The number of days until the new part arrives.
+ *
* @return True if the part was purchased, otherwise false.
*/
public boolean buyPart(Part part, int transitDays) {
@@ -784,9 +884,11 @@ public boolean buyPart(Part part, int transitDays) {
/**
* Tries to buy a part with a cost multiplier, arriving in a given number of days.
- * @param part The part to buy.
+ *
+ * @param part The part to buy.
* @param costMultiplier The cost multiplier for the purchase.
- * @param transitDays The number of days until the new part arrives.
+ * @param transitDays The number of days until the new part arrives.
+ *
* @return True if the part was purchased, otherwise false.
*/
public boolean buyPart(Part part, double costMultiplier, int transitDays) {
@@ -794,9 +896,12 @@ public boolean buyPart(Part part, double costMultiplier, int transitDays) {
if (getCampaignOptions().isPayForParts()) {
Money cost = part.getActualValue().multipliedBy(costMultiplier);
- if (getCampaign().getFinances().debit(TransactionType.EQUIPMENT_PURCHASE,
- getCampaign().getLocalDate(), cost, "Purchase of " + part.getName())) {
- addPart(part, transitDays, true);
+ if (getCampaign().getFinances()
+ .debit(TransactionType.EQUIPMENT_PURCHASE,
+ getCampaign().getLocalDate(),
+ cost,
+ "Purchase of " + part.getName())) {
+ addPart(part, transitDays, true);
return true;
} else {
return false;
diff --git a/MekHQ/src/mekhq/campaign/finances/Accountant.java b/MekHQ/src/mekhq/campaign/finances/Accountant.java
index 2a642fe2d46..55bd07e7d5e 100644
--- a/MekHQ/src/mekhq/campaign/finances/Accountant.java
+++ b/MekHQ/src/mekhq/campaign/finances/Accountant.java
@@ -118,6 +118,14 @@ public Money getMaintenanceCosts() {
return Money.zero();
}
+ public Money getLeaseCosts() {
+ LocalDate thisDate = campaign.getLocalDate();
+ return getHangar().getUnitsStream()
+ .filter(Unit::hasLease)
+ .map(u -> u.getUnitLease().getLeaseCostNow(thisDate))
+ .reduce(Money.zero(), Money::plus);
+ }
+
public Money getWeeklyMaintenanceCosts() {
return getHangar().getUnitsStream().map(Unit::getWeeklyMaintenanceCost).reduce(Money.zero(), Money::plus);
}
@@ -147,6 +155,7 @@ 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
*/
diff --git a/MekHQ/src/mekhq/campaign/finances/Finances.java b/MekHQ/src/mekhq/campaign/finances/Finances.java
index 72222077e20..ab1617cea45 100644
--- a/MekHQ/src/mekhq/campaign/finances/Finances.java
+++ b/MekHQ/src/mekhq/campaign/finances/Finances.java
@@ -33,6 +33,8 @@
*/
package mekhq.campaign.finances;
+import static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;
+
import java.io.BufferedWriter;
import java.io.File;
import java.io.PrintWriter;
@@ -357,7 +359,7 @@ public void newDay(final Campaign campaign, final LocalDate yesterday, final Loc
// Handle assets
getAssets().forEach(asset -> asset.processNewDay(campaign, yesterday, today, this));
- // Handle peacetime operating expenses, payroll, and loan payments
+ // Handle peacetime operating expenses, payroll, loan payments, and leases
if (isNewMonth) {
if (campaignOptions.isUsePeacetimeCost()) {
if (!campaignOptions.isShowPeacetimeCost()) {
@@ -492,6 +494,23 @@ public void newDay(final Campaign campaign, final LocalDate yesterday, final Loc
}
}
+ // Leases - on the 1st, go get the cost of units in the hanger looking for leases. Charge money if possible.
+ // Even with the option disabled, some units may have lingering leases.
+ if (isNewMonth && !(Money.zero().equals(campaign.getAccountant().getLeaseCosts()))) {
+ Money leaseCosts = campaign.getAccountant().getLeaseCosts();
+ if (debit(TransactionType.LEASE_PAYMENT, today, leaseCosts, resourceMap.getString("LeaseCosts.title"))) {
+ campaign.addReport(String.format(resourceMap.getString("LeaseCosts.text"),
+ leaseCosts.toAmountAndSymbolString()));
+ } else {
+ String color = spanOpeningWithCustomColor(MekHQ.getMHQOptions().getFontColorNegativeHexColor());
+ String msg = String.format(color +
+ resourceMap.getString("LeaseCosts.insufficient.report") +
+ leaseCosts +
+ "");
+ campaign.addReport(msg);
+ }
+ }
+
if ((getWentIntoDebt() != null) && !isInDebt()) {
setWentIntoDebt(null);
}
diff --git a/MekHQ/src/mekhq/campaign/finances/Lease.java b/MekHQ/src/mekhq/campaign/finances/Lease.java
new file mode 100644
index 00000000000..651c37c442f
--- /dev/null
+++ b/MekHQ/src/mekhq/campaign/finances/Lease.java
@@ -0,0 +1,196 @@
+/*
+ * 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 java.io.PrintWriter;
+import java.time.LocalDate;
+
+import megamek.common.Entity;
+import megamek.common.annotations.Nullable;
+import megamek.logging.MMLogger;
+import mekhq.campaign.unit.Unit;
+import mekhq.utilities.MHQXMLUtility;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+public class Lease {
+ private static final MMLogger LOGGER = MMLogger.create(Lease.class);
+ private Money leaseCost;
+ private LocalDate acquisitionDate;
+
+ //Multiplier for unit cost for leases is 0.5%, see CamOps p43
+ //Using divisor instead for floating point accuracy
+ private final int leaseDivisor = 200;
+
+ /**
+ * Leases are nominally attached to units while they are in the hanger.
+ */
+ public Lease(LocalDate currentDay, Unit unit) {
+ acquisitionDate = currentDay;
+ leaseCost = unit.getSellValue().dividedBy(leaseDivisor);
+ }
+
+ /**
+ * This constructor is only used for reloading leases from the XML.
+ */
+ private Lease() {
+ }
+
+ /**
+ * Gets the lease cost, for the accountant. Lease cost is prorated for the first month, so we need to check if
+ * yesterday was the first month. Should only be called on the 1st. It's also possible we get a lease object before
+ * the lease actually starts, if it takes a while to deliver the unit. Cost will be zero in this case.
+ *
+ * @param time The current campaign LocalDate
+ */
+ @Nullable
+ public Money getLeaseCostNow(LocalDate time) {
+ if (time.getDayOfMonth() == 1) {
+ if (getLeaseStart().isBefore(time)) {
+ if (isLeaseFirstMonth(time.minusDays(1))) {
+ return getFirstLeaseCost(time);
+ }
+ return leaseCost;
+ }
+ return Money.zero();
+ } else {
+ LOGGER.error("getLeaseCostNow(time) cannot be called on other days of the month then the 1st.");
+ return null;
+ }
+ }
+
+ /**
+ * This is the total monthly cost for the entire lease.
+ */
+ public Money getLeaseCost() {
+ return leaseCost;
+ }
+
+ /**
+ * Leases can start at any time of the month, but they are only processed on the 1st by the accountant.
+ */
+ public LocalDate getLeaseStart() {
+ return acquisitionDate;
+ }
+
+ /**
+ * Utility function. Is this the first month of the lease?
+ *
+ * @param today The LocalDate to check with. No corrections done.
+ */
+ private boolean isLeaseFirstMonth(LocalDate today) {
+ return (today.getYear() == acquisitionDate.getYear() && today.getMonth() == acquisitionDate.getMonth());
+ }
+
+ /**
+ * Gets the final cost of the lease remaining in the last month for use when ending a lease. If you call this in the
+ * same month you acquired the unit, only the days between lease start and now are counted. Can be called on any day
+ * of the month
+ *
+ * @return Money Prorated last payment of lease
+ */
+ public Money getFinalLeaseCost(LocalDate today) {
+ int startDay = 1;
+ int currentDay = today.getDayOfMonth();
+ if (isLeaseFirstMonth(today)) {
+ startDay = acquisitionDate.getDayOfMonth();
+ }
+ int daysElapsed = currentDay - startDay + 1;
+ float fractionOfMonth = (float) daysElapsed / today.lengthOfMonth();
+ return leaseCost.multipliedBy(fractionOfMonth);
+ }
+
+ /**
+ * Gets the cost of the lease, prorated in the first month. Assumes that it's only called on the first day of the
+ * month, so we need to find yesterday for the last.
+ *
+ * @return Money Prorated first payment of lease
+ */
+ @Nullable
+ public Money getFirstLeaseCost(LocalDate today) {
+ if (today.getDayOfMonth() == 1) {
+ int startDay = acquisitionDate.getDayOfMonth();
+ LocalDate yesterday = today.minusDays(1);
+ int daysInMonth = yesterday.lengthOfMonth();
+ int daysElapsed = yesterday.getDayOfMonth() - startDay + 1;
+ float fractionOfMonth = (float) daysElapsed / daysInMonth;
+ return leaseCost.multipliedBy(fractionOfMonth);
+ } else {
+ LOGGER.error("getFirstLeaseCost(today) cannot be called on other days of the month than the 1st.");
+ return null;
+ }
+ }
+
+ /**
+ * This function checks to see if the Entity is of a leasable type. This is currently hardcoded to restrict it to
+ * Dropships and Jumpships only.
+ *
+ * @return True if unit is leasable
+ */
+ public static boolean isLeasable(Entity check) {
+ return check.isDropShip() || check.isJumpShip();
+ }
+
+ public void writeToXML(final PrintWriter writer, int indent) {
+ MHQXMLUtility.writeSimpleXMLOpenTag(writer, indent++, "lease");
+ MHQXMLUtility.writeSimpleXMLTag(writer, indent, "leaseCost", leaseCost);
+ MHQXMLUtility.writeSimpleXMLTag(writer, indent, "acquisitionDate", acquisitionDate);
+ MHQXMLUtility.writeSimpleXMLCloseTag(writer, --indent, "lease");
+ }
+
+ @Nullable
+ public static Lease generateInstanceFromXML(Node writerNode, Unit parseUnit) {
+ Lease savedLease = new Lease();
+ NodeList childNodeList = writerNode.getChildNodes();
+
+ try {
+ for (int x = 0; x < childNodeList.getLength(); x++) {
+ Node childNode = childNodeList.item(x);
+ String nodeName = childNode.getNodeName();
+ String nodeText = childNode.getTextContent();
+
+ if (nodeName.equalsIgnoreCase("leaseCost")) {
+ savedLease.leaseCost = Money.fromXmlString(nodeText);
+ } else if (nodeName.equalsIgnoreCase("acquisitionDate")) {
+ savedLease.acquisitionDate = LocalDate.parse(nodeText);
+ }
+ }
+ return savedLease;
+ } catch (Exception ex) {
+ LOGGER.error("Could not parse lease for unit {}", parseUnit.getId(), ex);
+ }
+ return null;
+ }
+}
+
diff --git a/MekHQ/src/mekhq/campaign/finances/LeaseOrder.java b/MekHQ/src/mekhq/campaign/finances/LeaseOrder.java
new file mode 100644
index 00000000000..91027c51ef3
--- /dev/null
+++ b/MekHQ/src/mekhq/campaign/finances/LeaseOrder.java
@@ -0,0 +1,143 @@
+/*
+ * 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.utilities.ReportingUtilities.getPositiveColor;
+import static mekhq.utilities.ReportingUtilities.messageSurroundedBySpanWithColor;
+
+import java.io.PrintWriter;
+
+import megamek.common.Entity;
+import megamek.logging.MMLogger;
+import mekhq.campaign.Campaign;
+import mekhq.campaign.unit.UnitOrder;
+import mekhq.utilities.MHQXMLUtility;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+public class LeaseOrder extends UnitOrder {
+ private static final MMLogger LOGGER = MMLogger.create(LeaseOrder.class);
+
+ /*
+ * LeaseOrders is in the shopping list, it doesn't have a unit yet to attach to.
+ */
+ public LeaseOrder(Entity entity, Campaign campaign) {
+ super(entity, campaign);
+ }
+
+ // For the XML reader.
+ private LeaseOrder() {
+ }
+
+ /**
+ * This function is called from Campaign newDay when successfully acquiring equipment, or when using the GM function
+ * to acquire things in the Acquisition panel.
+ *
+ * @param transitDays How long it takes for the unit to arrive.
+ *
+ * @return The string for a successful find report.
+ */
+ @Override
+ public String find(int transitDays) {
+ this.getCampaign().getQuartermaster().createLeasedUnit((megamek.common.Entity) getNewEquipment(), transitDays);
+ return messageSurroundedBySpanWithColor(getPositiveColor(),
+ " unit found for leasing. It will be delivered in " + transitDays + " days.");
+ }
+
+ /**
+ * Displays the correct name for the Procurement List, so they're not confused with standard orders.
+ *
+ * @return {String} Lease for UnitName
+ */
+ @Override
+ public String getAcquisitionName() {
+ return "Lease for " + getName();
+ }
+
+ /**
+ * Leases don't actually COST anything when a unit is obtained, so this returns zero.
+ */
+ @Override
+ public Money getTotalBuyCost() {
+ return Money.zero();
+ }
+
+ /**
+ * Leases don't actually COST anything when a unit is obtained, so this returns zero.
+ */
+ @Override
+ public Money getBuyCost() {
+ return Money.zero();
+ }
+
+ @Override
+ public void writeToXML(final PrintWriter pw, int indent) {
+ MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, "leaseOrder");
+ pw.println(MHQXMLUtility.writeEntityToXmlString(getEntity(), indent, getCampaign().getEntities()));
+ MHQXMLUtility.writeSimpleXMLTag(pw, indent, "quantity", quantity);
+ MHQXMLUtility.writeSimpleXMLTag(pw, indent, "daysToWait", daysToWait);
+ MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, "leaseOrder");
+ }
+
+ public static LeaseOrder generateInstanceFromXML(Node writerNode, Campaign campaign) {
+ LeaseOrder savedLeaseOrder = new LeaseOrder();
+ savedLeaseOrder.setCampaign(campaign);
+
+ NodeList childNodeList = writerNode.getChildNodes();
+
+ try {
+ for (int x = 0; x < childNodeList.getLength(); x++) {
+ Node childNode = childNodeList.item(x);
+ String nodeName = childNode.getNodeName();
+ String nodeText = childNode.getTextContent();
+
+ if (nodeName.equalsIgnoreCase("quantity")) {
+ savedLeaseOrder.quantity = Integer.parseInt(nodeText);
+ } else if (nodeName.equalsIgnoreCase("daysToWait")) {
+ savedLeaseOrder.daysToWait = Integer.parseInt(nodeText);
+ } else if (nodeName.equalsIgnoreCase("entity")) {
+ savedLeaseOrder.entity = MHQXMLUtility.parseSingleEntityMul((Element) childNode, campaign);
+ }
+ }
+ } catch (Exception ex) {
+ LOGGER.error("Exception while parsing lease order from {}", childNodeList);
+ }
+
+ savedLeaseOrder.initializeParts(false);
+
+ return savedLeaseOrder;
+ }
+}
+
diff --git a/MekHQ/src/mekhq/campaign/finances/enums/TransactionType.java b/MekHQ/src/mekhq/campaign/finances/enums/TransactionType.java
index 372c36e3872..f5da04f43f0 100644
--- a/MekHQ/src/mekhq/campaign/finances/enums/TransactionType.java
+++ b/MekHQ/src/mekhq/campaign/finances/enums/TransactionType.java
@@ -44,6 +44,7 @@ public enum TransactionType {
FINANCIAL_TERM_END_CARRYOVER("TransactionType.FINANCIAL_TERM_END_CARRYOVER.text",
"TransactionType.FINANCIAL_TERM_END_CARRYOVER.toolTipText"),
FINE("TransactionType.FINE.text", "TransactionType.FINE.toolTipText"),
+ LEASE_PAYMENT("TransactionType.LEASE_PAYMENT.text", "TransactionType.LEASE_PAYMENT.toolTipText"),
LOAN_PAYMENT("TransactionType.LOAN_PAYMENT.text", "TransactionType.LOAN_PAYMENT.toolTipText"),
LOAN_PRINCIPAL("TransactionType.LOAN_PRINCIPAL.text", "TransactionType.LOAN_PRINCIPAL.toolTipText"),
MAINTENANCE("TransactionType.MAINTENANCE.text", "TransactionType.MAINTENANCE.toolTipText"),
@@ -64,6 +65,7 @@ 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"),
+ UNIT_CANCEL_LEASE("TransactionType.UNIT_CANCEL_LEASE.text", "TransactionType.UNIT_CANCEL_LEASE.toolTipText"),
BONUS_EXCHANGE("TransactionType.BONUS_EXCHANGE.text", "TransactionType.BONUS_EXCHANGE.toolTipText"),
WEALTH("TransactionType.WEALTH.text", "TransactionType.WEALTH.toolTipText");
// endregion Enum Declarations
@@ -121,6 +123,10 @@ public boolean isFine() {
return this == FINE;
}
+ public boolean isLeasePayment() {
+ return this == LEASE_PAYMENT;
+ }
+
public boolean isLoanPayment() {
return this == LOAN_PAYMENT;
}
diff --git a/MekHQ/src/mekhq/campaign/market/ShoppingList.java b/MekHQ/src/mekhq/campaign/market/ShoppingList.java
index 7f3b37057b9..7fa649052bc 100644
--- a/MekHQ/src/mekhq/campaign/market/ShoppingList.java
+++ b/MekHQ/src/mekhq/campaign/market/ShoppingList.java
@@ -44,6 +44,7 @@
import mekhq.MekHQ;
import mekhq.campaign.Campaign;
import mekhq.campaign.event.ProcurementEvent;
+import mekhq.campaign.finances.LeaseOrder;
import mekhq.campaign.finances.Money;
import mekhq.campaign.parts.Part;
import mekhq.campaign.parts.Refit;
@@ -186,6 +187,8 @@ public void writeToXML(final PrintWriter pw, int indent) {
// when we parse units and find refit kits that have not been found
if ((shoppingItem instanceof Part) && !(shoppingItem instanceof Refit)) {
((Part) shoppingItem).writeToXML(pw, indent);
+ } else if (shoppingItem instanceof LeaseOrder) {
+ ((LeaseOrder) shoppingItem).writeToXML(pw, indent);
} else if (shoppingItem instanceof UnitOrder) {
((UnitOrder) shoppingItem).writeToXML(pw, indent);
}
@@ -193,25 +196,30 @@ public void writeToXML(final PrintWriter pw, int indent) {
MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, "shoppingList");
}
- public static ShoppingList generateInstanceFromXML(Node wn, Campaign c, Version version) {
- ShoppingList retVal = new ShoppingList();
+ public static ShoppingList generateInstanceFromXML(Node writerNode, Campaign campaign, Version version) {
+ ShoppingList savedShoppingList = new ShoppingList();
- NodeList nl = wn.getChildNodes();
+ NodeList childNodeList = writerNode.getChildNodes();
try {
- for (int x = 0; x < nl.getLength(); x++) {
- Node wn2 = nl.item(x);
- if (wn2.getNodeName().equalsIgnoreCase("part")) {
- Part p = Part.generateInstanceFromXML(wn2, version);
- p.setCampaign(c);
- if (p instanceof IAcquisitionWork) {
- retVal.getShoppingList().add((IAcquisitionWork) p);
+ for (int x = 0; x < childNodeList.getLength(); x++) {
+ Node childNode = childNodeList.item(x);
+ if (childNode.getNodeName().equalsIgnoreCase("part")) {
+ Part savedPart = Part.generateInstanceFromXML(childNode, version);
+ savedPart.setCampaign(campaign);
+ if (savedPart instanceof IAcquisitionWork) {
+ savedShoppingList.getShoppingList().add((IAcquisitionWork) savedPart);
}
- } else if (wn2.getNodeName().equalsIgnoreCase("unitOrder")) {
- UnitOrder u = UnitOrder.generateInstanceFromXML(wn2, c);
- u.setCampaign(c);
- if (u.getEntity() != null) {
- retVal.getShoppingList().add(u);
+ } else if (childNode.getNodeName().equalsIgnoreCase("unitOrder")) {
+ UnitOrder savedUnit = UnitOrder.generateInstanceFromXML(childNode, campaign);
+ savedUnit.setCampaign(campaign);
+ if (savedUnit.getEntity() != null) {
+ savedShoppingList.getShoppingList().add(savedUnit);
+ }
+ } else if (childNode.getNodeName().equalsIgnoreCase("leaseOrder")) {
+ LeaseOrder savedLeaseOrder = LeaseOrder.generateInstanceFromXML(childNode, campaign);
+ if (savedLeaseOrder.getEntity() != null) {
+ savedShoppingList.getShoppingList().add(savedLeaseOrder);
}
}
}
@@ -219,7 +227,7 @@ public static ShoppingList generateInstanceFromXML(Node wn, Campaign c, Version
logger.error("", ex);
}
- return retVal;
+ return savedShoppingList;
}
public void restore() {
diff --git a/MekHQ/src/mekhq/campaign/unit/Unit.java b/MekHQ/src/mekhq/campaign/unit/Unit.java
index 557aa667e41..c8474dc0d22 100644
--- a/MekHQ/src/mekhq/campaign/unit/Unit.java
+++ b/MekHQ/src/mekhq/campaign/unit/Unit.java
@@ -90,6 +90,7 @@
import mekhq.campaign.event.PersonCrewAssignmentEvent;
import mekhq.campaign.event.PersonTechAssignmentEvent;
import mekhq.campaign.event.UnitArrivedEvent;
+import mekhq.campaign.finances.Lease;
import mekhq.campaign.finances.Money;
import mekhq.campaign.force.Force;
import mekhq.campaign.log.AssignmentLogger;
@@ -196,6 +197,8 @@ public class Unit implements ITechnology {
private MothballInfo mothballInfo;
+ private Lease unitLease;
+
public Unit() {
this(null, null);
}
@@ -1391,8 +1394,11 @@ public boolean isDamaged() {
}
public Money getSellValue() {
- Money partsValue = Money.zero();
+ if (this.hasLease()) {
+ return Money.zero();
+ } // don't count leased vehicles
+ Money partsValue = Money.zero();
partsValue = partsValue.plus(parts.stream()
.map(x -> x.getActualValue().multipliedBy(x.getQuantity()))
.collect(Collectors.toList()));
@@ -2515,6 +2521,11 @@ public void writeToXML(final PrintWriter pw, int indent) {
}
// END new transports
+ //Leases
+ if (hasLease()) {
+ unitLease.writeToXML(pw, indent);
+ }
+
// Salvage status
if (salvaged) {
MHQXMLUtility.writeSimpleXMLTag(pw, indent, "salvaged", true);
@@ -2731,6 +2742,9 @@ public static Unit generateInstanceFromXML(final Node wn, final Version version,
retVal.lastMaintenanceReport = wn2.getTextContent();
} else if (wn2.getNodeName().equalsIgnoreCase("mothballInfo")) {
retVal.mothballInfo = MothballInfo.generateInstanceFromXML(wn2, version);
+ } else if (wn2.getNodeName().equalsIgnoreCase("lease")) {
+ // Leases
+ retVal.unitLease = Lease.generateInstanceFromXML(wn2, retVal);
}
// Set up bay space values after we've loaded everything from the unit record
// Used for older campaign
@@ -6882,4 +6896,22 @@ public static PartQuality getRandomUnitQuality(int modifier) {
"Unexpected value in mekhq/campaign/unit/Unit.java/getRandomUnitQuality: " + roll);
};
}
+
+ public void addLease(Lease lease) {
+ unitLease = lease;
+ }
+
+ public void removeLease() {
+ unitLease = null;
+ }
+
+ public boolean hasLease() {
+ return (unitLease != null);
+ }
+
+ @Nullable
+ public Lease getUnitLease() {
+ return unitLease;
+ }
+
}
diff --git a/MekHQ/src/mekhq/campaign/unit/UnitOrder.java b/MekHQ/src/mekhq/campaign/unit/UnitOrder.java
index a6de9d8928f..73b23db0f0f 100644
--- a/MekHQ/src/mekhq/campaign/unit/UnitOrder.java
+++ b/MekHQ/src/mekhq/campaign/unit/UnitOrder.java
@@ -35,21 +35,19 @@
import java.io.PrintWriter;
-import mekhq.utilities.ReportingUtilities;
-import org.w3c.dom.Element;
-import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
-
import megamek.common.*;
import megamek.common.loaders.EntityLoadingException;
import megamek.logging.MMLogger;
-import mekhq.MekHQ;
import mekhq.campaign.Campaign;
import mekhq.campaign.parts.Availability;
import mekhq.campaign.parts.Part;
import mekhq.campaign.personnel.Person;
import mekhq.campaign.work.IAcquisitionWork;
import mekhq.utilities.MHQXMLUtility;
+import mekhq.utilities.ReportingUtilities;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
/**
* We use an extension of unit to create a unit order acquisition work
@@ -59,8 +57,8 @@
public class UnitOrder extends Unit implements IAcquisitionWork {
private static final MMLogger logger = MMLogger.create(UnitOrder.class);
- int quantity;
- int daysToWait;
+ protected int quantity;
+ protected int daysToWait;
public UnitOrder() {
super(null, null);
@@ -120,6 +118,7 @@ public String getAcquisitionName() {
/**
* @param quantity - the number of parts of this type
+ *
* @return a string that gives a grammatical correct name based on the quantity
*/
@Override
@@ -199,18 +198,47 @@ public void decrementDaysToWait() {
public String find(int transitDays) {
// TODO: probably get a duplicate entity
if (getCampaign().getQuartermaster().buyUnit((Entity) getNewEquipment(), transitDays)) {
- return " unit found. It will be delivered in " + transitDays + " days.";
+ return " unit found. It will be delivered in " +
+ transitDays +
+ " days.";
} else {
- return " You cannot afford this unit. Transaction cancelled.";
+ return " You cannot afford this unit. Transaction cancelled.";
}
}
@Override
public String failToFind() {
- return " unit not found.";
+ return " unit not found.";
+ }
+
+ private int getDropshipEraAcquisitionModifier(int year) {
+ int dropshipModifier = 0;
+ if (year > 3084) {
+ dropshipModifier = 0;
+ } else if (year > 3068) {
+ dropshipModifier = -2;
+ } else if (year > 3050) {
+ dropshipModifier = -1;
+ } else if (year > 2901) {
+ dropshipModifier = +0;
+ } else if (year > 2821) {
+ dropshipModifier = -2;
+ } else if (year > 2751) {
+ dropshipModifier = -6;
+ } else if (year > 2651) {
+ dropshipModifier = -6;
+ } else if (year > 2571) {
+ dropshipModifier = -5;
+ } else if (year > 2412) {
+ dropshipModifier = -4;
+ } else {
+ dropshipModifier = -3;
+ }
+ return dropshipModifier;
}
@Override
@@ -225,7 +253,45 @@ public TargetRoll getAllAcquisitionMods() {
target.addModifier(getCampaign().getCampaignOptions().getIsAcquisitionPenalty(), "Inner Sphere tech");
}
// TODO: Fix weight classes
- // TODO: aero large craft
+ if (entity instanceof Dropship || entity instanceof Jumpship) {
+ //FIXME ?
+ /* In theory, these should be the base target numbers of these vehicles. The base TNs in the book are
+ * static and don't adjust for Admin skill or anything. Instead, we're going to just kind of assume
+ * a base TN of 6 and adjust for windage.
+ *
+ */
+ if (entity instanceof SpaceStation) {
+ target.addModifier((int) Math.ceil(entity.getCost(false) / 50000000) + 5 - 6, "Spacestation Base");
+ } else if (entity instanceof Warship) {
+ double collars = ((Warship) entity).getDockingCollars().size();
+ target.addModifier((int) (Math.ceil(Math.sqrt(entity.getWeight() / 5000)) +
+ Math.ceil(Math.sqrt(collars))) - 6, "Warship Base");
+ } else if (entity instanceof Dropship) {
+ target.addModifier((int) Math.ceil(entity.getCost(false) / 50000000) + 5 - 6, "Dropship Base");
+ } else {
+ int collars = ((Jumpship) entity).getDockingCollars().size();
+ target.addModifier((int) Math.ceil(entity.getCost(false) / 100000000) + collars - 6, "Jumpship Base");
+ }
+
+ // Since the actual rules are ambiguous, we're ignoring 'uniqueness' until someone can come up with a table
+ // counting how many dropships/jumpships of which types exist in each particular era.
+
+ // Other Misc Mods
+ if (getCampaign().isClanCampaign()) {
+ target.addModifier(-4, "Clan Force");
+ } else if (getCampaign().getFaction().isGovernment()) {
+ target.addModifier(-2, "Government");
+ }
+ if (entity.isMilitary()) {
+ target.addModifier(1, "Military Vessel");
+ }
+ //Don't believe anyone recorded which faction manufacturers which dropship/jumpship.
+
+ //Eras
+ int year = getCampaign().getGameYear();
+ target.addModifier(getDropshipEraAcquisitionModifier(year), "Era");
+ return target;
+ }
// TODO: support vehicles
// see EntityWeightClass.java in megamek for weight classes
if (entity instanceof Mek) {
@@ -321,8 +387,9 @@ public TargetRoll getAllAcquisitionMods() {
@Override
public AvailabilityValue getAvailability() {
- return calcYearAvailability(getCampaign().getGameYear(), getCampaign().useClanTechBase(),
- getCampaign().getTechFaction());
+ return calcYearAvailability(getCampaign().getGameYear(),
+ getCampaign().useClanTechBase(),
+ getCampaign().getTechFaction());
}
@Override
diff --git a/MekHQ/src/mekhq/gui/adapter/ProcurementTableMouseAdapter.java b/MekHQ/src/mekhq/gui/adapter/ProcurementTableMouseAdapter.java
index 6ef2d07e9f7..55607c20a20 100644
--- a/MekHQ/src/mekhq/gui/adapter/ProcurementTableMouseAdapter.java
+++ b/MekHQ/src/mekhq/gui/adapter/ProcurementTableMouseAdapter.java
@@ -45,9 +45,11 @@
import megamek.logging.MMLogger;
import mekhq.MekHQ;
import mekhq.campaign.event.ProcurementEvent;
+import mekhq.campaign.finances.LeaseOrder;
import mekhq.campaign.parts.Part;
import mekhq.campaign.parts.enums.PartQuality;
import mekhq.campaign.unit.Unit;
+import mekhq.campaign.unit.UnitOrder;
import mekhq.campaign.work.IAcquisitionWork;
import mekhq.gui.CampaignGUI;
import mekhq.gui.model.ProcurementTableModel;
@@ -176,8 +178,20 @@ protected Optional createPopupMenu() {
return Optional.of(popup);
}
+ private boolean tryProcureEntity(IAcquisitionWork acquisition, int transitTime) {
+ boolean success;
+ final Object equipment = acquisition.getNewEquipment();
+ if (acquisition instanceof LeaseOrder) {
+ success = gui.getCampaign().getQuartermaster().createLeasedUnit((Entity) equipment, transitTime);
+ } else {
+ success = gui.getCampaign().getQuartermaster().buyUnit((Entity) equipment, transitTime);
+ }
+ return success;
+ }
+
/**
* @param acquisition the
+ *
* @return whether the procurement attempt succeeded or not
*/
private boolean tryProcureOneItem(final IAcquisitionWork acquisition) {
@@ -190,8 +204,8 @@ private boolean tryProcureOneItem(final IAcquisitionWork acquisition) {
final boolean success;
if (equipment instanceof Part) {
success = gui.getCampaign().getQuartermaster().buyPart((Part) equipment, transitTime);
- } else if (equipment instanceof Entity) {
- success = gui.getCampaign().getQuartermaster().buyUnit((Entity) equipment, transitTime);
+ } else if (acquisition instanceof UnitOrder) {
+ success = tryProcureEntity((UnitOrder) acquisition, transitTime);
} else {
logger.error("Attempted to acquire unknown equipment of {}", acquisition.getAcquisitionName());
return false;
@@ -207,14 +221,14 @@ private boolean tryProcureOneItem(final IAcquisitionWork acquisition) {
+ String.format(
resources.getString("ProcurementTableMouseAdapter.CannotAffordToPurchaseItem.report")
+ "",
- acquisition.getAcquisitionName()));
+ acquisition.getAcquisitionName()));
}
return success;
}
/**
- * Processes the acquisition of a single item, adding it to the campaign as either a part or a unit,
- * or logging an error if the acquisition type is unrecognized.
+ * Processes the acquisition of a single item, adding it to the campaign as either a part or a unit, or logging an
+ * error if the acquisition type is unrecognized.
*
* The method performs the following steps:
*
@@ -234,6 +248,7 @@ private boolean tryProcureOneItem(final IAcquisitionWork acquisition) {
*
*
* @param acquisition The acquisition work containing the item and metadata to process. Must not be {@code null}.
+ *
* @throws NullPointerException If {@code acquisition} is {@code null}.
*/
private void addOneItem(final IAcquisitionWork acquisition) {
diff --git a/MekHQ/src/mekhq/gui/adapter/UnitTableMouseAdapter.java b/MekHQ/src/mekhq/gui/adapter/UnitTableMouseAdapter.java
index b24592efb77..76cd65bd00f 100644
--- a/MekHQ/src/mekhq/gui/adapter/UnitTableMouseAdapter.java
+++ b/MekHQ/src/mekhq/gui/adapter/UnitTableMouseAdapter.java
@@ -151,6 +151,7 @@ public class UnitTableMouseAdapter extends JPopupMenuAdapter {
public static final String COMMAND_HIRE_FULL = "HIRE_FULL";
public static final String COMMAND_DISBAND = "DISBAND";
public static final String COMMAND_SELL = "SELL";
+ public static final String COMMAND_UNLEASE = "CANCEL_LEASE";
public static final String COMMAND_LOSS = "LOSS";
public static final String COMMAND_MAINTENANCE_REPORT = "MAINTENANCE_REPORT";
public static final String COMMAND_QUIRKS = "QUIRKS";
@@ -279,6 +280,20 @@ public void actionPerformed(ActionEvent action) {
}
}
}
+ } else if (command.equals(COMMAND_UNLEASE)) {
+ for (Unit unit : units) {
+ if (!unit.isDeployed() && unit.hasLease()) {
+ Money unleaseValue = unit.getUnitLease().getFinalLeaseCost(unit.getCampaign().getLocalDate());
+ String text = unleaseValue.toAmountAndSymbolString();
+ if (0 ==
+ JOptionPane.showConfirmDialog(null,
+ "Do you really want to cancel the lease on " + unit.getName() + " for " + text,
+ "Cancel Unit Lease?",
+ JOptionPane.YES_NO_OPTION)) {
+ gui.getCampaign().getQuartermaster().cancelUnitLease(unit);
+ }
+ }
+ }
} else if (command.equals(COMMAND_LOSS)) {
for (Unit unit : units) {
if (0 ==
@@ -1109,7 +1124,7 @@ protected Optional createPopupMenu() {
}
// sell unit
- if (!allDeployed && gui.getCampaign().getCampaignOptions().isSellUnits()) {
+ if (!allDeployed && gui.getCampaign().getCampaignOptions().isSellUnits() && !unit.hasLease()) {
popup.addSeparator();
menuItem = new JMenuItem("Sell Unit");
menuItem.setActionCommand(COMMAND_SELL);
@@ -1117,6 +1132,15 @@ protected Optional createPopupMenu() {
popup.add(menuItem);
}
+ // cancel lease
+ if (!allDeployed && gui.getCampaign().getCampaignOptions().isSellUnits() && unit.hasLease()) {
+ popup.addSeparator();
+ menuItem = new JMenuItem("Cancel Lease");
+ menuItem.setActionCommand(COMMAND_UNLEASE);
+ menuItem.addActionListener(this);
+ popup.add(menuItem);
+ }
+
// region GM Mode
// GM mode - only show to GMs
if (isGM) {
diff --git a/MekHQ/src/mekhq/gui/campaignOptions/contents/FinancesTab.java b/MekHQ/src/mekhq/gui/campaignOptions/contents/FinancesTab.java
index 1009e844ff5..9fc5e93ab54 100644
--- a/MekHQ/src/mekhq/gui/campaignOptions/contents/FinancesTab.java
+++ b/MekHQ/src/mekhq/gui/campaignOptions/contents/FinancesTab.java
@@ -75,6 +75,7 @@ public class FinancesTab {
private CampaignOptionsHeaderPanel financesGeneralOptions;
private JPanel pnlGeneralOptions;
private JCheckBox useLoanLimitsBox;
+ private JCheckBox trackLeasesBox;
private JCheckBox usePercentageMaintenanceBox;
private JCheckBox useExtendedPartsModifierBox;
private JCheckBox usePeacetimeCostBox;
@@ -181,6 +182,7 @@ private void initializeGeneralOptionsTab() {
// General Options
pnlGeneralOptions = new JPanel();
useLoanLimitsBox = new JCheckBox();
+ trackLeasesBox = new JCheckBox();
usePercentageMaintenanceBox = new JCheckBox();
useExtendedPartsModifierBox = new JCheckBox();
usePeacetimeCostBox = new JCheckBox();
@@ -236,7 +238,8 @@ private void initializeGeneralOptionsTab() {
public JPanel createFinancesGeneralOptionsTab() {
// Header
financesGeneralOptions = new CampaignOptionsHeaderPanel("FinancesGeneralTab",
- getImageDirectory() + "logo_star_league.png", 6);
+ getImageDirectory() + "logo_star_league.png",
+ 6);
// Contents
pnlGeneralOptions = createGeneralOptionsPanel();
@@ -385,6 +388,8 @@ private JPanel createGeneralOptionsPanel() {
// Contents
useLoanLimitsBox = new CampaignOptionsCheckBox("UseLoanLimitsBox");
useLoanLimitsBox.addMouseListener(createTipPanelUpdater(financesGeneralOptions, "UseLoanLimitsBox"));
+ trackLeasesBox = new CampaignOptionsCheckBox("TrackLeasesBox");
+ trackLeasesBox.addMouseListener(createTipPanelUpdater(financesGeneralOptions, "UseTrackLeasesBox"));
usePercentageMaintenanceBox = new CampaignOptionsCheckBox("UsePercentageMaintenanceBox");
usePercentageMaintenanceBox.addMouseListener(createTipPanelUpdater(financesGeneralOptions,
"UsePercentageMaintenanceBox"));
@@ -421,6 +426,9 @@ private JPanel createGeneralOptionsPanel() {
layout.gridwidth = 2;
panel.add(useLoanLimitsBox, layout);
+ layout.gridy++;
+ panel.add(trackLeasesBox, layout);
+
layout.gridy++;
panel.add(usePercentageMaintenanceBox, layout);
@@ -592,7 +600,10 @@ private void initializePriceMultipliersTab() {
public JPanel createPriceMultipliersTab() {
// Header
priceMultipliersHeader = new CampaignOptionsHeaderPanel("PriceMultipliersTab",
- getImageDirectory() + "logo_clan_stone_lion.png", true, true, 2);
+ getImageDirectory() + "logo_clan_stone_lion.png",
+ true,
+ true,
+ 2);
// Contents
pnlGeneralMultipliers = createGeneralMultipliersPanel();
@@ -857,6 +868,7 @@ public void applyCampaignOptionsToCampaign(@Nullable CampaignOptions presetCampa
// General Options
options.setLoanLimits(useLoanLimitsBox.isSelected());
+ options.setTrackLeases(trackLeasesBox.isSelected());
options.setUsePercentageMaint(usePercentageMaintenanceBox.isSelected());
options.setUseExtendedPartsModifier(useExtendedPartsModifierBox.isSelected());
options.setUsePeacetimeCost(usePeacetimeCostBox.isSelected());
@@ -925,6 +937,7 @@ public void loadValuesFromCampaignOptions(@Nullable CampaignOptions presetCampai
// General Options
useLoanLimitsBox.setSelected(options.isUseLoanLimits());
+ trackLeasesBox.setSelected(options.isTrackLeases());
usePercentageMaintenanceBox.setSelected(options.isUsePercentageMaint());
useExtendedPartsModifierBox.setSelected(options.isUseExtendedPartsModifier());
usePeacetimeCostBox.setSelected(options.isUsePeacetimeCost());
diff --git a/MekHQ/src/mekhq/gui/dialog/MekHQUnitSelectorDialog.java b/MekHQ/src/mekhq/gui/dialog/MekHQUnitSelectorDialog.java
index 0c41957b385..20b78cb3607 100644
--- a/MekHQ/src/mekhq/gui/dialog/MekHQUnitSelectorDialog.java
+++ b/MekHQ/src/mekhq/gui/dialog/MekHQUnitSelectorDialog.java
@@ -49,8 +49,8 @@
import javax.swing.RowFilter;
import megamek.client.ui.Messages;
-import megamek.client.ui.dialogs.advancedsearch.MekSearchFilter;
import megamek.client.ui.dialogs.UnitLoadingDialog;
+import megamek.client.ui.dialogs.advancedsearch.MekSearchFilter;
import megamek.client.ui.dialogs.unitSelectorDialogs.AbstractUnitSelectorDialog;
import megamek.common.Entity;
import megamek.common.EntityWeightClass;
@@ -62,7 +62,10 @@
import megamek.common.annotations.Nullable;
import mekhq.MekHQ;
import mekhq.campaign.Campaign;
+import mekhq.campaign.finances.Lease;
+import mekhq.campaign.finances.LeaseOrder;
import mekhq.campaign.parts.enums.PartQuality;
+import mekhq.campaign.unit.Unit;
import mekhq.campaign.unit.UnitOrder;
import mekhq.campaign.unit.UnitTechProgression;
import mekhq.utilities.MHQInternationalization;
@@ -75,6 +78,8 @@ public class MekHQUnitSelectorDialog extends AbstractUnitSelectorDialog {
private JButton buttonBuy;
private JButton buttonAddGM;
+ protected JButton buttonLease;
+ protected JButton buttonLeaseGM;
private static final String TARGET_UNKNOWN = "--";
@@ -118,6 +123,30 @@ public void updateOptionValues() {
}
}
+ public void leaseButtonHandler(boolean isGM) {
+ // Block acquisition if the unit type is unsupported
+ if (getSelectedEntity() == null) {
+ return;
+ }
+ Entity entity = selectedUnit.getEntity();
+ // Remove this check if we ever allow other units to be leased.
+ if (!(Lease.isLeasable(entity))) {
+ return; //TODO: Add a dialog box for why it doesn't work
+ }
+
+ if (isGM) {
+ PartQuality quality = PartQuality.QUALITY_D;
+ if (campaign.getCampaignOptions().isUseRandomUnitQualities()) {
+ quality = UnitOrder.getRandomUnitQuality(0);
+ }
+ Unit unit = campaign.addNewUnit(selectedUnit.getEntity(), false, 0, quality);
+ unit.addLease(new Lease(campaign.getLocalDate(), unit));
+ } else {
+ LeaseOrder thisLease = new LeaseOrder(entity, campaign);
+ campaign.getShoppingList().addShoppingItem(thisLease, 1, campaign);
+ }
+ }
+
/**
* This is the initialization function for all the buttons involved in this panel.
*/
@@ -131,6 +160,8 @@ protected JPanel createButtonsPanel() {
buttonBuy = new JButton();
buttonAddGM = new JButton();
buttonShowBV = new JButton();
+ buttonLease = new JButton();
+ buttonLeaseGM = new JButton();
if (addToCampaign) {
//This branch is for purchases and adding to the hanger directly.
@@ -140,12 +171,28 @@ protected JPanel createButtonsPanel() {
buttonBuy.setEnabled(false);
panelButtons.add(buttonBuy, new GridBagConstraints());
+ if (campaign.getCampaignOptions().isTrackLeases()) {
+ buttonLease.setText(Messages.getString("MekSelectorDialog.Lease", TARGET_UNKNOWN));
+ buttonLease.setName("buttonLease");
+ buttonLease.addActionListener(e -> leaseButtonHandler(false));
+ panelButtons.add(buttonLease, new GridBagConstraints());
+ buttonLease.setEnabled(false);
+ }
+
if (campaign.isGM()) {
buttonAddGM.setText(Messages.getString("MekSelectorDialog.AddGM"));
buttonAddGM.setName("buttonAddGM");
buttonAddGM.addActionListener(evt -> addGM());
buttonAddGM.setEnabled(false);
+
panelButtons.add(buttonAddGM, new GridBagConstraints());
+ if (campaign.getCampaignOptions().isTrackLeases()) {
+ buttonLeaseGM.setText(Messages.getString("MekSelectorDialog.LeaseGM", TARGET_UNKNOWN));
+ buttonLeaseGM.setName("buttonLeaseGM");
+ buttonLeaseGM.addActionListener(e -> leaseButtonHandler(true));
+ panelButtons.add(buttonLeaseGM, new GridBagConstraints());
+ buttonLeaseGM.setEnabled(false);
+ }
}
buttonClose = new JButton(Messages.getString("Close"));
buttonClose.setName("buttonClose");
@@ -267,6 +314,14 @@ public Entity getSelectedEntity() {
buttonBuy.setText(Messages.getString("MekSelectorDialog.Buy", TARGET_UNKNOWN));
buttonBuy.setToolTipText(null);
buttonAddGM.setEnabled(false);
+ if (campaign.getCampaignOptions().isTrackLeases()) {
+ buttonLease.setEnabled(false);
+ buttonLease.setText(Messages.getString("MekSelectorDialog.Lease", TARGET_UNKNOWN));
+ buttonLease.setToolTipText(null);
+ buttonLeaseGM.setEnabled(false);
+ buttonLeaseGM.setText(Messages.getString("MekSelectorDialog.LeaseGM", TARGET_UNKNOWN));
+ buttonLeaseGM.setToolTipText(null);
+ }
}
} else {
selectedUnit = new UnitOrder(entity, campaign);
@@ -277,6 +332,14 @@ public Entity getSelectedEntity() {
buttonBuy.setText(Messages.getString("MekSelectorDialog.Buy", target.getValueAsString()));
buttonBuy.setToolTipText(target.getDesc());
buttonAddGM.setEnabled(true);
+ if (campaign.getCampaignOptions().isTrackLeases() && (Lease.isLeasable(entity))) {
+ buttonLease.setEnabled(true);
+ buttonLeaseGM.setEnabled(true);
+ buttonLease.setText(Messages.getString("MekSelectorDialog.Lease", target.getValueAsString()));
+ buttonLeaseGM.setText(Messages.getString("MekSelectorDialog.LeaseGM", target.getValueAsString()));
+ buttonLease.setToolTipText(target.getDesc());
+ buttonLeaseGM.setToolTipText(target.getDesc());
+ }
}
}
@@ -299,17 +362,17 @@ protected Entity refreshUnitView() {
* This function is to simplify logic in filterUnits. It runs a series of checks to determine if a unit is valid
* within the current filtering context.
*
- * @param unitSummary The unit being evaluated.
+ * @param unitSummary The unit being evaluated.
* @param weightClassSelectorIndex The current weight class selection
- * @param tech The current tech selection
- * @param techLevelMatch whether the current tech selection matches
- * @param checkSupportVee Whether the special 'Support Vehicle' unit type was selected
- * @param unitTypeSelectorIndex Which unit type is currently selected (Depends on the combo box order!)
+ * @param tech The current tech selection
+ * @param techLevelMatch whether the current tech selection matches
+ * @param checkSupportVee Whether the special 'Support Vehicle' unit type was selected
+ * @param unitTypeSelectorIndex Which unit type is currently selected (Depends on the combo box order!)
*
* @return true if the unit passes all filters and allowed, false otherwise
*/
- private boolean isAllowedUnit(MekSummary unitSummary, int weightClassSelectorIndex, ITechnology tech, boolean techLevelMatch,
- boolean checkSupportVee, int unitTypeSelectorIndex) {
+ private boolean isAllowedUnit(MekSummary unitSummary, int weightClassSelectorIndex, ITechnology tech,
+ boolean techLevelMatch, boolean checkSupportVee, int unitTypeSelectorIndex) {
if (enableYearLimits && (unitSummary.getYear() > allowedYear)) {
return false;
}
@@ -322,7 +385,8 @@ private boolean isAllowedUnit(MekSummary unitSummary, int weightClassSelectorInd
if (canonOnly && !unitSummary.isCanon()) {
return false;
}
- if ((weightClassSelectorIndex != unitSummary.getWeightClass()) && weightClassSelectorIndex != EntityWeightClass.SIZE) {
+ if ((weightClassSelectorIndex != unitSummary.getWeightClass()) &&
+ weightClassSelectorIndex != EntityWeightClass.SIZE) {
return false;
}
if ((tech == null) || !campaign.isLegal(tech)) {
@@ -386,7 +450,12 @@ public boolean include(Entry extends MekTableModel, ? extends Integer> entry)
break;
}
}
- return isAllowedUnit(mek, weightClassSelectorIndex, tech, techLevelMatch, checkSupportVee, unitTypeSelectorIndex);
+ return isAllowedUnit(mek,
+ weightClassSelectorIndex,
+ tech,
+ techLevelMatch,
+ checkSupportVee,
+ unitTypeSelectorIndex);
}
};
} catch (PatternSyntaxException ignored) {
diff --git a/MekHQ/src/mekhq/gui/view/UnitViewPanel.java b/MekHQ/src/mekhq/gui/view/UnitViewPanel.java
index 07ef2e122c4..3e5d3445451 100644
--- a/MekHQ/src/mekhq/gui/view/UnitViewPanel.java
+++ b/MekHQ/src/mekhq/gui/view/UnitViewPanel.java
@@ -59,7 +59,8 @@
/**
* A custom panel that gets filled in with goodies from a unit record
- * @author Jay Lawson (jaylawson39 at yahoo.com)
+ *
+ * @author Jay Lawson (jaylawson39 at yahoo.com)
*/
public class UnitViewPanel extends JScrollablePanel {
private Unit unit;
@@ -98,7 +99,7 @@ private void initComponents() {
pnlStats = new JPanel();
final ResourceBundle resourceMap = ResourceBundle.getBundle("mekhq.resources.UnitViewPanel",
- MekHQ.getMHQOptions().getLocale());
+ MekHQ.getMHQOptions().getLocale());
setLayout(new GridBagLayout());
@@ -106,7 +107,7 @@ private void initComponents() {
Image image = FluffImageHelper.getFluffImage(entity);
if (null != image) {
// fluff image exists so use custom ImgLabel to get full mek porn
- lblImage = new ImgLabel(image);
+ lblImage = new ImgLabel(image);
gridBagConstraints = new GridBagConstraints();
gridBagConstraints.gridx = 1;
gridBagConstraints.gridy = 0;
@@ -154,7 +155,11 @@ private void initComponents() {
txtReadout.setContentType(resourceMap.getString("txtReadout.contentType"));
txtReadout.setEditable(false);
txtReadout.setFont(Font.decode(resourceMap.getString("txtReadout.font")));
- txtReadout.setText("" + mview.getMekReadoutBasic() + "
" + mview.getMekReadoutLoadout() + "
");
+ txtReadout.setText("" +
+ mview.getMekReadoutBasic() +
+ "
" +
+ mview.getMekReadoutLoadout() +
+ "
");
txtReadout.setBorder(RoundedLineBorder.createRoundedLineBorder("Technical Readout"));
gridBagConstraints = new GridBagConstraints();
gridBagConstraints.gridx = 0;
@@ -291,7 +296,13 @@ private void fillStats(ResourceBundle resourceMap) {
pnlStats.add(lblCost, gridBagConstraints);
txtCost.setName("lblCost2");
- txtCost.setText(unit.getSellValue().toAmountAndSymbolString());
+ if (unit.hasLease()) {
+ txtCost.setText(unit.getUnitLease().getLeaseCost().toAmountAndSymbolString() +
+ " " +
+ resourceMap.getString("lblLease.text"));
+ } else {
+ txtCost.setText(unit.getSellValue().toAmountAndSymbolString());
+ }
gridBagConstraints = new GridBagConstraints();
gridBagConstraints.gridx = 1;
gridBagConstraints.gridy = 4;
diff --git a/MekHQ/unittests/mekhq/campaign/finances/LeaseTest.java b/MekHQ/unittests/mekhq/campaign/finances/LeaseTest.java
new file mode 100644
index 00000000000..66e5e7b6527
--- /dev/null
+++ b/MekHQ/unittests/mekhq/campaign/finances/LeaseTest.java
@@ -0,0 +1,188 @@
+/*
+ * 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 org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+
+import java.time.LocalDate;
+
+import megamek.common.Dropship;
+import megamek.common.Mek;
+import mekhq.campaign.unit.Unit;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+public class LeaseTest {
+
+ Unit mockUnit;
+
+ //Arrange
+ @BeforeEach
+ void beforeEach() {
+ Unit unit = new Unit();
+ mockUnit = Mockito.spy(unit);
+ Mockito.doReturn(Money.of(600000.00)).when(mockUnit).getSellValue();
+
+ Lease testLease = new Lease(LocalDate.parse("3025-03-31"), mockUnit);
+ mockUnit.addLease(testLease);
+ }
+
+ @Test
+ void getLeaseCost_whenSetUp_hasValidBaseLeaseCost() {
+ //Act
+ Money testCost = mockUnit.getUnitLease().getLeaseCost();
+
+ //Assert
+ assertEquals(Money.of(3000), testCost);
+ }
+
+ @Test
+ void getLeaseCostNow_whenFullMonthCost_calculatesBaseLeaseCostCorrectly() {
+ //Arrange
+ LocalDate testDate = LocalDate.parse("3025-05-01");
+
+ //Act
+ Money testCost = mockUnit.getUnitLease().getLeaseCostNow(testDate);
+
+ //Assert
+ assertEquals(Money.of(3000), testCost);
+ }
+
+ @Test
+ void getLeaseCostNow_whenLeaseHasNotStarted_equalsZeroCost() {
+ //Arrange
+ LocalDate testDate = LocalDate.parse("3025-03-01");
+
+ //Act
+ Money testCost = mockUnit.getUnitLease().getLeaseCostNow(testDate);
+
+ //Assert
+ assertEquals(Money.zero(), testCost);
+ }
+
+ @Test
+ void getLeaseCostNow_whenNotThe1st_returnsNull() {
+ //Arrange
+ LocalDate testDate = LocalDate.parse("3025-03-02");
+
+ //Act
+ Money testCost = mockUnit.getUnitLease().getLeaseCostNow(testDate);
+
+ //Assert
+ assertNull(testCost);
+ }
+
+ @Test
+ void getFirstLeaseCost_whenInFirstMonth_shouldGivePartialLeaseAmount() {
+ LocalDate date = LocalDate.parse("3025-04-01");
+
+ //Act
+ Money testCost = mockUnit.getUnitLease().getFirstLeaseCost(date);
+
+ //Assert
+ assertEquals(Math.round(Money.of(3000.00 / 31).getAmount().doubleValue()),
+ Math.round(testCost.getAmount().doubleValue()),
+ "This should be a single day's lease");
+ }
+
+ @Test
+ void getFirstLeaseCost_whenNotThe1st_returnsNull() {
+ LocalDate date = LocalDate.parse("3025-04-02");
+
+ //Act
+ Money testCost = mockUnit.getUnitLease().getFirstLeaseCost(date);
+
+ //Assert
+ assertNull(testCost);
+ }
+
+ @Test
+ void getLeaseStart_whenInstantiated_shouldReturnStartDate() {
+ //Arrange
+ LocalDate testDate = LocalDate.parse("3025-03-31");
+
+ //Act
+ LocalDate date = mockUnit.getUnitLease().getLeaseStart();
+
+ //Assert
+ assertEquals(date, testDate);
+ }
+
+ @Test
+ void getFinalLeaseCost_whenCalled_shouldProrateLeaseCostUsedInMonth() {
+ //Arrange
+ LocalDate testDate = LocalDate.parse("3025-06-05");
+
+ //Act
+ Money testLeaseCost = mockUnit.getUnitLease().getFinalLeaseCost(testDate);
+
+ // 5 days of lease cost in June - rounded, because doubles.
+ double testCost = Math.round(testLeaseCost.getAmount().doubleValue());
+ double cost = Math.round(Money.of(5 * 3000.00 / 30).getAmount().doubleValue());
+
+ //Assert
+ assertEquals(cost, testCost);
+ }
+
+ @Test
+ void isLeaseable_whenUnitIsDropship_shouldReturnTrue() {
+ //Arrange
+ Dropship mockDropship = mock(Dropship.class);
+ Mockito.doReturn(true).when(mockDropship).isDropShip();
+
+ //Act
+ boolean returnValue = Lease.isLeasable(mockDropship);
+
+ //Assert
+ assertTrue(returnValue);
+ }
+
+ @Test
+ void isLeaseable_whenUnitIsMek_shouldReturnFalse() {
+ //Arrange
+ Mek mockMek = mock(Mek.class);
+ Mockito.doReturn(false).when(mockMek).isDropShip();
+
+ //Act
+ boolean returnValue = Lease.isLeasable(mockMek);
+
+ //Assert
+ assertFalse(returnValue);
+ }
+}