diff --git a/MekHQ/resources/mekhq/resources/Resupply.properties b/MekHQ/resources/mekhq/resources/Resupply.properties index 54119a3a15..16c58f407c 100644 --- a/MekHQ/resources/mekhq/resources/Resupply.properties +++ b/MekHQ/resources/mekhq/resources/Resupply.properties @@ -67,8 +67,8 @@ usePlayerConvoyOptional.text=%s, our employer has a delivery ready for us but is
\
This enhanced delivery requires an estimated %s tons of cargo space across all\ \ convoys. However, as this is an estimate final tonnage may vary. We currently have a total of\ - \ %s available space across %s convoy%s. Damaged or partially vehicles are not\ - \ considered available.\ + \ %s available space across %s convoy%s. Damaged or partially crewed vehicles are\ + \ not considered available.\
\
Be aware that this can be a risky job and if we fail to defend any intercepted convoys all\ \ units and personnel will be lost.\ @@ -81,7 +81,7 @@ usePlayerConvoyForced.text=%s, our employer has a delivery ready for us, but we
\
This delivery requires an estimated %s tons of cargo space across all convoys. However,\ \ as this is an estimate final tonnage may vary. We currently have a total of %s available\ - \ space across %s convoy%s. Damaged or partially vehicles are not considered available.\ + \ space across %s convoy%s. Damaged or partially crewed vehicles are not considered available.\
\
Be aware that this can be a risky job and if we fail to defend any intercepted convoys all\ \ units and personnel will be lost.\ diff --git a/MekHQ/src/mekhq/campaign/market/procurement/Procurement.java b/MekHQ/src/mekhq/campaign/market/procurement/Procurement.java index 92533651e7..de97c58c16 100644 --- a/MekHQ/src/mekhq/campaign/market/procurement/Procurement.java +++ b/MekHQ/src/mekhq/campaign/market/procurement/Procurement.java @@ -112,8 +112,6 @@ public List makeProcurementChecks(List parts, boolean useHardExtinct } } - logger.info(successfulParts); - return successfulParts; } diff --git a/MekHQ/src/mekhq/campaign/mission/Loot.java b/MekHQ/src/mekhq/campaign/mission/Loot.java index 242025bcd9..8dd7a6efe7 100644 --- a/MekHQ/src/mekhq/campaign/mission/Loot.java +++ b/MekHQ/src/mekhq/campaign/mission/Loot.java @@ -33,8 +33,10 @@ import mekhq.campaign.finances.Money; import mekhq.campaign.finances.enums.TransactionType; import mekhq.campaign.mission.enums.ScenarioType; +import mekhq.campaign.parts.Armor; import mekhq.campaign.parts.Part; import mekhq.campaign.parts.enums.PartQuality; +import mekhq.campaign.parts.equipment.AmmoBin; import mekhq.campaign.rating.IUnitRating; import mekhq.campaign.unit.Unit; import mekhq.utilities.MHQXMLUtility; @@ -45,6 +47,8 @@ import java.util.*; import static mekhq.campaign.mission.resupplyAndCaches.GenerateResupplyContents.RESUPPLY_MINIMUM_PART_WEIGHT; +import static mekhq.campaign.mission.resupplyAndCaches.Resupply.RESUPPLY_AMMO_TONNAGE; +import static mekhq.campaign.mission.resupplyAndCaches.Resupply.RESUPPLY_ARMOR_TONNAGE; import static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG; import static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor; @@ -214,6 +218,12 @@ public void getLoot(Campaign campaign, Scenario scenario, double partWeight = part.getTonnage(); partWeight = partWeight == 0 ? RESUPPLY_MINIMUM_PART_WEIGHT : partWeight; + if (part instanceof AmmoBin) { + partWeight = RESUPPLY_AMMO_TONNAGE; + } else if (part instanceof Armor) { + partWeight = RESUPPLY_ARMOR_TONNAGE; + } + if (isResupply) { if (cargo - partWeight < 0) { abandonedParts.add("
- " + part.getName() + " (" + partWeight + " tons)"); diff --git a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/GenerateResupplyContents.java b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/GenerateResupplyContents.java index 26ba01902f..13b1cbab33 100644 --- a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/GenerateResupplyContents.java +++ b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/GenerateResupplyContents.java @@ -76,10 +76,6 @@ public enum DropType { * @param usePlayerConvoys Indicates whether player convoy cargo capacity should be applied. */ static void getResupplyContents(Resupply resupply, DropType dropType, boolean usePlayerConvoys) { - // Ammo and Armor are delivered in batches of 5, so we need to make sure to multiply their - // weight by five when picking these items. - final int WEIGHT_MULTIPLIER = dropType == DropType.DROP_TYPE_PARTS ? 1 : 5; - double targetCargoTonnage = resupply.getTargetCargoTonnage(); if (usePlayerConvoys) { final int targetCargoTonnagePlayerConvoy = resupply.getTargetCargoTonnagePlayerConvoy(); @@ -112,7 +108,8 @@ static void getResupplyContents(Resupply resupply, DropType dropType, boolean us case DROP_TYPE_AMMO -> ammoBinPool; }; - while ((availableSpace > 0) && (!relevantPartsPool.isEmpty())) { + double currentLoad = 0; + while ((currentLoad < availableSpace) && (!relevantPartsPool.isEmpty())) { Part potentialPart = switch(dropType) { case DROP_TYPE_PARTS -> getRandomDrop(partsPool, negotiatorSkill); case DROP_TYPE_ARMOR -> getRandomDrop(armorPool, negotiatorSkill); @@ -155,10 +152,16 @@ static void getResupplyContents(Resupply resupply, DropType dropType, boolean us case DROP_TYPE_AMMO -> ammoBinPool.remove(potentialPart); } - double partWeight = potentialPart.getTonnage(); - partWeight = partWeight == 0 ? RESUPPLY_MINIMUM_PART_WEIGHT : partWeight; + // Ammo and Armor are delivered in batches of 5t, + // so we need to make sure we're treating them as 5t no matter their actual weight. + double partWeight = 5; + + if (dropType == DropType.DROP_TYPE_PARTS) { + partWeight = potentialPart.getTonnage(); + partWeight = partWeight == 0 ? RESUPPLY_MINIMUM_PART_WEIGHT : partWeight; + } - availableSpace -= partWeight * WEIGHT_MULTIPLIER; + currentLoad += partWeight; droppedItems.add(potentialPart); } } diff --git a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/PerformResupply.java b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/PerformResupply.java index 2bcff6920f..6db3ff71f9 100644 --- a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/PerformResupply.java +++ b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/PerformResupply.java @@ -45,13 +45,14 @@ import static mekhq.campaign.mission.enums.AtBMoraleLevel.CRITICAL; import static mekhq.campaign.mission.enums.AtBMoraleLevel.DOMINATING; -import static mekhq.campaign.mission.enums.AtBMoraleLevel.ROUTED; import static mekhq.campaign.mission.enums.AtBMoraleLevel.STALEMATE; import static mekhq.campaign.mission.resupplyAndCaches.GenerateResupplyContents.DropType.DROP_TYPE_AMMO; import static mekhq.campaign.mission.resupplyAndCaches.GenerateResupplyContents.DropType.DROP_TYPE_ARMOR; import static mekhq.campaign.mission.resupplyAndCaches.GenerateResupplyContents.DropType.DROP_TYPE_PARTS; import static mekhq.campaign.mission.resupplyAndCaches.GenerateResupplyContents.RESUPPLY_MINIMUM_PART_WEIGHT; import static mekhq.campaign.mission.resupplyAndCaches.GenerateResupplyContents.getResupplyContents; +import static mekhq.campaign.mission.resupplyAndCaches.Resupply.RESUPPLY_AMMO_TONNAGE; +import static mekhq.campaign.mission.resupplyAndCaches.Resupply.RESUPPLY_ARMOR_TONNAGE; import static mekhq.campaign.mission.resupplyAndCaches.Resupply.ResupplyType.RESUPPLY_CONTRACT_END; import static mekhq.campaign.mission.resupplyAndCaches.Resupply.ResupplyType.RESUPPLY_LOOT; import static mekhq.campaign.mission.resupplyAndCaches.ResupplyUtilities.forceContainsMajorityVTOLForces; @@ -167,7 +168,13 @@ public static void performResupply(Resupply resupply, AtBContract contract, int double totalTonnage = 0; for (Part part : resupply.getConvoyContents()) { - totalTonnage += part.getTonnage() * (part instanceof Armor || part instanceof AmmoBin ? 5 : 1); + if (part instanceof AmmoBin) { + totalTonnage += RESUPPLY_AMMO_TONNAGE; + } else if (part instanceof Armor) { + totalTonnage += RESUPPLY_ARMOR_TONNAGE; + } else { + totalTonnage += part.getTonnage(); + } } logger.info("totalTonnage: " + totalTonnage); @@ -207,9 +214,9 @@ public static void makeDelivery(Resupply resupply, @Nullable List contents for (Part part : contents) { if (part instanceof AmmoBin) { campaign.getQuartermaster().addAmmo(((AmmoBin) part).getType(), - ((AmmoBin) part).getFullShots() * 5); + ((AmmoBin) part).getFullShots() * RESUPPLY_AMMO_TONNAGE); } else if (part instanceof Armor) { - int quantity = (int) Math.ceil(((Armor) part).getArmorPointsPerTon() * 5); + int quantity = (int) Math.ceil(((Armor) part).getArmorPointsPerTon() * RESUPPLY_ARMOR_TONNAGE); ((Armor) part).setAmount(quantity); campaign.getWarehouse().addPart(part, true); } else { @@ -255,7 +262,6 @@ public static void makeSmugglerDelivery(Resupply resupply) { public static void loadPlayerConvoys(Resupply resupply) { // Ammo and Armor are delivered in batches of 5, so we need to make sure to multiply their // weight by five when picking these items. - final int WEIGHT_MULTIPLIER = 5; final Campaign campaign = resupply.getCampaign(); final Map playerConvoys = resupply.getPlayerConvoys(); @@ -285,8 +291,10 @@ public static void loadPlayerConvoys(Resupply resupply) { double tonnage = part.getTonnage(); tonnage = tonnage == 0 ? RESUPPLY_MINIMUM_PART_WEIGHT : tonnage; - if (part instanceof AmmoBin || part instanceof Armor) { - tonnage *= WEIGHT_MULTIPLIER; + if (part instanceof AmmoBin) { + tonnage = RESUPPLY_AMMO_TONNAGE; + } else if (part instanceof Armor) { + tonnage = RESUPPLY_ARMOR_TONNAGE; } if (cargoCapacity - tonnage >= 0) { @@ -326,18 +334,14 @@ public static void processConvoy(Resupply resupply, List convoyContents, @ // First, we need to identify whether the convoy has been intercepted. AtBMoraleLevel morale = contract.getMoraleLevel(); + // There isn't any chance of an interception if the enemy is Routed, so early-exit if (morale.isRouted()) { completeSuccessfulDelivery(resupply, convoyContents); + return; } int interceptionChance = morale.ordinal(); - // There isn't any chance of an interception if the enemy is Routed, so early-exit - if (interceptionChance == ROUTED.ordinal()) { - completeSuccessfulDelivery(resupply, convoyContents); - return; - } - // This chance is modified by convoy weight, for player convoys this is easy - we just // calculate the weight of all units in the convoy. For NPC convoys, we need to get a bit // creative, as we have no way to determine their size prior to any interception scenario. diff --git a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java index 0ab0c1dcd1..4e98dab774 100644 --- a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java +++ b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java @@ -21,6 +21,7 @@ import java.math.BigInteger; import java.util.*; +import static java.lang.Math.floor; import static java.lang.Math.min; import static java.lang.Math.round; import static megamek.common.MiscType.F_SPONSON_TURRET; @@ -65,6 +66,8 @@ public class Resupply { private Money convoyContentsValueCalculated; public static final int CARGO_MULTIPLIER = 4; + public static final int RESUPPLY_AMMO_TONNAGE = 1; + public static final int RESUPPLY_ARMOR_TONNAGE = 5; private static final MMLogger logger = MMLogger.create(Resupply.class); @@ -545,9 +548,11 @@ private Map collectParts() { } int dropWeight = part instanceof MissingPart ? 10 : 1; + dropWeight = (int) floor(dropWeight * getPartMultiplier(part)); + PartDetails partDetails = new PartDetails(part, dropWeight); - processedParts.merge(part.toString(), partDetails, (oldValue, newValue) -> { + processedParts.merge(getPartKey(part), partDetails, (oldValue, newValue) -> { oldValue.setWeight(oldValue.getWeight() + newValue.getWeight()); return oldValue; }); @@ -563,6 +568,31 @@ private Map collectParts() { return processedParts; } + /** + * Generates a key for the given part based on its name and tonnage. + * + *

The key is a combination of the part's name and its tonnage, separated by a colon. + * For specific part types such as {@link AmmoBin} and {@link Armor}, the tonnage is + * always set to a set value, regardless of the actual tonnage.

+ * + * @param part The {@link Part} for which the key is generated. Must not be {@code null}. + * @return A unique key in the format {@code "partName:partTonnage"}, where + * {@code partName} is the name of the part and {@code partTonnage} is the + * tonnage of the part or a fixed value for {@link AmmoBin} and {@link Armor}. + */ + private static String getPartKey(Part part) { + String partName = part.getName(); + double partTonnage = part.getTonnage(); + + if (part instanceof AmmoBin) { + partTonnage = RESUPPLY_AMMO_TONNAGE; + } else if (part instanceof Armor) { + partTonnage = RESUPPLY_ARMOR_TONNAGE; + } + + return partName + ':' + partTonnage; + } + /** * Checks if a part is ineligible for inclusion in the resupply process. Ineligibility is * determined based on exclusion lists, unit structure compatibility, and transporter checks. @@ -648,28 +678,60 @@ private boolean checkTransporter(Part part) { } /** - * Applies warehouse weight modifiers to the collected parts list by comparing the - * in-campaign warehouse spare parts with the current list. Removes parts from the pool - * if the warehouse already contains enough resources to offset demand. + * Adjusts the provided parts list by applying warehouse weight modifiers. + * + *

This method compares the in-campaign warehouse's spare parts inventory with the given + * parts list and reduces the weight (quantity) of parts in the list based on the warehouse + * stock. If the warehouse contains enough resources to fully satisfy the demand for a part, + * the part is removed from the parts list.

+ * + *

The adjustments are performed as follows: + *

    + *
  • For each part in the warehouse: + *
      + *
    • The weight of the part in the part list is reduced by the quantity available + * in the warehouse.
    • + *
    • If the weight becomes zero or negative, the part is flagged for removal.
    • + *
    + *
  • + *
  • All flagged parts are then removed from the part list.
  • + *
+ *

* - * @param partsList A map of part names and their respective part details to modify. + * @param partsList A map containing part identifiers (keys) and their corresponding {@link PartDetails}. + * The map will be modified to reflect the warehouse adjustments. */ private void applyWarehouseWeightModifiers(Map partsList) { - // Because of how AmmoBins work, we're always considering the campaign to have 0 rounds - // of ammo in storage, we could avoid this, but I don't think it's necessary. + // Adjust based on the quantity in the warehouse for (Part part : campaign.getWarehouse().getSpareParts()) { - PartDetails targetPart = partsList.get(part.toString()); - if (targetPart != null) { - int spareCount = part.getQuantity(); - double multiplier = getPartMultiplier(part); - - double targetPartCount = targetPart.getWeight() * multiplier; - if ((targetPartCount - spareCount) < 1) { - partsList.remove(part.toString()); - } else { - targetPart.setWeight(min(1, targetPartCount - spareCount)); - } + int weight = part.getQuantity(); + + // We don't want empty AmmoStorage to reduce Resupply weighting + if (part instanceof AmmoStorage && (((AmmoStorage) part).getShots() == 0)) { + continue; } + + PartDetails partDetails = new PartDetails(part, weight); + + partsList.merge(getPartKey(part), partDetails, (oldValue, newValue) -> { + oldValue.setWeight(oldValue.getWeight() - newValue.getWeight()); + return oldValue; + }); + } + + // Remove any items that now have 0 (or negative) tickets left in the pool + List removalList = new ArrayList<>(); + for (PartDetails partDetails : partsList.values()) { + Part part = partDetails.getPart(); + double weight = partDetails.getWeight(); + + if (weight <= 0) { + removalList.add(getPartKey(part)); + } + } + + for (String removalKey : removalList) { + partsList.remove(removalKey); } } diff --git a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/ResupplyUtilities.java b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/ResupplyUtilities.java index ca0150aede..121e06313a 100644 --- a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/ResupplyUtilities.java +++ b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/ResupplyUtilities.java @@ -32,9 +32,11 @@ import java.util.UUID; import java.util.Vector; -import static java.lang.Math.ceil; import static java.lang.Math.floor; +import static java.lang.Math.max; import static mekhq.campaign.mission.resupplyAndCaches.Resupply.CARGO_MULTIPLIER; +import static mekhq.campaign.mission.resupplyAndCaches.Resupply.RESUPPLY_AMMO_TONNAGE; +import static mekhq.campaign.mission.resupplyAndCaches.Resupply.RESUPPLY_ARMOR_TONNAGE; import static mekhq.campaign.mission.resupplyAndCaches.Resupply.calculateTargetCargoTonnage; import static mekhq.campaign.personnel.enums.PersonnelStatus.KIA; import static mekhq.utilities.EntityUtilities.getEntityFromUnitId; @@ -143,7 +145,10 @@ private static void decideCrewMemberFate(Campaign campaign, Person person) { */ public static int estimateCargoRequirements(Campaign campaign, AtBContract contract) { double targetTonnage = calculateTargetCargoTonnage(campaign, contract) * CARGO_MULTIPLIER; - return (int) Math.ceil(targetTonnage); + + // Armor and ammo are always delivered in blocks, so cargo will never be less than the sum + // of those blocks + return max(RESUPPLY_AMMO_TONNAGE + RESUPPLY_ARMOR_TONNAGE, (int) Math.ceil(targetTonnage)); } /** diff --git a/MekHQ/src/mekhq/gui/view/MissionViewPanel.java b/MekHQ/src/mekhq/gui/view/MissionViewPanel.java index 75178e3d97..07bbfaeef0 100644 --- a/MekHQ/src/mekhq/gui/view/MissionViewPanel.java +++ b/MekHQ/src/mekhq/gui/view/MissionViewPanel.java @@ -1000,7 +1000,7 @@ public void mouseClicked(MouseEvent e) { pnlStats.add(lblCargoRequirement, gridBagConstraints); txtCargoRequirement.setName("txtCargoRequirement"); - txtCargoRequirement.setText(estimateCargoRequirements(campaign, contract) + "t"); + txtCargoRequirement.setText(estimateCargoRequirements(campaign, contract) + "t+"); gridBagConstraints = new GridBagConstraints(); gridBagConstraints.gridx = 1; gridBagConstraints.gridy = y++;