From d0e1ed6c08f27dcdf0b86cc548b0b1aeadd42858 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Tue, 21 Jan 2025 22:14:42 -0600 Subject: [PATCH 001/112] Simplify delivery transit time calculation logic Removed redundant margin of success (MoS) and minimum delivery time configuration for transit calculations. Standardized transit time determination using item availability, campaign settings, and a random roll. Updated the UI and campaign options to align with the simplified calculation method. --- .../CampaignOptionsDialog.properties | 11 +- MekHQ/src/mekhq/campaign/Campaign.java | 120 ++++++++---------- MekHQ/src/mekhq/campaign/CampaignOptions.java | 78 ------------ .../adapter/ProcurementTableMouseAdapter.java | 18 +-- .../contents/EquipmentAndSuppliesTab.java | 90 +------------ 5 files changed, 70 insertions(+), 247 deletions(-) diff --git a/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties b/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties index 11d76844f22..c53edd0e7c9 100644 --- a/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties +++ b/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties @@ -236,14 +236,9 @@ lblAutoLogisticsOther.tooltip=autoLogistics counts each part in use, that is not # createDeliveryPanel lblDeliveryPanel.text=Deliveries -lblNDiceTransitTime.text=Delivery Time -lblNDiceTransitTime.tooltip=How many dice are rolled to determine delivery time? -lblConstantTransitTime.text=d6+ -lblConstantTransitTime.tooltip=How long is added to the delivery time roll? -lblAcquireMosBonus.text=Delivery Time Reduction -lblAcquireMosBonus.tooltip=How much should the delivery time be reduced per margin of success? -lblAcquireMinimum.text=Minimum Delivery Time -lblAcquireMinimum.tooltip=What is the minimum delivery duration? +lblTransitTimeUnits.text=Delivery Scale +lblTransitTimeUnits.tooltip=Should deliveries be scaled using days, weeks, or months? Campaign\ + \ Operations uses months. transitUnitNamesDays.text=Days transitUnitNamesWeeks.text=Weeks transitUnitNamesMonths.text=Months diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index 609721ffd49..79a9182223a 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -152,6 +152,9 @@ import static java.lang.Math.floor; import static java.lang.Math.max; import static java.lang.Math.round; +import static megamek.common.Compute.d6; +import static mekhq.campaign.CampaignOptions.TRANSIT_UNIT_MONTH; +import static mekhq.campaign.CampaignOptions.TRANSIT_UNIT_WEEK; import static mekhq.campaign.enums.CampaignTransportType.SHIP_TRANSPORT; import static mekhq.campaign.enums.CampaignTransportType.TACTICAL_TRANSPORT; import static mekhq.campaign.force.CombatTeam.getStandardForceSize; @@ -816,7 +819,7 @@ private void processShipSearch() { long numDays = ChronoUnit.DAYS.between(getShipSearchStart(), getLocalDate()); if (numDays > 21) { - int roll = Compute.d6(2); + int roll = d6(2); TargetRoll target = getAtBConfig().shipSearchTargetRoll(shipSearchType, this); setShipSearchStart(null); report.append("
Ship search target: ").append(target.getValueAsString()).append(" roll: ") @@ -877,7 +880,7 @@ public void purchaseShipSearchResult() { Entity en = mekFileParser.getEntity(); int transitDays = getCampaignOptions().isInstantUnitMarketDelivery() ? 0 - : calculatePartTransitTime(Compute.d6(2) - 2); + : calculatePartTransitTime(en.calcYearAvailability(getGameYear(), useClanTechBase(), getTechFaction())); getFinances().debit(TransactionType.UNIT_PURCHASE, getLocalDate(), cost, "Purchased " + en.getShortName()); PartQuality quality = PartQuality.QUALITY_D; @@ -2002,11 +2005,11 @@ private void simulateRelationshipHistory(Person person) { // set loyalty if (experienceLevel <= 0) { - person.setLoyalty(Compute.d6(3) + 2); + person.setLoyalty(d6(3) + 2); } else if (experienceLevel == 1) { - person.setLoyalty(Compute.d6(3) + 1); + person.setLoyalty(d6(3) + 1); } else { - person.setLoyalty(Compute.d6(3)); + person.setLoyalty(d6(3)); } if (experienceLevel >= 0) { @@ -2180,7 +2183,7 @@ public void checkBloodnameAdd(Person person, boolean ignoreDice) { bloodnameTarget += Math.min(0, getRankSystem().getOfficerCut() - person.getRankNumeric()); } - if (ignoreDice || (Compute.d6(2) >= bloodnameTarget)) { + if (ignoreDice || (d6(2) >= bloodnameTarget)) { final Phenotype phenotype = person.getPhenotype().isNone() ? Phenotype.GENERAL : person.getPhenotype(); final Bloodname bloodname = Bloodname.randomBloodname( @@ -2840,7 +2843,7 @@ public String healPerson(Person medWork, Person doctor) { report += doctor.getHyperlinkedFullTitle() + " attempts to heal " + medWork.getFullName(); TargetRoll target = getTargetFor(medWork, doctor); - int roll = Compute.d6(2); + int roll = d6(2); report = report + ", needs " + target.getValueAsString() + " and rolls " + roll + ':'; int xpGained = 0; @@ -2850,7 +2853,7 @@ public String healPerson(Person medWork, Person doctor) { && doctor.getOptions().booleanOption(PersonnelOptions.EDGE_MEDICAL)) { if ((roll == 2) && (doctor.getCurrentEdge() > 0) && (target.getValue() != TargetRoll.AUTOMATIC_SUCCESS)) { doctor.changeCurrentEdge(-1); - roll = Compute.d6(2); + roll = d6(2); report += medWork.fail() + '\n' + doctor.getHyperlinkedFullTitle() + " uses Edge to reroll:" + " rolls " + roll + ':'; } @@ -3365,7 +3368,7 @@ public PartAcquisitionResult findContactForAcquisition(IAcquisitionWork acquisit } return PartAcquisitionResult.PlanetSpecificFailure; } - if (Compute.d6(2) < target.getValue()) { + if (d6(2) < target.getValue()) { // no contacts on this planet, move along if (getCampaignOptions().isPlanetAcquisitionVerbose()) { addReport("" @@ -3448,7 +3451,7 @@ private boolean acquireEquipment(IAcquisitionWork acquisition, Person person, Pl return false; } - int roll = Compute.d6(2); + int roll = d6(2); report += " needs " + target.getValueAsString(); report += " and rolls " + roll + ':'; // Edge reroll, if applicable @@ -3456,17 +3459,13 @@ private boolean acquireEquipment(IAcquisitionWork acquisition, Person person, Pl && person.getOptions().booleanOption(PersonnelOptions.EDGE_ADMIN_ACQUIRE_FAIL) && (person.getCurrentEdge() > 0)) { person.changeCurrentEdge(-1); - roll = Compute.d6(2); + roll = d6(2); report += " failed! but uses Edge to reroll...getting a " + roll + ": "; } - int mos = roll - target.getValue(); - if (target.getValue() == TargetRoll.AUTOMATIC_SUCCESS) { - mos = roll - 2; - } int xpGained = 0; if (roll >= target.getValue()) { if (transitDays < 0) { - transitDays = calculatePartTransitTime(mos); + transitDays = calculatePartTransitTime(acquisition.getAvailability()); } report = report + acquisition.find(transitDays); found = true; @@ -3656,7 +3655,7 @@ public void refit(Refit theRefit) { int roll; String wrongType = ""; if (tech.isRightTechTypeFor(theRefit)) { - roll = Compute.d6(2); + roll = d6(2); } else { roll = Utilities.roll3d6(); wrongType = " Warning: wrong tech type for this refit."; @@ -3666,7 +3665,7 @@ public void refit(Refit theRefit) { && tech.getOptions().booleanOption(PersonnelOptions.EDGE_REPAIR_FAILED_REFIT) && (tech.getCurrentEdge() > 0)) { tech.changeCurrentEdge(-1); - roll = tech.isRightTechTypeFor(theRefit) ? Compute.d6(2) : Utilities.roll3d6(); + roll = tech.isRightTechTypeFor(theRefit) ? d6(2) : Utilities.roll3d6(); // This is needed to update the edge values of individual crewmen if (tech.isEngineer()) { tech.setEdgeUsed(tech.getEdgeUsed() - 1); @@ -3857,7 +3856,7 @@ public String fixPart(IPartWork partWork, Person tech) { int roll; String wrongType = ""; if (tech.isRightTechTypeFor(partWork)) { - roll = Compute.d6(2); + roll = d6(2); } else { roll = Utilities.roll3d6(); wrongType = " Warning: wrong tech type for this repair."; @@ -3879,7 +3878,7 @@ public String fixPart(IPartWork partWork, Person tech) { || tech.getPrimaryRole().isVehicleCrew())) // For vessel crews && (roll < target.getValue())) { tech.changeCurrentEdge(-1); - roll = tech.isRightTechTypeFor(partWork) ? Compute.d6(2) : Utilities.roll3d6(); + roll = tech.isRightTechTypeFor(partWork) ? d6(2) : Utilities.roll3d6(); // This is needed to update the edge values of individual crewmen if (tech.isEngineer()) { tech.setEdgeUsed(tech.getEdgeUsed() + 1); @@ -4058,7 +4057,7 @@ && getLocation().getJumpPath().getLastSystem().getId().equals(contract.getSystem if (campaignOptions.isUseStratCon() && contract.getMoraleLevel().isRouted()) { LocalDate newRoutEndDate = contract.getStartDate() - .plusMonths(max(1, Compute.d6() - 3)) + .plusMonths(max(1, d6() - 3)) .minusDays(1); contract.setRoutEndDate(newRoutEndDate); } @@ -4273,7 +4272,7 @@ private void processNewDayATB() { private void processResupply(AtBContract contract) { boolean isGuerrilla = contract.getContractType().isGuerrillaWarfare(); - if (!isGuerrilla || Compute.d6(1) > 4) { + if (!isGuerrilla || d6(1) > 4) { ResupplyType resupplyType = isGuerrilla ? ResupplyType.RESUPPLY_SMUGGLER : ResupplyType.RESUPPLY_NORMAL; Resupply resupply = new Resupply(this, contract, resupplyType); performResupply(resupply, contract); @@ -4460,7 +4459,7 @@ private boolean processMonthlyVocationalXp(Person person, int vocationalXpRate) person.setVocationalXPTimer(person.getVocationalXPTimer() + 1); if (person.getVocationalXPTimer() >= checkFrequency) { - if (Compute.d6(2) >= targetNumber) { + if (d6(2) >= targetNumber) { person.awardXP(this, vocationalXpRate); person.setVocationalXPTimer(0); return true; @@ -4540,7 +4539,7 @@ private void processMonthlyAutoAwards(Person person) { int dice = person.getExperienceLevel(this, false); if (dice > 0) { - score = Compute.d6(dice); + score = d6(dice); } multiplier += 0.5; @@ -4550,7 +4549,7 @@ private void processMonthlyAutoAwards(Person person) { int dice = person.getExperienceLevel(this, true); if (dice > 0) { - score += Compute.d6(dice); + score += d6(dice); } multiplier += 0.5; @@ -8135,55 +8134,42 @@ public int calculatePartTransitTime(PlanetarySystem system) { // if you are delivering from the same planet then no transit times int currentTransitTime = (distance > 0) ? (int) Math.ceil(getCurrentSystem().getTimeToJumpPoint(1.0)) : 0; int originTransitTime = (distance > 0) ? (int) Math.ceil(system.getTimeToJumpPoint(1.0)) : 0; - int amazonFreeShipping = Compute.d6(1 + jumps); + int amazonFreeShipping = d6(1 + jumps); return (recharges * 7) + currentTransitTime + originTransitTime + amazonFreeShipping; } - /*** - * Calculate transit times based on the margin of success from an acquisition - * roll. The values here - * are all based on what the user entered for the campaign options. + /** + * Calculates the transit time for the arrival of parts or supplies based on the availability + * of the item, a random roll, and campaign-specific transit time settings. * - * @param mos - an integer of the margin of success of an acquisition roll - * @return the number of days that supplies will take to arrive. + *

The transit time is calculated using the following factors: + *

    + *
  • A fixed base modifier value defined by campaign rules.
  • + *
  • A random roll of 1d6 to add variability to the calculation.
  • + *
  • The availability value of the requested parts or supplies from the acquisition details.
  • + *
+ * + *

The calculated duration is applied in units (days, weeks, or months) based on the campaign's + * configuration for transit time.

+ * + * @param availability the availability code of the part or unit being acquired as an integer. + * @return the number of days required for the parts or units to arrive based on the + * calculated transit time. */ - public int calculatePartTransitTime(int mos) { - int nDice = getCampaignOptions().getNDiceTransitTime(); - int time = getCampaignOptions().getConstantTransitTime(); - if (nDice > 0) { - time += Compute.d6(nDice); - } - // now step forward through the calendar - LocalDate arrivalDate = getLocalDate(); - arrivalDate = switch (getCampaignOptions().getUnitTransitTime()) { - case CampaignOptions.TRANSIT_UNIT_MONTH -> arrivalDate.plusMonths(time); - case CampaignOptions.TRANSIT_UNIT_WEEK -> arrivalDate.plusWeeks(time); - default -> arrivalDate.plusDays(time); - }; - - // now adjust for MoS and minimums - int mosBonus = getCampaignOptions().getAcquireMosBonus() * mos; - arrivalDate = switch (getCampaignOptions().getAcquireMosUnit()) { - case CampaignOptions.TRANSIT_UNIT_MONTH -> arrivalDate.minusMonths(mosBonus); - case CampaignOptions.TRANSIT_UNIT_WEEK -> arrivalDate.minusWeeks(mosBonus); - default -> arrivalDate.minusDays(mosBonus); - }; + public int calculatePartTransitTime(int availability) { + final int BASE_MODIFIER = 7; // CamOps p51 + final int roll = d6(1); + final int total = max(1, (BASE_MODIFIER + roll + availability) / 4); // CamOps p51 - // now establish minimum date and if this is before - LocalDate minimumDate = getLocalDate(); - minimumDate = switch (getCampaignOptions().getAcquireMinimumTimeUnit()) { - case CampaignOptions.TRANSIT_UNIT_MONTH -> - minimumDate.plusMonths(getCampaignOptions().getAcquireMinimumTime()); - case CampaignOptions.TRANSIT_UNIT_WEEK -> - minimumDate.plusWeeks(getCampaignOptions().getAcquireMinimumTime()); - default -> minimumDate.plusDays(getCampaignOptions().getAcquireMinimumTime()); + // now step forward through the calendar + LocalDate arrivalDate = currentDay; + arrivalDate = switch (campaignOptions.getUnitTransitTime()) { + case TRANSIT_UNIT_MONTH -> arrivalDate.plusMonths(total); + case TRANSIT_UNIT_WEEK -> arrivalDate.plusWeeks(total); + default -> arrivalDate.plusDays(total); }; - if (arrivalDate.isBefore(minimumDate)) { - return Math.toIntExact(ChronoUnit.DAYS.between(getLocalDate(), minimumDate)); - } else { - return Math.toIntExact(ChronoUnit.DAYS.between(getLocalDate(), arrivalDate)); - } + return Math.toIntExact(ChronoUnit.DAYS.between(getLocalDate(), arrivalDate)); } /** @@ -8524,7 +8510,7 @@ private String doMaintenanceOnUnitPart(Unit u, Part p, Map partsT } partReport += ", TN " + target.getValue() + '[' + target.getDesc() + ']'; - int roll = Compute.d6(2); + int roll = d6(2); int margin = roll - target.getValue(); partReport += " rolled a " + roll + ", margin of " + margin; diff --git a/MekHQ/src/mekhq/campaign/CampaignOptions.java b/MekHQ/src/mekhq/campaign/CampaignOptions.java index 370403fa234..742e997b283 100644 --- a/MekHQ/src/mekhq/campaign/CampaignOptions.java +++ b/MekHQ/src/mekhq/campaign/CampaignOptions.java @@ -159,13 +159,7 @@ public static String getTransitUnitName(final int unit) { private int autoLogisticsOther; // Delivery - private int nDiceTransitTime; - private int constantTransitTime; private int unitTransitTime; - private int acquireMinimumTime; - private int acquireMinimumTimeUnit; - private int acquireMosBonus; - private int acquireMosUnit; // Planetary Acquisition private boolean usePlanetaryAcquisition; @@ -681,13 +675,7 @@ public CampaignOptions() { autoLogisticsOther = 50; // Delivery - nDiceTransitTime = 1; - constantTransitTime = 0; unitTransitTime = TRANSIT_UNIT_MONTH; - acquireMinimumTime = 1; - acquireMinimumTimeUnit = TRANSIT_UNIT_MONTH; - acquireMosBonus = 1; - acquireMosUnit = TRANSIT_UNIT_MONTH; // Planetary Acquisition usePlanetaryAcquisition = false; @@ -4007,22 +3995,6 @@ public void setAcquisitionSupportStaffOnly(final boolean acquisitionSupportStaff this.acquisitionSupportStaffOnly = acquisitionSupportStaffOnly; } - public int getNDiceTransitTime() { - return nDiceTransitTime; - } - - public void setNDiceTransitTime(final int nDiceTransitTime) { - this.nDiceTransitTime = nDiceTransitTime; - } - - public int getConstantTransitTime() { - return constantTransitTime; - } - - public void setConstantTransitTime(final int constantTransitTime) { - this.constantTransitTime = constantTransitTime; - } - public int getUnitTransitTime() { return unitTransitTime; } @@ -4031,38 +4003,6 @@ public void setUnitTransitTime(final int unitTransitTime) { this.unitTransitTime = unitTransitTime; } - public int getAcquireMosUnit() { - return acquireMosUnit; - } - - public void setAcquireMosUnit(final int acquireMosUnit) { - this.acquireMosUnit = acquireMosUnit; - } - - public int getAcquireMosBonus() { - return acquireMosBonus; - } - - public void setAcquireMosBonus(final int acquireMosBonus) { - this.acquireMosBonus = acquireMosBonus; - } - - public int getAcquireMinimumTimeUnit() { - return acquireMinimumTimeUnit; - } - - public void setAcquireMinimumTimeUnit(final int acquireMinimumTimeUnit) { - this.acquireMinimumTimeUnit = acquireMinimumTimeUnit; - } - - public int getAcquireMinimumTime() { - return acquireMinimumTime; - } - - public void setAcquireMinimumTime(final int acquireMinimumTime) { - this.acquireMinimumTime = acquireMinimumTime; - } - public boolean isUsePlanetaryAcquisition() { return usePlanetaryAcquisition; } @@ -4815,13 +4755,7 @@ public void writeToXml(final PrintWriter pw, int indent) { MHQXMLUtility.writeSimpleXMLTag(pw, indent, "acquisitionSkill", acquisitionSkill); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "acquisitionSupportStaffOnly", acquisitionSupportStaffOnly); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "techLevel", techLevel); - MHQXMLUtility.writeSimpleXMLTag(pw, indent, "nDiceTransitTime", nDiceTransitTime); - MHQXMLUtility.writeSimpleXMLTag(pw, indent, "constantTransitTime", constantTransitTime); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "unitTransitTime", unitTransitTime); - MHQXMLUtility.writeSimpleXMLTag(pw, indent, "acquireMosBonus", acquireMosBonus); - MHQXMLUtility.writeSimpleXMLTag(pw, indent, "acquireMosUnit", acquireMosUnit); - MHQXMLUtility.writeSimpleXMLTag(pw, indent, "acquireMinimumTime", acquireMinimumTime); - MHQXMLUtility.writeSimpleXMLTag(pw, indent, "acquireMinimumTimeUnit", acquireMinimumTimeUnit); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "usePlanetaryAcquisition", usePlanetaryAcquisition); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "planetAcquisitionFactionLimit", getPlanetAcquisitionFactionLimit().name()); @@ -5425,20 +5359,8 @@ public static CampaignOptions generateCampaignOptionsFromXml(Node wn, Version ve retVal.waitingPeriod = Integer.parseInt(wn2.getTextContent().trim()); } else if (wn2.getNodeName().equalsIgnoreCase("acquisitionSkill")) { retVal.acquisitionSkill = wn2.getTextContent().trim(); - } else if (wn2.getNodeName().equalsIgnoreCase("nDiceTransitTime")) { - retVal.nDiceTransitTime = Integer.parseInt(wn2.getTextContent().trim()); - } else if (wn2.getNodeName().equalsIgnoreCase("constantTransitTime")) { - retVal.constantTransitTime = Integer.parseInt(wn2.getTextContent().trim()); } else if (wn2.getNodeName().equalsIgnoreCase("unitTransitTime")) { retVal.unitTransitTime = Integer.parseInt(wn2.getTextContent().trim()); - } else if (wn2.getNodeName().equalsIgnoreCase("acquireMosBonus")) { - retVal.acquireMosBonus = Integer.parseInt(wn2.getTextContent().trim()); - } else if (wn2.getNodeName().equalsIgnoreCase("acquireMosUnit")) { - retVal.acquireMosUnit = Integer.parseInt(wn2.getTextContent().trim()); - } else if (wn2.getNodeName().equalsIgnoreCase("acquireMinimumTime")) { - retVal.acquireMinimumTime = Integer.parseInt(wn2.getTextContent().trim()); - } else if (wn2.getNodeName().equalsIgnoreCase("acquireMinimumTimeUnit")) { - retVal.acquireMinimumTimeUnit = Integer.parseInt(wn2.getTextContent().trim()); } else if (wn2.getNodeName().equalsIgnoreCase("clanAcquisitionPenalty")) { retVal.clanAcquisitionPenalty = Integer.parseInt(wn2.getTextContent().trim()); } else if (wn2.getNodeName().equalsIgnoreCase("isAcquisitionPenalty")) { diff --git a/MekHQ/src/mekhq/gui/adapter/ProcurementTableMouseAdapter.java b/MekHQ/src/mekhq/gui/adapter/ProcurementTableMouseAdapter.java index 4246b845e6a..8a334f3f17b 100644 --- a/MekHQ/src/mekhq/gui/adapter/ProcurementTableMouseAdapter.java +++ b/MekHQ/src/mekhq/gui/adapter/ProcurementTableMouseAdapter.java @@ -18,16 +18,6 @@ */ package mekhq.gui.adapter; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.ResourceBundle; - -import javax.swing.JMenu; -import javax.swing.JMenuItem; -import javax.swing.JPopupMenu; -import javax.swing.JTable; - import megamek.common.Entity; import megamek.logging.MMLogger; import mekhq.MekHQ; @@ -40,6 +30,12 @@ import mekhq.gui.model.ProcurementTableModel; import mekhq.gui.utilities.JMenuHelpers; +import javax.swing.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.ResourceBundle; + public class ProcurementTableMouseAdapter extends JPopupMenuAdapter { private static final MMLogger logger = MMLogger.create(ProcurementTableMouseAdapter.class); @@ -172,7 +168,7 @@ private boolean tryProcureOneItem(final IAcquisitionWork acquisition) { return false; } final Object equipment = acquisition.getNewEquipment(); - final int transitTime = gui.getCampaign().calculatePartTransitTime(0); + final int transitTime = gui.getCampaign().calculatePartTransitTime(acquisition.getAvailability()); final boolean success; if (equipment instanceof Part) { success = gui.getCampaign().getQuartermaster().buyPart((Part) equipment, transitTime); diff --git a/MekHQ/src/mekhq/gui/campaignOptions/contents/EquipmentAndSuppliesTab.java b/MekHQ/src/mekhq/gui/campaignOptions/contents/EquipmentAndSuppliesTab.java index 2839d0934bd..a8b27fc9642 100644 --- a/MekHQ/src/mekhq/gui/campaignOptions/contents/EquipmentAndSuppliesTab.java +++ b/MekHQ/src/mekhq/gui/campaignOptions/contents/EquipmentAndSuppliesTab.java @@ -95,17 +95,8 @@ public class EquipmentAndSuppliesTab { //start Delivery Tab private JPanel pnlDeliveries; - private JLabel lblNDiceTransitTime; - private JSpinner spnNDiceTransitTime; - private JLabel lblConstantTransitTime; - private JSpinner spnConstantTransitTime; - private JLabel lblAcquireMosBonus; - private JSpinner spnAcquireMosBonus; - private JLabel lblAcquireMinimum; - private JSpinner spnAcquireMinimum; + private JLabel lblTransitTimeUnits; private MMComboBox choiceTransitTimeUnits; - private MMComboBox choiceAcquireMosUnits; - private MMComboBox choiceAcquireMinimumUnit; private static final int TRANSIT_UNIT_DAY = 0; private static final int TRANSIT_UNIT_WEEK = 1; private static final int TRANSIT_UNIT_MONTH = 2; @@ -233,19 +224,8 @@ private void initializePlanetaryAcquisitionsTab() { */ private void initializeDelivery() { pnlDeliveries = new JPanel(); - lblNDiceTransitTime = new JLabel(); - spnNDiceTransitTime = new JSpinner(); - lblConstantTransitTime = new JLabel(); - spnConstantTransitTime = new JSpinner(); + lblTransitTimeUnits = new JLabel(); choiceTransitTimeUnits = new MMComboBox<>("choiceTransitTimeUnits", getTransitUnitOptions()); - - lblAcquireMosBonus = new JLabel(); - spnAcquireMosBonus = new JSpinner(); - choiceAcquireMosUnits = new MMComboBox<>("choiceAcquireMosUnits", getTransitUnitOptions()); - - lblAcquireMinimum = new JLabel(); - spnAcquireMinimum = new JSpinner(); - choiceAcquireMinimumUnit = new MMComboBox<>("choiceAcquireMinimumUnit", getTransitUnitOptions()); } /** @@ -345,11 +325,11 @@ public JPanel createAcquisitionTab() { panel.add(pnlAcquisitions, layoutParent); layoutParent.gridx++; - panel.add(pnlDeliveries, layoutParent); + panel.add(pnlAutoLogistics, layoutParent); layoutParent.gridx = 0; layoutParent.gridy++; - panel.add(pnlAutoLogistics, layoutParent); + panel.add(pnlDeliveries, layoutParent); // Create Parent Panel and return @@ -527,21 +507,8 @@ private JPanel createAutoLogisticsPanel() { * @return a {@code JPanel} instance representing the delivery panel with all configured sub-panels and components. */ private JPanel createDeliveryPanel() { - lblNDiceTransitTime = new CampaignOptionsLabel("NDiceTransitTime"); - spnNDiceTransitTime = new CampaignOptionsSpinner("NDiceTransitTime", 0, - 0, 365, 1); - - lblConstantTransitTime = new CampaignOptionsLabel("ConstantTransitTime"); - spnConstantTransitTime = new CampaignOptionsSpinner("ConstantTransitTime", - 0, 0, 365, 1); - - lblAcquireMosBonus = new CampaignOptionsLabel("AcquireMosBonus"); - spnAcquireMosBonus = new CampaignOptionsSpinner("AcquireMosBonus", - 0, 0, 365, 1); - - lblAcquireMinimum = new CampaignOptionsLabel("AcquireMinimum"); - spnAcquireMinimum = new CampaignOptionsSpinner("AcquireMinimum", - 0, 0, 365, 1); + // Contents + lblTransitTimeUnits = new CampaignOptionsLabel("TransitTimeUnits"); // Layout the Panel final JPanel panelTransit = new CampaignOptionsStandardPanel("DeliveryPanelTransit"); @@ -550,37 +517,10 @@ private JPanel createDeliveryPanel() { layoutTransit.gridy = 0; layoutTransit.gridx = 0; layoutTransit.gridwidth = 1; - panelTransit.add(lblNDiceTransitTime, layoutTransit); - layoutTransit.gridx++; - panelTransit.add(spnNDiceTransitTime, layoutTransit); - layoutTransit.gridx++; - panelTransit.add(lblConstantTransitTime, layoutTransit); - layoutTransit.gridx++; - panelTransit.add(spnConstantTransitTime, layoutTransit); + panelTransit.add(lblTransitTimeUnits, layoutTransit); layoutTransit.gridx++; panelTransit.add(choiceTransitTimeUnits, layoutTransit); - // Layout the Panel - final JPanel panelDeliveries = new CampaignOptionsStandardPanel("DeliveryPanelDeliveries"); - final GridBagConstraints layoutDeliveries = new CampaignOptionsGridBagConstraints(panelDeliveries); - - layoutDeliveries.gridy = 0; - layoutDeliveries.gridx = 0; - layoutDeliveries.gridwidth = 1; - panelDeliveries.add(lblAcquireMosBonus, layoutDeliveries); - layoutDeliveries.gridx++; - panelDeliveries.add(spnAcquireMosBonus, layoutDeliveries); - layoutDeliveries.gridx++; - panelDeliveries.add(choiceAcquireMosUnits, layoutDeliveries); - - layoutDeliveries.gridx = 0; - layoutDeliveries.gridy++; - panelDeliveries.add(lblAcquireMinimum, layoutDeliveries); - layoutDeliveries.gridx++; - panelDeliveries.add(spnAcquireMinimum, layoutDeliveries); - layoutDeliveries.gridx++; - panelDeliveries.add(choiceAcquireMinimumUnit, layoutDeliveries); - final JPanel panelParent = new CampaignOptionsStandardPanel("DeliveryPanel", true, "DeliveryPanel"); final GridBagConstraints layoutParent = new CampaignOptionsGridBagConstraints(panelParent); @@ -589,8 +529,6 @@ private JPanel createDeliveryPanel() { layoutParent.gridx = 0; layoutParent.gridwidth = 2; panelParent.add(panelTransit, layoutParent); - layoutParent.gridy++; - panelParent.add(panelDeliveries, layoutParent); return panelParent; } @@ -1049,13 +987,7 @@ public void applyCampaignOptionsToCampaign(@Nullable CampaignOptions presetCampa // Delivery - options.setNDiceTransitTime((int) spnNDiceTransitTime.getValue()); - options.setConstantTransitTime((int) spnConstantTransitTime.getValue()); options.setUnitTransitTime(choiceTransitTimeUnits.getSelectedIndex()); - options.setAcquireMosBonus((int) spnAcquireMosBonus.getValue()); - options.setAcquireMosUnit(choiceAcquireMosUnits.getSelectedIndex()); - options.setAcquireMinimumTime((int) spnAcquireMinimum.getValue()); - options.setAcquireMinimumTimeUnit(choiceAcquireMinimumUnit.getSelectedIndex()); // Planetary Acquisitions options.setPlanetaryAcquisition(usePlanetaryAcquisitions.isSelected()); @@ -1128,16 +1060,8 @@ public void loadValuesFromCampaignOptions(@Nullable CampaignOptions presetCampai spnAutoLogisticsOther.setValue(options.getAutoLogisticsOther()); // Delivery - spnNDiceTransitTime.setValue(options.getNDiceTransitTime()); - spnConstantTransitTime.setValue(options.getConstantTransitTime()); choiceTransitTimeUnits.setSelectedIndex(options.getUnitTransitTime()); - spnAcquireMosBonus.setValue(options.getAcquireMosBonus()); - choiceAcquireMosUnits.setSelectedIndex(options.getAcquireMosUnit()); - - spnAcquireMinimum.setValue(options.getAcquireMinimumTime()); - choiceAcquireMinimumUnit.setSelectedIndex(options.getAcquireMinimumTimeUnit()); - // Planetary Acquisitions usePlanetaryAcquisitions.setSelected(options.isUsePlanetaryAcquisition()); spnMaxJumpPlanetaryAcquisitions.setValue(options.getMaxJumpsPlanetaryAcquisition()); From 0d2e498666952457f77a13ec426ad1c5f2ac7694 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Tue, 21 Jan 2025 22:41:14 -0600 Subject: [PATCH 002/112] Refactored parts acquisition logic and streamlined availability checks Simplified and optimized the logic for determining parts availability, consolidating redundant methods and enhancing code clarity. Adjusted the parts availability modifier to rely on unified calculations, ensuring consistency across checks and GUI updates. --- MekHQ/src/mekhq/campaign/Campaign.java | 173 ++++-------------- .../mission/enums/AtBContractType.java | 34 ++-- MekHQ/src/mekhq/gui/CampaignGUI.java | 8 +- 3 files changed, 54 insertions(+), 161 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index 609721ffd49..f69c5b12717 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -29,7 +29,6 @@ import megamek.client.generator.RandomUnitGenerator; import megamek.client.ui.swing.util.PlayerColour; import megamek.common.*; -import megamek.common.AmmoType.Munitions; import megamek.common.annotations.Nullable; import megamek.common.enums.Gender; import megamek.common.equipment.BombMounted; @@ -40,10 +39,6 @@ import megamek.common.loaders.EntitySavingException; import megamek.common.options.*; import megamek.common.util.BuildingBlock; -import megamek.common.weapons.autocannons.ACWeapon; -import megamek.common.weapons.flamers.FlamerWeapon; -import megamek.common.weapons.gaussrifles.GaussWeapon; -import megamek.common.weapons.lasers.EnergyWeapon; import megamek.logging.MMLogger; import mekhq.MHQConstants; import mekhq.MekHQ; @@ -6957,6 +6952,26 @@ public TargetRoll getTargetForAcquisition(final IAcquisitionWork acquisition, return getTargetForAcquisition(acquisition, person, false); } + /** + * Determines the target roll required for successfully acquiring a specific part or unit + * based on various campaign settings, the acquisition details, and the person attempting the + * acquisition. + * + *

This method evaluates multiple conditions and factors to calculate the target roll, returning + * one of the following outcomes: + *

    + *
  • {@code TargetRoll.AUTOMATIC_SUCCESS} if acquisitions are set to be automatic in the campaign options.
  • + *
  • {@code TargetRoll.IMPOSSIBLE} if the acquisition is not permitted based on campaign settings, + * such as missing personnel, parts restrictions, or unavailable technology.
  • + *
  • A calculated target roll value based on the skill of the assigned person, acquisition modifiers, + * and adjustments for specific campaign rules (e.g., {@code AtB} restrictions).
  • + *
+ * + * @param acquisition the {@link IAcquisitionWork} object containing details about the requested part or supply, + * such as tech base, technology level, and availability. + * @return a {@link TargetRoll} object representing the roll required to successfully acquire the requested item, + * or an impossible/automatic result under specific circumstances. + */ public TargetRoll getTargetForAcquisition(final IAcquisitionWork acquisition, final @Nullable Person person, final boolean checkDaysToWait) { @@ -6974,7 +6989,8 @@ public TargetRoll getTargetForAcquisition(final IAcquisitionWork acquisition, && checkDaysToWait) { return new TargetRoll( TargetRoll.AUTOMATIC_FAIL, - "You must wait until the new cycle to check for this part. Further attempts will be added to the shopping list."); + "You must wait until the new cycle to check for this part. Further" + + " attempts will be added to the shopping list."); } if (acquisition.getTechBase() == Part.T_CLAN && !getCampaignOptions().isAllowClanPurchases()) { @@ -7002,149 +7018,30 @@ public TargetRoll getTargetForAcquisition(final IAcquisitionWork acquisition, return new TargetRoll(TargetRoll.IMPOSSIBLE, "It is extinct!"); } - if (getCampaignOptions().isUseAtB() && - getCampaignOptions().isRestrictPartsByMission() && acquisition instanceof Part) { - int partAvailability = ((Part) acquisition).getAvailability(); - EquipmentType et = null; - if (acquisition instanceof EquipmentPart) { - et = ((EquipmentPart) acquisition).getType(); - } else if (acquisition instanceof MissingEquipmentPart) { - et = ((MissingEquipmentPart) acquisition).getType(); - } - - StringBuilder partAvailabilityLog = new StringBuilder(""); - partAvailabilityLog.append("Part Rating Level: ").append(partAvailability) - .append(" (").append(EquipmentType.ratingNames[partAvailability]).append(')'); - /* - * Even if we can acquire Clan parts, they have a minimum availability of F for - * non-Clan units - */ - if (acquisition.getTechBase() == Part.T_CLAN && !getFaction().isClan()) { - partAvailability = max(partAvailability, EquipmentType.RATING_F); - partAvailabilityLog.append("
[clan part for non clan faction]"); - } else if (et != null) { - /* - * AtB rules do not simply affect the difficulty of getting parts, but whether - * they can be obtained at all. Changing the system to use availability codes - * can have a serious effect on game play, so we apply a few tweaks to keep some - * of the more basic items from becoming completely unobtainable, while applying - * a minimum for non-flamer energy weapons, which was the reason this rule was - * included in AtB to begin with. - */ - if (et instanceof EnergyWeapon - && !(et instanceof FlamerWeapon) - && partAvailability < EquipmentType.RATING_C) { - partAvailability = EquipmentType.RATING_C; - partAvailabilityLog.append("
[Non-Flamer Lasers]"); - } - if (et instanceof ACWeapon) { - partAvailability -= 2; - partAvailabilityLog.append("
Autocannon: -2"); - } - if (et instanceof GaussWeapon - || et instanceof FlamerWeapon) { - partAvailability--; - partAvailabilityLog.append("
Gauss Rifle or Flamer: -1"); - } - if (et instanceof AmmoType) { - switch (((AmmoType) et).getAmmoType()) { - case AmmoType.T_AC: - partAvailability -= 2; - partAvailabilityLog.append("
Autocannon Ammo: -2"); - break; - case AmmoType.T_GAUSS: - partAvailability -= 1; - partAvailabilityLog.append("
Gauss Ammo: -1"); - break; - } - if (EnumSet.of(Munitions.M_STANDARD).containsAll( - ((AmmoType) et).getMunitionType())) { - partAvailability--; - partAvailabilityLog.append("
Standard Ammo: -1"); - } - } - } - - if (((getGameYear() < 2950) || (getGameYear() > 3040)) - && (acquisition instanceof Armor || acquisition instanceof MissingMekActuator - || acquisition instanceof MissingMekCockpit - || acquisition instanceof MissingMekLifeSupport - || acquisition instanceof MissingMekLocation - || acquisition instanceof MissingMekSensor)) { - partAvailability--; - partAvailabilityLog.append("
Mek part prior to 2950 or after 3040: - 1"); - } - - int AtBPartsAvailability = findAtBPartsAvailabilityLevel(acquisition, null); - partAvailabilityLog.append("
Total part availability: ").append(partAvailability) - .append("
Current campaign availability: ").append(AtBPartsAvailability) - .append(""); - if (partAvailability > AtBPartsAvailability) { - return new TargetRoll(TargetRoll.IMPOSSIBLE, partAvailabilityLog.toString()); - } - } TargetRoll target = new TargetRoll(skill.getFinalSkillValue(), skill.getSkillLevel().toString()); target.append(acquisition.getAllAcquisitionMods()); - return target; - } - - public @Nullable AtBContract getAttachedAtBContract(Unit unit) { - if (null != unit && null != combatTeams.get(unit.getForceId())) { - return combatTeams.get(unit.getForceId()).getContract(this); - } - return null; - } - - public int findAtBPartsAvailabilityLevel(IAcquisitionWork acquisition, StringBuilder reportBuilder) { - AtBContract contract = (acquisition != null) ? getAttachedAtBContract(acquisition.getUnit()) : null; - - /* - * If the unit is not assigned to a contract, use the least restrictive active - * contract. Don't restrict parts availability by contract if it has not - * started. - */ - if (hasActiveContract()) { - for (final AtBContract c : getActiveAtBContracts()) { - if ((contract == null) - || (c.getPartsAvailabilityLevel() > contract.getPartsAvailabilityLevel())) { - contract = c; - } - } - } - // if we have a contract and it has started - if ((null != contract) && contract.isActiveOn(getLocalDate(), true)) { - if (reportBuilder != null) { - reportBuilder.append(contract.getPartsAvailabilityLevel()).append(" (").append(contract.getType()) - .append(')'); - } - return contract.getPartsAvailabilityLevel(); + if (getCampaignOptions().isUseAtB() && + getCampaignOptions().isRestrictPartsByMission()) { + int AtBPartsAvailability = findAtBPartsAvailabilityLevel(); + target.addModifier(AtBPartsAvailability, "Contract"); } - /* If contract is still null, the unit is not in a contract. */ - final Person person = getLogisticsPerson(); - final int experienceLevel; - if (person == null) { - experienceLevel = SkillType.EXP_ULTRA_GREEN; - } else if (CampaignOptions.S_TECH.equals(getCampaignOptions().getAcquisitionSkill())) { - experienceLevel = person.getBestTechSkill().getExperienceLevel(); - } else { - experienceLevel = person.getSkill(getCampaignOptions().getAcquisitionSkill()).getExperienceLevel(); - } + return target; + } - final int modifier = experienceLevel - SkillType.EXP_REGULAR; + public int findAtBPartsAvailabilityLevel() { + Integer availabilityModifier = null; + for (AtBContract contract : getActiveAtBContracts()) { + int contractAvailability = contract.getPartsAvailabilityLevel(); - if (reportBuilder != null) { - reportBuilder.append(getAtBUnitRatingMod()).append("(unit rating)"); - if (person != null) { - reportBuilder.append(modifier).append('(').append(person.getFullName()).append(", logistics admin)"); - } else { - reportBuilder.append(modifier).append("(no logistics admin)"); + if (availabilityModifier == null || contractAvailability < availabilityModifier) { + availabilityModifier = contractAvailability; } } - return getAtBUnitRatingMod() + modifier; + return Objects.requireNonNullElse(availabilityModifier, 0); } public void resetAstechMinutes() { diff --git a/MekHQ/src/mekhq/campaign/mission/enums/AtBContractType.java b/MekHQ/src/mekhq/campaign/mission/enums/AtBContractType.java index 0aecd6c02e4..5c16bea851e 100644 --- a/MekHQ/src/mekhq/campaign/mission/enums/AtBContractType.java +++ b/MekHQ/src/mekhq/campaign/mission/enums/AtBContractType.java @@ -171,27 +171,23 @@ private int calculateVariableLength(final AtBContract contract) { } /** - * AtB Rules apply an additional -1 from 2950 to 3040, which is superseded by - * MekHQ's era - * variation code + * Determines the availability level of parts and units based on the type of operation being + * conducted. + * + *

The availability level is represented as an integer and varies depending on the specific + * mission type. Higher values indicate worse availability, while lower values signify more + * restricted access to parts. + * + * @return an integer representing the availability level of parts for the current mission type. */ public int calculatePartsAvailabilityLevel() { - switch (this) { - case GUERRILLA_WARFARE: - return 0; - case DIVERSIONARY_RAID: - case OBJECTIVE_RAID: - case RECON_RAID: - case EXTRACTION_RAID: - return 1; - case PLANETARY_ASSAULT: - case RELIEF_DUTY: - return 2; - case PIRATE_HUNTING: - return 3; - default: - return 4; - } + return switch (this) { + case GUERRILLA_WARFARE -> 2; + case DIVERSIONARY_RAID, OBJECTIVE_RAID, RECON_RAID, EXTRACTION_RAID -> 1; + case PLANETARY_ASSAULT, RELIEF_DUTY -> 0; + case PIRATE_HUNTING -> -1; + default -> -2; + }; } /** diff --git a/MekHQ/src/mekhq/gui/CampaignGUI.java b/MekHQ/src/mekhq/gui/CampaignGUI.java index cab97788a8f..8bc21af3e1c 100644 --- a/MekHQ/src/mekhq/gui/CampaignGUI.java +++ b/MekHQ/src/mekhq/gui/CampaignGUI.java @@ -70,9 +70,9 @@ import mekhq.campaign.report.TransportReport; import mekhq.campaign.unit.Unit; import mekhq.campaign.universe.NewsItem; +import mekhq.gui.campaignOptions.CampaignOptionsDialog; import mekhq.gui.dialog.*; import mekhq.gui.dialog.CampaignExportWizard.CampaignExportWizardState; -import mekhq.gui.campaignOptions.CampaignOptionsDialog; import mekhq.gui.dialog.reportDialogs.*; import mekhq.gui.enums.MHQTabType; import mekhq.gui.model.PartsTableModel; @@ -2358,11 +2358,11 @@ private void refreshPartsAvailability() { || CampaignOptions.S_AUTO.equals(getCampaign().getCampaignOptions().getAcquisitionSkill())) { lblPartsAvailabilityRating.setText(""); } else { - StringBuilder report = new StringBuilder(); - int partsAvailability = getCampaign().findAtBPartsAvailabilityLevel(null, report); + int partsAvailability = getCampaign().findAtBPartsAvailabilityLevel(); // FIXME : Localize lblPartsAvailabilityRating - .setText("Campaign Parts Availability: " + partsAvailability + ""); + .setText(String.format("Parts Availability Modifier: %d", + partsAvailability)); } } From f4e4e14bdea3de06e6688ffa2d91b17f203714d1 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Tue, 21 Jan 2025 22:57:40 -0600 Subject: [PATCH 003/112] Refined AtB parts availability modifier logic. Previously, a modifier was always added regardless of its value. Updated the logic to only add the modifier when its value is non-zero, preventing unnecessary modifications and ensuring consistent behavior. --- MekHQ/src/mekhq/campaign/Campaign.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index f69c5b12717..b3266b2b8bc 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -7024,8 +7024,11 @@ public TargetRoll getTargetForAcquisition(final IAcquisitionWork acquisition, if (getCampaignOptions().isUseAtB() && getCampaignOptions().isRestrictPartsByMission()) { - int AtBPartsAvailability = findAtBPartsAvailabilityLevel(); - target.addModifier(AtBPartsAvailability, "Contract"); + int contractAvailability = findAtBPartsAvailabilityLevel(); + + if (contractAvailability != 0) { + target.addModifier(contractAvailability, "Contract"); + } } return target; From 06f6cd553cdd1c1fc8a7abb0fefe97bacbbb9dbf Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Wed, 22 Jan 2025 20:58:13 -0600 Subject: [PATCH 004/112] Update scenario effects to use SupplyCache over SupportPointUpdate Replaced the `SupportPointUpdate` effect with `SupplyCache` in several scenario templates and associated code. Implemented new functionality in `ScenarioObjectiveProcessor` to handle the `SupplyCache` effect, enabling supply cache adjustments during missions. These changes improve clarity and consistency in effect handling. --- .../FacilityHostileExtract.xml | 2 +- .../scenariotemplates/Convoy Interdiction.xml | 2 +- MekHQ/data/scenariotemplates/Convoy Raid.xml | 5 ++--- .../campaign/mission/ObjectiveEffect.java | 4 ++++ .../mission/ScenarioObjectiveProcessor.java | 18 ++++++++++++++++-- 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/MekHQ/data/scenariomodifiers/FacilityHostileExtract.xml b/MekHQ/data/scenariomodifiers/FacilityHostileExtract.xml index 293dbe92ddf..c0196195010 100644 --- a/MekHQ/data/scenariomodifiers/FacilityHostileExtract.xml +++ b/MekHQ/data/scenariomodifiers/FacilityHostileExtract.xml @@ -17,7 +17,7 @@ - SupportPointUpdate + SupplyCache Linear 1 diff --git a/MekHQ/data/scenariotemplates/Convoy Interdiction.xml b/MekHQ/data/scenariotemplates/Convoy Interdiction.xml index e5ec14a33db..06bf4029059 100644 --- a/MekHQ/data/scenariotemplates/Convoy Interdiction.xml +++ b/MekHQ/data/scenariotemplates/Convoy Interdiction.xml @@ -152,7 +152,7 @@ - SupportPointUpdate + SupplyCache Linear 1 diff --git a/MekHQ/data/scenariotemplates/Convoy Raid.xml b/MekHQ/data/scenariotemplates/Convoy Raid.xml index e1af1038685..256ccf466b4 100644 --- a/MekHQ/data/scenariotemplates/Convoy Raid.xml +++ b/MekHQ/data/scenariotemplates/Convoy Raid.xml @@ -147,15 +147,14 @@ - SupportPointUpdate + SupplyCache Linear 1 - Capture intact units from the following force(s) for additional support - points: + Capture intact units from the following force(s) for additional support points: NONE 1 Capture diff --git a/MekHQ/src/mekhq/campaign/mission/ObjectiveEffect.java b/MekHQ/src/mekhq/campaign/mission/ObjectiveEffect.java index 035436a6160..97c756a8a1f 100644 --- a/MekHQ/src/mekhq/campaign/mission/ObjectiveEffect.java +++ b/MekHQ/src/mekhq/campaign/mission/ObjectiveEffect.java @@ -66,6 +66,10 @@ public enum ObjectiveEffectType { * */ SupportPointUpdate("%d Support Points", true), + /* changes the size of supply cache + * + */ + SupplyCache("%d Looted Supplies", true), /* * changes the contract morale up or down */ diff --git a/MekHQ/src/mekhq/campaign/mission/ScenarioObjectiveProcessor.java b/MekHQ/src/mekhq/campaign/mission/ScenarioObjectiveProcessor.java index cfad77a3050..08d6e99664f 100644 --- a/MekHQ/src/mekhq/campaign/mission/ScenarioObjectiveProcessor.java +++ b/MekHQ/src/mekhq/campaign/mission/ScenarioObjectiveProcessor.java @@ -28,13 +28,13 @@ import mekhq.campaign.mission.ObjectiveEffect.ObjectiveEffectType; import mekhq.campaign.mission.enums.ScenarioStatus; import mekhq.campaign.mission.resupplyAndCaches.Resupply; -import mekhq.campaign.mission.resupplyAndCaches.Resupply.ResupplyType; import mekhq.campaign.stratcon.StratconRulesManager; import org.apache.logging.log4j.LogManager; import java.util.*; import static mekhq.campaign.mission.resupplyAndCaches.PerformResupply.performResupply; +import static mekhq.campaign.mission.resupplyAndCaches.Resupply.ResupplyType.RESUPPLY_LOOT; /** * Handles processing for objectives for a scenario that has them @@ -404,6 +404,20 @@ private String processObjectiveEffect(Campaign campaign, ObjectiveEffect effect, } } break; + case SupplyCache: + if (tracker.getMission() instanceof AtBContract contract) { + Resupply resupply = new Resupply(campaign, contract, RESUPPLY_LOOT); + + int effectMultiplier = effect.effectScaling == EffectScalingType.Fixed ? 1 : scaleFactor; + int size = effect.howMuch * effectMultiplier; + + if (dryRun) { + return String.format("A size %d supply cache will be added", size); + } else { + performResupply(resupply, contract, size); + } + } + break; case ContractMoraleUpdate: break; case ContractVictory: @@ -440,7 +454,7 @@ private String processObjectiveEffect(Campaign campaign, ObjectiveEffect effect, if (dropSize > 0) { LogManager.getLogger().info("ScenarioObjectiveProcessor.java"); campaign.addReport("Bonus: Captured Supplies"); - Resupply resupply = new Resupply(campaign, contract, ResupplyType.RESUPPLY_LOOT); + Resupply resupply = new Resupply(campaign, contract, RESUPPLY_LOOT); performResupply(resupply, contract, dropSize); } } From e2abaa4bb4465979a6441c10943feaea6c9f95a2 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Wed, 22 Jan 2025 23:23:44 -0600 Subject: [PATCH 005/112] Refactor dependent removal and personnel cleanup logic Streamlined logic for dependent removal and personnel status checks, incorporating genealogy-based validations to prevent unintended removals. Added an `isActive` method to `Genealogy` to identify active family links and updated resource strings for improved clarity. --- .../mekhq/resources/Campaign.properties | 2 +- MekHQ/src/mekhq/campaign/Campaign.java | 84 +++++++++++++------ .../personnel/familyTree/Genealogy.java | 23 +++++ 3 files changed, 82 insertions(+), 27 deletions(-) diff --git a/MekHQ/resources/mekhq/resources/Campaign.properties b/MekHQ/resources/mekhq/resources/Campaign.properties index ce735dc81a4..1884f237d2f 100644 --- a/MekHQ/resources/mekhq/resources/Campaign.properties +++ b/MekHQ/resources/mekhq/resources/Campaign.properties @@ -82,7 +82,7 @@ turnoverPersonnelKilled.text=You have personnel who have left the unit or divorce.text=%s has divorced %s. #### Unsorted Campaign Resources -dependentLeavesForce.text=%s is no longer traveling with the force. +dependentLeavesForce.text=%s dependent%s have departed the force. dependentJoinsForce.text=%s has started traveling with the force. relativeJoinsForce.text=%s has started traveling with the force. They are %s's %s. relativeJoinsForceSpouse.text=spouse diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index 609721ffd49..73a2412a464 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -92,6 +92,7 @@ import mekhq.campaign.personnel.education.Academy; import mekhq.campaign.personnel.education.EducationController; import mekhq.campaign.personnel.enums.*; +import mekhq.campaign.personnel.familyTree.Genealogy; import mekhq.campaign.personnel.generator.*; import mekhq.campaign.personnel.marriage.AbstractMarriage; import mekhq.campaign.personnel.marriage.DisabledRandomMarriage; @@ -4969,6 +4970,8 @@ private void processRandomDependents() { * @return The updated number of dependents. */ int dependentsRollForRemoval(List dependents, int dependentCapacity) { + List dependentsToRemove = new ArrayList<>(); + if (getCampaignOptions().isUseRandomDependentRemoval()) { for (Person dependent : dependents) { if (!isRemovalEligible(dependent, currentDay)) { @@ -4984,12 +4987,30 @@ int dependentsRollForRemoval(List dependents, int dependentCapacity) { int targetNumber = 5 - getAtBUnitRatingMod(); if (roll <= targetNumber) { - addReport(String.format(resources.getString("dependentLeavesForce.text"), - dependent.getFullTitle())); + dependentsToRemove.add(dependent); - removePerson(dependent, false); + Genealogy genealogy = dependent.getGenealogy(); + for (Person child : genealogy.getChildren()) { + if (child.isChild(currentDay)) { + dependentsToRemove.add(child); + } + } + + Person spouse = genealogy.getSpouse(); + if (spouse.isDependent()) { + dependentsToRemove.add(spouse); + } } } + + if (!dependentsToRemove.isEmpty()) { + addReport(String.format(resources.getString("dependentLeavesForce.text"), + dependentsToRemove.size(), dependentsToRemove.size() == 1 ? "" : "s")); + } + + for (Person dependent : dependentsToRemove) { + dependent.changeStatus(this, currentDay, PersonnelStatus.LEFT); + } } return dependents.size(); @@ -5063,7 +5084,7 @@ public void processPersonnelRemoval() { PersonnelStatus status = person.getStatus(); if (status.isDepartedUnit()) { - if (shouldRemovePerson(person, status)) { + if (shouldRemovePerson(person)) { personnelToRemove.add(person); } } @@ -5080,39 +5101,50 @@ public void processPersonnelRemoval() { /** * Determines whether a person's records should be removed from the campaign - * based on their status and retirement month. + * based on their retirement date, date of death, personnel status, and genealogy activity. + * + *

The method evaluates the following conditions in order: + *

    + *
  • If the person has a retirement date and retirees are exempt from removal as per + * campaign options, the method returns {@code false}.
  • + *
  • If the person has a date of death, and cemeteries are exempt from removal as per + * campaign options, the method returns {@code false}.
  • + *
  • If the person has an active genealogy, the method returns {@code false}.
  • + *
  • If the person's retirement date is more than one month ago, the method returns {@code true}.
  • + *
  • If the person's date of death is more than one month ago, the method returns {@code true}.
  • + *
+ * + *

If none of the above conditions are met, the method returns {@code false}. * * @param person The individual being checked. - * @param status The personnel status of the individual. - * @return true if the person should be removed, false otherwise. + * @return {@code true} if the person should be removed, {@code false} otherwise. */ - private boolean shouldRemovePerson(Person person, PersonnelStatus status) { - // don't remove a character if they are related to a genealogy - // with at least one member still present in the campaign - Map> family = person.getGenealogy().getFamily(); - if (family.keySet().stream() - .flatMap(relationshipType -> family.get(relationshipType).stream()) - .anyMatch(relation -> relation.getStatus().isDepartedUnit())) { + private boolean shouldRemovePerson(Person person) { + // We do these checks first, as they're cheaper than parsing the entire genealogy + LocalDate retirementDate = person.getRetirement(); + if (retirementDate != null && campaignOptions.isUseRemovalExemptRetirees()) { return false; } - int retirementMonthValue; + LocalDate deathDate = person.getDateOfDeath(); + if (deathDate != null && campaignOptions.isUseRemovalExemptCemetery()) { + return false; + } - if (person.getRetirement() != null) { - retirementMonthValue = person.getRetirement().getMonthValue(); - } else { - person.setRetirement(getLocalDate()); + // Do not remove if the character has an active genealogy + Genealogy genealogy = person.getGenealogy(); + + if (genealogy.isActive()) { return false; } - // return true if the individual has left the campaign for over a month - // *AND* - // is dead (and we're not exempting the cemetery) *OR* is retired (and we're not - // exempting retirees) - return (retirementMonthValue < (getLocalDate().getMonthValue() + 1)) && - (((status.isDead()) && (!campaignOptions.isUseRemovalExemptCemetery())) - || ((status.isRetired()) && (!campaignOptions.isUseRemovalExemptRetirees()))); + // Did the departure occur more than a month ago? + LocalDate aMonthAgo = currentDay.minusMonths(1); + if (retirementDate != null && retirementDate.isBefore(aMonthAgo)) { + return true; + } + return deathDate != null && deathDate.isBefore(aMonthAgo); } /** diff --git a/MekHQ/src/mekhq/campaign/personnel/familyTree/Genealogy.java b/MekHQ/src/mekhq/campaign/personnel/familyTree/Genealogy.java index 09151f7cca0..4722b6e85f6 100644 --- a/MekHQ/src/mekhq/campaign/personnel/familyTree/Genealogy.java +++ b/MekHQ/src/mekhq/campaign/personnel/familyTree/Genealogy.java @@ -433,6 +433,29 @@ public void clearGenealogyLinks() { } } + /** + * Checks if there is at least one person in the family who is not marked as "departed". + * + *

The method iterates through all relationship groups in the {@code family} map and checks + * the status of each person. If any person is found whose status is not marked as "departed", + * the method returns {@code true}. Otherwise, it returns {@code false} once all groups have + * been checked. + * + * @return {@code true} if at least one person in the family is active (not "departed"), + * {@code false} otherwise. + */ + public boolean isActive() { + for (List relationshipGroup : family.values()) { + for (Person relation : relationshipGroup) { + if (!relation.getStatus().isDepartedUnit()) { + return true; + } + } + } + + return false; + } + // region File I/O /** * @param pw the PrintWriter to write to From d3b505e7d9b6b3a43c83db1048d630137a0e0d05 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Wed, 22 Jan 2025 23:24:50 -0600 Subject: [PATCH 006/112] Change personnel removal to run on the first day of the month Previously, personnel removal was processed on the last day of the month. This change ensures it now runs at the start of the month, aligning better with other monthly processes and improving data handling consistency. --- MekHQ/src/mekhq/campaign/Campaign.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index 73a2412a464..4d172cb7106 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -4813,15 +4813,13 @@ public boolean newDay() { // check for anything in finances finances.newDay(this, yesterday, getLocalDate()); - // process removal of old personnel data on the last day of each month - if ((campaignOptions.isUsePersonnelRemoval()) - && (currentDay.getMonth().length(false) == currentDay.getDayOfMonth())) { + // process removal of old personnel data on the first day of each month + if ((campaignOptions.isUsePersonnelRemoval()) && (currentDay.getDayOfMonth() == 1)) { processPersonnelRemoval(); } // this duplicates any turnover information so that it is still available on the - // new day. - // otherwise, it's only available if the user inspects history records + // new day. otherwise, it's only available if the user inspects history records if (!turnoverRetirementInformation.isEmpty()) { for (String entry : turnoverRetirementInformation) { addReport(entry); From ed8a677775d57e9804faadace9dc9c3c3a3f511c Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Thu, 23 Jan 2025 13:27:04 -0600 Subject: [PATCH 007/112] Removed outdated random death methods and tests Simplified the random death system by removing unused methods, tests, and configurations including Percentage, Age Range, and associated logic. Updated the interface, properties, and campaign logic to streamline and consolidate random death handling into a single, more realistic implementation. --- .../CampaignOptionsDialog.properties | 10 +- .../mekhq/resources/Personnel.properties | 12 +- MekHQ/src/mekhq/campaign/Campaign.java | 12 +- MekHQ/src/mekhq/campaign/CampaignOptions.java | 160 ------------------ .../personnel/death/AbstractDeath.java | 30 +--- .../personnel/death/AgeRangeRandomDeath.java | 86 ---------- .../death/ExponentialRandomDeath.java | 93 +++++++--- .../death/PercentageRandomDeath.java | 52 ------ .../personnel/enums/RandomDeathMethod.java | 37 ++-- MekHQ/src/mekhq/gui/CampaignGUI.java | 25 +-- .../contents/BiographyTab.java | 46 +---- .../personnel/death/AbstractDeathTest.java | 45 ++--- .../death/AgeRangeRandomDeathTest.java | 88 ---------- .../death/DisabledRandomDeathTest.java | 2 - .../death/ExponentialRandomDeathTest.java | 2 - .../death/PercentageRandomDeathTest.java | 68 -------- .../enums/RandomDeathMethodTest.java | 51 ++---- 17 files changed, 122 insertions(+), 697 deletions(-) delete mode 100644 MekHQ/src/mekhq/campaign/personnel/death/AgeRangeRandomDeath.java delete mode 100644 MekHQ/src/mekhq/campaign/personnel/death/PercentageRandomDeath.java delete mode 100644 MekHQ/unittests/mekhq/campaign/personnel/death/AgeRangeRandomDeathTest.java delete mode 100644 MekHQ/unittests/mekhq/campaign/personnel/death/PercentageRandomDeathTest.java diff --git a/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties b/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties index bdab6ec3b3f..dfd12666037 100644 --- a/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties +++ b/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties @@ -801,13 +801,11 @@ lblExtraRandomOrigin.tooltip=Random origin is randomized to the planetary level # createDeathTab lblDeathTab.text=Death Options \u270E lblKeepMarriedNameUponSpouseDeath.text=Keep Married Name on Spouse Death -lblKeepMarriedNameUponSpouseDeath.tooltip=When a person's spouse dies they will keep their marital\ +lblKeepMarriedNameUponSpouseDeath.tooltip=When a person's spouse dies, they will keep their marital\ \ name instead of returning to their birth name. -lblRandomDeathMethod.text=Random Death Method \u2714 -lblRandomDeathMethod.tooltip=This is the method used to determine if a person dies from random death.\ -
\ -
Recommended: Exponential Random Death is the best system as it closely follows a\ - \ realistic death curve. +lblRandomDeathMethod.text=Use Random Death +lblRandomDeathMethod.tooltip=Should characters randomly die? This system follows a realistic death\ + \ curve based on real world data. lblUseRandomClanPersonnelDeath.text=Enable Random Clan Death lblUseRandomClanPersonnelDeath.tooltip=Allow clan-origin personnel to randomly die. lblUseRandomPrisonerDeath.text=Enable Random Prisoner Death diff --git a/MekHQ/resources/mekhq/resources/Personnel.properties b/MekHQ/resources/mekhq/resources/Personnel.properties index 31d89a0efe3..0e049e46610 100644 --- a/MekHQ/resources/mekhq/resources/Personnel.properties +++ b/MekHQ/resources/mekhq/resources/Personnel.properties @@ -503,12 +503,12 @@ Profession.CIVILIAN.toolTipText=The Civilian Profession contains Dependents and # RandomDeathMethod Enum RandomDeathMethod.NONE.text=Disabled RandomDeathMethod.NONE.toolTipText=Random death is disabled -RandomDeathMethod.PERCENTAGE.text=Percentage -RandomDeathMethod.PERCENTAGE.toolTipText=This checks if a random value is lower than the percentage to determine if an eligible person dies on a given day. -RandomDeathMethod.EXPONENTIAL.text=Exponential -RandomDeathMethod.EXPONENTIAL.toolTipText=This uses an exponential equation in the format c * 10^n * e^(k * age) to determine the probability of a person dying a random death on a given day.
Infant mortality is significantly lower than would be expected when using this equation.
The default equation was derived from the death rate by age and sex in the United States of America in 2018, as per Statista -RandomDeathMethod.AGE_RANGE.text=Age Range -RandomDeathMethod.AGE_RANGE.toolTipText=This uses the number of deaths per 100,000 people by gender in an age range to determine the probability of a person dying from random death on a given day.
This handles infant mortality reasonably well, although it is more spread out than is realistic.
During calculation, the numbers provided are divided by 100,000, then by 365.25, and then by the number of years within the range (assuming 15 for the 85 and older range), to determine the daily chance of a person within that range dying.
The default values are the death rate by age and sex in the United States of America in 2018, as per Statista. +RandomDeathMethod.RANDOM.text=Enabled +RandomDeathMethod.RANDOM.toolTipText=This uses an exponential equation in the format c *\ + \ 10^n * e^(k * age) to determine the probability of a person dying a random death on a given\ + \ day.
Infant mortality is significantly lower than would be expected when using this equation.\ + \
The default equation was derived from the death rate by age and sex in the United States of\ + \ America in 2018, as per Statista # RandomDependentMethod Enum RandomDependentMethod.NONE.text=Disabled diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index 609721ffd49..ab83950468f 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -4340,11 +4340,6 @@ public void processNewDayPersonnel() { } // Daily events - if (getDeath().processNewDay(this, getLocalDate(), person)) { - // The person has died, so don't continue to process the dead - continue; - } - person.resetMinutesLeft(); person.setAcquisition(0); @@ -4354,7 +4349,10 @@ public void processNewDayPersonnel() { // Weekly events if (currentDay.getDayOfWeek() == DayOfWeek.MONDAY) { - processWeeklyRelationshipEvents(person); + if (!getDeath().processNewWeek(this, getLocalDate(), person)) { + // If the character has died, we don't need to process relationship events + processWeeklyRelationshipEvents(person); + } processWeeklyEdgeResets(person); } @@ -4772,7 +4770,7 @@ public boolean newDay() { personnelMarket.generatePersonnelForDay(this); // TODO : AbstractContractMarket : Uncomment - // getContractMarket().processNewDay(this); + // getContractMarket().processNewWeek(this); unitMarket.processNewDay(this); // Process New Day for AtB diff --git a/MekHQ/src/mekhq/campaign/CampaignOptions.java b/MekHQ/src/mekhq/campaign/CampaignOptions.java index a44bc9d57b4..5068bbc35bc 100644 --- a/MekHQ/src/mekhq/campaign/CampaignOptions.java +++ b/MekHQ/src/mekhq/campaign/CampaignOptions.java @@ -365,14 +365,7 @@ public static String getTechLevelName(final int techLevel) { private boolean keepMarriedNameUponSpouseDeath; private RandomDeathMethod randomDeathMethod; private Map enabledRandomDeathAgeGroups; - private boolean useRandomClanPersonnelDeath; - private boolean useRandomPrisonerDeath; private boolean useRandomDeathSuicideCause; - private double percentageRandomDeathChance; - private double[] exponentialRandomDeathMaleValues; - private double[] exponentialRandomDeathFemaleValues; - private Map ageRangeRandomDeathMaleValues; - private Map ageRangeRandomDeathFemaleValues; // endregion Life Paths Tab //region Turnover and Retention @@ -966,39 +959,6 @@ public CampaignOptions() { getEnabledRandomDeathAgeGroups().put(AgeGroup.TODDLER, false); getEnabledRandomDeathAgeGroups().put(AgeGroup.BABY, false); setUseRandomDeathSuicideCause(false); - setUseRandomClanPersonnelDeath(true); - setUseRandomPrisonerDeath(true); - setPercentageRandomDeathChance(0.00002); - // The following four setups are all based on the 2018 US death rate: - // https://www.statista.com/statistics/241572/death-rate-by-age-and-sex-in-the-us/ - setExponentialRandomDeathMaleValues(5.4757, -7.0, 0.0709); // base equation of 2 * 10^-4 * e^(0.0709 * age) per - // year, divided by 365.25 - setExponentialRandomDeathFemaleValues(2.4641, -7.0, 0.0752); // base equation of 9 * 10^-5 * e^(0.0752 * age) - // per year, divided by 365.25 - setAgeRangeRandomDeathMaleValues(new HashMap<>()); - getAgeRangeRandomDeathMaleValues().put(TenYearAgeRange.UNDER_ONE, 613.1); - getAgeRangeRandomDeathMaleValues().put(TenYearAgeRange.ONE_FOUR, 27.5); - getAgeRangeRandomDeathMaleValues().put(TenYearAgeRange.FIVE_FOURTEEN, 14.7); - getAgeRangeRandomDeathMaleValues().put(TenYearAgeRange.FIFTEEN_TWENTY_FOUR, 100.1); - getAgeRangeRandomDeathMaleValues().put(TenYearAgeRange.TWENTY_FIVE_THIRTY_FOUR, 176.1); - getAgeRangeRandomDeathMaleValues().put(TenYearAgeRange.THIRTY_FIVE_FORTY_FOUR, 249.5); - getAgeRangeRandomDeathMaleValues().put(TenYearAgeRange.FORTY_FIVE_FIFTY_FOUR, 491.8); - getAgeRangeRandomDeathMaleValues().put(TenYearAgeRange.FIFTY_FIVE_SIXTY_FOUR, 1119.0); - getAgeRangeRandomDeathMaleValues().put(TenYearAgeRange.SIXTY_FIVE_SEVENTY_FOUR, 2196.5); - getAgeRangeRandomDeathMaleValues().put(TenYearAgeRange.SEVENTY_FIVE_EIGHTY_FOUR, 5155.0); - getAgeRangeRandomDeathMaleValues().put(TenYearAgeRange.EIGHTY_FIVE_OR_OLDER, 14504.0); - setAgeRangeRandomDeathFemaleValues(new HashMap<>()); - getAgeRangeRandomDeathFemaleValues().put(TenYearAgeRange.UNDER_ONE, 500.0); - getAgeRangeRandomDeathFemaleValues().put(TenYearAgeRange.ONE_FOUR, 20.4); - getAgeRangeRandomDeathFemaleValues().put(TenYearAgeRange.FIVE_FOURTEEN, 11.8); - getAgeRangeRandomDeathFemaleValues().put(TenYearAgeRange.FIFTEEN_TWENTY_FOUR, 38.8); - getAgeRangeRandomDeathFemaleValues().put(TenYearAgeRange.TWENTY_FIVE_THIRTY_FOUR, 80.0); - getAgeRangeRandomDeathFemaleValues().put(TenYearAgeRange.THIRTY_FIVE_FORTY_FOUR, 140.2); - getAgeRangeRandomDeathFemaleValues().put(TenYearAgeRange.FORTY_FIVE_FIFTY_FOUR, 302.5); - getAgeRangeRandomDeathFemaleValues().put(TenYearAgeRange.FIFTY_FIVE_SIXTY_FOUR, 670.0); - getAgeRangeRandomDeathFemaleValues().put(TenYearAgeRange.SIXTY_FIVE_SEVENTY_FOUR, 1421.0); - getAgeRangeRandomDeathFemaleValues().put(TenYearAgeRange.SEVENTY_FIVE_EIGHTY_FOUR, 3788.0); - getAgeRangeRandomDeathFemaleValues().put(TenYearAgeRange.EIGHTY_FIVE_OR_OLDER, 12870.0); // endregion Life Paths Tab // region Turnover and Retention @@ -3019,22 +2979,6 @@ public void setEnabledRandomDeathAgeGroups(final Map enabledR this.enabledRandomDeathAgeGroups = enabledRandomDeathAgeGroups; } - public boolean isUseRandomClanPersonnelDeath() { - return useRandomClanPersonnelDeath; - } - - public void setUseRandomClanPersonnelDeath(final boolean useRandomClanPersonnelDeath) { - this.useRandomClanPersonnelDeath = useRandomClanPersonnelDeath; - } - - public boolean isUseRandomPrisonerDeath() { - return useRandomPrisonerDeath; - } - - public void setUseRandomPrisonerDeath(final boolean useRandomPrisonerDeath) { - this.useRandomPrisonerDeath = useRandomPrisonerDeath; - } - public boolean isUseRandomDeathSuicideCause() { return useRandomDeathSuicideCause; } @@ -3042,46 +2986,6 @@ public boolean isUseRandomDeathSuicideCause() { public void setUseRandomDeathSuicideCause(final boolean useRandomDeathSuicideCause) { this.useRandomDeathSuicideCause = useRandomDeathSuicideCause; } - - public double getPercentageRandomDeathChance() { - return percentageRandomDeathChance; - } - - public void setPercentageRandomDeathChance(final double percentageRandomDeathChance) { - this.percentageRandomDeathChance = percentageRandomDeathChance; - } - - public double[] getExponentialRandomDeathMaleValues() { - return exponentialRandomDeathMaleValues; - } - - public void setExponentialRandomDeathMaleValues(final double... exponentialRandomDeathMaleValues) { - this.exponentialRandomDeathMaleValues = exponentialRandomDeathMaleValues; - } - - public double[] getExponentialRandomDeathFemaleValues() { - return exponentialRandomDeathFemaleValues; - } - - public void setExponentialRandomDeathFemaleValues(final double... exponentialRandomDeathFemaleValues) { - this.exponentialRandomDeathFemaleValues = exponentialRandomDeathFemaleValues; - } - - public Map getAgeRangeRandomDeathMaleValues() { - return ageRangeRandomDeathMaleValues; - } - - public void setAgeRangeRandomDeathMaleValues(final Map ageRangeRandomDeathMaleValues) { - this.ageRangeRandomDeathMaleValues = ageRangeRandomDeathMaleValues; - } - - public Map getAgeRangeRandomDeathFemaleValues() { - return ageRangeRandomDeathFemaleValues; - } - - public void setAgeRangeRandomDeathFemaleValues(final Map ageRangeRandomDeathFemaleValues) { - this.ageRangeRandomDeathFemaleValues = ageRangeRandomDeathFemaleValues; - } // endregion Death // region Awards @@ -5127,23 +5031,10 @@ public void writeToXml(final PrintWriter pw, int indent) { MHQXMLUtility.writeSimpleXMLTag(pw, indent, entry.getKey().name(), entry.getValue()); } MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, "enabledRandomDeathAgeGroups"); - MHQXMLUtility.writeSimpleXMLTag(pw, indent, "useRandomClanPersonnelDeath", isUseRandomClanPersonnelDeath()); - MHQXMLUtility.writeSimpleXMLTag(pw, indent, "useRandomPrisonerDeath", isUseRandomPrisonerDeath()); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "useRandomDeathSuicideCause", isUseRandomDeathSuicideCause()); - MHQXMLUtility.writeSimpleXMLTag(pw, indent, "percentageRandomDeathChance", getPercentageRandomDeathChance()); - MHQXMLUtility.writeSimpleXMLTag(pw, indent, "exponentialRandomDeathMaleValues", - getExponentialRandomDeathMaleValues()); - MHQXMLUtility.writeSimpleXMLTag(pw, indent, "exponentialRandomDeathFemaleValues", - getExponentialRandomDeathFemaleValues()); MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, "ageRangeRandomDeathMaleValues"); - for (final Entry entry : getAgeRangeRandomDeathMaleValues().entrySet()) { - MHQXMLUtility.writeSimpleXMLTag(pw, indent, entry.getKey().name(), entry.getValue()); - } MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, "ageRangeRandomDeathMaleValues"); MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, "ageRangeRandomDeathFemaleValues"); - for (final Entry entry : getAgeRangeRandomDeathFemaleValues().entrySet()) { - MHQXMLUtility.writeSimpleXMLTag(pw, indent, entry.getKey().name(), entry.getValue()); - } MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, "ageRangeRandomDeathFemaleValues"); // endregion Death // endregion Life Paths Tab @@ -5949,59 +5840,8 @@ public static CampaignOptions generateCampaignOptionsFromXml(Node wn, Version ve } } - } else if (wn2.getNodeName().equalsIgnoreCase("useRandomClanPersonnelDeath")) { - retVal.setUseRandomClanPersonnelDeath(Boolean.parseBoolean(wn2.getTextContent().trim())); - } else if (wn2.getNodeName().equalsIgnoreCase("useRandomPrisonerDeath")) { - retVal.setUseRandomPrisonerDeath(Boolean.parseBoolean(wn2.getTextContent().trim())); } else if (wn2.getNodeName().equalsIgnoreCase("useRandomDeathSuicideCause")) { retVal.setUseRandomDeathSuicideCause(Boolean.parseBoolean(wn2.getTextContent().trim())); - } else if (wn2.getNodeName().equalsIgnoreCase("percentageRandomDeathChance")) { - retVal.setPercentageRandomDeathChance(Double.parseDouble(wn2.getTextContent().trim())); - } else if (wn2.getNodeName().equalsIgnoreCase("exponentialRandomDeathMaleValues")) { - final String[] values = wn2.getTextContent().trim().split(","); - retVal.setExponentialRandomDeathMaleValues(Arrays.stream(values) - .mapToDouble(Double::parseDouble) - .toArray()); - } else if (wn2.getNodeName().equalsIgnoreCase("exponentialRandomDeathFemaleValues")) { - final String[] values = wn2.getTextContent().trim().split(","); - retVal.setExponentialRandomDeathFemaleValues(Arrays.stream(values) - .mapToDouble(Double::parseDouble) - .toArray()); - } else if (wn2.getNodeName().equalsIgnoreCase("ageRangeRandomDeathMaleValues")) { - if (!wn2.hasChildNodes()) { - continue; - } - final NodeList nl2 = wn2.getChildNodes(); - for (int i = 0; i < nl2.getLength(); i++) { - final Node wn3 = nl2.item(i); - try { - retVal.getAgeRangeRandomDeathMaleValues().put( - TenYearAgeRange.valueOf(wn3.getNodeName()), - Double.parseDouble(wn3.getTextContent().trim())); - } catch (Exception ignored) { - - } - } - } else if (wn2.getNodeName().equalsIgnoreCase("ageRangeRandomDeathFemaleValues")) { - if (!wn2.hasChildNodes()) { - continue; - } - final NodeList nl2 = wn2.getChildNodes(); - for (int i = 0; i < nl2.getLength(); i++) { - final Node wn3 = nl2.item(i); - try { - retVal.getAgeRangeRandomDeathFemaleValues().put( - TenYearAgeRange.valueOf(wn3.getNodeName()), - Double.parseDouble(wn3.getTextContent().trim())); - } catch (Exception ignored) { - - } - } - // endregion Death - // endregion Life Paths Tab - - // region Finances Tab - // region Turnover and Retention } else if (wn2.getNodeName().equalsIgnoreCase("useRandomRetirement")) { retVal.setUseRandomRetirement(Boolean.parseBoolean(wn2.getTextContent().trim())); } else if (wn2.getNodeName().equalsIgnoreCase("turnoverBaseTn")) { diff --git a/MekHQ/src/mekhq/campaign/personnel/death/AbstractDeath.java b/MekHQ/src/mekhq/campaign/personnel/death/AbstractDeath.java index 4abac2e98be..e31879e0510 100644 --- a/MekHQ/src/mekhq/campaign/personnel/death/AbstractDeath.java +++ b/MekHQ/src/mekhq/campaign/personnel/death/AbstractDeath.java @@ -54,8 +54,6 @@ public abstract class AbstractDeath { // region Variable Declarations private final RandomDeathMethod method; private Map enabledAgeGroups; - private boolean useRandomClanPersonnelDeath; - private boolean useRandomPrisonerDeath; private final boolean enableRandomDeathSuicideCause; private final Map>> causes; @@ -68,8 +66,6 @@ protected AbstractDeath(final RandomDeathMethod method, final CampaignOptions op final boolean initializeCauses) { this.method = method; setEnabledAgeGroups(options.getEnabledRandomDeathAgeGroups()); - setUseRandomClanPersonnelDeath(options.isUseRandomClanPersonnelDeath()); - setUseRandomPrisonerDeath(options.isUseRandomPrisonerDeath()); this.enableRandomDeathSuicideCause = options.isUseRandomDeathSuicideCause(); this.causes = new HashMap<>(); if (initializeCauses && !method.isNone()) { @@ -91,22 +87,6 @@ public void setEnabledAgeGroups(final Map enabledAgeGroups) { this.enabledAgeGroups = enabledAgeGroups; } - public boolean isUseRandomClanPersonnelDeath() { - return useRandomClanPersonnelDeath; - } - - public void setUseRandomClanPersonnelDeath(final boolean useRandomClanPersonnelDeath) { - this.useRandomClanPersonnelDeath = useRandomClanPersonnelDeath; - } - - public boolean isUseRandomPrisonerDeath() { - return useRandomPrisonerDeath; - } - - public void setUseRandomPrisonerDeath(final boolean useRandomPrisonerDeath) { - this.useRandomPrisonerDeath = useRandomPrisonerDeath; - } - public boolean isEnableRandomDeathSuicideCause() { return enableRandomDeathSuicideCause; } @@ -133,10 +113,6 @@ public Map>> get return resources.getString("cannotDie.Immortal.text"); } else if (!getEnabledAgeGroups().get(ageGroup)) { return resources.getString("cannotDie.AgeGroupDisabled.text"); - } else if (!isUseRandomClanPersonnelDeath() && person.isClanPersonnel()) { - return resources.getString("cannotDie.RandomClanPersonnel.text"); - } else if (!isUseRandomPrisonerDeath() && person.getPrisonerStatus().isCurrentPrisoner()) { - return resources.getString("cannotDie.RandomPrisoner.text"); } } @@ -145,14 +121,14 @@ public Map>> get // region New Day /** - * Processes new day random death for an individual. + * Processes new week random death for an individual. * * @param campaign the campaign to process * @param today the current day * @param person the person to process */ - public boolean processNewDay(final Campaign campaign, final LocalDate today, - final Person person) { + public boolean processNewWeek(final Campaign campaign, final LocalDate today, + final Person person) { final int age = person.getAge(today); final AgeGroup ageGroup = AgeGroup.determineAgeGroup(age); if (canDie(person, ageGroup, true) != null) { diff --git a/MekHQ/src/mekhq/campaign/personnel/death/AgeRangeRandomDeath.java b/MekHQ/src/mekhq/campaign/personnel/death/AgeRangeRandomDeath.java deleted file mode 100644 index 4f9f59315a5..00000000000 --- a/MekHQ/src/mekhq/campaign/personnel/death/AgeRangeRandomDeath.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (c) 2021-2022 - 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 as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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. - * - * You should have received a copy of the GNU General Public License - * along with MekHQ. If not, see . - */ -package mekhq.campaign.personnel.death; - -import megamek.common.Compute; -import megamek.common.enums.Gender; -import mekhq.campaign.CampaignOptions; -import mekhq.campaign.personnel.enums.RandomDeathMethod; -import mekhq.campaign.personnel.enums.TenYearAgeRange; - -import java.util.HashMap; -import java.util.Map; - -public class AgeRangeRandomDeath extends AbstractDeath { - //region Variable Declarations - private Map male; - private Map female; - //endregion Variable Declarations - - //region Constructors - public AgeRangeRandomDeath(final CampaignOptions options, final boolean initializeCauses) { - super(RandomDeathMethod.AGE_RANGE, options, initializeCauses); - adjustRangeValues(options); - } - //endregion Constructors - - //region Getters/Setters - public Map getMale() { - return male; - } - - public void setMale(final Map male) { - this.male = male; - } - - public Map getFemale() { - return female; - } - - public void setFemale(final Map female) { - this.female = female; - } - //endregion Getters/Setters - - /** - * Odds are over an entire year per 100,000 people, so we need to adjust the numbers to be - * the individual odds for a day. We do this now, so it only occurs once per option set. - * @param options the options to set the ranges based on - */ - public void adjustRangeValues(final CampaignOptions options) { - setMale(new HashMap<>()); - setFemale(new HashMap<>()); - final double adjustment = 365.25 * 100000.0; - for (final TenYearAgeRange ageRange : TenYearAgeRange.values()) { - getMale().put(ageRange, options.getAgeRangeRandomDeathMaleValues().get(ageRange) / adjustment); - getFemale().put(ageRange, options.getAgeRangeRandomDeathFemaleValues().get(ageRange) / adjustment); - } - } - - /** - * @param age the person's age - * @param gender the person's gender - * @return true if the person is selected to randomly die - */ - @Override - public boolean randomlyDies(final int age, final Gender gender) { - final TenYearAgeRange ageRange = TenYearAgeRange.determineAgeRange(age); - return Compute.randomFloat() < ((gender.isMale() ? getMale() : getFemale()).get(ageRange)); - } -} diff --git a/MekHQ/src/mekhq/campaign/personnel/death/ExponentialRandomDeath.java b/MekHQ/src/mekhq/campaign/personnel/death/ExponentialRandomDeath.java index a3a65ce2706..39311d13e69 100644 --- a/MekHQ/src/mekhq/campaign/personnel/death/ExponentialRandomDeath.java +++ b/MekHQ/src/mekhq/campaign/personnel/death/ExponentialRandomDeath.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2022 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2021-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * @@ -23,49 +23,90 @@ import mekhq.campaign.CampaignOptions; import mekhq.campaign.personnel.enums.RandomDeathMethod; +/** + * Implements a random death generator using gender-specific exponential equations. + * These equations estimate the probability of death based on the person's age, gender, + * and coefficients derived from real-world data (US death statistics in 2018). + * The death rates are calculated weekly, compared with a randomly generated value, + * and returned as a boolean indicating whether the person dies or not. + */ + public class ExponentialRandomDeath extends AbstractDeath { //region Variable Declarations - private double[] male; - private double[] female; + /** + *

An array of constants representing the male-specific coefficients (c, n, k) + * used in the gender-dependent exponential death equation in the format:

+ * + *
c * 10^n * e^(k * age)
+ */ + private final double[] MALE_DEATH_RATE = new double[]{5.4757, -7.0, 0.0709}; + + /** + *

An array of constants representing the female-specific coefficients (c, n, k) + * used in the gender-dependent exponential death equation in the format:

+ * + *
c * 10^n * e^(k * age)
+ */ + private final double[] FEMALE_DEATH_RATE = new double[]{2.4641, -7.0, 0.0752}; //endregion Variable Declarations //region Constructors + /** + * Constructor for ExponentialRandomDeath. + * Initializes the death method type, campaign options, and causes of death. + * + * @param options The campaign options object that contains relevant settings. + * @param initializeCauses Whether to initialize random causes of death. + */ public ExponentialRandomDeath(final CampaignOptions options, final boolean initializeCauses) { - super(RandomDeathMethod.EXPONENTIAL, options, initializeCauses); - setMale(options.getExponentialRandomDeathMaleValues()); - setFemale(options.getExponentialRandomDeathFemaleValues()); + super(RandomDeathMethod.RANDOM, options, initializeCauses); } //endregion Constructors //region Getters/Setters - public double[] getMale() { - return male; - } - - public void setMale(final double... male) { - this.male = male; - } - - public double[] getFemale() { - return female; + /** + * Gets the coefficients representing the male exponential death rate equation. + * + * @return The male-specific death rate coefficients (c, n, k). + */ + public double[] getMaleDeathRate() { + return MALE_DEATH_RATE; } - public void setFemale(final double... female) { - this.female = female; + /** + * Gets the coefficients representing the female exponential death rate equation. + * + * @return The female-specific death rate coefficients (c, n, k). + */ + public double[] getFemaleDeathRate() { + return FEMALE_DEATH_RATE; } //endregion Getters/Setters /** - * Determines if a person dies a random death based on gender-dependent exponential equations in - * the format c * 10^n * e^(k * age). - * @param age the person's age - * @param gender the person's gender - * @return true if the person is selected to randomly die + * Determines if a person dies a random death based on gender-specific exponential equations. + * The calculation uses weekly probabilities derived from gender-specific coefficients and age, + * compared to a randomly generated value. + * + *

The exponential equation used is:

+ *
c * 10^n * e^(k * age * 7)
+ * + * @param age The person's age. + * @param gender The person's gender. + * @return {@code true} if the person is selected to randomly die, {@code false} otherwise. */ @Override public boolean randomlyDies(final int age, final Gender gender) { - return Compute.randomFloat() < (gender.isMale() - ? (getMale()[0] * Math.pow(10, getMale()[1]) * Math.exp(getMale()[2] * age)) - : (getFemale()[0] * Math.pow(10, getFemale()[1]) * Math.exp(getFemale()[2] * age))); + double deathRate; + if (gender.isMale()) { + deathRate = getMaleDeathRate()[0] * Math.pow(10, getMaleDeathRate()[1]) * + Math.exp(getMaleDeathRate()[2] * age * 7); // Multiply age by 7 for weekly rate + } else { + deathRate = getFemaleDeathRate()[0] * Math.pow(10, getFemaleDeathRate()[1]) * + Math.exp(getFemaleDeathRate()[2] * age * 7); // Multiply age by 7 for weekly rate + } + + // Compare the random float with the calculated weekly death rate + return Compute.randomFloat() < deathRate; } } diff --git a/MekHQ/src/mekhq/campaign/personnel/death/PercentageRandomDeath.java b/MekHQ/src/mekhq/campaign/personnel/death/PercentageRandomDeath.java deleted file mode 100644 index e097f00668b..00000000000 --- a/MekHQ/src/mekhq/campaign/personnel/death/PercentageRandomDeath.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (c) 2022 - 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 as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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. - * - * You should have received a copy of the GNU General Public License - * along with MekHQ. If not, see . - */ -package mekhq.campaign.personnel.death; - -import megamek.common.Compute; -import megamek.common.enums.Gender; -import mekhq.campaign.CampaignOptions; -import mekhq.campaign.personnel.enums.RandomDeathMethod; - -public class PercentageRandomDeath extends AbstractDeath { - //region Variable Declarations - private double percentage; - //endregion Variable Declarations - - //region Constructors - public PercentageRandomDeath(final CampaignOptions options, final boolean initializeCauses) { - super(RandomDeathMethod.PERCENTAGE, options, initializeCauses); - setPercentage(options.getPercentageRandomDeathChance()); - } - //endregion Constructors - - //region Getters/Setters - public double getPercentage() { - return percentage; - } - - public void setPercentage(final double percentage) { - this.percentage = percentage; - } - //endregion Getters/Setters - - @Override - public boolean randomlyDies(final int age, final Gender gender) { - return Compute.randomFloat() < getPercentage(); - } -} diff --git a/MekHQ/src/mekhq/campaign/personnel/enums/RandomDeathMethod.java b/MekHQ/src/mekhq/campaign/personnel/enums/RandomDeathMethod.java index 7de03f69d31..468525c5d42 100644 --- a/MekHQ/src/mekhq/campaign/personnel/enums/RandomDeathMethod.java +++ b/MekHQ/src/mekhq/campaign/personnel/enums/RandomDeathMethod.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2022 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2020-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * @@ -20,16 +20,16 @@ import mekhq.MekHQ; import mekhq.campaign.CampaignOptions; -import mekhq.campaign.personnel.death.*; +import mekhq.campaign.personnel.death.AbstractDeath; +import mekhq.campaign.personnel.death.DisabledRandomDeath; +import mekhq.campaign.personnel.death.ExponentialRandomDeath; import java.util.ResourceBundle; public enum RandomDeathMethod { //region Enum Declarations NONE("RandomDeathMethod.NONE.text", "RandomDeathMethod.NONE.toolTipText"), - PERCENTAGE("RandomDeathMethod.PERCENTAGE.text", "RandomDeathMethod.PERCENTAGE.toolTipText"), - EXPONENTIAL("RandomDeathMethod.EXPONENTIAL.text", "RandomDeathMethod.EXPONENTIAL.toolTipText"), - AGE_RANGE("RandomDeathMethod.AGE_RANGE.text", "RandomDeathMethod.AGE_RANGE.toolTipText"); + RANDOM("RandomDeathMethod.RANDOM.text", "RandomDeathMethod.RANDOM.toolTipText"); //endregion Enum Declarations //region Variable Declarations @@ -57,16 +57,8 @@ public boolean isNone() { return this == NONE; } - public boolean isPercentage() { - return this == PERCENTAGE; - } - - public boolean isExponential() { - return this == EXPONENTIAL; - } - - public boolean isAgeRange() { - return this == AGE_RANGE; + public boolean isRandom() { + return this == RANDOM; } //endregion Boolean Comparison Methods @@ -75,17 +67,10 @@ public AbstractDeath getMethod(final CampaignOptions options) { } public AbstractDeath getMethod(final CampaignOptions options, final boolean initializeCauses) { - switch (this) { - case PERCENTAGE: - return new PercentageRandomDeath(options, initializeCauses); - case EXPONENTIAL: - return new ExponentialRandomDeath(options, initializeCauses); - case AGE_RANGE: - return new AgeRangeRandomDeath(options, initializeCauses); - case NONE: - default: - return new DisabledRandomDeath(options, initializeCauses); - } + return switch (this) { + case RANDOM -> new ExponentialRandomDeath(options, initializeCauses); + case NONE -> new DisabledRandomDeath(options, initializeCauses); + }; } @Override diff --git a/MekHQ/src/mekhq/gui/CampaignGUI.java b/MekHQ/src/mekhq/gui/CampaignGUI.java index cab97788a8f..b367486e130 100644 --- a/MekHQ/src/mekhq/gui/CampaignGUI.java +++ b/MekHQ/src/mekhq/gui/CampaignGUI.java @@ -54,9 +54,6 @@ import mekhq.campaign.personnel.Person; import mekhq.campaign.personnel.SkillType; import mekhq.campaign.personnel.autoAwards.AutoAwardsController; -import mekhq.campaign.personnel.death.AgeRangeRandomDeath; -import mekhq.campaign.personnel.death.ExponentialRandomDeath; -import mekhq.campaign.personnel.death.PercentageRandomDeath; import mekhq.campaign.personnel.divorce.RandomDivorce; import mekhq.campaign.personnel.enums.*; import mekhq.campaign.personnel.marriage.RandomMarriage; @@ -70,9 +67,9 @@ import mekhq.campaign.report.TransportReport; import mekhq.campaign.unit.Unit; import mekhq.campaign.universe.NewsItem; +import mekhq.gui.campaignOptions.CampaignOptionsDialog; import mekhq.gui.dialog.*; import mekhq.gui.dialog.CampaignExportWizard.CampaignExportWizardState; -import mekhq.gui.campaignOptions.CampaignOptionsDialog; import mekhq.gui.dialog.reportDialogs.*; import mekhq.gui.enums.MHQTabType; import mekhq.gui.model.PartsTableModel; @@ -1493,26 +1490,6 @@ private void menuOptionsActionPerformed(final ActionEvent evt) { if ((randomDeathMethod != newOptions.getRandomDeathMethod()) || (useRandomDeathSuicideCause != newOptions.isUseRandomDeathSuicideCause())) { getCampaign().setDeath(newOptions.getRandomDeathMethod().getMethod(newOptions)); - } else { - getCampaign().getDeath().setUseRandomClanPersonnelDeath(newOptions.isUseRandomClanPersonnelDeath()); - getCampaign().getDeath().setUseRandomPrisonerDeath(newOptions.isUseRandomPrisonerDeath()); - switch (getCampaign().getDeath().getMethod()) { - case PERCENTAGE: - ((PercentageRandomDeath) getCampaign().getDeath()).setPercentage( - newOptions.getPercentageRandomDeathChance()); - break; - case EXPONENTIAL: - ((ExponentialRandomDeath) getCampaign().getDeath()).setMale( - newOptions.getExponentialRandomDeathMaleValues()); - ((ExponentialRandomDeath) getCampaign().getDeath()).setFemale( - newOptions.getExponentialRandomDeathFemaleValues()); - break; - case AGE_RANGE: - ((AgeRangeRandomDeath) getCampaign().getDeath()).adjustRangeValues(newOptions); - break; - default: - break; - } } if (randomDivorceMethod != newOptions.getRandomDivorceMethod()) { diff --git a/MekHQ/src/mekhq/gui/campaignOptions/contents/BiographyTab.java b/MekHQ/src/mekhq/gui/campaignOptions/contents/BiographyTab.java index 7721d74041f..7256d5fbc13 100644 --- a/MekHQ/src/mekhq/gui/campaignOptions/contents/BiographyTab.java +++ b/MekHQ/src/mekhq/gui/campaignOptions/contents/BiographyTab.java @@ -38,7 +38,6 @@ import mekhq.gui.panes.RankSystemsPane; import javax.swing.*; -import javax.swing.JSpinner.NumberEditor; import java.awt.*; import java.util.*; @@ -109,14 +108,10 @@ public class BiographyTab { //end Backgrounds Tab //start Death Tab - private JCheckBox chkKeepMarriedNameUponSpouseDeath; private JLabel lblRandomDeathMethod; private MMComboBox comboRandomDeathMethod; - private JCheckBox chkUseRandomClanPersonnelDeath; - private JCheckBox chkUseRandomPrisonerDeath; + private JCheckBox chkKeepMarriedNameUponSpouseDeath; private JCheckBox chkUseRandomDeathSuicideCause; - private JLabel lblPercentageRandomDeathChance; - private JSpinner spnPercentageRandomDeathChance; private JPanel pnlDeathAgeGroup; private Map chkEnabledRandomDeathAgeGroups; @@ -281,14 +276,10 @@ private void initializeEducationTab() { * */ private void initializeDeathTab() { - chkKeepMarriedNameUponSpouseDeath = new JCheckBox(); lblRandomDeathMethod = new JLabel(); comboRandomDeathMethod = new MMComboBox<>("comboRandomDeathMethod", RandomDeathMethod.values()); - chkUseRandomClanPersonnelDeath = new JCheckBox(); - chkUseRandomPrisonerDeath = new JCheckBox(); + chkKeepMarriedNameUponSpouseDeath = new JCheckBox(); chkUseRandomDeathSuicideCause = new JCheckBox(); - lblPercentageRandomDeathChance = new JLabel(); - spnPercentageRandomDeathChance = new JSpinner(); pnlDeathAgeGroup = new JPanel(); chkEnabledRandomDeathAgeGroups = new HashMap<>(); @@ -756,8 +747,6 @@ public JPanel createDeathTab() { getImageDirectory() + "logo_clan_fire_mandrills.png"); // Contents - chkKeepMarriedNameUponSpouseDeath = new CampaignOptionsCheckBox("KeepMarriedNameUponSpouseDeath"); - lblRandomDeathMethod = new CampaignOptionsLabel("RandomDeathMethod"); comboRandomDeathMethod.setRenderer(new DefaultListCellRenderer() { @Override @@ -771,18 +760,9 @@ public Component getListCellRendererComponent(final JList list, final Object return this; } }); - - chkUseRandomClanPersonnelDeath = new CampaignOptionsCheckBox("UseRandomClanPersonnelDeath"); - chkUseRandomPrisonerDeath = new CampaignOptionsCheckBox("UseRandomPrisonerDeath"); + chkKeepMarriedNameUponSpouseDeath = new CampaignOptionsCheckBox("KeepMarriedNameUponSpouseDeath"); chkUseRandomDeathSuicideCause = new CampaignOptionsCheckBox("UseRandomDeathSuicideCause"); - lblPercentageRandomDeathChance = new CampaignOptionsLabel("PercentageRandomDeathChance"); - spnPercentageRandomDeathChance = new CampaignOptionsSpinner("PercentageRandomDeathChance", - 0, 0, 100, 0.000001); - NumberEditor editor = new NumberEditor(spnPercentageRandomDeathChance, - "0.000000"); - spnPercentageRandomDeathChance.setEditor(editor); - pnlDeathAgeGroup = createDeathAgeGroupsPanel(); // Layout the Panel @@ -791,10 +771,6 @@ public Component getListCellRendererComponent(final JList list, final Object layoutLeft.gridy = 0; layoutLeft.gridx = 0; - layoutLeft.gridwidth = 2; - panelLeft.add(chkKeepMarriedNameUponSpouseDeath, layoutLeft); - - layoutLeft.gridy++; layoutLeft.gridwidth = 1; panelLeft.add(lblRandomDeathMethod, layoutLeft); layoutLeft.gridx++; @@ -802,19 +778,11 @@ public Component getListCellRendererComponent(final JList list, final Object layoutLeft.gridx = 0; layoutLeft.gridy++; - panelLeft.add(chkUseRandomClanPersonnelDeath, layoutLeft); - - layoutLeft.gridy++; - panelLeft.add(chkUseRandomPrisonerDeath, layoutLeft); + panelLeft.add(chkKeepMarriedNameUponSpouseDeath, layoutLeft); layoutLeft.gridy++; panelLeft.add(chkUseRandomDeathSuicideCause, layoutLeft); - layoutLeft.gridy++; - panelLeft.add(lblPercentageRandomDeathChance, layoutLeft); - layoutLeft.gridx++; - panelLeft.add(spnPercentageRandomDeathChance, layoutLeft); - final JPanel panelParent = new CampaignOptionsStandardPanel("DeathTab", true); final GridBagConstraints layoutParent = new CampaignOptionsGridBagConstraints(panelParent); @@ -1344,10 +1312,7 @@ public void loadValuesFromCampaignOptions(@Nullable CampaignOptions presetCampai // Death chkKeepMarriedNameUponSpouseDeath.setSelected(options.isKeepMarriedNameUponSpouseDeath()); comboRandomDeathMethod.setSelectedItem(options.getRandomDeathMethod()); - chkUseRandomClanPersonnelDeath.setSelected(options.isUseRandomClanPersonnelDeath()); - chkUseRandomPrisonerDeath.setSelected(options.isUseRandomPrisonerDeath()); chkUseRandomDeathSuicideCause.setSelected(options.isUseRandomDeathSuicideCause()); - spnPercentageRandomDeathChance.setValue(options.getPercentageRandomDeathChance()); Map deathAgeGroups = options.getEnabledRandomDeathAgeGroups(); for (final AgeGroup ageGroup : AgeGroup.values()) { @@ -1437,10 +1402,7 @@ public void applyCampaignOptionsToCampaign(@Nullable CampaignOptions presetCampa // Death options.setKeepMarriedNameUponSpouseDeath(chkKeepMarriedNameUponSpouseDeath.isSelected()); options.setRandomDeathMethod(comboRandomDeathMethod.getSelectedItem()); - options.setUseRandomClanPersonnelDeath(chkUseRandomClanPersonnelDeath.isSelected()); - options.setUseRandomPrisonerDeath(chkUseRandomPrisonerDeath.isSelected()); options.setUseRandomDeathSuicideCause(chkUseRandomDeathSuicideCause.isSelected()); - options.setPercentageRandomDeathChance((double) spnPercentageRandomDeathChance.getValue()); for (final AgeGroup ageGroup : AgeGroup.values()) { options.getEnabledRandomDeathAgeGroups().put(ageGroup, chkEnabledRandomDeathAgeGroups.get(ageGroup).isSelected()); diff --git a/MekHQ/unittests/mekhq/campaign/personnel/death/AbstractDeathTest.java b/MekHQ/unittests/mekhq/campaign/personnel/death/AbstractDeathTest.java index 5fecc7eca60..6003bfc1df0 100644 --- a/MekHQ/unittests/mekhq/campaign/personnel/death/AbstractDeathTest.java +++ b/MekHQ/unittests/mekhq/campaign/personnel/death/AbstractDeathTest.java @@ -23,7 +23,6 @@ import mekhq.campaign.personnel.Person; import mekhq.campaign.personnel.enums.AgeGroup; import mekhq.campaign.personnel.enums.PersonnelStatus; -import mekhq.campaign.personnel.enums.PrisonerStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -37,7 +36,10 @@ import java.util.HashMap; import java.util.Map; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -144,41 +146,16 @@ public void testCanDie() { when(mockPerson.getStatus()).thenReturn(PersonnelStatus.ACTIVE); when(mockPerson.isImmortal()).thenReturn(true); assertNotNull(mockDeath.canDie(mockPerson, AgeGroup.ADULT, true)); - + // Age Group must be enabled when(mockPerson.isImmortal()).thenReturn(false); assertNotNull(mockDeath.canDie(mockPerson, AgeGroup.CHILD, true)); - - // Can't be Clan Personnel with Random Clan Death Disabled - when(mockPerson.isClanPersonnel()).thenReturn(true); - when(mockDeath.isUseRandomClanPersonnelDeath()).thenReturn(false); - when(mockDeath.isUseRandomPrisonerDeath()).thenReturn(true); - assertNotNull(mockDeath.canDie(mockPerson, AgeGroup.ADULT, true)); - - // Can be Non-Clan Personnel with Random Clan Death Disabled - when(mockPerson.isClanPersonnel()).thenReturn(false); - assertNull(mockDeath.canDie(mockPerson, AgeGroup.ADULT, true)); - - // Can be a Non-Prisoner with Random Prisoner Death Disabled - when(mockPerson.getPrisonerStatus()).thenReturn(PrisonerStatus.FREE); - when(mockDeath.isUseRandomPrisonerDeath()).thenReturn(false); - assertNull(mockDeath.canDie(mockPerson, AgeGroup.ADULT, true)); - - // Can't be a Prisoner with Random Prisoner Death Disabled - when(mockPerson.getPrisonerStatus()).thenReturn(PrisonerStatus.PRISONER); - assertNotNull(mockDeath.canDie(mockPerson, AgeGroup.ADULT, true)); - - // Can be a Clan Prisoner with Random Clan and Random Prisoner Death Enabled - lenient().when(mockPerson.isClanPersonnel()).thenReturn(true); - when(mockDeath.isUseRandomClanPersonnelDeath()).thenReturn(true); - when(mockDeath.isUseRandomPrisonerDeath()).thenReturn(true); - assertNull(mockDeath.canDie(mockPerson, AgeGroup.ADULT, true)); } //region New Day @Test - public void testProcessNewDay() { - doCallRealMethod().when(mockDeath).processNewDay(any(), any(), any()); + public void testProcessNewWeek() { + doCallRealMethod().when(mockDeath).processNewWeek(any(), any(), any()); when(mockDeath.getCause(any(), any(), anyInt())).thenReturn(PersonnelStatus.DISEASE); final Person mockPerson = mock(Person.class); @@ -189,21 +166,21 @@ public void testProcessNewDay() { // Can't be dead when(mockDeath.canDie(any(), any(), anyBoolean())).thenReturn("Dead"); - assertFalse(mockDeath.processNewDay(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson)); + assertFalse(mockDeath.processNewWeek(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson)); // Randomly Dies - Change Status Works Properly when(mockDeath.canDie(any(), any(), anyBoolean())).thenReturn(null); when(mockDeath.randomlyDies(anyInt(), any())).thenReturn(true); when(mockPerson.getStatus()).thenReturn(PersonnelStatus.DISEASE); - assertTrue(mockDeath.processNewDay(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson)); + assertTrue(mockDeath.processNewWeek(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson)); // Randomly Dies - Issue Changing Status when(mockPerson.getStatus()).thenReturn(PersonnelStatus.ACTIVE); - assertFalse(mockDeath.processNewDay(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson)); + assertFalse(mockDeath.processNewWeek(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson)); // Doesn't Randomly Die when(mockDeath.randomlyDies(anyInt(), any())).thenReturn(false); - assertFalse(mockDeath.processNewDay(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson)); + assertFalse(mockDeath.processNewWeek(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson)); } } //endregion New Day diff --git a/MekHQ/unittests/mekhq/campaign/personnel/death/AgeRangeRandomDeathTest.java b/MekHQ/unittests/mekhq/campaign/personnel/death/AgeRangeRandomDeathTest.java deleted file mode 100644 index bf4b1c288a2..00000000000 --- a/MekHQ/unittests/mekhq/campaign/personnel/death/AgeRangeRandomDeathTest.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (c) 2022 - 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 as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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. - * - * You should have received a copy of the GNU General Public License - * along with MekHQ. If not, see . - */ -package mekhq.campaign.personnel.death; - -import megamek.common.Compute; -import megamek.common.enums.Gender; -import mekhq.campaign.CampaignOptions; -import mekhq.campaign.personnel.enums.TenYearAgeRange; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.MockedStatic; -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.HashMap; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.when; - -@ExtendWith(value = MockitoExtension.class) -public class AgeRangeRandomDeathTest { - @Mock - private CampaignOptions mockOptions; - - @BeforeEach - public void beforeEach() { - when(mockOptions.getEnabledRandomDeathAgeGroups()).thenReturn(new HashMap<>()); - when(mockOptions.isUseRandomClanPersonnelDeath()).thenReturn(false); - when(mockOptions.isUseRandomPrisonerDeath()).thenReturn(false); - when(mockOptions.isUseRandomDeathSuicideCause()).thenReturn(false); - - final Map maleAgeRangeMap = new HashMap<>(); - final Map femaleAgeRangeMap = new HashMap<>(); - for (final TenYearAgeRange range : TenYearAgeRange.values()) { - maleAgeRangeMap.put(range, 18262500d); - femaleAgeRangeMap.put(range, 14610000d); - } - when(mockOptions.getAgeRangeRandomDeathMaleValues()).thenReturn(maleAgeRangeMap); - when(mockOptions.getAgeRangeRandomDeathFemaleValues()).thenReturn(femaleAgeRangeMap); - } - - @Test - public void testRandomlyDies() { - final AgeRangeRandomDeath ageRangeRandomDeath = new AgeRangeRandomDeath(mockOptions, false); - // We're using the same percentages for all age ranges, so we only need to test Genders - // Testing Minimum (0f), Below Value (0.49f), At Value (0.5f), and Maximum (1f) - try (MockedStatic compute = Mockito.mockStatic(Compute.class)) { - compute.when(Compute::randomFloat).thenReturn(0f); - assertTrue(ageRangeRandomDeath.randomlyDies(50, Gender.MALE)); - assertTrue(ageRangeRandomDeath.randomlyDies(50, Gender.FEMALE)); - compute.when(Compute::randomFloat).thenReturn(0.39f); - assertTrue(ageRangeRandomDeath.randomlyDies(50, Gender.MALE)); - assertTrue(ageRangeRandomDeath.randomlyDies(50, Gender.FEMALE)); - compute.when(Compute::randomFloat).thenReturn(0.40f); - assertTrue(ageRangeRandomDeath.randomlyDies(50, Gender.MALE)); - assertFalse(ageRangeRandomDeath.randomlyDies(50, Gender.FEMALE)); - compute.when(Compute::randomFloat).thenReturn(0.49f); - assertTrue(ageRangeRandomDeath.randomlyDies(50, Gender.MALE)); - assertFalse(ageRangeRandomDeath.randomlyDies(50, Gender.FEMALE)); - compute.when(Compute::randomFloat).thenReturn(0.5f); - assertFalse(ageRangeRandomDeath.randomlyDies(50, Gender.MALE)); - assertFalse(ageRangeRandomDeath.randomlyDies(50, Gender.FEMALE)); - compute.when(Compute::randomFloat).thenReturn(1f); - assertFalse(ageRangeRandomDeath.randomlyDies(50, Gender.MALE)); - assertFalse(ageRangeRandomDeath.randomlyDies(50, Gender.FEMALE)); - } - } -} diff --git a/MekHQ/unittests/mekhq/campaign/personnel/death/DisabledRandomDeathTest.java b/MekHQ/unittests/mekhq/campaign/personnel/death/DisabledRandomDeathTest.java index 09ae70a532b..09592b66468 100644 --- a/MekHQ/unittests/mekhq/campaign/personnel/death/DisabledRandomDeathTest.java +++ b/MekHQ/unittests/mekhq/campaign/personnel/death/DisabledRandomDeathTest.java @@ -39,8 +39,6 @@ public class DisabledRandomDeathTest { @BeforeEach public void beforeEach() { when(mockOptions.getEnabledRandomDeathAgeGroups()).thenReturn(new HashMap<>()); - when(mockOptions.isUseRandomClanPersonnelDeath()).thenReturn(false); - when(mockOptions.isUseRandomPrisonerDeath()).thenReturn(false); when(mockOptions.isUseRandomDeathSuicideCause()).thenReturn(false); } diff --git a/MekHQ/unittests/mekhq/campaign/personnel/death/ExponentialRandomDeathTest.java b/MekHQ/unittests/mekhq/campaign/personnel/death/ExponentialRandomDeathTest.java index 149e23b761a..e7da346837a 100644 --- a/MekHQ/unittests/mekhq/campaign/personnel/death/ExponentialRandomDeathTest.java +++ b/MekHQ/unittests/mekhq/campaign/personnel/death/ExponentialRandomDeathTest.java @@ -43,8 +43,6 @@ public class ExponentialRandomDeathTest { @BeforeEach public void beforeEach() { when(mockOptions.getEnabledRandomDeathAgeGroups()).thenReturn(new HashMap<>()); - when(mockOptions.isUseRandomClanPersonnelDeath()).thenReturn(false); - when(mockOptions.isUseRandomPrisonerDeath()).thenReturn(false); when(mockOptions.isUseRandomDeathSuicideCause()).thenReturn(false); when(mockOptions.getExponentialRandomDeathMaleValues()).thenReturn(new double[] { 5.4757, -7.0, 0.0709 }); when(mockOptions.getExponentialRandomDeathFemaleValues()).thenReturn(new double[] { 2.4641, -7.0, 0.0752 }); diff --git a/MekHQ/unittests/mekhq/campaign/personnel/death/PercentageRandomDeathTest.java b/MekHQ/unittests/mekhq/campaign/personnel/death/PercentageRandomDeathTest.java deleted file mode 100644 index 1096281654a..00000000000 --- a/MekHQ/unittests/mekhq/campaign/personnel/death/PercentageRandomDeathTest.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (c) 2022 - 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 as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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. - * - * You should have received a copy of the GNU General Public License - * along with MekHQ. If not, see . - */ -package mekhq.campaign.personnel.death; - -import megamek.common.Compute; -import megamek.common.enums.Gender; -import mekhq.campaign.CampaignOptions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.MockedStatic; -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.HashMap; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.when; - -@ExtendWith(value = MockitoExtension.class) -public class PercentageRandomDeathTest { - @Mock - private CampaignOptions mockOptions; - - @BeforeEach - public void beforeEach() { - when(mockOptions.getEnabledRandomDeathAgeGroups()).thenReturn(new HashMap<>()); - when(mockOptions.isUseRandomClanPersonnelDeath()).thenReturn(false); - when(mockOptions.isUseRandomPrisonerDeath()).thenReturn(false); - when(mockOptions.isUseRandomDeathSuicideCause()).thenReturn(false); - when(mockOptions.getPercentageRandomDeathChance()).thenReturn(0.5); - } - - @Test - public void testRandomlyDies() { - final PercentageRandomDeath percentageRandomDeath = new PercentageRandomDeath(mockOptions, false); - // This ignores age and gender, so just using 50 and Male. - // Testing Minimum (0f), Below Value (0.49f), At Value (0.5f), and Maximum (1f) - try (MockedStatic compute = Mockito.mockStatic(Compute.class)) { - compute.when(Compute::randomFloat).thenReturn(0f); - assertTrue(percentageRandomDeath.randomlyDies(50, Gender.MALE)); - compute.when(Compute::randomFloat).thenReturn(0.49f); - assertTrue(percentageRandomDeath.randomlyDies(50, Gender.MALE)); - compute.when(Compute::randomFloat).thenReturn(0.5f); - assertFalse(percentageRandomDeath.randomlyDies(50, Gender.MALE)); - compute.when(Compute::randomFloat).thenReturn(1f); - assertFalse(percentageRandomDeath.randomlyDies(50, Gender.MALE)); - } - } -} diff --git a/MekHQ/unittests/mekhq/campaign/personnel/enums/RandomDeathMethodTest.java b/MekHQ/unittests/mekhq/campaign/personnel/enums/RandomDeathMethodTest.java index 52c26933246..fe5fb899187 100644 --- a/MekHQ/unittests/mekhq/campaign/personnel/enums/RandomDeathMethodTest.java +++ b/MekHQ/unittests/mekhq/campaign/personnel/enums/RandomDeathMethodTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2022-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * @@ -20,10 +20,8 @@ import mekhq.MekHQ; import mekhq.campaign.CampaignOptions; -import mekhq.campaign.personnel.death.AgeRangeRandomDeath; import mekhq.campaign.personnel.death.DisabledRandomDeath; import mekhq.campaign.personnel.death.ExponentialRandomDeath; -import mekhq.campaign.personnel.death.PercentageRandomDeath; import org.junit.jupiter.api.Test; import java.util.HashMap; @@ -50,8 +48,8 @@ public class RandomDeathMethodTest { public void testGetToolTipText() { assertEquals(resources.getString("RandomDeathMethod.NONE.toolTipText"), RandomDeathMethod.NONE.getToolTipText()); - assertEquals(resources.getString("RandomDeathMethod.AGE_RANGE.toolTipText"), - RandomDeathMethod.AGE_RANGE.getToolTipText()); + assertEquals(resources.getString("RandomDeathMethod.RANDOM.toolTipText"), + RandomDeathMethod.RANDOM.getToolTipText()); } //endregion Getters @@ -68,34 +66,12 @@ public void testIsNone() { } @Test - public void testIsPercentage() { + public void testIsDiceRoll() { for (final RandomDeathMethod randomDeathMethod : methods) { - if (randomDeathMethod == RandomDeathMethod.PERCENTAGE) { - assertTrue(randomDeathMethod.isPercentage()); + if (randomDeathMethod == RandomDeathMethod.RANDOM) { + assertTrue(randomDeathMethod.isRandom()); } else { - assertFalse(randomDeathMethod.isPercentage()); - } - } - } - - @Test - public void testIsExponential() { - for (final RandomDeathMethod randomDeathMethod : methods) { - if (randomDeathMethod == RandomDeathMethod.EXPONENTIAL) { - assertTrue(randomDeathMethod.isExponential()); - } else { - assertFalse(randomDeathMethod.isExponential()); - } - } - } - - @Test - public void testIsAgeRange() { - for (final RandomDeathMethod randomDeathMethod : methods) { - if (randomDeathMethod == RandomDeathMethod.AGE_RANGE) { - assertTrue(randomDeathMethod.isAgeRange()); - } else { - assertFalse(randomDeathMethod.isAgeRange()); + assertFalse(randomDeathMethod.isRandom()); } } } @@ -105,10 +81,7 @@ public void testIsAgeRange() { public void testGetMethod() { final CampaignOptions mockOptions = mock(CampaignOptions.class); when(mockOptions.getEnabledRandomDeathAgeGroups()).thenReturn(new HashMap<>()); - when(mockOptions.isUseRandomClanPersonnelDeath()).thenReturn(false); - when(mockOptions.isUseRandomPrisonerDeath()).thenReturn(false); when(mockOptions.isUseRandomDeathSuicideCause()).thenReturn(false); - when(mockOptions.getPercentageRandomDeathChance()).thenReturn(0.5); when(mockOptions.getExponentialRandomDeathMaleValues()).thenReturn(new double[] { 1d }); when(mockOptions.getExponentialRandomDeathFemaleValues()).thenReturn(new double[] { 1d }); @@ -116,20 +89,16 @@ public void testGetMethod() { for (final TenYearAgeRange range : TenYearAgeRange.values()) { ageRangeMap.put(range, 1d); } - when(mockOptions.getAgeRangeRandomDeathMaleValues()).thenReturn(ageRangeMap); - when(mockOptions.getAgeRangeRandomDeathFemaleValues()).thenReturn(ageRangeMap); assertInstanceOf(DisabledRandomDeath.class, RandomDeathMethod.NONE.getMethod(mockOptions)); - assertInstanceOf(PercentageRandomDeath.class, RandomDeathMethod.PERCENTAGE.getMethod(mockOptions, false)); - assertInstanceOf(ExponentialRandomDeath.class, RandomDeathMethod.EXPONENTIAL.getMethod(mockOptions, false)); - assertInstanceOf(AgeRangeRandomDeath.class, RandomDeathMethod.AGE_RANGE.getMethod(mockOptions, false)); + assertInstanceOf(ExponentialRandomDeath.class, RandomDeathMethod.RANDOM.getMethod(mockOptions, false)); } @Test public void testToStringOverride() { assertEquals(resources.getString("RandomDeathMethod.NONE.text"), RandomDeathMethod.NONE.toString()); - assertEquals(resources.getString("RandomDeathMethod.EXPONENTIAL.text"), - RandomDeathMethod.EXPONENTIAL.toString()); + assertEquals(resources.getString("RandomDeathMethod.RANDOM.text"), + RandomDeathMethod.RANDOM.toString()); } } From cdfaf2b58ef795a76ba19e919815789866be9571 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Thu, 23 Jan 2025 13:31:40 -0600 Subject: [PATCH 008/112] Refactored exponential death rate logic for gender coefficients Removed redundant getter methods for death rate coefficients and streamlined logic for calculating random deaths based on age and gender. Updated tests to reflect these changes by removing unnecessary mock setups for the eliminated methods. --- .../death/ExponentialRandomDeath.java | 33 +++---------------- .../death/ExponentialRandomDeathTest.java | 2 -- .../enums/RandomDeathMethodTest.java | 2 -- 3 files changed, 4 insertions(+), 33 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/personnel/death/ExponentialRandomDeath.java b/MekHQ/src/mekhq/campaign/personnel/death/ExponentialRandomDeath.java index 39311d13e69..81439904956 100644 --- a/MekHQ/src/mekhq/campaign/personnel/death/ExponentialRandomDeath.java +++ b/MekHQ/src/mekhq/campaign/personnel/death/ExponentialRandomDeath.java @@ -63,26 +63,6 @@ public ExponentialRandomDeath(final CampaignOptions options, final boolean initi } //endregion Constructors - //region Getters/Setters - /** - * Gets the coefficients representing the male exponential death rate equation. - * - * @return The male-specific death rate coefficients (c, n, k). - */ - public double[] getMaleDeathRate() { - return MALE_DEATH_RATE; - } - - /** - * Gets the coefficients representing the female exponential death rate equation. - * - * @return The female-specific death rate coefficients (c, n, k). - */ - public double[] getFemaleDeathRate() { - return FEMALE_DEATH_RATE; - } - //endregion Getters/Setters - /** * Determines if a person dies a random death based on gender-specific exponential equations. * The calculation uses weekly probabilities derived from gender-specific coefficients and age, @@ -97,16 +77,11 @@ public double[] getFemaleDeathRate() { */ @Override public boolean randomlyDies(final int age, final Gender gender) { - double deathRate; - if (gender.isMale()) { - deathRate = getMaleDeathRate()[0] * Math.pow(10, getMaleDeathRate()[1]) * - Math.exp(getMaleDeathRate()[2] * age * 7); // Multiply age by 7 for weekly rate - } else { - deathRate = getFemaleDeathRate()[0] * Math.pow(10, getFemaleDeathRate()[1]) * - Math.exp(getFemaleDeathRate()[2] * age * 7); // Multiply age by 7 for weekly rate - } + double[] deathRateArray = gender.isMale() ? MALE_DEATH_RATE : FEMALE_DEATH_RATE; + double chanceOfDeath = deathRateArray[0] * Math.pow(10, deathRateArray[1]) + * Math.exp(deathRateArray[2] * age * 7); // Multiply character age by 7 for weekly rate // Compare the random float with the calculated weekly death rate - return Compute.randomFloat() < deathRate; + return Compute.randomFloat() < chanceOfDeath; } } diff --git a/MekHQ/unittests/mekhq/campaign/personnel/death/ExponentialRandomDeathTest.java b/MekHQ/unittests/mekhq/campaign/personnel/death/ExponentialRandomDeathTest.java index e7da346837a..a877c5347e8 100644 --- a/MekHQ/unittests/mekhq/campaign/personnel/death/ExponentialRandomDeathTest.java +++ b/MekHQ/unittests/mekhq/campaign/personnel/death/ExponentialRandomDeathTest.java @@ -44,8 +44,6 @@ public class ExponentialRandomDeathTest { public void beforeEach() { when(mockOptions.getEnabledRandomDeathAgeGroups()).thenReturn(new HashMap<>()); when(mockOptions.isUseRandomDeathSuicideCause()).thenReturn(false); - when(mockOptions.getExponentialRandomDeathMaleValues()).thenReturn(new double[] { 5.4757, -7.0, 0.0709 }); - when(mockOptions.getExponentialRandomDeathFemaleValues()).thenReturn(new double[] { 2.4641, -7.0, 0.0752 }); } @Test diff --git a/MekHQ/unittests/mekhq/campaign/personnel/enums/RandomDeathMethodTest.java b/MekHQ/unittests/mekhq/campaign/personnel/enums/RandomDeathMethodTest.java index fe5fb899187..093e10ef032 100644 --- a/MekHQ/unittests/mekhq/campaign/personnel/enums/RandomDeathMethodTest.java +++ b/MekHQ/unittests/mekhq/campaign/personnel/enums/RandomDeathMethodTest.java @@ -82,8 +82,6 @@ public void testGetMethod() { final CampaignOptions mockOptions = mock(CampaignOptions.class); when(mockOptions.getEnabledRandomDeathAgeGroups()).thenReturn(new HashMap<>()); when(mockOptions.isUseRandomDeathSuicideCause()).thenReturn(false); - when(mockOptions.getExponentialRandomDeathMaleValues()).thenReturn(new double[] { 1d }); - when(mockOptions.getExponentialRandomDeathFemaleValues()).thenReturn(new double[] { 1d }); final Map ageRangeMap = new HashMap<>(); for (final TenYearAgeRange range : TenYearAgeRange.values()) { From 0b612fa4d6e9cd40f09bfc51ed6479979270971f Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Thu, 23 Jan 2025 13:47:06 -0600 Subject: [PATCH 009/112] Refactored ExponentialRandomDeath tests for clarity. Simplified and expanded unit tests to cover more edge cases, ensuring clearer intent and improved readability. Removed redundant checks and enhanced comments for understanding test logic. Updated copyright to reflect the current year. --- .../death/ExponentialRandomDeath.java | 1 - .../death/ExponentialRandomDeathTest.java | 115 ++++++++++++------ 2 files changed, 80 insertions(+), 36 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/personnel/death/ExponentialRandomDeath.java b/MekHQ/src/mekhq/campaign/personnel/death/ExponentialRandomDeath.java index 81439904956..0c08c7f92d4 100644 --- a/MekHQ/src/mekhq/campaign/personnel/death/ExponentialRandomDeath.java +++ b/MekHQ/src/mekhq/campaign/personnel/death/ExponentialRandomDeath.java @@ -30,7 +30,6 @@ * The death rates are calculated weekly, compared with a randomly generated value, * and returned as a boolean indicating whether the person dies or not. */ - public class ExponentialRandomDeath extends AbstractDeath { //region Variable Declarations /** diff --git a/MekHQ/unittests/mekhq/campaign/personnel/death/ExponentialRandomDeathTest.java b/MekHQ/unittests/mekhq/campaign/personnel/death/ExponentialRandomDeathTest.java index a877c5347e8..81dcd335eda 100644 --- a/MekHQ/unittests/mekhq/campaign/personnel/death/ExponentialRandomDeathTest.java +++ b/MekHQ/unittests/mekhq/campaign/personnel/death/ExponentialRandomDeathTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2022-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * @@ -29,58 +29,103 @@ import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; -import java.util.HashMap; - import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.when; @ExtendWith(value = MockitoExtension.class) public class ExponentialRandomDeathTest { @Mock private CampaignOptions mockOptions; + private ExponentialRandomDeath exponentialRandomDeath; + @BeforeEach - public void beforeEach() { - when(mockOptions.getEnabledRandomDeathAgeGroups()).thenReturn(new HashMap<>()); - when(mockOptions.isUseRandomDeathSuicideCause()).thenReturn(false); + public void setUp() { + exponentialRandomDeath = new ExponentialRandomDeath(mockOptions, false); } @Test - public void testRandomlyDies() { - final ExponentialRandomDeath exponentialRandomDeath = new ExponentialRandomDeath(mockOptions, false); + public void testRandomlyDiesForYoungMale() { try (MockedStatic compute = Mockito.mockStatic(Compute.class)) { + // Mock random float to always return 0 (smallest possible value, ensuring "true") compute.when(Compute::randomFloat).thenReturn(0f); + + // A male age 0 should "die" since the random value is always less than the death chance assertTrue(exponentialRandomDeath.randomlyDies(0, Gender.MALE)); + } + } + + @Test + public void testRandomlyDiesForYoungFemale() { + try (MockedStatic compute = Mockito.mockStatic(Compute.class)) { + compute.when(Compute::randomFloat).thenReturn(0f); + + // A female age 0 should "die" since the random value is 0 assertTrue(exponentialRandomDeath.randomlyDies(0, Gender.FEMALE)); + } + } + + @Test + public void testRandomlyDiesForHigherAgeMale() { + try (MockedStatic compute = Mockito.mockStatic(Compute.class)) { + // Mock random float to return a value slightly higher than expected for age 50 male + compute.when(Compute::randomFloat).thenReturn(0.00001f); + + // A male age 50 (relatively high chance) should still die based on the random value assertTrue(exponentialRandomDeath.randomlyDies(50, Gender.MALE)); - assertTrue(exponentialRandomDeath.randomlyDies(50, Gender.FEMALE)); - compute.when(Compute::randomFloat).thenReturn(0.000001f); - assertFalse(exponentialRandomDeath.randomlyDies(0, Gender.MALE)); - assertFalse(exponentialRandomDeath.randomlyDies(0, Gender.FEMALE)); - compute.when(Compute::randomFloat).thenReturn(0.0000692f); - assertTrue(exponentialRandomDeath.randomlyDies(75, Gender.MALE)); + } + } + + @Test + public void testRandomlyDiesForHigherAgeFemale() { + try (MockedStatic compute = Mockito.mockStatic(Compute.class)) { + // Mock random float to return a value slightly higher than expected for age 75 female + compute.when(Compute::randomFloat).thenReturn(0.0001f); + + // A female age 75 should die since the random value is lower than her chance of death assertTrue(exponentialRandomDeath.randomlyDies(75, Gender.FEMALE)); - compute.when(Compute::randomFloat).thenReturn(0.0000694f); - assertTrue(exponentialRandomDeath.randomlyDies(75, Gender.MALE)); - assertFalse(exponentialRandomDeath.randomlyDies(75, Gender.FEMALE)); - compute.when(Compute::randomFloat).thenReturn(0.000111f); - assertTrue(exponentialRandomDeath.randomlyDies(75, Gender.MALE)); - assertFalse(exponentialRandomDeath.randomlyDies(75, Gender.FEMALE)); - compute.when(Compute::randomFloat).thenReturn(0.000112f); - assertFalse(exponentialRandomDeath.randomlyDies(75, Gender.MALE)); - assertFalse(exponentialRandomDeath.randomlyDies(75, Gender.FEMALE)); - compute.when(Compute::randomFloat).thenReturn(1f); - assertFalse(exponentialRandomDeath.randomlyDies(0, Gender.MALE)); - assertFalse(exponentialRandomDeath.randomlyDies(0, Gender.FEMALE)); - assertFalse(exponentialRandomDeath.randomlyDies(50, Gender.MALE)); - assertFalse(exponentialRandomDeath.randomlyDies(50, Gender.FEMALE)); - assertFalse(exponentialRandomDeath.randomlyDies(100, Gender.MALE)); - assertFalse(exponentialRandomDeath.randomlyDies(100, Gender.FEMALE)); - assertFalse(exponentialRandomDeath.randomlyDies(200, Gender.MALE)); - assertFalse(exponentialRandomDeath.randomlyDies(200, Gender.FEMALE)); - assertTrue(exponentialRandomDeath.randomlyDies(205, Gender.MALE)); - assertTrue(exponentialRandomDeath.randomlyDies(205, Gender.FEMALE)); + } + } + + @Test + public void testNoRandomDeathForLowChanceMale() { + try (MockedStatic compute = Mockito.mockStatic(Compute.class)) { + // Mock random float to return a large value, higher than any plausible death chance + compute.when(Compute::randomFloat).thenReturn(5.0f); + + // A male at any age will not die since the random value is very high + assertFalse(exponentialRandomDeath.randomlyDies(30, Gender.MALE)); + } + } + + @Test + public void testNoRandomDeathForLowChanceFemale() { + try (MockedStatic compute = Mockito.mockStatic(Compute.class)) { + compute.when(Compute::randomFloat).thenReturn(5.0f); + + // A female at any age will also not die since the random value is high + assertFalse(exponentialRandomDeath.randomlyDies(30, Gender.FEMALE)); + } + } + + @Test + public void testEdgeCaseForExtremelyOldMale() { + try (MockedStatic compute = Mockito.mockStatic(Compute.class)) { + // Mock random float to return a value close to 0, ensuring death for very old males + compute.when(Compute::randomFloat).thenReturn(0f); + + // A male age 200+ has an extremely high chance of death + assertTrue(exponentialRandomDeath.randomlyDies(200, Gender.MALE)); + } + } + + @Test + public void testEdgeCaseForExtremelyOldFemale() { + try (MockedStatic compute = Mockito.mockStatic(Compute.class)) { + compute.when(Compute::randomFloat).thenReturn(0f); + + // A female age 200+ has an extremely high chance of death + assertTrue(exponentialRandomDeath.randomlyDies(200, Gender.FEMALE)); } } } From f7bbe77f05af5527473ebd57088c844461b62e86 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Thu, 23 Jan 2025 13:52:24 -0600 Subject: [PATCH 010/112] Refactored random death method handling for compatibility Simplified references to RandomDeathMethod by using static imports. Added a compatibility handler for pre-50.04 random death method parsing, ensuring backward compatibility with older campaign files. --- MekHQ/src/mekhq/campaign/CampaignOptions.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/CampaignOptions.java b/MekHQ/src/mekhq/campaign/CampaignOptions.java index 5068bbc35bc..8b268b33934 100644 --- a/MekHQ/src/mekhq/campaign/CampaignOptions.java +++ b/MekHQ/src/mekhq/campaign/CampaignOptions.java @@ -48,6 +48,9 @@ import java.util.*; import java.util.Map.Entry; +import static mekhq.campaign.personnel.enums.RandomDeathMethod.NONE; +import static mekhq.campaign.personnel.enums.RandomDeathMethod.RANDOM; + /** * @author natit */ @@ -949,7 +952,7 @@ public CampaignOptions() { // Death setKeepMarriedNameUponSpouseDeath(true); - setRandomDeathMethod(RandomDeathMethod.NONE); + setRandomDeathMethod(NONE); setEnabledRandomDeathAgeGroups(new HashMap<>()); getEnabledRandomDeathAgeGroups().put(AgeGroup.ELDER, true); getEnabledRandomDeathAgeGroups().put(AgeGroup.ADULT, true); @@ -5824,7 +5827,14 @@ public static CampaignOptions generateCampaignOptionsFromXml(Node wn, Version ve } else if (wn2.getNodeName().equalsIgnoreCase("keepMarriedNameUponSpouseDeath")) { retVal.setKeepMarriedNameUponSpouseDeath(Boolean.parseBoolean(wn2.getTextContent().trim())); } else if (wn2.getNodeName().equalsIgnoreCase("randomDeathMethod")) { - retVal.setRandomDeathMethod(RandomDeathMethod.valueOf(wn2.getTextContent().trim())); + // <50.04 compatibility handler + if (wn2.getTextContent().trim().equalsIgnoreCase(NONE.name())) { + retVal.setRandomDeathMethod(NONE); + } else { + retVal.setRandomDeathMethod(RANDOM); + } + // Replace above with below when compatibility handler is removed. +// retVal.setRandomDeathMethod(RandomDeathMethod.valueOf(wn2.getTextContent().trim())); } else if (wn2.getNodeName().equalsIgnoreCase("enabledRandomDeathAgeGroups")) { if (!wn2.hasChildNodes()) { continue; From 50ca2f66f24be51616fdac69626a4e730590dcbb Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Thu, 23 Jan 2025 13:53:43 -0600 Subject: [PATCH 011/112] Update copyright years to include 2025 Updated copyright headers across several files to reflect the inclusion of 2025. This ensures compliance with proper attribution and keeps the project metadata up to date. --- MekHQ/src/mekhq/campaign/personnel/death/AbstractDeath.java | 2 +- .../mekhq/campaign/personnel/death/AbstractDeathTest.java | 2 +- .../mekhq/campaign/personnel/death/DisabledRandomDeathTest.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/personnel/death/AbstractDeath.java b/MekHQ/src/mekhq/campaign/personnel/death/AbstractDeath.java index e31879e0510..9bc5f1db53d 100644 --- a/MekHQ/src/mekhq/campaign/personnel/death/AbstractDeath.java +++ b/MekHQ/src/mekhq/campaign/personnel/death/AbstractDeath.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2020-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * diff --git a/MekHQ/unittests/mekhq/campaign/personnel/death/AbstractDeathTest.java b/MekHQ/unittests/mekhq/campaign/personnel/death/AbstractDeathTest.java index 6003bfc1df0..b4dc657c265 100644 --- a/MekHQ/unittests/mekhq/campaign/personnel/death/AbstractDeathTest.java +++ b/MekHQ/unittests/mekhq/campaign/personnel/death/AbstractDeathTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2022-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * diff --git a/MekHQ/unittests/mekhq/campaign/personnel/death/DisabledRandomDeathTest.java b/MekHQ/unittests/mekhq/campaign/personnel/death/DisabledRandomDeathTest.java index 09592b66468..5409be458ca 100644 --- a/MekHQ/unittests/mekhq/campaign/personnel/death/DisabledRandomDeathTest.java +++ b/MekHQ/unittests/mekhq/campaign/personnel/death/DisabledRandomDeathTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2022-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * From bcea914314be81aa76052f00d8a126b5b6a70d02 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Thu, 23 Jan 2025 14:00:05 -0600 Subject: [PATCH 012/112] Remove 'Keep Married Name Upon Spouse Death' feature This commit removed the 'Keep Married Name Upon Spouse Death' option from the campaign settings and its associated logic. Related UI components, properties, and code handling this feature were deleted to streamline the codebase and simplify death tab options. --- .../CampaignOptionsDialog.properties | 3 --- MekHQ/src/mekhq/campaign/Campaign.java | 4 ---- MekHQ/src/mekhq/campaign/CampaignOptions.java | 21 ------------------- .../personnel/divorce/AbstractDivorce.java | 5 ++--- .../contents/BiographyTab.java | 8 ------- 5 files changed, 2 insertions(+), 39 deletions(-) diff --git a/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties b/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties index dfd12666037..e6258454f3c 100644 --- a/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties +++ b/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties @@ -800,9 +800,6 @@ lblExtraRandomOrigin.tooltip=Random origin is randomized to the planetary level # createDeathTab lblDeathTab.text=Death Options \u270E -lblKeepMarriedNameUponSpouseDeath.text=Keep Married Name on Spouse Death -lblKeepMarriedNameUponSpouseDeath.tooltip=When a person's spouse dies, they will keep their marital\ - \ name instead of returning to their birth name. lblRandomDeathMethod.text=Use Random Death lblRandomDeathMethod.tooltip=Should characters randomly die? This system follows a realistic death\ \ curve based on real world data. diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index ab83950468f..50979280438 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -5527,10 +5527,6 @@ public void cleanUp() { if (p.getGenealogy().hasSpouse()) { if (!personnel.containsKey(p.getGenealogy().getSpouse().getId())) { p.getGenealogy().setSpouse(null); - if (!getCampaignOptions().isKeepMarriedNameUponSpouseDeath() - && (p.getMaidenName() != null)) { - p.setSurname(p.getMaidenName()); - } p.setMaidenName(null); } } diff --git a/MekHQ/src/mekhq/campaign/CampaignOptions.java b/MekHQ/src/mekhq/campaign/CampaignOptions.java index 8b268b33934..6d653e0e574 100644 --- a/MekHQ/src/mekhq/campaign/CampaignOptions.java +++ b/MekHQ/src/mekhq/campaign/CampaignOptions.java @@ -365,7 +365,6 @@ public static String getTechLevelName(final int techLevel) { private Integer militaryAcademyAccidents; // Death - private boolean keepMarriedNameUponSpouseDeath; private RandomDeathMethod randomDeathMethod; private Map enabledRandomDeathAgeGroups; private boolean useRandomDeathSuicideCause; @@ -951,7 +950,6 @@ public CampaignOptions() { setMilitaryAcademyAccidents(10000); // Death - setKeepMarriedNameUponSpouseDeath(true); setRandomDeathMethod(NONE); setEnabledRandomDeathAgeGroups(new HashMap<>()); getEnabledRandomDeathAgeGroups().put(AgeGroup.ELDER, true); @@ -2817,21 +2815,6 @@ public void setRandomProcreationRelationshiplessDiceSize(final int randomProcrea // endregion Procreation // region Death - /** - * @return whether to keep ones married name upon spouse death or not - */ - public boolean isKeepMarriedNameUponSpouseDeath() { - return keepMarriedNameUponSpouseDeath; - } - - /** - * @param keepMarriedNameUponSpouseDeath whether to keep ones married name upon - * spouse death or not - */ - public void setKeepMarriedNameUponSpouseDeath(final boolean keepMarriedNameUponSpouseDeath) { - this.keepMarriedNameUponSpouseDeath = keepMarriedNameUponSpouseDeath; - } - public boolean isUseEducationModule() { return useEducationModule; } @@ -5026,8 +5009,6 @@ public void writeToXml(final PrintWriter pw, int indent) { // endregion Education // region Death - MHQXMLUtility.writeSimpleXMLTag(pw, indent, "keepMarriedNameUponSpouseDeath", - isKeepMarriedNameUponSpouseDeath()); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "randomDeathMethod", getRandomDeathMethod().name()); MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, "enabledRandomDeathAgeGroups"); for (final Entry entry : getEnabledRandomDeathAgeGroups().entrySet()) { @@ -5824,8 +5805,6 @@ public static CampaignOptions generateCampaignOptionsFromXml(Node wn, Version ve // endregion Education // region Death - } else if (wn2.getNodeName().equalsIgnoreCase("keepMarriedNameUponSpouseDeath")) { - retVal.setKeepMarriedNameUponSpouseDeath(Boolean.parseBoolean(wn2.getTextContent().trim())); } else if (wn2.getNodeName().equalsIgnoreCase("randomDeathMethod")) { // <50.04 compatibility handler if (wn2.getTextContent().trim().equalsIgnoreCase(NONE.name())) { diff --git a/MekHQ/src/mekhq/campaign/personnel/divorce/AbstractDivorce.java b/MekHQ/src/mekhq/campaign/personnel/divorce/AbstractDivorce.java index 5fd433fcd53..a292ba57022 100644 --- a/MekHQ/src/mekhq/campaign/personnel/divorce/AbstractDivorce.java +++ b/MekHQ/src/mekhq/campaign/personnel/divorce/AbstractDivorce.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2024 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2021-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * @@ -181,8 +181,7 @@ public void setUseRandomPrisonerDivorce(final boolean useRandomPrisonerDivorce) * @param person the person whose spouse has died */ public void widowed(final Campaign campaign, final LocalDate today, final Person person) { - divorce(campaign, today, person, campaign.getCampaignOptions().isKeepMarriedNameUponSpouseDeath() - ? SplittingSurnameStyle.BOTH_KEEP_SURNAME : SplittingSurnameStyle.ORIGIN_CHANGES_SURNAME); + divorce(campaign, today, person, SplittingSurnameStyle.BOTH_KEEP_SURNAME); } /** diff --git a/MekHQ/src/mekhq/gui/campaignOptions/contents/BiographyTab.java b/MekHQ/src/mekhq/gui/campaignOptions/contents/BiographyTab.java index 7256d5fbc13..607e86a430a 100644 --- a/MekHQ/src/mekhq/gui/campaignOptions/contents/BiographyTab.java +++ b/MekHQ/src/mekhq/gui/campaignOptions/contents/BiographyTab.java @@ -110,7 +110,6 @@ public class BiographyTab { //start Death Tab private JLabel lblRandomDeathMethod; private MMComboBox comboRandomDeathMethod; - private JCheckBox chkKeepMarriedNameUponSpouseDeath; private JCheckBox chkUseRandomDeathSuicideCause; private JPanel pnlDeathAgeGroup; @@ -278,7 +277,6 @@ private void initializeEducationTab() { private void initializeDeathTab() { lblRandomDeathMethod = new JLabel(); comboRandomDeathMethod = new MMComboBox<>("comboRandomDeathMethod", RandomDeathMethod.values()); - chkKeepMarriedNameUponSpouseDeath = new JCheckBox(); chkUseRandomDeathSuicideCause = new JCheckBox(); pnlDeathAgeGroup = new JPanel(); @@ -760,7 +758,6 @@ public Component getListCellRendererComponent(final JList list, final Object return this; } }); - chkKeepMarriedNameUponSpouseDeath = new CampaignOptionsCheckBox("KeepMarriedNameUponSpouseDeath"); chkUseRandomDeathSuicideCause = new CampaignOptionsCheckBox("UseRandomDeathSuicideCause"); pnlDeathAgeGroup = createDeathAgeGroupsPanel(); @@ -777,9 +774,6 @@ public Component getListCellRendererComponent(final JList list, final Object panelLeft.add(comboRandomDeathMethod, layoutLeft); layoutLeft.gridx = 0; - layoutLeft.gridy++; - panelLeft.add(chkKeepMarriedNameUponSpouseDeath, layoutLeft); - layoutLeft.gridy++; panelLeft.add(chkUseRandomDeathSuicideCause, layoutLeft); @@ -1310,7 +1304,6 @@ public void loadValuesFromCampaignOptions(@Nullable CampaignOptions presetCampai chkExtraRandomOrigin.setSelected(originOptions.isExtraRandomOrigin()); // Death - chkKeepMarriedNameUponSpouseDeath.setSelected(options.isKeepMarriedNameUponSpouseDeath()); comboRandomDeathMethod.setSelectedItem(options.getRandomDeathMethod()); chkUseRandomDeathSuicideCause.setSelected(options.isUseRandomDeathSuicideCause()); @@ -1400,7 +1393,6 @@ public void applyCampaignOptionsToCampaign(@Nullable CampaignOptions presetCampa options.setRandomOriginOptions(originOptions); // Death - options.setKeepMarriedNameUponSpouseDeath(chkKeepMarriedNameUponSpouseDeath.isSelected()); options.setRandomDeathMethod(comboRandomDeathMethod.getSelectedItem()); options.setUseRandomDeathSuicideCause(chkUseRandomDeathSuicideCause.isSelected()); for (final AgeGroup ageGroup : AgeGroup.values()) { From 3b0006e3c9f0ee459ff8f35f9e336a65bd0772d2 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Thu, 23 Jan 2025 14:02:08 -0600 Subject: [PATCH 013/112] Remove unused random death options from campaign settings Removed labels and tooltips related to random clan and prisoner deaths as well as the random death percentage option. These settings were likely no longer used or relevant, streamlining the campaign options. This enhances clarity and reduces unnecessary complexity in the UI. --- .../mekhq/resources/CampaignOptionsDialog.properties | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties b/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties index e6258454f3c..d09d61c30e3 100644 --- a/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties +++ b/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties @@ -803,18 +803,8 @@ lblDeathTab.text=Death Options \u270E lblRandomDeathMethod.text=Use Random Death lblRandomDeathMethod.tooltip=Should characters randomly die? This system follows a realistic death\ \ curve based on real world data. -lblUseRandomClanPersonnelDeath.text=Enable Random Clan Death -lblUseRandomClanPersonnelDeath.tooltip=Allow clan-origin personnel to randomly die. -lblUseRandomPrisonerDeath.text=Enable Random Prisoner Death -lblUseRandomPrisonerDeath.tooltip=Allow prisoners to randomly die. Bondsmen are treated as free\ - \ personnel when it comes to this option, and are thus not affected by it. lblUseRandomDeathSuicideCause.text=Enable Cause of Death: Suicide lblUseRandomDeathSuicideCause.tooltip=This includes suicide as a potential cause for a random death. -lblPercentageRandomDeathChance.text=Random Death Percentage \u26A0 -lblPercentageRandomDeathChance.tooltip=This is the percent chance per day that any member of your\ - \ force will randomly die.\ -
\ -
Requirement: This option is only relevant if 'Percentage' random death is selected. # createDeathAgeGroupsPanel lblDeathAgeGroupsPanel.text=Death by Age Group From b64e3645f9a228514ec08322abe745c494d3d2f9 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Thu, 23 Jan 2025 20:45:16 -0600 Subject: [PATCH 014/112] Refactored force type handling with ForceType enum Replaced boolean combat and convoy force checks with a new ForceType enum, introducing distinct force types (Standard, Support, Convoy, Security). Updated related logic, file I/O, and GUI elements to align with the new structure, improving code readability and extensibility. --- .../src/mekhq/campaign/force/CombatTeam.java | 11 +-- MekHQ/src/mekhq/campaign/force/Force.java | 52 ++++------- MekHQ/src/mekhq/campaign/force/ForceType.java | 72 ++++++++++++++++ .../mekhq/campaign/mission/AtBContract.java | 2 +- .../mission/resupplyAndCaches/Resupply.java | 59 +++++++------ .../resupplyAndCaches/ResupplyUtilities.java | 4 +- MekHQ/src/mekhq/gui/ForceRenderer.java | 11 ++- .../mekhq/gui/adapter/TOEMouseAdapter.java | 86 ++++++++----------- .../DialogContractStart.java | 60 ++++++------- .../src/mekhq/gui/utilities/StaticChecks.java | 4 +- MekHQ/src/mekhq/gui/view/ForceViewPanel.java | 19 ++-- 11 files changed, 210 insertions(+), 170 deletions(-) create mode 100644 MekHQ/src/mekhq/campaign/force/ForceType.java diff --git a/MekHQ/src/mekhq/campaign/force/CombatTeam.java b/MekHQ/src/mekhq/campaign/force/CombatTeam.java index acb6d6cc073..03bc2fdb055 100644 --- a/MekHQ/src/mekhq/campaign/force/CombatTeam.java +++ b/MekHQ/src/mekhq/campaign/force/CombatTeam.java @@ -73,9 +73,6 @@ public class CombatTeam { public static final int STAR_SIZE = 5; public static final int LEVEL_II_SIZE = 6; - public static final long ETYPE_GROUND = ETYPE_MEK | - ETYPE_TANK | Entity.ETYPE_INFANTRY | ETYPE_PROTOMEK; - /** Indicates a lance has no assigned mission */ public static final int NO_MISSION = -1; @@ -311,7 +308,7 @@ public boolean isEligible(Campaign campaign) { return false; } - if (!force.isCombatForce()) { + if (!force.getForceType().isStandard()) { force.setCombatTeamStatus(false); return false; } @@ -373,14 +370,10 @@ size > getStandardForceSize(campaign.getFaction()) + 2) { return false; } - if (parentForce.isConvoyForce()) { + if (!parentForce.getForceType().isStandard()) { force.setCombatTeamStatus(false); return false; } - - if (!parentForce.isCombatForce()) { - return false; - } } force.setCombatTeamStatus(true); diff --git a/MekHQ/src/mekhq/campaign/force/Force.java b/MekHQ/src/mekhq/campaign/force/Force.java index 955d8402dc7..943209aba20 100644 --- a/MekHQ/src/mekhq/campaign/force/Force.java +++ b/MekHQ/src/mekhq/campaign/force/Force.java @@ -76,8 +76,7 @@ public class Force { private StandardForceIcon forceIcon; private Camouflage camouflage; private String desc; - private boolean combatForce; - private boolean convoyForce; + private ForceType forceType; private boolean isCombatTeam; private int overrideCombatTeam; private FormationLevel formationLevel; @@ -100,8 +99,7 @@ public Force(String name) { setForceIcon(new LayeredForceIcon()); setCamouflage(new Camouflage()); setDescription(""); - this.combatForce = true; - this.convoyForce = false; + this.forceType = ForceType.STANDARD; this.isCombatTeam = false; this.overrideCombatTeam = COMBAT_TEAM_OVERRIDE_NONE; this.formationLevel = FormationLevel.NONE; @@ -160,15 +158,15 @@ public void setDescription(String d) { this.desc = d; } - public boolean isCombatForce() { - return combatForce && !convoyForce; + public ForceType getForceType() { + return forceType; } - public void setCombatForce(boolean combatForce, boolean setForSubForces) { - this.combatForce = combatForce; + public void setForceType(ForceType forceType, boolean setForSubForces) { + this.forceType = forceType; if (setForSubForces) { for (Force force : subForces) { - force.setCombatForce(combatForce, true); + force.setForceType(forceType, true); } } } @@ -177,24 +175,6 @@ public boolean isCombatTeam() { return isCombatTeam; } - /** - * @return {@code true} if this is a convoy force, {@code false} otherwise. - */ - public boolean isConvoyForce() { - return convoyForce; - } - - /** - * Sets the status of the force as a convoy force. If requested, propagate this status to all - * sub-forces recursively. - * - * @param convoyForce {@code true} to mark force as a convoy force, {@code false} to mark force - * as non-convoy. - */ - public void setConvoyForce(boolean convoyForce) { - this.convoyForce = convoyForce; - } - public void setCombatTeamStatus(final boolean isCombatTeam) { this.isCombatTeam = isCombatTeam; } @@ -403,21 +383,21 @@ public Vector getUnits() { } /** - * @param combatForcesOnly to only include combat forces or to also include + * @param standardForcesOnly to only include combat forces or to also include * support forces * @return all the unit ids in this force and all of its subforces */ - public Vector getAllUnits(boolean combatForcesOnly) { + public Vector getAllUnits(boolean standardForcesOnly) { Vector allUnits; - if (combatForcesOnly && !isCombatForce() && !isConvoyForce()) { + if (standardForcesOnly && forceType.isStandard()) { allUnits = new Vector<>(); } else { allUnits = new Vector<>(units); } for (Force force : subForces) { - allUnits.addAll(force.getAllUnits(combatForcesOnly)); + allUnits.addAll(force.getAllUnits(standardForcesOnly)); } return allUnits; @@ -725,8 +705,7 @@ public void writeToXML(PrintWriter pw1, int indent) { if (!getDescription().isBlank()) { MHQXMLUtility.writeSimpleXMLTag(pw1, indent, "desc", desc); } - MHQXMLUtility.writeSimpleXMLTag(pw1, indent, "combatForce", combatForce); - MHQXMLUtility.writeSimpleXMLTag(pw1, indent, "convoyForce", convoyForce); + MHQXMLUtility.writeSimpleXMLTag(pw1, indent, "forceType", forceType.ordinal()); MHQXMLUtility.writeSimpleXMLTag(pw1, indent, "overrideCombatTeam", overrideCombatTeam); MHQXMLUtility.writeSimpleXMLTag(pw1, indent, "formationLevel", formationLevel.toString()); MHQXMLUtility.writeSimpleXMLTag(pw1, indent, "populateOriginNode", overrideFormationLevel.toString()); @@ -774,10 +753,9 @@ public void writeToXML(PrintWriter pw1, int indent) { force.setCamouflage(Camouflage.parseFromXML(wn2)); } else if (wn2.getNodeName().equalsIgnoreCase("desc")) { force.setDescription(wn2.getTextContent().trim()); - } else if (wn2.getNodeName().equalsIgnoreCase("combatForce")) { - force.setCombatForce(Boolean.parseBoolean(wn2.getTextContent().trim()), false); - } else if (wn2.getNodeName().equalsIgnoreCase("convoyForce")) { - force.setConvoyForce(Boolean.parseBoolean(wn2.getTextContent().trim())); + } else if (wn2.getNodeName().equalsIgnoreCase("forceType")) { + force.setForceType(ForceType.fromOrdinal( + Integer.parseInt(wn2.getTextContent().trim())), false); } else if (wn2.getNodeName().equalsIgnoreCase("overrideCombatTeam")) { force.setOverrideCombatTeam(Integer.parseInt(wn2.getTextContent().trim())); } else if (wn2.getNodeName().equalsIgnoreCase("formationLevel")) { diff --git a/MekHQ/src/mekhq/campaign/force/ForceType.java b/MekHQ/src/mekhq/campaign/force/ForceType.java new file mode 100644 index 00000000000..3fd673ed46d --- /dev/null +++ b/MekHQ/src/mekhq/campaign/force/ForceType.java @@ -0,0 +1,72 @@ +/* + * 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 as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with MekHQ. If not, see . + */ +package mekhq.campaign.force; + +import megamek.logging.MMLogger; + +public enum ForceType { + // region Enum Declarations + STANDARD("Standard"), + SUPPORT("Support"), + CONVOY("Convoy"), + SECURITY("Security"); + + // Fields + private final String name; + + // Constructor + ForceType(String name) { + this.name = name; + } + + + // region Getters + public String getName() { + return name; + } + + public boolean isStandard() { + return this == STANDARD; + } + + public boolean isSupport() { + return this == SUPPORT; + } + + public boolean isConvoy() { + return this == CONVOY; + } + + public boolean isSecurity() { + return this == SECURITY; + } + // endregion Boolean Comparison Methods + + // region File I/O + public static ForceType fromOrdinal(int ordinal) { + if ((ordinal >= 0) && (ordinal < values().length)) { + return values()[ordinal]; + } + + MMLogger logger = MMLogger.create(ForceType.class); + logger.error(String.format("Unknown ForceType ordinal: %s - returning COMBAT.", ordinal)); + + return STANDARD; + } +} diff --git a/MekHQ/src/mekhq/campaign/mission/AtBContract.java b/MekHQ/src/mekhq/campaign/mission/AtBContract.java index 17357cf54b1..3e56384d10c 100644 --- a/MekHQ/src/mekhq/campaign/mission/AtBContract.java +++ b/MekHQ/src/mekhq/campaign/mission/AtBContract.java @@ -2027,7 +2027,7 @@ private static double estimatePlayerPower(Campaign campaign) { int playerGBV = 0; int playerUnitCount = 0; for (Force force : campaign.getAllForces()) { - if (!force.isCombatForce()) { + if (!force.getForceType().isStandard()) { continue; } diff --git a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java index 561634fe0b2..21ca3afeba0 100644 --- a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java +++ b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java @@ -372,7 +372,7 @@ static int calculateTargetCargoTonnage(Campaign campaign, AtBContract contract) continue; } - if (!force.isCombatForce()) { + if (!force.getForceType().isStandard()) { continue; } @@ -862,42 +862,45 @@ private void calculatePlayerConvoyValues() { totalPlayerCargoCapacity = 0; for (Force force : campaign.getAllForces()) { - if (!force.isConvoyForce()) { + if (!force.getForceType().isConvoy()) { continue; } - double cargoCapacitySubTotal = 0; - if (force.isConvoyForce()) { - boolean hasCargo = false; - for (UUID unitId : force.getAllUnits(false)) { - try { - Unit unit = campaign.getUnit(unitId); - Entity entity = unit.getEntity(); - - if (unit.isDamaged() - || !unit.isFullyCrewed() - || isProhibitedUnitType(entity, true)) { - continue; - } + // This ensures each convoy is only counted once + if (force.getParentForce() != null && force.getParentForce().getForceType().isConvoy()) { + continue; + } - double individualCargo = unit.getCargoCapacity(); + double cargoCapacitySubTotal = 0; + boolean hasCargo = false; + for (UUID unitId : force.getAllUnits(false)) { + try { + Unit unit = campaign.getUnit(unitId); + Entity entity = unit.getEntity(); + + if (unit.isDamaged() + || !unit.isFullyCrewed() + || isProhibitedUnitType(entity, true)) { + continue; + } - if (individualCargo > 0) { - hasCargo = true; - } + double individualCargo = unit.getCargoCapacity(); - cargoCapacitySubTotal += individualCargo; - } catch (Exception ignored) { - // If we run into an exception, it's because we failed to get Unit or Entity. - // In either case, we just ignore that unit. + if (individualCargo > 0) { + hasCargo = true; } + + cargoCapacitySubTotal += individualCargo; + } catch (Exception ignored) { + // If we run into an exception, it's because we failed to get Unit or Entity. + // In either case, we just ignore that unit. } + } - if (hasCargo) { - if (cargoCapacitySubTotal > 0) { - totalPlayerCargoCapacity += cargoCapacitySubTotal; - playerConvoys.put(force, cargoCapacitySubTotal); - } + if (hasCargo) { + if (cargoCapacitySubTotal > 0) { + totalPlayerCargoCapacity += cargoCapacitySubTotal; + playerConvoys.put(force, cargoCapacitySubTotal); } } } diff --git a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/ResupplyUtilities.java b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/ResupplyUtilities.java index deb222ef34e..58770b41f5a 100644 --- a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/ResupplyUtilities.java +++ b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/ResupplyUtilities.java @@ -83,11 +83,11 @@ public static void processAbandonedConvoy(Campaign campaign, AtBContract contrac for (Force force : campaign.getAllForces()) { Force parentForce = force.getParentForce(); - if (parentForce != null && (force.getParentForce().isConvoyForce())) { + if (parentForce != null && (force.getParentForce().getForceType().isConvoy())) { continue; } - if (force.isConvoyForce() && force.getScenarioId() == scenarioId) { + if (force.getForceType().isConvoy() && force.getScenarioId() == scenarioId) { new DialogAbandonedConvoy(campaign, contract, force); for (UUID unitID : force.getAllUnits(false)) { diff --git a/MekHQ/src/mekhq/gui/ForceRenderer.java b/MekHQ/src/mekhq/gui/ForceRenderer.java index b24e298ab48..5e5121d0b36 100644 --- a/MekHQ/src/mekhq/gui/ForceRenderer.java +++ b/MekHQ/src/mekhq/gui/ForceRenderer.java @@ -24,6 +24,7 @@ import megamek.logging.MMLogger; import mekhq.MekHQ; import mekhq.campaign.force.Force; +import mekhq.campaign.force.ForceType; import mekhq.campaign.personnel.Person; import mekhq.campaign.unit.Unit; import mekhq.utilities.ReportingUtilities; @@ -155,13 +156,21 @@ public Component getTreeCellRendererComponent(JTree tree, Object value, boolean setOpaque(true); } + ForceType forceType = force.getForceType(); + String typeKey = switch (forceType) { + case STANDARD -> ""; + case SUPPORT -> " \u2205"; + case CONVOY -> " \u039E"; + case SECURITY -> " \u2727"; + }; + String formattedForceName = String.format("%s%s%s%s%s%s", force.isCombatTeam() ? "" : "", force.getOverrideCombatTeam() != COMBAT_TEAM_OVERRIDE_NONE ? "" : "", force.getName(), force.isCombatTeam() ? "" : "", force.getOverrideCombatTeam() != COMBAT_TEAM_OVERRIDE_NONE ? "" : "", - force.isConvoyForce() ? " Ξ" : force.isCombatForce() ? "" : " ∅"); + typeKey); setText(formattedForceName); } else { diff --git a/MekHQ/src/mekhq/gui/adapter/TOEMouseAdapter.java b/MekHQ/src/mekhq/gui/adapter/TOEMouseAdapter.java index 2e609db69f5..5d485899455 100644 --- a/MekHQ/src/mekhq/gui/adapter/TOEMouseAdapter.java +++ b/MekHQ/src/mekhq/gui/adapter/TOEMouseAdapter.java @@ -32,6 +32,7 @@ import mekhq.campaign.event.OrganizationChangedEvent; import mekhq.campaign.event.UnitChangedEvent; import mekhq.campaign.force.Force; +import mekhq.campaign.force.ForceType; import mekhq.campaign.force.FormationLevel; import mekhq.campaign.log.ServiceLogger; import mekhq.campaign.mission.AtBDynamicScenario; @@ -130,9 +131,11 @@ public static void connect(CampaignGUI gui, JTree tree) { private static final String PASTE_ICON = "PASTE_ICON"; private static final String SUBFORCES_PASTE_ICON = "SUBFORCES_PASTE_ICON"; private static final String CHANGE_NAME = "CHANGE_NAME"; - private static final String CHANGE_COMBAT_STATUS = "CHANGE_COMBAT_STATUS"; - private static final String CHANGE_COMBAT_STATUSES = "CHANGE_COMBAT_STATUSES"; - private static final String CHANGE_CONVOY_STATUS = "CHANGE_CONVOY_STATUS"; + + private static final String COMMAND_CHANGE_FORCE_TYPE_STANDARD = "COMMAND_CHANGE_FORCE_TYPE_STANDARD|FORCE|empty|"; + private static final String COMMAND_CHANGE_FORCE_TYPE_SUPPORT = "COMMAND_CHANGE_FORCE_TYPE_SUPPORT|FORCE|empty|"; + private static final String COMMAND_CHANGE_FORCE_TYPE_CONVOY = "COMMAND_CHANGE_FORCE_TYPE_CONVOY|FORCE|empty|"; + private static final String COMMAND_CHANGE_FORCE_TYPE_SECURITY = "COMMAND_CHANGE_FORCE_TYPE_SECURITY|FORCE|empty|"; private static final String CHANGE_STRATEGIC_FORCE_OVERRIDE = "CHANGE_STRATEGIC_FORCE_OVERRIDE"; private static final String REMOVE_STRATEGIC_FORCE_OVERRIDE = "REMOVE_STRATEGIC_FORCE_OVERRIDE"; @@ -143,9 +146,6 @@ public static void connect(CampaignGUI gui, JTree tree) { private static final String COMMAND_PASTE_FORCE_ICON = "PASTE_ICON|FORCE|empty|"; private static final String COMMAND_SUBFORCES_PASTE_FORCE_ICON = "SUBFORCES_PASTE_ICON|FORCE|empty|"; private static final String COMMAND_CHANGE_FORCE_NAME = "CHANGE_NAME|FORCE|empty|"; - private static final String COMMAND_CHANGE_FORCE_COMBAT_STATUS = "CHANGE_COMBAT_STATUS|FORCE|empty|"; - private static final String COMMAND_CHANGE_FORCE_COMBAT_STATUSES = "CHANGE_COMBAT_STATUSES|FORCE|empty|"; - private static final String COMMAND_CHANGE_FORCE_CONVOY_STATUS = "CHANGE_CONVOY_STATUS|FORCE|empty|"; private static final String COMMAND_CHANGE_STRATEGIC_FORCE_OVERRIDE = "CHANGE_STRATEGIC_FORCE_OVERRIDE|FORCE|empty|"; private static final String COMMAND_REMOVE_STRATEGIC_FORCE_OVERRIDE = "REMOVE_STRATEGIC_FORCE_OVERRIDE|FORCE|empty|"; @@ -423,46 +423,27 @@ public void actionPerformed(ActionEvent action) { MekHQ.triggerEvent(new OrganizationChangedEvent(gui.getCampaign(), singleForce)); } } - } else if (command.contains(TOEMouseAdapter.CHANGE_COMBAT_STATUS)) { + } else if (command.contains("COMMAND_CHANGE_FORCE_TYPE")) { if (singleForce == null) { return; } - final boolean combatForce = !singleForce.isCombatForce(); - - final boolean subforces = command.contains(TOEMouseAdapter.CHANGE_COMBAT_STATUSES); - for (final Force force : forces) { - force.setCombatForce(combatForce, subforces); - force.setConvoyForce(false); - - + ForceType forceType = ForceType.STANDARD; + if (command.contains("SUPPORT")) { + forceType = ForceType.SUPPORT; } - for (Force formation : gui.getCampaign().getAllForces()) { - MekHQ.triggerEvent(new OrganizationChangedEvent(formation)); + if (command.contains("CONVOY")) { + forceType = ForceType.CONVOY; } - gui.getTOETab().refreshForceView(); - } else if (command.contains(TOEMouseAdapter.CHANGE_CONVOY_STATUS)) { - if (singleForce == null) { - return; + if (command.contains("SECURITY")) { + forceType = ForceType.SECURITY; } - final boolean convoyForce = !singleForce.isConvoyForce(); for (final Force force : forces) { - force.setConvoyForce(convoyForce); - } - - for (Force parentForce : singleForce.getAllParents()) { - parentForce.setConvoyForce(false); - } - - for (Force childForce : singleForce.getAllSubForces()) { - childForce.setConvoyForce(false); - } - - for (Force formation : gui.getCampaign().getAllForces()) { - MekHQ.triggerEvent(new OrganizationChangedEvent(formation)); + force.setForceType(forceType, true); + MekHQ.triggerEvent(new OrganizationChangedEvent(force)); } gui.getTOETab().refreshForceView(); @@ -1079,25 +1060,28 @@ protected Optional createPopupMenu() { } if (gui.getCampaign().getCampaignOptions().isUseAtB()) { - menuItem = new JMenuItem(force.isCombatForce() ? "Make Support Force" : "Remove Support Designation"); - menuItem.setActionCommand(COMMAND_CHANGE_FORCE_COMBAT_STATUS + forceIds); + menu = new JMenu("Change Force Type"); + + menuItem = new JMenuItem("Make Standard Force"); + menuItem.setActionCommand(COMMAND_CHANGE_FORCE_TYPE_STANDARD + forceIds); menuItem.addActionListener(this); - popup.add(menuItem); + menu.add(menuItem); - menuItem = new JMenuItem(force.isCombatForce() ? - "Mark All Forces as Support Forces" : "Remove Support Designation from All Forces"); - menuItem.setActionCommand(COMMAND_CHANGE_FORCE_COMBAT_STATUSES + forceIds); + menuItem = new JMenuItem("Make Support Force"); + menuItem.setActionCommand(COMMAND_CHANGE_FORCE_TYPE_SUPPORT + forceIds); menuItem.addActionListener(this); - menuItem.setEnabled(!force.isConvoyForce()); - popup.add(menuItem); + menu.add(menuItem); - if (gui.getCampaign().getCampaignOptions().isUseStratCon()) { - menuItem = new JMenuItem(!force.isConvoyForce() ? - "Mark force as a Resupply Convoy" : "Remove Resupply Convoy Designation"); - menuItem.setActionCommand(COMMAND_CHANGE_FORCE_CONVOY_STATUS + forceIds); - menuItem.addActionListener(this); - popup.add(menuItem); - } + menuItem = new JMenuItem("Make Convoy Force"); + menuItem.setActionCommand(COMMAND_CHANGE_FORCE_TYPE_CONVOY + forceIds); + menuItem.addActionListener(this); + menu.add(menuItem); + + menuItem = new JMenuItem("Make Security Force"); + menuItem.setActionCommand(COMMAND_CHANGE_FORCE_TYPE_SECURITY + forceIds); + menuItem.addActionListener(this); + menu.add(menuItem); + popup.add(menu); JMenuItem optionStrategicForceOverride = new JMenuItem((force.isCombatTeam() ? "Never" : "Always") + " Consider Force a Combat Team"); @@ -1113,7 +1097,7 @@ protected Optional createPopupMenu() { } if (StaticChecks.areAllForcesUndeployed(gui.getCampaign(), forces) - && StaticChecks.areAllCombatForces(forces)) { + && StaticChecks.areAllStandardForces(forces)) { menu = new JMenu("Deploy Force"); JMenu missionMenu; diff --git a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogContractStart.java b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogContractStart.java index b8f50045dda..231927f2d1f 100644 --- a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogContractStart.java +++ b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogContractStart.java @@ -195,42 +195,44 @@ private static String generateContractStartMessage(Campaign campaign, AtBContrac double totalPlayerCargoCapacity = 0; for (Force force : campaign.getAllForces()) { - if (!force.isConvoyForce()) { + if (!force.getForceType().isConvoy()) { + continue; + } + + if (force.getParentForce() != null && force.getParentForce().getForceType().isConvoy()) { continue; } double cargoCapacitySubTotal = 0; - if (force.isConvoyForce()) { - boolean hasCargo = false; - for (UUID unitId : force.getAllUnits(false)) { - try { - Unit unit = campaign.getUnit(unitId); - Entity entity = unit.getEntity(); - - if (unit.isDamaged() - || !unit.isFullyCrewed() - || isProhibitedUnitType(entity, true)) { - continue; - } - - double individualCargo = unit.getCargoCapacity(); - - if (individualCargo > 0) { - hasCargo = true; - } - - cargoCapacitySubTotal += individualCargo; - } catch (Exception ignored) { - // If we run into an exception, it's because we failed to get Unit or Entity. - // In either case, we just ignore that unit. + boolean hasCargo = false; + for (UUID unitId : force.getAllUnits(false)) { + try { + Unit unit = campaign.getUnit(unitId); + Entity entity = unit.getEntity(); + + if (unit.isDamaged() + || !unit.isFullyCrewed() + || isProhibitedUnitType(entity, true)) { + continue; } - } - if (hasCargo) { - if (cargoCapacitySubTotal > 0) { - totalPlayerCargoCapacity += cargoCapacitySubTotal; - playerConvoys++; + double individualCargo = unit.getCargoCapacity(); + + if (individualCargo > 0) { + hasCargo = true; } + + cargoCapacitySubTotal += individualCargo; + } catch (Exception ignored) { + // If we run into an exception, it's because we failed to get Unit or Entity. + // In either case, we just ignore that unit. + } + } + + if (hasCargo) { + if (cargoCapacitySubTotal > 0) { + totalPlayerCargoCapacity += cargoCapacitySubTotal; + playerConvoys++; } } } diff --git a/MekHQ/src/mekhq/gui/utilities/StaticChecks.java b/MekHQ/src/mekhq/gui/utilities/StaticChecks.java index c5f774a9878..ae4b7fed4c2 100644 --- a/MekHQ/src/mekhq/gui/utilities/StaticChecks.java +++ b/MekHQ/src/mekhq/gui/utilities/StaticChecks.java @@ -36,8 +36,8 @@ public static boolean areAllForcesUndeployed(final Campaign campaign, final List .map(campaign::getUnit).noneMatch(unit -> (unit != null) && unit.isDeployed()); } - public static boolean areAllCombatForces(Vector forces) { - return forces.stream().allMatch(Force::isCombatForce); + public static boolean areAllStandardForces(Vector forces) { + return forces.stream().allMatch(force -> force.getForceType().isStandard()); } public static boolean areAllUnitsAvailable(Vector units) { diff --git a/MekHQ/src/mekhq/gui/view/ForceViewPanel.java b/MekHQ/src/mekhq/gui/view/ForceViewPanel.java index 1b8730a5cd6..6f68de5278f 100644 --- a/MekHQ/src/mekhq/gui/view/ForceViewPanel.java +++ b/MekHQ/src/mekhq/gui/view/ForceViewPanel.java @@ -25,6 +25,7 @@ import mekhq.campaign.Campaign; import mekhq.campaign.finances.Money; import mekhq.campaign.force.Force; +import mekhq.campaign.force.ForceType; import mekhq.campaign.personnel.Person; import mekhq.campaign.personnel.SkillType; import mekhq.campaign.unit.Unit; @@ -193,19 +194,17 @@ private void fillStats() { if (null != type) { lblType.setName("lblType"); - String forceType; - if (force.isCombatForce()) { - forceType = type + ' ' + force.getFormationLevel().toString(); + ForceType forceType = force.getForceType(); + + String forceLabel = ""; + if (forceType.isStandard()) { + forceLabel = force.getFormationLevel().toString(); } else { - if (force.isConvoyForce()) { - forceType = "Resupply " + force.getFormationLevel().toString(); - } else { - forceType = "Support " + force.getFormationLevel().toString(); - } + forceLabel = forceType.getName() + ' ' + force.getFormationLevel().toString(); } - lblType.setText("" + forceType + ""); - lblType.getAccessibleContext().setAccessibleDescription("Force Type: " + forceType); + lblType.setText("" + forceLabel + ""); + lblType.getAccessibleContext().setAccessibleDescription("Force Type: " + forceLabel); gridBagConstraints = new GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = nexty; From 87065c3e34164f9852ee8035ed5c509d2ba02abb Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Thu, 23 Jan 2025 20:47:48 -0600 Subject: [PATCH 015/112] Update copyright years to include 2025 Updated copyright notices across multiple files to reflect the extension to 2025. This ensures compliance with copyright requirements and maintains consistency across the project. --- MekHQ/src/mekhq/campaign/force/Force.java | 2 +- .../mission/resupplyAndCaches/Resupply.java | 18 ++++++++++++++++++ .../resupplyAndCaches/ResupplyUtilities.java | 2 +- .../src/mekhq/gui/utilities/StaticChecks.java | 2 +- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/force/Force.java b/MekHQ/src/mekhq/campaign/force/Force.java index 943209aba20..fd4bae1ddcc 100644 --- a/MekHQ/src/mekhq/campaign/force/Force.java +++ b/MekHQ/src/mekhq/campaign/force/Force.java @@ -2,7 +2,7 @@ * Force.java * * Copyright (c) 2011 - Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved. - * Copyright (c) 2020-2024 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2020-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * diff --git a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java index 21ca3afeba0..c34f5e32411 100644 --- a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java +++ b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java @@ -1,3 +1,21 @@ +/* + * Copyright (c) 2024-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 as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with MekHQ. If not, see . + */ package mekhq.campaign.mission.resupplyAndCaches; import megamek.common.Entity; diff --git a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/ResupplyUtilities.java b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/ResupplyUtilities.java index 58770b41f5a..a0d4aab829b 100644 --- a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/ResupplyUtilities.java +++ b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/ResupplyUtilities.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2024-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * diff --git a/MekHQ/src/mekhq/gui/utilities/StaticChecks.java b/MekHQ/src/mekhq/gui/utilities/StaticChecks.java index ae4b7fed4c2..2c7f57b61ab 100644 --- a/MekHQ/src/mekhq/gui/utilities/StaticChecks.java +++ b/MekHQ/src/mekhq/gui/utilities/StaticChecks.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014-2021 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2014-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * From b63ad806ce98b203b465061849af945adf6189fd Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Thu, 23 Jan 2025 20:50:40 -0600 Subject: [PATCH 016/112] Refactored and documented ForceType enum in MekHQ Added detailed Javadoc comments for ForceType enum, its fields, methods, and enum values. Updated logger message for invalid ordinal to correctly return the STANDARD type. Improved code clarity and maintainability. --- MekHQ/src/mekhq/campaign/force/ForceType.java | 61 ++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/MekHQ/src/mekhq/campaign/force/ForceType.java b/MekHQ/src/mekhq/campaign/force/ForceType.java index 3fd673ed46d..f3508e99fe9 100644 --- a/MekHQ/src/mekhq/campaign/force/ForceType.java +++ b/MekHQ/src/mekhq/campaign/force/ForceType.java @@ -20,52 +20,111 @@ import megamek.logging.MMLogger; +/** + * Represents the various types of forces available. + * + * It is used to classify and manipulate forces within the game. + */ public enum ForceType { // region Enum Declarations + /** + * Standard force type, typically used for combat and general operations. + */ STANDARD("Standard"), + + /** + * Support force type, generally ignored by MekHQ. + */ SUPPORT("Support"), + + /** + * Convoy force type, typically used for transport and supply operations. + */ CONVOY("Convoy"), + + /** + * Security force type, typically used for protection and guarding operations. + */ SECURITY("Security"); + // Fields private final String name; // Constructor + /** + * Constructs a {@code ForceType} with a specified name. + * + * @param name the name of the force type, used for displaying or referencing. + */ ForceType(String name) { this.name = name; } // region Getters + /** + * Returns the name of this force type. + * + * @return a string representing the name of the force type. + */ public String getName() { return name; } + /** + * Checks if this force type is {@code STANDARD}. + * + * @return {@code true} if this is the {@code STANDARD} type; otherwise, {@code false}. + */ public boolean isStandard() { return this == STANDARD; } + /** + * Checks if this force type is {@code SUPPORT}. + * + * @return {@code true} if this is the {@code SUPPORT} type; otherwise, {@code false}. + */ public boolean isSupport() { return this == SUPPORT; } + /** + * Checks if this force type is {@code CONVOY}. + * + * @return {@code true} if this is the {@code CONVOY} type; otherwise, {@code false}. + */ public boolean isConvoy() { return this == CONVOY; } + + /** + * Checks if this force type is {@code SECURITY}. + * + * @return {@code true} if this is the {@code SECURITY} type; otherwise, {@code false}. + */ public boolean isSecurity() { return this == SECURITY; } // endregion Boolean Comparison Methods // region File I/O + /** + * Retrieves a {@code ForceType} based on its ordinal value. + * + * @param ordinal the ordinal index of the force type. + * @return the corresponding {@code ForceType} if the ordinal is valid; + * otherwise, defaults to {@code STANDARD}. + */ public static ForceType fromOrdinal(int ordinal) { if ((ordinal >= 0) && (ordinal < values().length)) { return values()[ordinal]; } MMLogger logger = MMLogger.create(ForceType.class); - logger.error(String.format("Unknown ForceType ordinal: %s - returning COMBAT.", ordinal)); + logger.error(String.format("Unknown ForceType ordinal: %s - returning STANDARD.", ordinal)); return STANDARD; } From b0b9804914ef0435d65845534077947810d2e14d Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sun, 26 Jan 2025 19:02:44 -0600 Subject: [PATCH 017/112] Refactored Resupply Messaging to use Internalization - As per title --- .../mekhq/resources/Resupply.properties | 2087 +++++++++-------- .../resupplyAndCaches/PerformResupply.java | 31 +- .../DialogAbandonedConvoy.java | 17 +- .../DialogContractStart.java | 24 +- .../resupplyAndCaches/DialogInterception.java | 23 +- .../resupplyAndCaches/DialogItinerary.java | 73 +- .../DialogPlayerConvoyOption.java | 28 +- .../DialogResupplyFocus.java | 22 +- .../DialogRoleplayEvent.java | 14 +- .../resupplyAndCaches/DialogSwindled.java | 16 +- 10 files changed, 1163 insertions(+), 1172 deletions(-) diff --git a/MekHQ/resources/mekhq/resources/Resupply.properties b/MekHQ/resources/mekhq/resources/Resupply.properties index 02f48636f43..5d02e825f6d 100644 --- a/MekHQ/resources/mekhq/resources/Resupply.properties +++ b/MekHQ/resources/mekhq/resources/Resupply.properties @@ -6,87 +6,87 @@ confirmAccept.text=Accept confirmRefuse.text=Refuse convoyConfirm.text=Understood commander.text=Commander -dialogBorderTitle.text=%s -dialogBorderConvoySpeakerDefault.text=%s Convoy +dialogBorderTitle.text={0} +dialogBorderConvoySpeakerDefault.text={0} Convoy static.text=[Static] smugglerFee.text=Local Resupply roleplayItems.prompt=Items in italics are roleplay items and are not tracked by MekHQ. documentation.prompt=Full documentation can be found in 'MekHQ/docs/Stratcon and Against the Bot' -convoySuccessful.text=A convoy has %sarrived%s, and its supplies have been distributed by\ +convoySuccessful.text=A convoy has {0}arrived{1}, and its supplies have been distributed by\ \ your logistics personnel. -convoyUnsuccessful.text=Despite their best efforts, your negotiator was %sunsuccessful%s and\ +convoyUnsuccessful.text=Despite their best efforts, your negotiator was {0}unsuccessful{1} and\ \ any available supplies were assigned to other forces. -convoySuccessfulSmuggler.text=The smuggler was true to their word, and the supplies have %sarrived%s. -convoyErrorTemplate.text=%sWe encountered an ERROR when trying to fetch template (Address: %s). Please\ - \ report this error.%s -convoyErrorTracks.text=%sWe encountered an ERROR when trying to fetch a random track. Please\ - \ report this error.%s -convoyInterceptedStratCon.text=A convoy was %sintercepted%s, and must be defended or the\ +convoySuccessfulSmuggler.text=The smuggler was true to their word, and the supplies have {0}arrived{1}. +convoyErrorTemplate.text={0}We encountered an ERROR when trying to fetch template (Address: {1}). Please\ + \ report this error.{2} +convoyErrorTracks.text={0}We encountered an ERROR when trying to fetch a random track. Please\ + \ report this error.{1} +convoyInterceptedStratCon.text=A convoy was {0}intercepted{1}, and must be defended or the\ \ supplies will be lost. -convoyEscaped.text=Miraculously, the convoy was able to %sescape%s. +convoyEscaped.text=Miraculously, the convoy was able to {0}escape{1}. -convoyDispatched.text=Convoy `%s` has finished loading their cargo and is en route. +convoyDispatched.text=Convoy `{0}` has finished loading their cargo and is en route. convoyInsufficientSize.text=Despite the crews' best efforts, the following items could not be fit in\ \ the transports and have to be left behind: -contractStartMessageGeneric.text=%s, for this contract we have the support of local resupply depots.\ +contractStartMessageGeneric.text={0}, for this contract we have the support of local resupply depots.\ \ However, we can negotiate for larger resupplies if we provide our own personnel and vehicles. If we do this,\ \ we will need to designate Resupply Convoys in our TO&E.\
\
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.\
\ -
This contract currently requires an estimated %s tons of cargo space across all\ - \ convoys. We have a total of %s available space across %s convoy%s. Damaged or\ +
This contract currently requires an estimated {1} tons of cargo space across all\ + \ convoys. We have a total of {2}/b> available space across {3} convoy{4}. Damaged or\ \ uncrewed vehicles are not considered available. The exact tonnage required is based on our\ \ current TO&E and may change. We should make sure to have a surplus, if possible, as sometimes\ \ convoys can run overweight. -contractStartMessageIndependent.text=%s, for this contract we won't have the support of local\ +contractStartMessageIndependent.text={0}, for this contract we won't have the support of local\ \ resupply convoys. If we want to secure monthly resupplies, we will need to designate Resupply\ \ Convoys in our TO&E.\
\
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.\
\ -
This contract requires an estimated %s tons of cargo space across all convoys. We\ - \ currently have a total of %s available space across %s convoy%s. Damaged or\ +
This contract requires an estimated {1} tons of cargo space across all convoys. We\ + \ currently have a total of {2} available space across {3} convoy{4}. Damaged or\ \ uncrewed vehicles are not considered available. The exact tonnage required is based on our\ \ current TO&E and may change. We should make sure to have a surplus, if possible, as sometimes\ \ convoys can run overweight. -contractStartMessageGuerrilla.text=%s, for this contract it won't be safe to establish normal supply\ +contractStartMessageGuerrilla.text={0}, for this contract it won't be safe to establish normal supply\ \ networks. Instead, we will be at the mercy of local smugglers. Our employer has provided\ \ a list of suitable contacts, however there is no guarantee these contacts can be trusted to\ \ deliver on their promises. Be careful. -usePlayerConvoyOptional.text=%s, our employer has a delivery ready for us but is offering to expand\ +usePlayerConvoyOptional.text={0}, our employer has a delivery ready for us but is offering to expand\ \ it if we supply our own transports.\
\ -
This enhanced delivery requires an estimated %s tons of cargo space across all\ +
This enhanced delivery requires an estimated {1} 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 crewed vehicles are\ + \ {2} available space across {3} convoy{4}. 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.\
\ -
If we refuse to deploy our own transports the delivery is estimated to be just %s tons.\ - \ Should I tell the team%s to saddle up? +
If we refuse to deploy our own transports the delivery is estimated to be just {5} tons.\ + \ Should I tell the team{4} to saddle up? -usePlayerConvoyForced.text=%s, our employer has a delivery ready for us, but we will need to supply\ +usePlayerConvoyForced.text={0}, our employer has a delivery ready for us, but we will need to supply\ \ our own convoys.\
\ -
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 crewed vehicles are not considered available.\ +
This delivery requires an estimated {1} tons of cargo space across all convoys. However,\ + \ as this is an estimate final tonnage may vary. We currently have a total of %{2} available\ + \ space across {3} convoy{4}. 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.\
\ -
Should I tell the team%s to saddle up? +
Should I tell the team{4} to saddle up? # Roleplay items resourcesRations.text=48-Hour Ration Packs @@ -149,727 +149,728 @@ optionAmmo.text=Ammunition optionAmmo.tooltip=Deliver ammunition only. Overall resupply size reduced by 25%. optionArmor.text=Armor optionArmor.tooltip=Deliver armor only. Overall resupply size reduced by 25%. -focusDescription.text=%s, we might be able to lean a little on our contact and ask them to focus on\ +focusDescription.text={0}, we might be able to lean a little on our contact and ask them to focus on\ \ specific types of supplies. This will be at the cost of overall supply quantity.\
\
Would you like to pick a focus? -supplyCostFull.text=

This resupply will cost %s.\ -

The value of these supplies is %s. -supplyCostAbridged.text=

The value of these supplies is %s. +supplyCostFull.text=

This resupply will cost {0}.\ +

The value of these supplies is {1}. +supplyCostAbridged.text=

The value of these supplies is {0}. # getDialogReference (salvage) salvaged0.text=The salvage team just finished their sweep. Some of the cargo is\ \ practically brand new, with no visible wear or damage - a rare find in these\ \ conditions. On the other hand, much of it looks like it's been scavenged\ \ multiple times before. Even the rougher pieces could still serve us well in a\ - \ pinch, provided we use them wisely. %s + \ pinch, provided we use them wisely. {0} salvaged1.text=We pulled a variety of supplies from the last skirmish. Some items are in\ \ nearly perfect condition, almost as if they were never used. Others appear\ \ cobbled together, likely just to stay functional. Still, this batch has clear\ \ tactical potential, especially once we sift through it to separate the gems\ - \ from the junk. %s + \ from the junk. {0} salvaged2.text=This latest batch of captured cargo is better than anticipated. A fair portion\ \ is fully functional, ready for immediate use. However, some items seem to be\ \ held together by sheer desperation and duct tape. We'll need to sort through\ - \ everything quickly to identify which assets can be put to use right away. %s + \ everything quickly to identify which assets can be put to use right away. {0} salvaged3.text=We've secured a decent amount of usable material from the enemy's hold. There's\ \ a mix of pristine equipment and some more battered remnants. The newer gear\ \ could directly enhance our operations, while the rougher pieces might require\ - \ a few quick repairs before they're combat-ready again. %s + \ a few quick repairs before they're combat-ready again. {0} salvaged4.text=Our salvage efforts have yielded a diverse collection of materials. Some of\ \ it is in excellent condition, suggesting it was either new or barely used.\ \ However, quite a bit looks like it was hastily patched together. We'll need\ - \ to get creative if we want to make the most out of these less pristine items. %s + \ to get creative if we want to make the most out of these less pristine items. {0} salvaged5.text=The latest cache of captured supplies ranges from nearly untouched to barely\ \ usable. The high-quality finds could improve our tactical operations\ \ immediately. The more damaged pieces will require some work, but with effort,\ - \ they could still hold significant value for our ongoing needs. %s + \ they could still hold significant value for our ongoing needs. {0} salvaged6.text=The latest load presents an interesting mix. Some items are untouched,\ \ seemingly ready for immediate deployment, while others appear to have been\ \ salvaged several times over. The less pristine pieces will require some quick\ - \ maintenance, but we can still derive tactical utility from this batch. %s + \ maintenance, but we can still derive tactical utility from this batch. {0} salvaged7.text=This captured cargo exhibits a wide spectrum of quality. A few items are\ \ in mint condition, as if they were meant for delivery to a fresh unit.\ \ However, the rest will need more than a bit of maintenance before they can be\ - \ fielded, though they still have potential value. %s + \ fielded, though they still have potential value. {0} salvaged8.text=The latest haul includes a surprising number of intact materials, but not\ \ everything is in such good shape. While many items are ready for immediate\ \ use, others are clearly not worth adding to the attached itinerary due to\ - \ their poor condition. %s + \ their poor condition. {0} salvaged9.text=We've managed to pull a wide array of resources from the enemy's stores.\ \ Some of it is still in original packaging, which is rare given the\ \ circumstances. The rest ranges from passable to dilapidated, but even the\ - \ worst of it could be refurbished with some effort. %s + \ worst of it could be refurbished with some effort. {0} looted0.text=With our departure finally approaching, we managed to secure the most\ \ valuable supplies we could find. Most of it was in decent condition, but\ \ we were limited to what we could physically carry. The bulkier items were\ \ left behind, as we simply didn't have the energy or space to take them along\ - \ on this final stretch. %s + \ on this final stretch. {0} looted1.text=We looted what we could, focusing on the highest-value supplies to maximize\ \ our haul. Most of the items were ready for transport, but we were exhausted\ \ by then. We opted to leave behind anything that wasn't essential, or that would\ - \ have required significant effort to move. %s + \ have required significant effort to move. {0} looted2.text=We managed to grab the most essential supplies, but it felt like the last\ \ leg of a marathon. While some items were in good shape and easy to carry,\ \ we simply lacked the energy to deal with anything that required extra\ - \ handling or repair work, so we left those behind. %s + \ handling or repair work, so we left those behind. {0} looted3.text=We've secured the best supplies we could find, and now we're finally getting\ \ out of here! Most of the items we took are in good condition, and we only\ \ carried what was easily transportable. Leaving behind the bulkier stuff was\ - \ a strategic decision, and it feels like a win in this tough situation. %s + \ a strategic decision, and it feels like a win in this tough situation. {0} looted4.text=Securing the supplies went smoother than expected, and we're now ready to lift\ \ off! We focused on grabbing the most critical items, leaving the less essential\ \ ones behind without hesitation. Knowing that we're done here brings a sense\ - \ of relief, and it feels good to be moving forward. %s + \ of relief, and it feels good to be moving forward. {0} looted5.text=We managed to secure the most useful supplies, and it's finally time to say\ \ goodbye to this place. We took only the most transportable items, leaving behind\ \ anything too bulky or cumbersome. The team is in high spirits - just glad to be\ - \ getting off this rock and onto the next phase of our journey. %s + \ getting off this rock and onto the next phase of our journey. {0} looted6.text=We've gathered the necessary supplies as planned. Most of the items were\ \ in good condition, and we prioritized those that were easily transportable.\ \ Unfortunately, due to space constraints, we had to leave the bulkier items\ - \ behind, which was expected but still frustrating. %s + \ behind, which was expected but still frustrating. {0} looted7.text=We've grabbed what supplies we could manage, prioritizing items in decent\ \ condition. We had to leave many things behind, focusing only on the essentials.\ \ Given our limited capacity, we don't have the time or space to carry more,\ - \ so we're making do with what we have. %s + \ so we're making do with what we have. {0} looted8.text=We've packed up the most valuable supplies we could find, but it's a desperate\ \ load. We had to leave a lot of potentially useful materials behind due to space\ \ and time constraints. It feels like we're leaving part of our future here,\ - \ but there was simply no other option. %s + \ but there was simply no other option. {0} looted9.text=We finished securing the supplies, but it doesn't feel like enough. We\ \ managed to load the basics, but many vital items had to be left behind. We're\ \ departing with what we could carry, but it feels like we're barely scraping\ - \ by as we move forward. %s + \ by as we move forward. {0} routedSupplies0.text=We've processed your supply request. The convoy is currently en route.\ \ Please complete all standard receiving checks upon delivery to ensure accuracy.\ \ Let us know promptly if there are any urgent needs or issues so we can assist\ - \ as soon as possible. %s + \ as soon as possible. {0} routedSupplies1.text=Your requested supplies have been approved and dispatched. The shipment is\ \ currently en route to your location. Please confirm receipt upon delivery and\ - \ notify us of any discrepancies or urgent requirements for follow-up. %s + \ notify us of any discrepancies or urgent requirements for follow-up. {0} routedSupplies2.text=The supplies you requested have been dispatched and should arrive soon.\ \ Please ensure appropriate personnel are ready to receive the shipment and\ - \ verify its contents in accordance with standard protocols. %s + \ verify its contents in accordance with standard protocols. {0} routedSupplies3.text=Your recent supply request has been processed, and the shipment is on its way.\ \ Please follow standard verification procedures upon receipt, and report any\ - \ issues immediately to avoid disruptions in your operations. %s + \ issues immediately to avoid disruptions in your operations. {0} routedSupplies4.text=The requested supplies have been packed and shipped. Please ensure that personnel\ \ are prepared for standard inventory checks upon delivery, so we can maintain\ - \ accurate records and address any shortages promptly. %s + \ accurate records and address any shortages promptly. {0} routedSupplies5.text=Your requisitioned supplies have been authorized and are now on their way.\ \ Upon receipt, please ensure that verification protocols are followed to confirm\ - \ the delivery's accuracy and condition. %s + \ the delivery's accuracy and condition. {0} routedSupplies6.text=Supplies requested by your unit have been shipped and are currently in\ \ transit. Please make sure to follow standard protocols for receiving and\ - \ inventory verification once the shipment arrives. %s + \ inventory verification once the shipment arrives. {0} routedSupplies7.text=We are delivering the requested supplies as per your requisition form. Please\ \ complete standard verification upon receipt to confirm the shipment's contents,\ - \ and let us know if there are any issues that need to be addressed. %s + \ and let us know if there are any issues that need to be addressed. {0} routedSupplies8.text=Your supply request has been processed, and the shipment is on its way.\ \ Ensure that logistics teams are ready for the usual unloading and inventory,\ - \ so the process goes smoothly upon arrival. %s + \ so the process goes smoothly upon arrival. {0} routedSupplies9.text=Your requested supplies are currently en route. Please confirm delivery\ \ upon arrival and conduct routine inventory checks to ensure all items are\ - \ accounted for and in good condition. %s + \ accounted for and in good condition. {0} routedSupplies10.text=The supplies you requested have been dispatched. Once they arrive, please\ \ follow standard receipt and verification procedures to confirm accuracy and\ - \ condition of the shipment. %s + \ condition of the shipment. {0} routedSupplies11.text=We have processed your recent supply request, and the shipment is now\ \ en route. Please confirm delivery upon receipt and report any discrepancies\ - \ immediately, so we can address them promptly. %s + \ immediately, so we can address them promptly. {0} routedSupplies12.text=Your requested supplies have been expedited for faster delivery. We recommend\ \ having personnel prepared for swift unloading and verification to avoid\ - \ potential delays in processing. %s + \ potential delays in processing. {0} routedSupplies13.text=The supplies you requisitioned have been approved and dispatched. Ensure\ \ a thorough inventory check upon delivery, and let us know if there are any\ - \ concerns. We appreciate your timely response. %s + \ concerns. We appreciate your timely response. {0} routedSupplies14.text=Your requested supplies are on schedule for delivery. Please ensure your\ \ team is prepared for standard receiving procedures, and let us know if there\ - \ are any special handling instructions or concerns. %s + \ are any special handling instructions or concerns. {0} routedSupplies15.text=Your requisition has been processed, and the requested supplies are en\ \ route. Please ensure prompt unloading and confirmation once received to\ - \ maintain operational continuity. %s + \ maintain operational continuity. {0} routedSupplies16.text=The supplies requested by your unit have been shipped and are in transit.\ \ Please ensure all standard protocols for receipt and inventory are followed,\ - \ as we aim to support your operations effectively. %s + \ as we aim to support your operations effectively. {0} routedSupplies17.text=The shipment you requested is on its way. Please confirm the quantity and\ \ condition of the supplies upon arrival, and let us know if any adjustments\ - \ are needed for future shipments. %s + \ are needed for future shipments. {0} routedSupplies18.text=Your supply request has been fulfilled, and the shipment is currently en\ \ route. Have your logistics personnel ready for standard intake and verification\ - \ to ensure a smooth receiving process. %s + \ to ensure a smooth receiving process. {0} routedSupplies19.text=The supplies you requested have been dispatched and are on schedule for\ \ delivery. Ensure your team is prepared for receipt, and confirm delivery as\ - \ per usual protocols to maintain accurate records. %s + \ per usual protocols to maintain accurate records. {0} criticalSupplies0.text=We've processed your supply request, and the convoy is currently en route.\ \ With the enemy in full retreat, there's little risk of interference, but please\ \ complete standard receiving checks upon delivery. Let us know promptly if\ - \ there are any urgent needs or issues so we can assist as soon as possible. %s + \ there are any urgent needs or issues so we can assist as soon as possible. {0} criticalSupplies1.text=Your requested supplies have been approved and are en route. Given the\ \ enemy's retreat, we anticipate a smooth delivery. Please confirm receipt upon\ - \ arrival, but rest easy knowing the path is clear. %s + \ arrival, but rest easy knowing the path is clear. {0} criticalSupplies2.text=The supplies you requested have been dispatched and should arrive soon.\ \ Please ensure appropriate personnel are ready to receive the shipment and\ - \ verify its contents in accordance with standard protocols. %s + \ verify its contents in accordance with standard protocols. {0} criticalSupplies3.text=Your recent supply request has been processed, and the shipment is on its\ \ way. The enemy poses no threat now, so standard verification procedures\ - \ should be routine. Take your time, as there's no rush under current conditions. %s + \ should be routine. Take your time, as there's no rush under current conditions. {0} criticalSupplies4.text=The requested supplies have been packed and shipped. The enemy's withdrawal\ \ ensures a smooth delivery. Ensure personnel are prepared for standard checks,\ - \ though there's no pressure with the situation well in hand. %s + \ though there's no pressure with the situation well in hand. {0} criticalSupplies5.text=Your requisitioned supplies have been authorized and are now on their way.\ \ Upon receipt, please ensure that verification protocols are followed to confirm\ - \ the delivery's accuracy and condition. %s + \ the delivery's accuracy and condition. {0} criticalSupplies6.text=Supplies requested by your unit have been shipped, and the enemy's\ \ disarray makes for an easy delivery. Follow standard protocols for receiving\ - \ and inventory verification, but expect a relaxed process. %s + \ and inventory verification, but expect a relaxed process. {0} criticalSupplies7.text=We are delivering the requested supplies as per your requisition.\ \ Given the enemy's retreat, this should be straightforward. Complete standard\ - \ verification upon receipt, but feel free to take a breath - we're in control. %s + \ verification upon receipt, but feel free to take a breath - we're in control. {0} criticalSupplies8.text=Your supply request has been processed, and the shipment is on its way.\ \ With the enemy scattered; logistics teams can prepare for the usual unloading\ - \ without any urgency. %s + \ without any urgency. {0} criticalSupplies9.text=Your requested supplies are currently en route. With the enemy retreating,\ \ you can confirm delivery at your own pace and conduct routine checks without\ - \ any expected interference. %s + \ any expected interference. {0} criticalSupplies10.text=The supplies you requested have been dispatched. The route is secure,\ \ so follow standard receipt and verification procedures once they arrive,\ - \ though no rush is needed. %s + \ though no rush is needed. {0} criticalSupplies11.text=We have processed your recent supply request, and the shipment is now\ \ en route. With the enemy in disarray, there's no risk of delays. Confirm\ - \ delivery upon receipt, but feel at ease as things have calmed. %s + \ delivery upon receipt, but feel at ease as things have calmed. {0} criticalSupplies12.text=Your requested supplies have been expedited for quicker delivery.\ \ Given the enemy's retreat, this should be a smooth handover. Have personnel\ - \ ready for swift unloading, but enjoy the lull. %s + \ ready for swift unloading, but enjoy the lull. {0} criticalSupplies13.text=The supplies you requisitioned have been approved and dispatched.\ \ The enemy's disorganized state means a clear path ahead, so ensure a clear\ - \ inventory check upon delivery. %s + \ inventory check upon delivery. {0} criticalSupplies14.text=Your requested supplies are on schedule for delivery. The enemy poses\ \ no significant threat now, so the receiving process should be simple. Let us\ - \ know if any special instructions are needed, but take your time. %s + \ know if any special instructions are needed, but take your time. {0} criticalSupplies15.text=Your requisition has been processed, and the requested supplies are en\ \ route. With the enemy scattered, ensure prompt unloading, but you can relax\ - \ knowing there's no immediate danger. %s + \ knowing there's no immediate danger. {0} criticalSupplies16.text=The supplies requested by your unit have been shipped. With the\ \ enemy's retreat, we expect an easy delivery. Follow all standard protocols,\ - \ but feel assured of a safe process. %s + \ but feel assured of a safe process. {0} criticalSupplies17.text=The shipment you requested is on its way. The enemy's condition is\ \ dire, so expect an uneventful delivery. Confirm the quantity and condition\ - \ upon arrival, but rest assured, all is secure. %s + \ upon arrival, but rest assured, all is secure. {0} criticalSupplies18.text=Your supply request has been fulfilled, and the shipment is currently en\ \ route. With the enemy scattered, have your logistics team ready, but know there's\ - \ no urgency under the circumstances. %s + \ no urgency under the circumstances. {0} criticalSupplies19.text=The supplies you requested have been dispatched and are on schedule\ \ for delivery. The enemy is in full retreat, so the team can expect a smooth\ - \ receipt and confirmation process. %s + \ receipt and confirmation process. {0} weakenedSupplies0.text=We've processed your supply request, and the convoy is en route.\ \ With the enemy's forces nearly destroyed, we anticipate no issues during delivery.\ \ Complete standard checks upon arrival, but rest assured that any urgent needs\ - \ can be addressed promptly without interference. %s + \ can be addressed promptly without interference. {0} weakenedSupplies1.text=Your requested supplies have been approved and dispatched. The shipment\ \ is on its way, with no expected threats given the enemy's weakened state.\ - \ Confirm receipt upon delivery and let us know if any follow-up is needed. %s + \ Confirm receipt upon delivery and let us know if any follow-up is needed. {0} weakenedSupplies2.text=The supplies you requested have been dispatched and should arrive soon.\ \ The enemy's compromised position ensures minimal risks. Have personnel ready\ - \ to receive and verify the shipment following standard procedures. %s + \ to receive and verify the shipment following standard procedures. {0} weakenedSupplies3.text=Your recent supply request has been processed, and the shipment is on its\ \ way. With the enemy barely holding on, there's little to worry about. Standard\ - \ verification upon receipt should be straightforward. %s + \ verification upon receipt should be straightforward. {0} weakenedSupplies4.text=The requested supplies have been packed and shipped. The enemy's\ \ disorganized state guarantees safe passage, so personnel can conduct routine\ - \ inventory checks without urgency. %s + \ inventory checks without urgency. {0} weakenedSupplies5.text=Your requisitioned supplies have been authorized and are now en route.\ \ Given the enemy's dire condition, delivery should be smooth. Please ensure\ - \ that verification protocols are followed upon receipt. %s + \ that verification protocols are followed upon receipt. {0} weakenedSupplies6.text=Supplies requested by your unit have been shipped and are in transit.\ \ With the enemy incapacitated, expect a simple handover. Follow standard\ - \ receiving and verification procedures once the shipment arrives. %s + \ receiving and verification procedures once the shipment arrives. {0} weakenedSupplies7.text=We are delivering the requested supplies as per your requisition form.\ \ The enemy's severely compromised state means no anticipated delays. Complete\ - \ standard verification upon receipt. %s + \ standard verification upon receipt. {0} weakenedSupplies8.text=Your supply request has been processed, and the shipment is on its way.\ \ With enemy forces in disarray; logistics teams can expect routine unloading\ - \ and inventory procedures. %s + \ and inventory procedures. {0} weakenedSupplies9.text=Your requested supplies are currently en route. Given the enemy's near\ \ collapse, there should be no hindrances. Confirm delivery and conduct standard\ - \ checks at your convenience. %s + \ checks at your convenience. {0} weakenedSupplies10.text=The supplies you requested have been dispatched. With enemy morale\ \ breaking, the delivery route is clear. Please follow standard receipt and\ - \ verification procedures upon arrival. %s + \ verification procedures upon arrival. {0} weakenedSupplies11.text=We have processed your recent supply request, and the shipment is now en\ \ route. It should be an easy run for the convoy with the enemy withdrawing. Confirm delivery upon\ - \ receipt and report any discrepancies as needed. %s + \ receipt and report any discrepancies as needed. {0} weakenedSupplies12.text=Your requested supplies have been expedited for faster delivery. The\ \ enemy's shattered state ensures a swift and secure handover. Have personnel\ - \ prepared for unloading and verification. %s + \ prepared for unloading and verification. {0} weakenedSupplies13.text=The supplies you requisitioned have been approved and dispatched. With\ \ enemy forces near defeat, delivery should be seamless. Ensure thorough\ - \ inventory checks upon receipt. %s + \ inventory checks upon receipt. {0} weakenedSupplies14.text=Your requested supplies are on schedule for delivery. The enemy's\ \ inability to mount any resistance means no expected delays. Follow standard\ - \ receiving procedures and let us know of any special handling needs. %s + \ receiving procedures and let us know of any special handling needs. {0} weakenedSupplies15.text=Your requisition has been processed, and the requested supplies are en\ \ route. Given the enemy's decimated forces, expect a straightforward unloading\ - \ and confirmation process. %s + \ and confirmation process. {0} weakenedSupplies16.text=The supplies requested by your unit have been shipped and are in transit.\ \ Limited hostiles in your sector means an easy delivery. Follow all standard protocols\ - \ upon receipt. %s + \ upon receipt. {0} weakenedSupplies17.text=The shipment you requested is on its way. With the enemy's combat\ \ effectiveness diminished, confirm the quantity and condition of the supplies\ - \ upon arrival without concern. %s + \ upon arrival without concern. {0} weakenedSupplies18.text=Your supply request has been fulfilled, and the shipment is currently en\ \ route. The enemy's state means this should be a smooth run, so logistics\ - \ personnel can follow routine intake and verification. %s + \ personnel can follow routine intake and verification. {0} weakenedSupplies19.text=The supplies you requested have been dispatched and are on schedule for\ \ delivery. With the enemy's forces in disarray, ensure your team is prepared\ - \ for receipt and follow standard confirmation protocols. %s + \ for receipt and follow standard confirmation protocols. {0} stalemateSupplies0.text=We've processed your supply request. The convoy is currently en route,\ \ but given the ongoing skirmishes, expect potential delays. Please complete\ \ all standard receiving checks upon delivery, and let us know promptly if\ - \ there are any urgent needs or issues. %s + \ there are any urgent needs or issues. {0} stalemateSupplies1.text=Your requested supplies have been approved and dispatched. The shipment\ \ is currently en route, but with the ongoing conflict, please confirm receipt\ \ upon delivery and notify us of any discrepancies or urgent requirements as\ - \ soon as possible. %s + \ soon as possible. {0} stalemateSupplies2.text=The supplies you requested have been dispatched and should arrive soon.\ \ With both sides still clashing, ensure appropriate personnel are ready to\ - \ receive and verify the shipment according to standard protocols. %s + \ receive and verify the shipment according to standard protocols. {0} stalemateSupplies3.text=Your recent supply request has been processed, and the shipment is on its\ \ way. The conflict continues, so please follow standard verification procedures\ - \ upon receipt and report any issues immediately to avoid operational disruptions. %s + \ upon receipt and report any issues immediately to avoid operational disruptions. {0} stalemateSupplies4.text=The requested supplies have been packed and shipped. The situation\ \ remains tense, so ensure that personnel are prepared for standard inventory\ - \ checks upon delivery to maintain accurate records and address any shortages. %s + \ checks upon delivery to maintain accurate records and address any shortages. {0} stalemateSupplies5.text=Your requisitioned supplies have been authorized and are now en route.\ \ Given the ongoing stalemate, please ensure that verification protocols are\ - \ followed upon receipt to confirm accuracy and condition. %s + \ followed upon receipt to confirm accuracy and condition. {0} stalemateSupplies6.text=Supplies requested by your unit have been shipped and are in transit.\ \ With both sides evenly matched, standard protocols for receiving and inventory\ - \ verification should be followed, though delays are possible. %s + \ verification should be followed, though delays are possible. {0} stalemateSupplies7.text=We are delivering the requested supplies as per your requisition form.\ \ With skirmishes ongoing, please complete standard verification upon receipt\ - \ to confirm the shipment's contents and let us know if there are any issues. %s + \ to confirm the shipment's contents and let us know if there are any issues. {0} stalemateSupplies8.text=Your supply request has been processed, and the shipment is on its way.\ \ Prepare logistics teams for the usual unloading and inventory, but be aware\ - \ that the situation remains uncertain. %s + \ that the situation remains uncertain. {0} stalemateSupplies9.text=Your requested supplies are currently en route. With both sides locked\ \ in conflict, confirm delivery upon arrival and conduct routine inventory\ - \ checks to ensure all items are accounted for and in good condition. %s + \ checks to ensure all items are accounted for and in good condition. {0} stalemateSupplies10.text=The supplies you requested have been dispatched. Once they arrive,\ \ follow standard receipt and verification procedures to confirm accuracy,\ - \ as the ongoing conflict may impact delivery times. %s + \ as the ongoing conflict may impact delivery times. {0} stalemateSupplies11.text=We have processed your recent supply request, and the shipment is now\ \ en route. With battles still unfolding, confirm delivery upon receipt and\ - \ report any discrepancies immediately. %s + \ report any discrepancies immediately. {0} stalemateSupplies12.text=Your requested supplies have been expedited for faster delivery, given\ \ the unpredictable situation. We recommend having personnel prepared for swift\ - \ unloading and verification to avoid delays due to the ongoing fighting. %s + \ unloading and verification to avoid delays due to the ongoing fighting. {0} stalemateSupplies13.text=The supplies you requisitioned have been approved and dispatched. With\ \ the enemy active in your sector, ensure thorough inventory checks upon delivery,\ - \ and notify us of any concerns immediately. %s + \ and notify us of any concerns immediately. {0} stalemateSupplies14.text=Your requested supplies are on schedule for delivery. As the situation\ \ remains unpredictable, please ensure your team is prepared for standard\ - \ receiving procedures and let us know of any special handling needs. %s + \ receiving procedures and let us know of any special handling needs. {0} stalemateSupplies15.text=Your requisition has been processed, and the requested supplies are en\ \ route. With skirmishes ongoing, ensure prompt unloading and confirmation once\ - \ received to maintain operational continuity. %s + \ received to maintain operational continuity. {0} stalemateSupplies16.text=The supplies requested by your unit have been shipped and are in transit.\ \ With both sides still evenly matched, follow all standard protocols for\ - \ receipt and inventory to support your operations effectively. %s + \ receipt and inventory to support your operations effectively. {0} stalemateSupplies17.text=The shipment you requested is on its way. Given ongoing enemy action,\ \ confirm the quantity and condition of the supplies upon arrival and let us\ - \ know if any adjustments are needed for future shipments. %s + \ know if any adjustments are needed for future shipments. {0} stalemateSupplies18.text=Your supply request has been fulfilled, and the shipment is currently en\ \ route. Have logistics personnel ready for standard intake and verification,\ - \ but be mindful of potential delays given the ongoing skirmishes. %s + \ but be mindful of potential delays given the ongoing skirmishes. {0} stalemateSupplies19.text=The supplies you requested have been dispatched and are on schedule for\ \ delivery. Given the activity in your area, ensure your team is prepared for\ - \ receipt and confirm delivery as per usual protocols. %s + \ receipt and confirm delivery as per usual protocols. {0} advancingSupplies0.text=We've processed your supply request, and the convoy is currently en route.\ \ With the enemy gaining momentum, anticipate potential delays. Complete all\ \ standard receiving checks upon delivery and inform us of any urgent needs or\ - \ issues so we can assist promptly. %s + \ issues so we can assist promptly. {0} advancingSupplies1.text=Your requested supplies have been approved and dispatched. The shipment\ \ is on its way, but with the enemy pushing forward, confirm receipt upon\ - \ delivery and notify us of any urgent requirements or discrepancies immediately. %s + \ delivery and notify us of any urgent requirements or discrepancies immediately. {0} advancingSupplies2.text=The supplies you requested have been dispatched and should arrive soon.\ \ With the enemy making coordinated strikes in all sectors, ensure personnel are ready to\ \ receive the shipment and verify its contents promptly, following standard\ - \ protocols. %s + \ protocols. {0} advancingSupplies3.text=Your recent supply request has been processed, and the shipment is en route.\ \ As the enemy advances, follow standard verification procedures upon receipt,\ - \ and report any issues swiftly to avoid further disruptions. %s + \ and report any issues swiftly to avoid further disruptions. {0} advancingSupplies4.text=The requested supplies have been packed and shipped. Given the enemy's\ \ increasing control over key areas, ensure that personnel are prepared for\ - \ inventory checks upon delivery to maintain accurate records. %s + \ inventory checks upon delivery to maintain accurate records. {0} advancingSupplies5.text=Your requisitioned supplies have been authorized and are now on their way.\ \ With the enemy's momentum increasing, please ensure verification protocols\ - \ are followed upon receipt to confirm accuracy and condition of the shipment. %s + \ are followed upon receipt to confirm accuracy and condition of the shipment. {0} advancingSupplies6.text=Supplies requested by your unit have been shipped and are currently in\ \ transit. Given the enemy's strong advance, follow standard protocols for\ \ receiving and inventory verification upon arrival, but be ready for potential\ - \ disruptions. %s + \ disruptions. {0} advancingSupplies7.text=We are delivering the requested supplies as per your requisition form.\ \ With the increased enemy activity in the area, complete standard verification upon\ - \ receipt swiftly, and let us know of any issues requiring immediate attention. %s + \ receipt swiftly, and let us know of any issues requiring immediate attention. {0} advancingSupplies8.text=Your supply request has been processed, and the shipment is on its way.\ \ With the battlefield situation deteriorating, prepare logistics teams for\ - \ rapid unloading and inventory to avoid unnecessary delays. %s + \ rapid unloading and inventory to avoid unnecessary delays. {0} advancingSupplies9.text=Your requested supplies are currently en route. Given the enemy's\ \ increasing pressure, confirm delivery upon arrival and conduct routine\ - \ inventory checks promptly to ensure all items are accounted for. %s + \ inventory checks promptly to ensure all items are accounted for. {0} advancingSupplies10.text=The supplies you requested have been dispatched. With enemy forces\ \ gaining ground, follow standard receipt and verification procedures as\ - \ quickly as possible to confirm the shipment's accuracy and condition. %s + \ quickly as possible to confirm the shipment's accuracy and condition. {0} advancingSupplies11.text=We have processed your recent supply request, and the shipment is now\ \ en route. As the enemy's position strengthens, confirm delivery upon receipt\ - \ and report any discrepancies immediately to avoid delays. %s + \ and report any discrepancies immediately to avoid delays. {0} advancingSupplies12.text=Your requested supplies have been expedited for faster delivery.\ \ Given the enemy's growing dominance, we recommend having personnel prepared\ - \ for swift unloading and verification to maintain operational readiness. %s + \ for swift unloading and verification to maintain operational readiness. {0} advancingSupplies13.text=The supplies you requisitioned have been approved and dispatched. With\ \ the enemy pressing hard, ensure a thorough inventory check upon delivery,\ - \ and report any concerns urgently. %s + \ and report any concerns urgently. {0} advancingSupplies14.text=Your requested supplies are on schedule for delivery. With the enemy\ \ making headway, please ensure your team is prepared for quick receiving\ - \ procedures, and notify us of any special handling needs. %s + \ procedures, and notify us of any special handling needs. {0} advancingSupplies15.text=Your requisition has been processed, and the requested supplies are en\ \ route. Given the enemy's advances, ensure prompt unloading and confirmation\ - \ upon receipt to maintain supply lines. %s + \ upon receipt to maintain supply lines. {0} advancingSupplies16.text=The supplies requested by your unit have been shipped and are in transit.\ \ As the enemy asserts control, follow all standard protocols for receipt and\ - \ inventory quickly to support your operations effectively. %s + \ inventory quickly to support your operations effectively. {0} advancingSupplies17.text=The shipment you requested is on its way. With the enemy dominating\ \ key areas, confirm the quantity and condition of the supplies upon arrival,\ - \ and let us know if adjustments are needed for future shipments. %s + \ and let us know if adjustments are needed for future shipments. {0} advancingSupplies18.text=Your supply request has been fulfilled, and the shipment is currently\ \ en route. As the enemy gains momentum, have logistics personnel ready for\ - \ rapid intake and verification. %s + \ rapid intake and verification. {0} advancingSupplies19.text=The supplies you requested have been dispatched and are on schedule for\ \ delivery. With the enemy's control tightening, ensure your team is prepared\ - \ for receipt, and confirm delivery as per usual protocols. %s + \ for receipt, and confirm delivery as per usual protocols. {0} dominatingSupplies0.text=We've processed your supply request, and the convoy is currently en route.\ \ With the enemy controlling key objectives, delivery may be delayed. Complete\ \ all standard receiving checks promptly upon arrival, and inform us of any\ - \ urgent needs or issues immediately. %s + \ urgent needs or issues immediately. {0} dominatingSupplies1.text=Your requested supplies have been approved and dispatched. The shipment\ \ is en route, but given the dire situation, confirm receipt upon delivery and\ - \ report any urgent discrepancies as soon as possible. %s + \ report any urgent discrepancies as soon as possible. {0} dominatingSupplies2.text=The supplies you requested have been dispatched and should arrive soon.\ \ As the enemy threatens our supply routes, ensure personnel are ready to receive the\ - \ shipment and verify its contents swiftly, following standard protocols. %s + \ shipment and verify its contents swiftly, following standard protocols. {0} dominatingSupplies3.text=Your recent supply request has been processed, and the shipment is on its way.\ \ With defeat looming, follow verification procedures urgently upon receipt\ - \ and report any issues immediately to avoid further disruptions. %s + \ and report any issues immediately to avoid further disruptions. {0} dominatingSupplies4.text=The requested supplies have been packed and shipped. With the enemy\ \ gaining ground, ensure that personnel conduct immediate inventory checks\ - \ upon delivery to maintain supply accuracy and address any shortages urgently. %s + \ upon delivery to maintain supply accuracy and address any shortages urgently. {0} dominatingSupplies5.text=Your requisitioned supplies have been authorized and are now en route.\ \ As the situation worsens, follow verification protocols promptly upon receipt\ - \ to confirm accuracy and prevent further setbacks. %s + \ to confirm accuracy and prevent further setbacks. {0} dominatingSupplies6.text=Supplies requested by your unit have been shipped and are currently in\ \ transit. With heavy casualties reported, adhere to standard protocols for\ - \ receiving and inventory verification as quickly as possible. %s + \ receiving and inventory verification as quickly as possible. {0} dominatingSupplies7.text=We are delivering the requested supplies as per your requisition form.\ \ Given the enemy's upper hand, complete verification upon receipt urgently,\ - \ and report any issues that need immediate attention. %s + \ and report any issues that need immediate attention. {0} dominatingSupplies8.text=Your supply request has been processed, and the shipment is on its way.\ \ As the enemy controls critical areas, prepare logistics teams for rapid\ - \ unloading and inventory to prevent further losses. %s + \ unloading and inventory to prevent further losses. {0} dominatingSupplies9.text=Your requested supplies are currently en route. With mounting pressure\ \ from the enemy, confirm delivery immediately upon arrival and conduct urgent\ - \ inventory checks to ensure all items are accounted for. %s + \ inventory checks to ensure all items are accounted for. {0} dominatingSupplies10.text=The supplies you requested have been dispatched. Despite defeat seeming\ \ imminent, follow receipt and verification procedures swiftly to confirm\ - \ the shipment's accuracy and condition. %s + \ the shipment's accuracy and condition. {0} dominatingSupplies11.text=We have processed your recent supply request, and the shipment is en route.\ \ With the enemy dominating the sector, confirm delivery urgently upon receipt and report\ - \ discrepancies immediately. %s + \ discrepancies immediately. {0} dominatingSupplies12.text=Your requested supplies have been expedited for faster delivery. Given\ \ the worsening situation, have personnel prepared for immediate unloading\ - \ and verification to prevent further delays. %s + \ and verification to prevent further delays. {0} dominatingSupplies13.text=The supplies you requisitioned have been approved and dispatched.\ \ Our forces are struggling in this sector, ensure a thorough inventory check upon delivery\ - \ and report any concerns urgently. %s + \ and report any concerns urgently. {0} dominatingSupplies14.text=Your requested supplies are on schedule for delivery. As the enemy\ \ is inflicting heavy casualties, ensure your team is prepared for rapid receiving,\ - \ and notify us of any special handling needs immediately. %s + \ and notify us of any special handling needs immediately. {0} dominatingSupplies15.text=Your requisition has been processed, and the requested supplies are en\ \ route. Despite loss of the depot approaching, ensure prompt unloading and confirmation to\ - \ maintain any remaining operational continuity. %s + \ maintain any remaining operational continuity. {0} dominatingSupplies16.text=The supplies requested by your unit have been shipped and are in transit.\ \ With the enemy controlling this sector, follow all protocols for receipt\ - \ and inventory swiftly to support your forces. %s + \ and inventory swiftly to support your forces. {0} dominatingSupplies17.text=The shipment you requested is on its way. As our forces are facing heavy\ \ casualties, confirm the quantity and condition of the supplies upon arrival\ - \ urgently and notify us of any immediate adjustments needed. %s + \ urgently and notify us of any immediate adjustments needed. {0} dominatingSupplies18.text=Your supply request has been fulfilled, and the shipment is en route.\ \ With the enemy gaining the upper hand, have logistics personnel ready for\ - \ rapid intake and verification to avoid further setbacks. %s + \ rapid intake and verification to avoid further setbacks. {0} dominatingSupplies19.text=The supplies you requested have been dispatched and are on schedule for\ \ delivery. Given the enemy's dominance, ensure your team is prepared for urgent\ - \ receipt and confirm delivery without delay. %s + \ receipt and confirm delivery without delay. {0} overwhelmingSupplies0.text=We've processed your supply request, and the convoy is en route.\ \ The enemy is executing a decisive push, so there's no time to waste. Complete\ \ receiving checks as quickly as possible upon arrival. Report urgent needs\ - \ immediately, as we have limited time to act. %s + \ immediately, as we have limited time to act. {0} overwhelmingSupplies1.text=Your requested supplies have been approved and dispatched. Given the\ \ enemy's overwhelming push, confirm receipt as soon as the shipment arrives,\ - \ and notify us of any urgent discrepancies without delay. %s + \ and notify us of any urgent discrepancies without delay. {0} overwhelmingSupplies2.text=The supplies you requested have been dispatched and should arrive soon.\ \ Ensure that personnel are on high alert to receive and verify the shipment,\ - \ as the situation is critical. %s + \ as the situation is critical. {0} overwhelmingSupplies3.text=Your recent supply request has been processed, and the shipment is on its\ \ way. Despite our forces facing collapse, follow verification procedures immediately\ - \ upon receipt and report any issues urgently to prevent further setbacks. %s + \ upon receipt and report any issues urgently to prevent further setbacks. {0} overwhelmingSupplies4.text=The requested supplies have been packed and shipped. With the enemy's\ \ overwhelming advance, personnel must conduct immediate inventory checks upon\ - \ delivery to secure any vital materials before it's too late. %s + \ delivery to secure any vital materials before it's too late. {0} overwhelmingSupplies5.text=Your requisitioned supplies have been authorized and are now en route.\ \ With defeat imminent, verification protocols must be followed swiftly upon\ - \ receipt to confirm accuracy and condition. %s + \ receipt to confirm accuracy and condition. {0} overwhelmingSupplies6.text=Supplies requested by your unit have been shipped and are currently in\ \ transit. Given the extreme circumstances, follow standard protocols for\ - \ receiving and verification as quickly as possible upon arrival. %s + \ receiving and verification as quickly as possible upon arrival. {0} overwhelmingSupplies7.text=We are delivering the requested supplies as per your requisition form.\ \ The enemy is pressing for total victory, so complete verification immediately\ - \ upon receipt and report any critical issues. %s + \ upon receipt and report any critical issues. {0} overwhelmingSupplies8.text=Your supply request has been processed, and the shipment is en route.\ \ The situation is desperate - ensure logistics teams are ready for rapid unloading\ - \ and inventory upon arrival. %s + \ and inventory upon arrival. {0} overwhelmingSupplies9.text=Your requested supplies are currently en route. With your forces on the\ \ verge of collapse, confirm delivery urgently upon arrival and conduct immediate\ - \ inventory checks to secure what you can. %s + \ inventory checks to secure what you can. {0} overwhelmingSupplies10.text=The supplies you requested have been dispatched. Given the dire situation,\ \ follow receipt and verification procedures immediately to confirm the shipment's\ - \ contents and condition. %s + \ contents and condition. {0} overwhelmingSupplies11.text=We have processed your recent supply request, and the shipment is now\ \ en route. As we prepare to withdraw from this sector, confirm delivery upon receipt and report any\ - \ discrepancies urgently. %s + \ discrepancies urgently. {0} overwhelmingSupplies12.text=Your requested supplies have been expedited for immediate delivery.\ \ Have personnel prepared for fast unloading and verification to secure the\ - \ supplies amid the enemy's final push. %s + \ supplies amid the enemy's final push. {0} overwhelmingSupplies13.text=The supplies you requisitioned have been approved and dispatched. Ensure\ \ a rapid inventory check upon delivery, and escalate any critical concerns\ - \ immediately given the deteriorating situation. %s + \ immediately given the deteriorating situation. {0} overwhelmingSupplies14.text=Your requested supplies are on schedule for delivery. With the enemy\ \ pushing for total victory, ensure your team is prepared for urgent receiving,\ - \ and report any special handling needs immediately. %s + \ and report any special handling needs immediately. {0} overwhelmingSupplies15.text=Your requisition has been processed, and the supplies are en\ \ route. Given the enemy's overwhelming advance, ensure immediate unloading\ - \ and confirmation to maintain any semblance of operational continuity. %s + \ and confirmation to maintain any semblance of operational continuity. {0} overwhelmingSupplies16.text=The supplies requested by your unit have been shipped and are in transit.\ \ As collapse appears imminent, follow all standard protocols for receipt and\ - \ inventory urgently. %s + \ inventory urgently. {0} overwhelmingSupplies17.text=The shipment you requested is on its way. Given the overwhelming enemy\ \ assault, confirm the quantity and condition of the supplies upon arrival\ - \ urgently, and notify us of any necessary adjustments immediately. %s + \ urgently, and notify us of any necessary adjustments immediately. {0} overwhelmingSupplies18.text=Your supply request has been fulfilled, and the shipment is currently\ \ en route. With the enemy pressing for total victory, have logistics personnel\ - \ ready for rapid intake and verification. %s + \ ready for rapid intake and verification. {0} overwhelmingSupplies19.text=The supplies you requested have been dispatched and are on schedule for\ \ delivery. As our forces face collapse, ensure your team is prepared for urgent\ - \ receipt, and confirm delivery immediately. %s + \ receipt, and confirm delivery immediately. {0} guerrillaSpeaker.text=Smuggler Contact -guerrillaSupplies0.text=%s, word's out that you're tangling with some rough forces from %s.\ +guerrillaSupplies0.text={0}, word's out that you're tangling with some rough forces from {1}.\ \ I've got some high-end, hard-to-find tech that could make your life a whole\ - \ lot easier. For %s, I'll have it delivered to your doorstep, no questions asked,\ - \ no strings attached. All you gotta do is give me the green light. %s -guerrillaSupplies1.text=Hey %s! I hear you're up against it with %s breathing down\ + \ lot easier. For {2}, I'll have it delivered to your doorstep, no questions asked,\ + \ no strings attached. All you gotta do is give me the green light. {3} +guerrillaSupplies1.text=Hey {0}! I hear you're up against it with {1} breathing down\ \ your neck. Lucky for you, I've got some spare parts and specialty tech that\ \ might just tip the odds in your favor. We're talking the kind of equipment\ - \ that could keep your machines and your crew one step ahead. %s,\ - \ and it's yours. Just say the word. %s -guerrillaSupplies2.text=%s, word around the streets is that you're in need of\ + \ that could keep your machines and your crew one step ahead. {2},\ + \ and it's yours. Just say the word. {3} +guerrillaSupplies2.text={0}, word around the streets is that you're in need of\ \ a little boost. I've got a stash of rare tech that could help turn the tables\ - \ on %s. Nothing too big, but potent enough to make a difference in your\ - \ campaign. It'll run you %s, and trust me, you won't find this stuff on the open market.\ - \ Think it over, but don't take too long. Gear like this disappears fast. %s -guerrillaSupplies3.text=%s, running a guerrilla war without the right equipment?\ + \ on {1}. Nothing too big, but potent enough to make a difference in your\ + \ campaign. It'll run you {2}, and trust me, you won't find this stuff on the open market.\ + \ Think it over, but don't take too long. Gear like this disappears fast. {3} +guerrillaSupplies3.text={0}, running a guerrilla war without the right equipment?\ \ That's just a recipe for headaches. Lucky for you, I've secured some\ - \ exclusive parts and upgrades that %s would hate to see in your hands.\ - \ %s, and it's all yours. I'll even throw in a few 'extra'\ - \ connections if you're interested. Just say the word. %s -guerrillaSupplies4.text=%s, I respect the hell outta what you're doing out there\ - \ against %s. But let's be real - you need the right kind of hardware to\ + \ exclusive parts and upgrades that {1} would hate to see in your hands.\ + \ {2}, and it's all yours. I'll even throw in a few 'extra'\ + \ connections if you're interested. Just say the word. {3} +guerrillaSupplies4.text={0}, I respect the hell outta what you're doing out there\ + \ against {1}. But let's be real - you need the right kind of hardware to\ \ keep up this fight. I've got access to some high-spec tech and specialized\ - \ parts that'll make life a lot easier for your crew. It's yours for %s.\ + \ parts that'll make life a lot easier for your crew. It's yours for {2}.\ \ A bit steep, maybe, but quality like this doesn't come cheap.\ - \ Let me know if you're in. %s -guerrillaSupplies5.text=%s, I get it - you're up against %s without enough gear to go\ + \ Let me know if you're in. {3} +guerrillaSupplies5.text={0}, I get it - you're up against {1} without enough gear to go\ \ around. I've got some select tech that'll give your forces a sharp edge.\ - \ We're talking black-market grade. It'll cost you %s, but\ - \ it'll be worth every one. No need to thank me - just give the money. %s -guerrillaSupplies6.text=Hey %s, %s folks don't fight fair, so why should you?\ + \ We're talking black-market grade. It'll cost you {2}, but\ + \ it'll be worth every one. No need to thank me - just give the money. {3} +guerrillaSupplies6.text=Hey {0}, {1} folks don't fight fair, so why should you?\ \ I've got some choice tech that could give you the upper hand, no problem.\ - \ I'll have it on your doorstep for %s. Let me know if you're\ - \ interested - gear like this doesn't wait around. %s -guerrillaSupplies7.text=%s, you're holding your own, but if you want to keep up the\ - \ pressure on %s, you're gonna need more than grit. I've got my hands on\ - \ some discreet tech that could make all the difference. %s, and it's yours.\ - \ You know where to find me. %s -guerrillaSupplies8.text=Word is, %s, you're scraping the bottom of the barrel to keep\ + \ I'll have it on your doorstep for {2}. Let me know if you're\ + \ interested - gear like this doesn't wait around. {3} +guerrillaSupplies7.text={0}, you're holding your own, but if you want to keep up the\ + \ pressure on {1}, you're gonna need more than grit. I've got my hands on\ + \ some discreet tech that could make all the difference. {2}, and it's yours.\ + \ You know where to find me. {3} +guerrillaSupplies8.text=Word is, {0}, you're scraping the bottom of the barrel to keep\ \ your forces operational. I've got a line on some parts and tech that'll\ - \ keep your machines running smooth even in the thick of it with %s.\ - \ %s, no questions asked. Just say the word, and it's yours. %s -guerrillaSupplies9.text=%s, your people need top-tier support if they're gonna stay\ - \ a step ahead of %s. Lucky for you, I've secured some high-grade components\ - \ that'll keep you in the fight a little longer. I'll let it go for %s.\ - \ Quick and quiet delivery - give the nod, and it's done. %s -guerrillaSupplies10.text=%s, it's tough running a fight like this on scraps, isn't it?\ - \ I've got just the kind of rare gear and parts you need to keep %s forces guessing.\ - \ It'll cost you %s. Smooth delivery, no mess. Just say the word, and it's all yours. %s -guerrillaSupplies11.text=Look, %s, I know the struggle of keeping up with %s while\ + \ keep your machines running smooth even in the thick of it with {1}.\ + \ {2}, no questions asked. Just say the word, and it's yours. {3} +guerrillaSupplies9.text={0}, your people need top-tier support if they're gonna stay\ + \ a step ahead of {1}. Lucky for you, I've secured some high-grade components\ + \ that'll keep you in the fight a little longer. I'll let it go for {2}.\ + \ Quick and quiet delivery - give the nod, and it's done. {3} +guerrillaSupplies10.text={0}, it's tough running a fight like this on scraps, isn't it?\ + \ I've got just the kind of rare gear and parts you need to keep {1} forces guessing.\ + \ It'll cost you {2}. Smooth delivery, no mess. Just say the word, and it's all yours. {3} +guerrillaSupplies11.text=Look, {0}, I know the struggle of keeping up with {1} while\ \ strapped for resources. But I've got a stash of quality tech that could\ - \ lighten the load for you. %s, and I'll have it shipped to you clean and under the radar.\ - \ A little investment goes a long way. %s -guerrillaSupplies12.text=%s, the odds are against you out there with %s, but you don't\ + \ lighten the load for you. {2}, and I'll have it shipped to you clean and under the radar.\ + \ A little investment goes a long way. {3} +guerrillaSupplies12.text={0}, the odds are against you out there with {1}, but you don't\ \ have to fight fair. I've secured some specialized parts that could give\ - \ you the edge you're looking for. For %s, I'll get it to you fast and quiet. You know the deal. %s -guerrillaSupplies13.text=%s, this campaign won't last long if your tech can't keep up.\ + \ you the edge you're looking for. For {2}, I'll get it to you fast and quiet. You know the deal.\ + \ {3} +guerrillaSupplies13.text={0}, this campaign won't last long if your tech can't keep up.\ \ I've got my hands on some rare parts that'd be a dream for your team,\ - \ especially with %s breathing down your neck. %s.\ - \ Smooth and discreet delivery, just say the word. %s -guerrillaSupplies14.text=%s, I know you're looking to stay ahead of %s, and I can help.\ + \ especially with {1} breathing down your neck. {2}.\ + \ Smooth and discreet delivery, just say the word. {3} +guerrillaSupplies14.text={0}, I know you're looking to stay ahead of {1}, and I can help.\ \ I've got a line on some hard-to-find tech that'll make a real difference out there.\ - \ %s, and I'll get it to you before your luck runs out. Just let me know. %s -guerrillaSupplies15.text=%s, scrounging for parts in a firefight with %s isn't ideal.\ + \ {2}, and I'll get it to you before your luck runs out. Just let me know. {3} +guerrillaSupplies15.text={0}, scrounging for parts in a firefight with {1} isn't ideal.\ \ Lucky for you, I've got access to some hard-to-get components that'll\ - \ keep you in the game. For %s, they're yours - no hassle, no trace. Just say the word. %s -guerrillaSupplies16.text=Hey %s, holding the line against %s isn't easy, especially\ + \ keep you in the game. For {2}, they're yours - no hassle, no trace. Just say the word. {3} +guerrillaSupplies16.text=Hey {0}, holding the line against {1} isn't easy, especially\ \ without the right tech. I've got something that could turn things in your favor.\ - \ %s gets it delivered straight to you, smooth as silk. Give me the nod, and it's done. %s -guerrillaSupplies17.text=%s, I hear your crew's running thin on quality parts. I've\ - \ got a shipment ready that could be just what you need to keep %s guessing.\ - \ Price is %s, and I guarantee no one will see it coming. Let me know if you're in. %s -guerrillaSupplies18.text=%s, taking on %s without the best gear? Risky. But I've got\ - \ something that'll help you stay one step ahead. It'll run you %s, but trust me,\ - \ it's worth every one. Just give me the go-ahead. %s -guerrillaSupplies19.text=%s, %s forces are tough, but I've got a few tricks up my sleeve.\ - \ Some prime tech ready to ship your way for %s.\ - \ No strings, no trouble - just tell me where to send it. %s -guerrillaSupplies20.text=%s, I know supplies are tight, and %s isn't making things easier.\ + \ {2} gets it delivered straight to you, smooth as silk. Give me the nod, and it's done. {3} +guerrillaSupplies17.text={0}, I hear your crew's running thin on quality parts. I've\ + \ got a shipment ready that could be just what you need to keep {1} guessing.\ + \ Price is {2}, and I guarantee no one will see it coming. Let me know if you're in. {3} +guerrillaSupplies18.text={0}, taking on {1} without the best gear? Risky. But I've got\ + \ something that'll help you stay one step ahead. It'll run you {2}, but trust me,\ + \ it's worth every one. Give me the go-ahead. {3} +guerrillaSupplies19.text={0}, {1} forces are tough, but I've got a few tricks up my sleeve.\ + \ Some prime tech ready to ship your way for {2}.\ + \ No strings, no trouble - just tell me where to send it. {3} +guerrillaSupplies20.text={0}, I know supplies are tight, and {1} isn't making things easier.\ \ I've got some premium parts lined up for you - no questions asked.\ - \ %s and they're yours, shipped right under their noses. Just say the word. %s -guerrillaSupplies21.text=%s, fighting %s without proper resources is a losing game. Lucky\ + \ {2} and they're yours, shipped right under their noses. Just say the word. {3} +guerrillaSupplies21.text={0}, fighting {1} without proper resources is a losing game. Lucky\ \ for you, I've secured some rare parts and tech that'll keep your edge sharp.\ - \ %s and I'll handle the rest. Just let me know. %s -guerrillaSupplies22.text=%s, looks like %s is putting on the pressure. I've got access\ + \ {2} and I'll handle the rest. Let me know. {3} +guerrillaSupplies22.text={0}, looks like {1} is putting on the pressure. I've got access\ \ to some quality tech that could make all the difference for your team.\ - \ For %s, I'll get it to you before they even know it's gone.\ - \ Just give me the signal. %s -guerrillaSupplies23.text=%s, it's not easy keeping up with %s in this sector.\ + \ For {2}, I'll get it to you before they even know it's gone.\ + \ Just give me the signal. {3} +guerrillaSupplies23.text={0}, it's not easy keeping up with {1} in this sector.\ \ But with a little help, I think you'll manage just fine.\ - \ I've got some choice tech lined up, %s. Quick, clean delivery.\ - \ Let me know if you're interested. %s -guerrillaSupplies24.text=%s, I've got a shipment ready that could put some serious\ - \ power back in your hands against %s. %s, and it's all yours.\ + \ I've got some choice tech lined up, {2}. Quick, clean delivery.\ + \ Let me know if you're interested. {3 +guerrillaSupplies24.text={0}, I've got a shipment ready that could put some serious\ + \ power back in your hands against {1}. {2}, and it's all yours.\ \ You know I don't offer this to just anyone.\ - \ Give me the word, and we're in business. %s + \ Give me the word, and we're in business. {3} -guerrillaSwindled0.text=%s, you really thought I'd come through, huh? It's almost sweet.\ +guerrillaSwindled0.text={0}, you really thought I'd come through, huh? It's almost sweet.\ \ Those C-Bills are gone, and so am I. While you're stuck out there\ - \ against %s, I'll be living easy. Chalk it up to experience, yeah? Maybe\ + \ against {1, I'll be living easy. Chalk it up to experience, yeah? Maybe\ \ next time, keep a tighter grip on your wallet. -guerrillaSwindled1.text=Hey %s, got a question for you: was it worth it? Those precious\ +guerrillaSwindled1.text=Hey {0}, got a question for you: was it worth it? Those precious\ \ C-Bills of yours sure went fast. I'm off to greener pastures, while\ - \ you're stuck knee-deep in trouble with %s. Keep dreaming of payback. I'm already a ghost. -guerrillaSwindled2.text=Ah, %s, it was fun while it lasted.\ + \ you're stuck knee-deep in trouble with {1}. Keep dreaming of payback. I'm already a ghost. +guerrillaSwindled2.text=Ah, {0}, it was fun while it lasted.\ \ But, as they say, 'business is business', and you were just too easy to take for a ride.\ - \ Enjoy your ongoing battle with %s - it's one you can keep fighting without my help.\ + \ Enjoy your ongoing battle with {1} - it's one you can keep fighting without my help.\ \ By the time you catch on, I'll be just a distant, expensive memory. -guerrillaSwindled3.text=%s, it's been a pleasure doing business...well, for me at least.\ +guerrillaSwindled3.text={0}, it's been a pleasure doing business...well, for me at least.\ \ Those C-Bills have already found a safer home, and trust me, it's nowhere\ \ you'd ever reach. Enjoy the generous donation, and next time? Maybe\ - \ don't trust strangers with a slick offer. -guerrillaSwindled4.text=%s, here's the hard truth: you were played. Those C-Bills\ - \ are fueling my next great escape, far away from your mess with %s.\ + \ don't trust strangers with a slick offer. Have fun with {1}! +guerrillaSwindled4.text={0}, here's the hard truth: you were played. Those C-Bills\ + \ are fueling my next great escape, far away from your mess with {1}.\ \ Think of it as a lesson learned, if that helps. I'll be thinking of\ \ you...while I enjoy every last C-Bill. -guerrillaSwindled5.text=%s, you've gotta admit, this was a classic move. You get stuck\ - \ with %s on your tail, and I get a fresh start funded by your generosity.\ +guerrillaSwindled5.text={0}, you've gotta admit, this was a classic move. You get stuck\ + \ with {1} on your tail, and I get a fresh start funded by your generosity.\ \ No hard feelings, yeah? Business is business, after all. -guerrillaSwindled6.text=%s, I almost feel bad... almost. Those C-Bills of yours will\ - \ be keeping me comfortable while you're stuck out there grinding against %s.\ +guerrillaSwindled6.text={0}, I almost feel bad... almost. Those C-Bills of yours will\ + \ be keeping me comfortable while you're stuck out there grinding against {1}.\ \ Consider it a generous donation. I'll be sure to spend it well. -guerrillaSwindled7.text=Hey %s, when you get a quiet moment, you'll have to laugh\ +guerrillaSwindled7.text=Hey {0}, when you get a quiet moment, you'll have to laugh\ \ at how smooth that was. Your C-Bills are already halfway across the galaxy,\ \ and I'm long gone. Hope the sting doesn't last too long, pal. -guerrillaSwindled8.text=%s, you put too much trust in a friendly smile. Those C-Bills\ +guerrillaSwindled8.text={0}, you put too much trust in a friendly smile. Those C-Bills\ \ are mine now, and you're left with nothing but the dust trail I left behind.\ - \ Say hello to %s for me. I'll be on my way to a very comfortable life. -guerrillaSwindled9.text=%s, consider this your wake-up call. You've got %s to worry\ + \ Say hello to {1} for me. I'll be on my way to a very comfortable life. +guerrillaSwindled9.text={0}, consider this your wake-up call. You've got {1} to worry\ \ about, and I've got your money. Fair trade, don't you think? I'd say\ \ it's been a pleasure, but I'd be lying. -guerrillaSwindled10.text=%s, sometimes you gotta respect the art of a good con.\ +guerrillaSwindled10.text={0}, sometimes you gotta respect the art of a good con.\ \ Your C-Bills are doing wonders for my retirement plans, while you're\ - \ still out there playing soldier against %s. Think of it as my fee for\ + \ still out there playing soldier against {1}. Think of it as my fee for\ \ giving you a lesson in trust. -guerrillaSwindled11.text=Hey %s, heard there's a fine line between bravery and\ +guerrillaSwindled11.text=Hey {0}, heard there's a fine line between bravery and\ \ foolishness - guess you found it. I'll be raising a glass to you from\ - \ somewhere warm, funded by your generosity. Keep it up with %s, and\ + \ somewhere warm, funded by your generosity. Keep it up with {1}, and\ \ don't worry about me. -guerrillaSwindled12.text=%s, all those C-Bills for a ghost and a promise. You've\ +guerrillaSwindled12.text={0}, all those C-Bills for a ghost and a promise. You've\ \ made it far, but you slipped up this time. I'll be long gone by the\ \ time you read this, probably sipping a PPC. Thanks for bankrolling the journey. -guerrillaSwindled13.text=%s, when it comes to playing the odds, sometimes you're\ +guerrillaSwindled13.text={0}, when it comes to playing the odds, sometimes you're\ \ on the wrong side. Consider this my parting gift - your C-Bills are\ - \ mine, and you're on your own against %s. Don't take it too hard.\ + \ mine, and you're on your own against {1}. Don't take it too hard.\ \ It's just business. -guerrillaSwindled14.text=%s, I was almost tempted to come through. But you made\ +guerrillaSwindled14.text={0}, I was almost tempted to come through. But you made\ \ the deal way too easy, and where's the fun in that? I'll be putting\ \ those C-Bills to better use than you ever would've. By the time\ - \ you've pieced this together, I'll be jumps away. Good luck out there with %s. -guerrillaSwindled15.text=%s, you really ought to be more careful who you trust.\ + \ you've pieced this together, I'll be jumps away. Good luck out there with {1}. +guerrillaSwindled15.text={0}, you really ought to be more careful who you trust.\ \ Those C-Bills of yours are already well spent, and you're left holding\ - \ the bag. Hope you and %s have fun without me. -guerrillaSwindled16.text=%s, next time, maybe don't believe everything you hear.\ + \ the bag. Hope you and {1} have fun without me. +guerrillaSwindled16.text={0}, next time, maybe don't believe everything you hear.\ \ I'm already gone, and your C-Bills are going to fuel a far more\ \ comfortable life than the one you're fighting for. Thanks for\ \ the funding! -guerrillaSwindled17.text=%s, business is all about taking opportunities, wouldn't\ +guerrillaSwindled17.text={0}, business is all about taking opportunities, wouldn't\ \ you say? You just happened to be my opportunity this time. Your C-Bills\ - \ are history, and so am I. Good luck keeping up with %s without my help. -guerrillaSwindled18.text=%s, it's impressive how easy it was to slip away with\ + \ are history, and so am I. Good luck keeping up with {1} without my help. +guerrillaSwindled18.text={0}, it's impressive how easy it was to slip away with\ \ those C-Bills. Consider it a compliment to my talents and a lesson\ - \ for you. You're on your own against %s now - I'll be sure to think\ + \ for you. You're on your own against {1} now - I'll be sure to think\ \ of you...occasionally. -guerrillaSwindled19.text=%s, let's be real - this was a one-sided deal from the start.\ +guerrillaSwindled19.text={0}, let's be real - this was a one-sided deal from the start.\ \ I walk away with your C-Bills, and you're left with nothing but regrets.\ - \ Enjoy the fight with %s; I'll enjoy the spoils. -guerrillaSwindled20.text=%s, I'd say I'm sorry, but I'd be lying. Your C-Bills are\ - \ funding my next venture, far from your troubles with %s. Maybe next time\ + \ Enjoy the fight with {1}; I'll enjoy the spoils. +guerrillaSwindled20.text={0}, I'd say I'm sorry, but I'd be lying. Your C-Bills are\ + \ funding my next venture, far from your troubles with {1}. Maybe next time\ \ you'll think twice before trusting a smooth talker. -guerrillaSwindled21.text=%s, you know, there's an art to taking someone for a ride.\ - \ Thanks for the contribution - I'll put it to good use. Meanwhile, %s is\ +guerrillaSwindled21.text={0}, you know, there's an art to taking someone for a ride.\ + \ Thanks for the contribution - I'll put it to good use. Meanwhile, {1} is\ \ still breathing down your neck, and I'm already in the wind. Take care! -guerrillaSwindled22.text=%s, by the time you figure out just how bad I played you,\ +guerrillaSwindled22.text={0}, by the time you figure out just how bad I played you,\ \ I'll be just a regret. Those C-Bills are long gone, just like me.\ - \ Good luck with %s - you'll need it. -guerrillaSwindled23.text=%s, looks like the student got schooled. Your C-Bills are\ + \ Good luck with {1} - you'll need it. +guerrillaSwindled23.text={0}, looks like the student got schooled. Your C-Bills are\ \ fueling my next big adventure, and all you've got is a bitter reminder.\ - \ Give %s my regards. I'll be miles away by now. -guerrillaSwindled24.text=%s, call it a life lesson: trust is expensive. While you're\ - \ out there scraping by against %s, I'll be enjoying every C-Bill you sent\ + \ Give {1} my regards. I'll be miles away by now. +guerrillaSwindled24.text={0}, call it a life lesson: trust is expensive. While you're\ + \ out there scraping by against {1}, I'll be enjoying every C-Bill you sent\ \ my way. Don't take it too hard - some of us are just born for this. @@ -877,18 +878,18 @@ guerrillaSwindled24.text=%s, call it a life lesson: trust is expensive. While yo logisticsDestroyed.text=Regrettable logisticsReceived.text=Message Received -statusUpdate0.text=Joined by an allied logistics convoy for a quick fuel exchange, %s. The process\ +statusUpdate0.text=Joined by an allied logistics convoy for a quick fuel exchange, {0}. The process\ \ was efficient, each team working methodically to ensure no time was lost. The convoy's pace has\ \ picked up again, with both units maintaining a strong formation. There's a sense of\ \ camaraderie, even in these brief encounters - each group focused on their respective tasks but\ \ united by the same overarching mission. -statusUpdate1.text=We're currently passing through burned-out villages, %s. The destruction is\ +statusUpdate1.text=We're currently passing through burned-out villages, {0}. The destruction is\ \ having an impact on crew morale. However, discipline remains intact, and the team understands\ \ the importance of maintaining focus on the mission. The convoy's pace is steady, and we are\ \ proceeding without deviation from the planned route. All units are maintaining formation, with\ \ heightened vigilance for potential ambushes or hidden threats. No delays expected; we continue\ \ to operate at optimal speed. -statusUpdate2.text=Evasive maneuvers drained fuel faster than expected, %s. The crew had to dodge\ +statusUpdate2.text=Evasive maneuvers drained fuel faster than expected, {0}. The crew had to dodge\ \ incoming missile salvos from enemy 'Meks, which spiked our fuel consumption. We've adjusted\ \ speed and switched to tighter convoy formations to conserve what's left. Spirits remain good,\ \ with jokes about how it's just another day dodging SRMs and LRMs. Scouts are looking for\ @@ -896,68 +897,68 @@ statusUpdate2.text=Evasive maneuvers drained fuel faster than expected, %s. The \ safer path, taking advantage of lower inclines to ease fuel demands. Morale is high, and the\ \ crew's handling it well - typical day on the job. We're confident about reaching the next\ \ checkpoint without delays. -statusUpdate3.text=Met an allied logistics convoy running low on fuel and rations, %s. The exchange\ +statusUpdate3.text=Met an allied logistics convoy running low on fuel and rations, {0}. The exchange\ \ was routine, carried out with the efficiency of those used to survival in harsh conditions.\ \ The strain was evident in their faces. The crew presses forward. ETA to delivery is 17 hours,\ \ with steady progress. -statusUpdate4.text=Reached a makeshift checkpoint run by starving civilians, %s. They asked for\ +statusUpdate4.text=Reached a makeshift checkpoint run by starving civilians, {0}. They asked for\ \ food, but we had none to give. Their desperation was palpable, etched into their gaunt faces\ \ and hollow eyes. We pushed through without incident, but the scene stays with us, another\ \ reminder of the misery that defines this war. The crew is quiet, morale low - there's no sense\ \ of victory in pushing forward, only a grim determination to complete the mission. ETA to\ \ delivery is 17 hours, but the burden of what we cannot change grows heavier. -statusUpdate5.text=Sorry for the blackout, %s, radio antenna was bent by a low-hanging branch. The\ +statusUpdate5.text=Sorry for the blackout, {0}, radio antenna was bent by a low-hanging branch. The\ \ team just finished a quick field repair, restoring full communication capabilities without any\ \ additional issues. All units are maintaining contact, with regular check-ins confirmed. The\ \ convoy is proceeding as planned, with no deviation from the established route. No delays are\ \ expected at this time. -statusUpdate6.text=Detour forced by fallen trees, %s. The arrangement seems too neat to be mere\ +statusUpdate6.text=Detour forced by fallen trees, {0}. The arrangement seems too neat to be mere\ \ chance - perhaps it's nature, or perhaps it's enemy sappers. Crew members swear they've seen\ \ movement in the treeline, dark shapes that vanish before they can be confirmed. We're advancing\ \ cautiously, weapons ready, each rustle of leaves amplified by the silence that follows. There's\ \ a growing sense that we're being funneled somewhere, like prey unaware of the hunter. Spirits\ \ are uneasy, but discipline holds for now. ETA still stands at 18 hours, but the sense of dread\ \ is growing. -statusUpdate7.text=Water levels have reached critical, %s. Hydration rations have been adjusted to\ +statusUpdate7.text=Water levels have reached critical, {0}. Hydration rations have been adjusted to\ \ extend remaining supplies. The crew is feeling the strain, but they understand the necessity.\ \ We're pushing forward, with all personnel adhering to updated water protocols. An adjusted ETA\ \ of 17 hours has been communicated to all units. No further complications are anticipated. -statusUpdate8.text=%s, we just left the remains of a bombed out village. We saw children huddled\ +statusUpdate8.text={0}, we just left the remains of a bombed out village. We saw children huddled\ \ under makeshift tents, some looked up as we passed, but most didn't bother. The sight was\ \ haunting, their small figures barely visible against the backdrop of ruin. The convoy pressed\ \ on, each driver focused on the road, yet the mood feels hollow - like a mechanical march\ \ through a world that's lost its meaning. There's no room for compassion here, only survival.\ \ ETA to delivery is 18 hours, but the emptiness remains. -statusUpdate9.text=Arrived at an allied field camp recently hit by enemy 'Meks, %s. The MedTechs\ +statusUpdate9.text=Arrived at an allied field camp recently hit by enemy 'Meks, {0}. The MedTechs\ \ were busy with fresh casualties, working swiftly in the smoky air filled with urgency. We\ \ dropped off our supplies, before pushing ahead. The crew is visibly shaken by what they saw,\ \ the 'Meks were using Infernos. but their focus is unbroken. We press on. ETA to delivery is 17\ \ hours. -statusUpdate10.text=A small group of refugees tried to approach, %s, but we had to speed up for\ +statusUpdate10.text=A small group of refugees tried to approach, {0}, but we had to speed up for\ \ security reasons. We're not running a charity service, after all. The crew's gone quiet again,\ \ trying to swallow the bitter truth that survival trumps sympathy out here. The guilt is heavy.\ \ ETA to delivery is still 18 hours, though each one feels longer than the last. -statusUpdate11.text=Stopped briefly to refuel an allied convoy, %s. The drivers looked tired, but\ +statusUpdate11.text=Stopped briefly to refuel an allied convoy, {0}. The drivers looked tired, but\ \ the handoff was quick and to the point. The convoy is back in motion, keeping pace toward the\ \ next checkpoint. ETA to delivery remains 17 hours, with no interruptions expected. -statusUpdate12.text=%s, a woman carrying an infant waved desperately as we approached the last\ +statusUpdate12.text={0}, a woman carrying an infant waved desperately as we approached the last\ \ village. We had nothing to give, so we kept moving. The crew tried to maintain focus, but\ \ there's a sense of heaviness that comes with repeated exposure to scenes like this. The reality\ \ of the situation is clear: we're here to deliver military supplies, not to play saviors. The\ \ convoy maintains its pace, though there's a weight in my heart. ETA to delivery remains 18\ \ hours. -statusUpdate13.text=Reached an allied checkpoint, %s. The soldiers took the supplies quietly, their\ +statusUpdate13.text=Reached an allied checkpoint, {0}. The soldiers took the supplies quietly, their\ \ tired eyes reflecting the same exhaustion we see in each other. It's a scene we've repeated\ \ countless times - quick exchanges under grim circumstances. We're moving on, but the mood is\ \ heavy. The crew feels it, a sense of helplessness that lingers long after we leave each\ \ checkpoint behind. ETA to delivery is 17 hours. -statusUpdate14.text=Recon vehicle experienced a sudden sensor glitch, %s - a brief but total blind\ +statusUpdate14.text=Recon vehicle experienced a sudden sensor glitch, {0} - a brief but total blind\ \ spot that left us vulnerable. Cause is unknown, and the crew is on edge, suspecting sabotage.\ \ The momentary blindness felt like more than just a malfunction; it was a gap, an invitation for\ \ disaster. The techs patched it up, but the fear of another unexpected failure lingers. We're\ \ moving carefully, with extra scans and sensors active. ETA remains 18 hours, but the atmosphere\ \ is heavy with paranoia. -statusUpdate15.text=Coolant levels dropped suddenly, %s, forcing an unexpected halt. Diagnostics\ +statusUpdate15.text=Coolant levels dropped suddenly, {0}, forcing an unexpected halt. Diagnostics\ \ traced the issue to a minor breach, likely caused by a stray AC/2 round from a previous\ \ skirmish. Techs patched it up efficiently, using reinforced seals to prevent recurrence. Crew\ \ is alert but remains relaxed - these sorts of issues are routine in active combat zones. Extra\ @@ -965,7 +966,7 @@ statusUpdate15.text=Coolant levels dropped suddenly, %s, forcing an unexpected h \ There's even some banter about past coolant leaks during more intense battles. Morale is\ \ strong, and we're pushing forward without any major delays. Crew knows this is all part of the\ \ convoy grind. -statusUpdate16.text=Fuel's dropping rapidly, %s. Rough terrain caught us off-guard, and retreating\ +statusUpdate16.text=Fuel's dropping rapidly, {0}. Rough terrain caught us off-guard, and retreating\ \ enemy 'Meks left craters along the roads, damaging key routes. We're making adjustments to\ \ stretch our reserves, but it's critical now. We've implemented lower-speed settings to maximize\ \ efficiency, but that won't last if more combat breaks out. Crew's maintaining focus, with\ @@ -973,1287 +974,1287 @@ statusUpdate16.text=Fuel's dropping rapidly, %s. Rough terrain caught us off-gua \ but we're far from any friendly base. We've sent a recon hovercraft ahead to scout for possible\ \ emergency caches or uncharted fueling points. No delays projected yet, but the situation is\ \ precarious. Crew's prepared for defensive actions if we encounter hostiles during fuel foraging. -statusUpdate17.text=Civilians waved us down, as we left through the last checkpoint, %s. They were\ +statusUpdate17.text=Civilians waved us down, as we left through the last checkpoint, {0}. They were\ \ begging for medical supplies, but we didn't stop. Their disappointment was clear, but the crew\ \ remained resolute - our orders leave no room for deviations. Morale is affected, but the\ \ mission's priority remains unchanged. ETA to delivery is 18 hours, and the convoy maintains\ \ operational efficiency. -statusUpdate18.text=An allied logistics unit requested spare parts, %s. The convoy maintained its\ +statusUpdate18.text=An allied logistics unit requested spare parts, {0}. The convoy maintained its\ \ course, adhering strictly to the schedule. The crew knows the importance of keeping pace, and\ \ there's no room for deviations. ETA to delivery is 17 hours, with no expected delays. -statusUpdate19.text=Missing fuel traced to a punctured tank, %s - likely damage from stray laser\ +statusUpdate19.text=Missing fuel traced to a punctured tank, {0} - likely damage from stray laser\ \ fire during a previous skirmish. We're redistributing remaining fuel reserves across the convoy\ \ to maintain movement. The situation is tight, but so far, no delays are projected. Crew's\ \ prepping for possible emergency refueling if needed. -statusUpdate20.text=Located missing crates near an abandoned checkpoint, %s. Evidence suggests a\ +statusUpdate20.text=Located missing crates near an abandoned checkpoint, {0}. Evidence suggests a\ \ previous supply convoy came under heavy fire and had to abandon cargo during their retreat. We\ \ secured what we could, including ammunition crates and medical supplies. Techs believe some\ \ crates contain Class-C Coolant, which could come in useful. The team's moving steadily, though\ \ the discovery has sparked concerns over enemy activity in the area. We've added patrols to\ \ cover the rear in case of ambush. We'll assess the contents of the salvaged crates at the next\ \ checkpoint. For now, we're staying cautious. -statusUpdate21.text=Blocked pass ahead, %s - fallen trees and debris, likely remnants of a recent\ +statusUpdate21.text=Blocked pass ahead, {0} - fallen trees and debris, likely remnants of a recent\ \ skirmish. Clearing it took longer than expected, with scouts maintaining a constant watch for\ \ potential ambushes. It's a bleak routine - clearing, advancing, expecting the worst, and\ \ finding nothing but more debris in a war that never seems to change. ETA is 17 hours, but no\ \ guarantees at this rate. -statusUpdate22.text=Misjudged water reserves, %s, forcing us to ration supplies more tightly.\ +statusUpdate22.text=Misjudged water reserves, {0}, forcing us to ration supplies more tightly.\ \ Located additional water canisters at an abandoned outpost, likely left behind during a rapid\ \ retreat. Crew is grumbling over the reduced rations, but discipline holds. No delays are\ \ expected for now, but the harsh conditions are affecting efficiency. We're actively searching\ \ for other possible resupply points. -statusUpdate23.text=Came across an allied patrol running low on fuel, %s. The exchange was handled\ +statusUpdate23.text=Came across an allied patrol running low on fuel, {0}. The exchange was handled\ \ efficiently - just a quick transfer of fuel before we both resumed our respective routes. The\ \ convoy is back in formation, maintaining a steady pace as we push forward. ETA to delivery is\ \ 17 hours, expecting no further delays. -statusUpdate24.text=An emergency frequency flared up briefly, %s. It sounded like a weak distress\ +statusUpdate24.text=An emergency frequency flared up briefly, {0}. It sounded like a weak distress\ \ call, likely from a downed 'MekWarrior or an allied recon unit caught behind lines. Given the\ \ risks, we didn't break formation to respond. The crew remains calm - it's not the first time\ \ we've heard distress signals on this route. We're keeping channels open, just in case it's a\ \ friendly in need. Morale is stable, with some light banter over the comms to maintain spirits.\ \ No delays expected as we continue. -statusUpdate25.text=A collapsed bridge forced a route change, %s - possibly the result of sabotage.\ +statusUpdate25.text=A collapsed bridge forced a route change, {0} - possibly the result of sabotage.\ \ An alternate crossing was identified, and the convoy is back on track. The delay was minimal.\ \ All units are aware of potential risks associated with the detour, including possible ambush\ \ points. The crew is maintaining focus. No major delays are expected as we proceed. -statusUpdate26.text=Crossed paths with allied scouts, %s. Their vehicles appeared intact, but the\ +statusUpdate26.text=Crossed paths with allied scouts, {0}. Their vehicles appeared intact, but the\ \ scouts themselves were tense, and reported possible activity in this sector. ETA to delivery remains 18\ \ hours, with no delays expected. -statusUpdate27.text=We linked up with an allied logistics convoy that needed basic repairs, %s. We\ +statusUpdate27.text=We linked up with an allied logistics convoy that needed basic repairs, {0}. We\ \ handed off some spare parts and took a break to get them moving again. A few of the crew shared\ \ small items with them - they seemed to be in pretty rough shape. Each stop on this route\ \ feels pretty tense; everybody seems to feel like we could get jumped out here.\ \ ETA to delivery is 17 hours. -statusUpdate28.text=Reached an allied outpost that showed clear signs of a recent skirmish, %s.\ +statusUpdate28.text=Reached an allied outpost that showed clear signs of a recent skirmish, {0}.\ \ Torn sandbags, bullet holes in the walls, and soldiers with dusty uniforms greeted us. Their\ \ readiness was high despite the signs of battle still fresh around them. The supply handoff was\ \ fast before we resumed our route. The crew is staying alert, eyes scanning the horizon for\ \ potential threats. We maintain speed, knowing that vigilance is as important as delivery. ETA\ \ to delivery remains 18 hours. -statusUpdate29.text=GPS signal cut out near a known ambush site, %s. This could be a result of\ +statusUpdate29.text=GPS signal cut out near a known ambush site, {0}. This could be a result of\ \ residual jamming from previous encounters. All units are proceeding with heightened caution,\ \ maintaining full situational awareness. Sensors have been recalibrated to account for potential\ \ interference, and the crew is prepared for ambush. No delays are expected, but all personnel\ \ will remain on alert until we clear the area. -statusUpdate30.text=Civilians just crowded the convoy, %s, desperate for water or some such. We had\ +statusUpdate30.text=Civilians just crowded the convoy, {0}, desperate for water or some such. We had\ \ to keep moving, because "rationing" doesn't include charity drops. The crew's trying to stay\ \ focused, but despair has a way of seeping in when you least expect it. At least we're\ \ efficient, if not empathetic. ETA to delivery holds at 17 hours. -statusUpdate31.text=Met an allied recon team, %s. They were well-prepared, with gear in order and\ +statusUpdate31.text=Met an allied recon team, {0}. They were well-prepared, with gear in order and\ \ weapons ready, but the fatigue was clear in their eyes. It's a burden we all share, each of us\ \ carrying the same weariness that has become part of the mission. The encounter was brief, but\ \ the crew feels the shared weight of it all, but the path ahead demands focus. ETA to delivery\ \ is 17 hours, with no expected delays. -statusUpdate32.text=Saw children scavenging in the ruins of an old market as we passed, %s. It was\ +statusUpdate32.text=Saw children scavenging in the ruins of an old market as we passed, {0}. It was\ \ hard to ignore, but the crew tried their best to focus on the mission. After all, emotional\ \ detachment is the closest thing we've got to armor these days. Spirits are low, but the convoy\ \ maintains speed. At least the engine hum drowns out the emptiness. ETA to delivery remains 17 hours. -statusUpdate33.text=Landslides have hit the main cargo road, %s, forcing us to reroute to a path\ +statusUpdate33.text=Landslides have hit the main cargo road, {0}, forcing us to reroute to a path\ \ closer to contested zones. It's a necessary risk, but one that adds to the sense of futility\ \ - rerouting, retreating, always adapting, yet never truly advancing. It's as if the road itself\ \ resists our passage. ETA remains at 18 hours, but the sense of inevitable confrontation looms. -statusUpdate34.text=Intercepted a faint distress signal, %s. It's weak, barely a whisper among the\ +statusUpdate34.text=Intercepted a faint distress signal, {0}. It's weak, barely a whisper among the\ \ static. Could be an ally trapped, or it could be a trap set by the enemy. The tone of the\ \ signal has an eerie, desperate quality, impossible to ignore. We've opened all channels, hoping\ \ to verify its origin, but no joy. We're proceeding cautiously, weapons primed. No delays\ \ expected, but the air is heavy with uncertainty. -statusUpdate35.text=Civilians tried to flag us down, %s, hoping for help. We didn't stop, as the\ +statusUpdate35.text=Civilians tried to flag us down, {0}, hoping for help. We didn't stop, as the\ \ risk to the convoy was too great. The crew barely reacted, their expressions indifferent.\ \ Desperate faces lingered in the rear-cam for a moment before disappearing. It's a familiar\ \ scene - lots of refugees on this route. It's shaken the crews a bit.\ \ ETA to delivery holds at 17 hours, with no significant delays expected. -statusUpdate36.text=A woman carrying a sick child approached the convoy, hoping for help, %s. We\ +statusUpdate36.text=A woman carrying a sick child approached the convoy, hoping for help, {0}. We\ \ had to keep moving, leaving her behind like so many others. It felt like another failure,\ \ another weight added to the growing burden of this war. The crew fell silent, the reality of\ \ our choices weighing heavily on everyone. It's a stark reminder of what we've become in the\ \ name of duty - just another part of the machine, indifferent to the suffering left in its wake.\ \ ETA to delivery is 17 hours, but the sense of loss remains. -statusUpdate37.text=Reached an allied checkpoint that had just repelled an attack, %s. The soldiers\ +statusUpdate37.text=Reached an allied checkpoint that had just repelled an attack, {0}. The soldiers\ \ were still on high alert, eyes scanning the surroundings for any lingering threats. We were\ \ passed through the checkpoint quickly. ETA to delivery remains 18 hours, with all systems\ \ nominal. -statusUpdate38.text=Dense fog has swallowed the convoy, %s, thick as smoke. Visibility is near\ +statusUpdate38.text=Dense fog has swallowed the convoy, {0}, thick as smoke. Visibility is near\ \ zero. We've seen this kind of cover before - an ideal screen for enemy ambushes. The convoy\ \ crawls forward, engines hushed, the only sound a distant metallic clanking that seems to\ \ come from nowhere and everywhere at once. Crew moves slowly, with every nerve on edge. Delays\ \ are likely. -statusUpdate39.text=Came across civilians searching through debris for food, %s. They were too\ +statusUpdate39.text=Came across civilians searching through debris for food, {0}. They were too\ \ exhausted to react as we passed, eyes hollow and movements slow. The sight was haunting - a\ \ reminder of what these endless conflicts have reduced people to. The crew remained silent,\ \ their expressions matching the somber scene outside. There was nothing we could do for them;\ \ all we could offer was a fleeting glance of sympathy before moving on. The mood in the convoy\ \ is heavy, and it's clear that the war is weighing on everyone. No delays expected, but the\ \ emptiness lingers. -statusUpdate40.text=Reached an allied resupply post, %s. The soldiers were organized, moving\ +statusUpdate40.text=Reached an allied resupply post, {0}. The soldiers were organized, moving\ \ quickly to handle the exchange. The convoy's speed is steady. ETA to delivery is 18 hours, with\ \ no expected delays. -statusUpdate41.text=Just passed a makeshift graveyard, %s. Civilians were burying the dead, their\ +statusUpdate41.text=Just passed a makeshift graveyard, {0}. Civilians were burying the dead, their\ \ faces etched with a grim acceptance of the war's toll. The convoy kept moving, the sight met\ \ with silent indifference from the crew. There's no shock, just a dull recognition that this\ \ conflict leaves no one untouched. ETA to delivery is 18 hours. -statusUpdate42.text=An allied MedTech unit flagged us for supplies, %s. They accepted the crates\ +statusUpdate42.text=An allied MedTech unit flagged us for supplies, {0}. They accepted the crates\ \ quietly, their expressions drained but determined. It's clear that the toll of this conflict is\ \ not just physical but mental as well. The convoy continues forward, each driver resolute,\ \ knowing that our delivery supports those still fighting. ETA to delivery holds at 18 hours. -statusUpdate43.text=Lead vehicle experienced a sudden loss of steering, %s. Emergency repairs were\ +statusUpdate43.text=Lead vehicle experienced a sudden loss of steering, {0}. Emergency repairs were\ \ conducted swiftly by mechanics, who suspect worn cables as the root cause. The crew is\ \ understandably cautious. We've resumed movement, maintaining full convoy integrity. Mechanics\ \ will perform a more thorough inspection at the next scheduled stop. All personnel have been\ \ briefed on emergency protocols, and no significant delays are anticipated. -statusUpdate44.text=Food rations came up short, %s. Inventory checks show missing supplies - either\ +statusUpdate44.text=Food rations came up short, {0}. Inventory checks show missing supplies - either\ \ an error or theft. There's a creeping paranoia among the crew, as if someone within our ranks\ \ is hiding something. We're rationing what's left, but the reduced meals add a gnawing hunger.\ \ ETA remains 18 hours, assuming no more surprises. -statusUpdate45.text=Discovered a slow fuel leak during inspection, %s. Likely caused by shrapnel\ +statusUpdate45.text=Discovered a slow fuel leak during inspection, {0}. Likely caused by shrapnel\ \ from our last skirmish with enemy BattleMeks. Techs performed a swift patch, reinforcing weak\ \ spots. We're monitoring fuel levels more closely to ensure stability. Crew remains alert but\ \ isn't worried - this kind of repair is par for the course. Spirits are high, with some crew\ \ sharing stories of repairs under fire. ETA holds at 18 hours, with no expected delays, so long\ \ as the repairs hold. -statusUpdate46.text=Cooling system failure reported on one of our cargo trucks, %s - strong\ +statusUpdate46.text=Cooling system failure reported on one of our cargo trucks, {0} - strong\ \ suspicion of sabotage by enemy operatives. This happened during a planned stop, making it\ \ likely that infiltrators tampered with our systems overnight. Tech crews executed rapid field\ \ repairs, but the incident has shaken trust among the team. We've doubled the watch and added\ \ sensor sweeps for sabotage detection. Tension is high, especially with scout reports of\ \ possible enemy 'Mek sightings nearby. We're continuing our push, aiming to maintain\ \ pace, but we're ready for an engagement. ETA to the drop-off remains at 18 hours. -statusUpdate47.text=Medical kits are running low, %s, after treating minor injuries during our last\ +statusUpdate47.text=Medical kits are running low, {0}, after treating minor injuries during our last\ \ retreat. Crew is rationing remaining supplies, focusing on essentials. Despite the shortage,\ \ there's a sense of camaraderie, with jokes about "field fixes" and toughing it out like the old\ \ days. Resupply will be necessary soon, but for now, spirits are positive. No delays expected,\ \ and ETA is steady at 18 hours as we press forward. -statusUpdate48.text=Civilians blocked the road, demanding supplies, %s. They left us no choice - we\ +statusUpdate48.text=Civilians blocked the road, demanding supplies, {0}. They left us no choice - we\ \ had to open fire. The crew executed my orders without hesitation. There's a sense of\ \ resignation among the team; it's just another reminder of the harsh rules that govern this war.\ \ Morale isn't high, but no one expected it to be. The convoy continues at full speed. No delays\ \ are expected. -statusUpdate49.text=Passed civilians getting water from a muddy stream, %s. We maintained convoy\ +statusUpdate49.text=Passed civilians getting water from a muddy stream, {0}. We maintained convoy\ \ speed. There was nothing we could offer anyway, not without compromising our own supplies.\ \ Each decision to move forward without helping weighs heavily, but operational parameters do not\ \ allow for deviations based on sympathy. ETA to delivery holds at 17 hours, with no anticipated\ \ delays. -statusUpdate50.text=A young boy tried to flag us down with a piece of uniform, %s. We couldn't slow\ +statusUpdate50.text=A young boy tried to flag us down with a piece of uniform, {0}. We couldn't slow\ \ down, so he eventually dropped his arm and watched us pass. It's the kind of scene that stings,\ \ but we've learned to keep our eyes on the road - easier to justify when you've got orders to\ \ follow. No delays expected, just a few more dents in whatever's left of our humanity. -statusUpdate51.text=An allied unit needed a quick resupply, %s. We're moving again. ETA to delivery\ +statusUpdate51.text=An allied unit needed a quick resupply, {0}. We're moving again. ETA to delivery\ \ is 17 hours. -statusUpdate52.text=Lead vehicle's brakes failed, %s - quick repairs got us moving again, but the\ +statusUpdate52.text=Lead vehicle's brakes failed, {0} - quick repairs got us moving again, but the\ \ terrain remains unforgiving. It's not just the land that wears us down; it's the unending\ \ struggle against obstacles, both mechanical and mental. Every fix feels temporary, like a\ \ bandage over a wound that never truly heals. Each mile feels like a testament to perseverance,\ \ yet the point of it all seems lost in the dust behind us. ETA is 18 hours, and the convoy moves\ \ on, because it must. -statusUpdate53.text=Children ran alongside the convoy, hoping for handouts, %s. We had nothing to\ +statusUpdate53.text=Children ran alongside the convoy, hoping for handouts, {0}. We had nothing to\ \ spare, so they eventually fell back. It's a hard reality, but one that we've accepted as part\ \ of this mission. The crew remains professional, aware that stopping could jeopardize our\ \ schedule. Every decision here is based on efficiency, not emotion. Morale may be low, but the\ \ convoy's pace is steady. No delays expected. -statusUpdate54.text=Stopped by a group of allied scout 'Meks in need of resupply, %s. They looked\ +statusUpdate54.text=Stopped by a group of allied scout 'Meks in need of resupply, {0}. They looked\ \ rough, armor melted to slag. The crew remains alert. ETA to delivery is 18 hours. -statusUpdate55.text=Passed a makeshift hospital, %s. Civilians lay on mats, too weak to move. We\ +statusUpdate55.text=Passed a makeshift hospital, {0}. Civilians lay on mats, too weak to move. We\ \ couldn't stop, of course. The crew knows the drill: save the rations for the ones we're\ \ supposed to help, not the ones already beyond it. There's no room for sentiment in this convoy.\ \ ETA to delivery remains 18 hours, with spirits as low as usual. -statusUpdate56.text=Near-miss with a mine due to driver fatigue, %s. Quick reactions prevented\ +statusUpdate56.text=Near-miss with a mine due to driver fatigue, {0}. Quick reactions prevented\ \ disaster, but it's a reminder of the toll continuous operations take. We're rotating drivers\ \ more frequently to maintain sharpness. There's some tension, but morale remains solid -\ \ everyone knows the stakes. Banter continues on the comms, lightening the mood despite the\ \ scare. We're holding pace, adjusting ETA to 17 hours, and maintaining vigilance as we navigate\ \ through the minefield. -statusUpdate57.text=Encountered a blocked pass, %s - defensive rock formations likely placed by\ +statusUpdate57.text=Encountered a blocked pass, {0} - defensive rock formations likely placed by\ \ enemy scouts. These barriers were effectively positioned, suggesting recon units were active\ \ here recently. We managed to clear the path using explosives, but the delay left us exposed.\ \ Crew remains alert, scanning for potential sniper or light 'Meks lurking nearby. We're moving\ \ through the pass slowly, given the likelihood of sensor mines or remote explosives. The\ \ situation is tense, but we've increased sensor sweeps. Prepared to engage if enemy units\ \ respond to our presence. -statusUpdate58.text=Civilians gathered by the roadside, %s, hoping for handouts. We moved on\ +statusUpdate58.text=Civilians gathered by the roadside, {0}, hoping for handouts. We moved on\ \ quickly, because stopping for them is about as likely as finding a peaceful solution to this\ \ whole mess. The crew's not thrilled, but they've learned that helplessness is part of the job\ \ description. We just keep moving, like clockwork. No delays expected - just another routine day\ \ in the logistics grind. -statusUpdate59.text=Convoy was halted by an abandoned vehicle barricade, %s. Clearing it took\ +statusUpdate59.text=Convoy was halted by an abandoned vehicle barricade, {0}. Clearing it took\ \ longer than expected. The blockade was a remnant of a past battle, with no immediate threats\ \ detected. Spirits are good, with jokes about finding "souvenirs" among the wreckage. We're\ \ moving forward, keeping formation intact. ETA remains at 18 hours, with all systems nominal\ \ and no expected delays. -statusUpdate60.text=Another sudden storm, %s. The barrage of wind can damage the vehicles, so we were\ +statusUpdate60.text=Another sudden storm, {0}. The barrage of wind can damage the vehicles, so we were\ \ forced the crew to take cover. Comms remain active, but there's an unspoken tension - a\ \ familiar sense of vulnerability that only adds to the weariness of war. The storm hammers the\ \ metal with a sound that's almost mocking, a reminder of how easily even nature can break us\ \ down. The storm should pass soon, ETA holds at 17 hours. -statusUpdate61.text=A discrepancy in the cargo manifest required a brief halt, %s. The issue has\ +statusUpdate61.text=A discrepancy in the cargo manifest required a brief halt, {0}. The issue has\ \ been identified and resolved, but it added to the overall tension among the crew. Personnel\ \ have been reminded of the importance of accuracy in supply management, and that errors can\ \ compromise mission success. All units remain alert for any additional issues that could arise\ \ from logistical errors. No further delays are expected. -statusUpdate62.text=Passed through a bombed-out market, %s. A few civilians were scavenging among\ +statusUpdate62.text=Passed through a bombed-out market, {0}. A few civilians were scavenging among\ \ the rubble, their faces gaunt and eyes empty. The air felt suffocating, filled with the stale\ \ scent of smoke and decay. Spirits are low, and the reality of the situation weighs heavy on\ \ the convoy. We're moving forward, but it's hard to escape the sense of futility in all this\ \ destruction. No delays. -statusUpdate63.text=Reached an allied trench that had recently been raided, %s. The soldiers were\ +statusUpdate63.text=Reached an allied trench that had recently been raided, {0}. The soldiers were\ \ still recovering, moving slowly as if the attack had stripped them of more than just\ \ resources. We managed a quick resupply. But as we pushed forward, the lingering unease was hard\ \ to shake - it's the kind of feeling that settles deep, reminding us of what we've all lost. ETA\ \ to delivery is 18 hours. -statusUpdate64.text=Caught a loose oil hose just in time, %s. The damage suggests shrapnel, but\ +statusUpdate64.text=Caught a loose oil hose just in time, {0}. The damage suggests shrapnel, but\ \ it's unclear when it happened. The crew managed a quick patch-up, but there's a lingering sense\ \ that the convoy's luck is wearing thin. Every repair feels more temporary, as if the machines\ \ themselves are giving in to age. We're maintaining speed, but no one is certain how long that\ \ will last. No delays expected, but the sense of foreboding is tangible, like a storm building\ \ in the distance. -statusUpdate65.text=GPS led us to a dead-end, %s - an old barricade, rusted and overgrown, yet\ +statusUpdate65.text=GPS led us to a dead-end, {0} - an old barricade, rusted and overgrown, yet\ \ still sturdy enough to block our path. It feels intentional, like someone wanted to trap us\ \ here. The crew is tense, eyes darting toward the dark woods beyond, half-expecting an attack.\ \ We're rerouting now, trying to find a way around this obstacle, but the delay adds to the\ \ growing anxiety. ETA is currently 18 hours, but remains uncertain. -statusUpdate66.text=Just passed some elderly civilians, %s. They watched us pass, but there was no\ +statusUpdate66.text=Just passed some elderly civilians, {0}. They watched us pass, but there was no\ \ movement toward us - likely a sign that they've lost any expectation of help. The crew didn't\ \ react beyond a few brief glances, understanding that this is just another facet of the war.\ \ It's a familiar sight, one that no longer surprises. No delays expected. -statusUpdate67.text=Signs of early heat exhaustion among drivers, %s. Hydration protocols are in\ +statusUpdate67.text=Signs of early heat exhaustion among drivers, {0}. Hydration protocols are in\ \ effect, but the relentless engine heat is taking its toll. The crew needs a longer rest soon.\ \ Each moment under this scorching sun feels like another step deeper into a conflict that never\ \ ends. ETA remains at 17 hours, but the weight of exhaustion is more evident than ever. -statusUpdate68.text=%s, convoy just left a village our forces cleared out last week. Mother and\ +statusUpdate68.text={0}, convoy just left a village our forces cleared out last week. Mother and\ \ child begged for food as we passed. The crew kept moving without hesitation. This kind of scene\ \ has become routine - an unpleasant reality we've grown accustomed to. Not everyone can be\ \ saved, and most of us have accepted that fact. Morale's low, but it's not unexpected. There's\ \ no room for sentiment here, just the necessity of pressing on toward the next checkpoint. No\ \ delays expected. -statusUpdate69.text=%s, a sudden dust storm has descended on the convoy, cutting visibility to\ +statusUpdate69.text={0}, a sudden dust storm has descended on the convoy, cutting visibility to\ \ almost nothing. The road we've been following is now obscured, swallowed by the swirling grit.\ \ The convoy presses on. The crew is tense, scanning for signs of an ambush lurking within the\ \ storm's cover. We're holding pace for now, but the sense of unseen eyes watching is relentless. -statusUpdate70.text=Reached an allied checkpoint, %s. The weariness in the soldiers eyes was hard\ +statusUpdate70.text=Reached an allied checkpoint, {0}. The weariness in the soldiers eyes was hard\ \ to miss. They moved with purpose, though, despite the fatigue. We're back on the move,\ \ maintaining speed and focus. ETA to delivery holds at 18 hours. -statusUpdate71.text=Water supplies are running low faster than expected, %s. The engine heat,\ +statusUpdate71.text=Water supplies are running low faster than expected, {0}. The engine heat,\ \ combined with evasive maneuvers, has taken a toll on hydration needs. We've issued tighter\ \ rationing protocols, but the crew is managing well, joking about old desert campaigns where\ \ water was even scarcer. Hydration is still enough for now, but reaching the next checkpoint is\ \ crucial. No delays are expected as we maintain course. -statusUpdate72.text=Engine overheated in Transport-3, %s. Coolant levels are critical, with\ +statusUpdate72.text=Engine overheated in Transport-3, {0}. Coolant levels are critical, with\ \ possible shrapnel damage as the cause. Repairs are underway, but the crew is anxious. They know\ \ that any further complications could leave us stranded in contested territory. We're adjusting\ \ the ETA to 17 hours, but it's a fragile estimate. -statusUpdate73.text=Fuel reserves are critically low, %s. The terrain has proven far more\ +statusUpdate73.text=Fuel reserves are critically low, {0}. The terrain has proven far more\ \ challenging than estimated, with steep inclines and uneven surfaces. We've initiated stringent\ \ rationing to ensure progress continues without compromising operational integrity. Drivers have\ \ been instructed to maintain optimal speed to conserve fuel. We're exploring potential emergency\ \ resupply points along the route, though options appear limited. No delays are expected for now. -statusUpdate74.text=Reached an allied outpost, %s, where fresh sandbags lined the perimeter. The\ +statusUpdate74.text=Reached an allied outpost, {0}, where fresh sandbags lined the perimeter. The\ \ soldiers were on edge, their movements sharp and eyes wary, but the resupply was quick and\ \ efficient. We're moving again, ETA to delivery is 18 hours. -statusUpdate75.text=Stopped to refuel an allied patrol, %s. The soldiers handled the exchange with\ +statusUpdate75.text=Stopped to refuel an allied patrol, {0}. The soldiers handled the exchange with\ \ practiced efficiency. There was no room for conversation, and the crew remains focused, but\ \ it's clear that the weight of this endless routine has become part of us. We accept it, knowing\ \ that stopping isn't an option. No delays expected, and the convoy maintains its pace. -statusUpdate76.text=Reached an allied outpost running low on rations, %s. The soldiers looked worn\ +statusUpdate76.text=Reached an allied outpost running low on rations, {0}. The soldiers looked worn\ \ out. The convoy is moving steadily, adhering to the planned route. ETA to delivery remains 18\ \ hours. -statusUpdate77.text=We just passed civilians gathering around a makeshift fire, %s. Fatigue marked\ +statusUpdate77.text=We just passed civilians gathering around a makeshift fire, {0}. Fatigue marked\ \ their faces, and their eyes were hollow. The crew remained silent, eyes focused on the road\ \ ahead, accustomed to these bleak images that now feel like just another part of the landscape.\ \ It's clear that repetition has worn down any sense of empathy. No delays expected, and the\ \ convoy maintains its course. -statusUpdate78.text=Several crew members are showing signs of severe fatigue, %s. The endless\ +statusUpdate78.text=Several crew members are showing signs of severe fatigue, {0}. The endless\ \ grind of war takes its toll on even the most experienced. Despite the exhaustion, the convoy\ \ continues its path. ETA stands at 18 hours, but spirits are undeniably low. -statusUpdate79.text=Regrouped with an allied recon unit that needed water and fuel, %s. The\ +statusUpdate79.text=Regrouped with an allied recon unit that needed water and fuel, {0}. The\ \ exchange was fast, but the strain of constant patrols was written all over their faces. As the\ \ convoy holds its pace, there's a shared silence among the crew, each person lost in their own\ \ thoughts. ETA to delivery is 18 hours. -statusUpdate80.text=Brakes failed suddenly on a support vehicle, %s - probable sabotage from enemy\ +statusUpdate80.text=Brakes failed suddenly on a support vehicle, {0} - probable sabotage from enemy\ \ infiltrators. The failure occurred on a steep incline, posing a serious risk. Fortunately,\ \ quick repairs by the tech team prevented a larger incident. Security measures have been\ \ intensified, with additional checks on all vehicles. Crew's been briefed on identifying\ \ potential signs of tampering and maintaining vigilance. Tension is high, given the proximity\ \ to enemy-held sectors, but no major delays are expected. -statusUpdate81.text=Encountered flooding on the lower roads, %s. We've had to reroute to higher\ +statusUpdate81.text=Encountered flooding on the lower roads, {0}. We've had to reroute to higher\ \ ground, less secure but necessary. There's a sense of resignation as we push forward. ETA\ \ adjusted to 17 hours. -statusUpdate82.text=Sudden static briefly disrupted communications, %s. The cause of the\ +statusUpdate82.text=Sudden static briefly disrupted communications, {0}. The cause of the\ \ interference is unclear, but deliberate jamming cannot be ruled out. Communication channels\ \ were quickly restored, with all units maintaining vigilance for further disruptions. Crews have\ \ been briefed on the E-WAR tactics the enemy has used previously and how to counter them. No\ \ delays expected at this time. -statusUpdate83.text=%s, we just reached an allied camp that had seen recent fighting. Burned-out\ +statusUpdate83.text={0}, we just reached an allied camp that had seen recent fighting. Burned-out\ \ vehicles were still smoking, and the soldiers looked exhausted. The convoy continues at a\ \ steady pace. ETA to delivery is 18 hours. -statusUpdate84.text=Sudden static cut communications briefly, %s. Could have been deliberate, but\ +statusUpdate84.text=Sudden static cut communications briefly, {0}. Could have been deliberate, but\ \ there's no way to confirm. The crew is monitoring closely, yet there's a sense of futility in\ \ the effort. Comms are restored, but it feels like another moment where the enemy is both\ \ everywhere and nowhere. No delays expected. -statusUpdate85.text=An allied recon team signaled for water and rations, %s. They looked exhausted\ +statusUpdate85.text=An allied recon team signaled for water and rations, {0}. They looked exhausted\ \ but still determined, taking what was needed before nodding their thanks and moving on. We\ \ maintain our pace, driven by the knowledge that every supply run matters. ETA to delivery is\ \ 17 hours. -statusUpdate86.text=Fuel pump failure on a support vehicle, %s. Debris was clogging the lines.\ +statusUpdate86.text=Fuel pump failure on a support vehicle, {0}. Debris was clogging the lines.\ \ Techs managed a quick fix. Even after repairs, there's a sense that something unseen lingers,\ \ waiting for another failure. We're moving again, but the pace is slower. ETA 18 hours. -statusUpdate87.text=A sudden hailstorm caused minor armor damage, %s. Crew took cover under a rocky\ +statusUpdate87.text=A sudden hailstorm caused minor armor damage, {0}. Crew took cover under a rocky\ \ overhang while visibility dropped to nearly zero. We maintained radio silence to avoid\ \ detection, as enemy skirmishers have been using storms for surprise attacks. We're back on\ \ track, now, moving at a good pace. ETA is still 18 hours, with no major concerns. -statusUpdate88.text=Heavy winds are disrupting convoy alignment, %s. Debris is blown across the\ +statusUpdate88.text=Heavy winds are disrupting convoy alignment, {0}. Debris is blown across the\ \ road. Progress is slower than planned. ETA 16 hours. -statusUpdate89.text=Passed through a camp of displaced civilians, %s. Makeshift tents lined the\ +statusUpdate89.text=Passed through a camp of displaced civilians, {0}. Makeshift tents lined the\ \ road, filled with hopeless faces that have seen too much of this war. The crew pressed on,\ \ maintaining speed without a second glance. No delays are expected. ETA to delivery is 18 hours. -statusUpdate90.text=An allied convoy requested urgent resupply, %s. The allied soldiers were\ +statusUpdate90.text=An allied convoy requested urgent resupply, {0}. The allied soldiers were\ \ prepared but wary. They warned us that enemy light 'Meks have been sweeping this sector. We've\ \ adjusted our route and are now maintaining steady progress toward the next depot. ETA to\ \ delivery remains 18 hours. -statusUpdate91.text=The road has been turned into mud pits, by recent engagements, %s. We've had to\ +statusUpdate91.text=The road has been turned into mud pits, by recent engagements, {0}. We've had to\ \ reduce speed to avoid getting bogged down. Escorts have been repositioned to offer better\ \ defensive coverage during the crawl. So far, no significant delays have occurred, but the crew\ \ is feeling the strain. -statusUpdate92.text=Driver fatigue is becoming clear, %s. We've rotated drivers to maintain\ +statusUpdate92.text=Driver fatigue is becoming clear, {0}. We've rotated drivers to maintain\ \ operational efficiency, but the relentless stress of each run is wearing down the crew. We've\ \ allowed brief rest periods, but the constant threat of attacks makes even short breaks risky.\ \ MedTechs are on standby to address exhaustion. Despite the strain, the convoy's progress\ \ remains steady, but I'm concerned about the long-term impact if we don't get pulled out of\ \ rotation soon. ETA to drop-off 12 hours. -statusUpdate93.text=Found a slow fuel leak during inspection, %s. Patched up quickly. Fixing\ +statusUpdate93.text=Found a slow fuel leak during inspection, {0}. Patched up quickly. Fixing\ \ leaks and moving forward has become second nature, yet each repair feels more futile than the\ \ last: it's just going to break again. ETA is 18 hours. -statusUpdate94.text=Comms were briefly disrupted, %s. Possible interference from enemy jamming\ +statusUpdate94.text=Comms were briefly disrupted, {0}. Possible interference from enemy jamming\ \ fields, but we're not ruling out environmental factors either. The sudden loss of signals\ \ heightened the tension among the crew, especially given our position along a known raiding\ \ route. We've restored channels, but the blackout underscored the vulnerability of our convoys.\ \ We've shifted to a staggered convoy formation to reduce vulnerability to surprise strikes.\ \ So far, no delays anticipated. -statusUpdate95.text=An allied patrol needed fuel, %s. We've just got back on course. ETA to\ +statusUpdate95.text=An allied patrol needed fuel, {0}. We've just got back on course. ETA to\ \ delivery is 17 hours, with no expected delays. -statusUpdate96.text=Interference spiked again, %s - this time significantly stronger. It's unclear\ +statusUpdate96.text=Interference spiked again, {0} - this time significantly stronger. It's unclear\ \ whether it's residual signals from previous engagements or lingering enemy jamming. Sensors are\ \ operating at full capacity, with operators running diagnostics to identify potential sources.\ \ Communication lines have been reinforced, and the convoy remains on course. We're prepared for\ \ further interruptions, with no delays expected at this point. -statusUpdate97.text=Failed to establish contact with a nearby support unit, %s - only dead air on\ +statusUpdate97.text=Failed to establish contact with a nearby support unit, {0} - only dead air on\ \ the comms. We're keeping formations tight, weapons primed, and all sensors sweeping for\ \ movement. Adjustments are being made to improve signal reception, but for now, we're moving\ \ steadily toward the next NavPoint. No delays expected. -statusUpdate98.text=Saw a line of civilians along the roadside, %s. They waved weakly as we\ +statusUpdate98.text=Saw a line of civilians along the roadside, {0}. They waved weakly as we\ \ approached, but we had nothing to offer. Some personnel averted their eyes, unable to meet the\ \ gaze of the figures watching us pass. The truth is harsh: stopping could risk the mission, so\ \ we keep moving. Morale is low, with the weight of helplessness bearing down on us. It's hard to\ \ ignore the misery that surrounds us, but the convoy must maintain speed. ETA to delivery\ \ remains 17 hours. -statusUpdate99.text=Drove through a wrecked town, %s. Civilians crowded near the road, eyes filled\ +statusUpdate99.text=Drove through a wrecked town, {0}. Civilians crowded near the road, eyes filled\ \ with hope that we might offer aid. But we couldn't stop - not here, not now. The crew's\ \ expressions were tight, a mix of frustration and resignation. It's another harsh truth of this\ \ war: we have to choose between helping a few or completing the mission. No delays expected, but\ \ the mood is grim as we press forward. -statusUpdateEnemyCritical0.text=%s, encountering minimal resistance in this sector, as any\ +statusUpdateEnemyCritical0.text={0}, encountering minimal resistance in this sector, as any\ \ remaining enemy forces are pulling out. Their retreat is chaotic, marked by scattered gunfire\ \ and abandoned positions. Vigilance is high; the fight isn't over yet. -statusUpdateEnemyCritical1.text=%s, made contact with scattered enemy forces that retreated quickly.\ +statusUpdateEnemyCritical1.text={0}, made contact with scattered enemy forces that retreated quickly.\ \ The engagement was light, and enemy units fell back without further resistance. Convoy moving\ \ forward with minimal adjustment. -statusUpdateEnemyCritical2.text=%s, we encountered a minimal picket line at the river crossing,\ +statusUpdateEnemyCritical2.text={0}, we encountered a minimal picket line at the river crossing,\ \ with most enemy forces withdrawing rapidly from this sector. Enemy presence is sparse, and\ \ their retreat is uncoordinated. -statusUpdateEnemyCritical3.text=%s, enemy presence nearly absent in this sector. Recon confirms\ +statusUpdateEnemyCritical3.text={0}, enemy presence nearly absent in this sector. Recon confirms\ \ only minor traces of recent troop movements, with abandoned gear everywhere. Comms chatter is\ \ quiet. Convoy remains vigilant, continuing to sweep for any lingering threats. Maintaining\ \ standard advance speed. -statusUpdateEnemyCritical4.text=%s, light picket defense encountered at the river crossing, with\ +statusUpdateEnemyCritical4.text={0}, light picket defense encountered at the river crossing, with\ \ enemy units withdrawing almost immediately. The crossing is clear, but the threat of rear\ \ ambushes remains real. We're pressing ahead, keeping formation tight and weapons ready for\ \ sudden contact. -statusUpdateEnemyCritical5.text=%s, light presence in this sector as enemy units pull back. The\ +statusUpdateEnemyCritical5.text={0}, light presence in this sector as enemy units pull back. The\ \ retreat appears uncoordinated, but all eyes are on the flanks for potential ambushes. -statusUpdateEnemyCritical6.text=%s, carefully navigating uneven terrain, remaining mindful of\ +statusUpdateEnemyCritical6.text={0}, carefully navigating uneven terrain, remaining mindful of\ \ potential enemy ambush points. Several possible choke points have been identified, prompting\ \ increased sensor sweeps and vigilance. Recent recon reports suggest scattered enemy scouts may\ \ be operating in the area, but no direct contact has been made. Convoy movement continues at a\ \ steady pace, with all vehicles maintaining formation and operational readiness. No delays\ \ expected. -statusUpdateEnemyCritical7.text=%s, minimal contact made at a narrow pass as withdrawing units\ +statusUpdateEnemyCritical7.text={0}, minimal contact made at a narrow pass as withdrawing units\ \ fired on us sporadically. The shots were ineffective, and convoy elements continued without\ \ interruption. -statusUpdateEnemyCritical8.text=%s, minimal contact made at a narrow pass as withdrawing enemy\ +statusUpdateEnemyCritical8.text={0}, minimal contact made at a narrow pass as withdrawing enemy\ \ units fired on our position. Engagement was limited to small arms fire and a few scattered LRM\ \ launches. Recon data indicates that enemy forces are\ \ continuing their retreat, with no reinforcements expected. -statusUpdateEnemyCritical9.text=%s, scattered enemy units encountered in this sector, firing while\ +statusUpdateEnemyCritical9.text={0}, scattered enemy units encountered in this sector, firing while\ \ retreating. The possibility of hidden threats keeps crews alert. Progress is steady, but\ \ caution is essential. Sensor sweeps continue nonstop. -statusUpdateEnemyCritical10.text=%s, negligible enemy presence in this sector, with enemy forces in\ +statusUpdateEnemyCritical10.text={0}, negligible enemy presence in this sector, with enemy forces in\ \ full retreat. Scattered wreckage marks their hasty withdrawal; it seems they're destroying\ \ whatever they can't take with them. -statusUpdateEnemyCritical11.text=%s, enemy forces have crumbled in this sector, offering negligible\ +statusUpdateEnemyCritical11.text={0}, enemy forces have crumbled in this sector, offering negligible\ \ resistance. Crew stays alert, aware that a cornered animal is still dangerous. No delays\ \ expected, but pace remains cautious. -statusUpdateEnemyCritical12.text=%s, maintaining vigilance while navigating through territory\ +statusUpdateEnemyCritical12.text={0}, maintaining vigilance while navigating through territory\ \ previously contested by enemy forces. Old defensive positions and makeshift barricades are\ \ visible, but no active resistance detected. No interference on comms, and convoy is moving at\ \ expected pace. -statusUpdateEnemyCritical13.text=%s, encountered minor enemy presence at a narrow pass. Enemy\ +statusUpdateEnemyCritical13.text={0}, encountered minor enemy presence at a narrow pass. Enemy\ \ remnants made a weak stand, launching a few shoulder-mounted SRMs before retreating into cover.\ \ Convoy pace maintaining constant sweeps for further ambushes or traps. -statusUpdateEnemyCritical14.text=%s, witnessing a rapid enemy retreat in this sector. The\ +statusUpdateEnemyCritical14.text={0}, witnessing a rapid enemy retreat in this sector. The\ \ withdrawal seems too sudden, leaving my crews tense and expecting an ambush. Convoy maintains\ \ pace, but vigilance remains high. -statusUpdateEnemyCritical15.text=%s, enemy forces nearly absent in this sector. The sudden lack of\ +statusUpdateEnemyCritical15.text={0}, enemy forces nearly absent in this sector. The sudden lack of\ \ opposition feels like the calm before a storm. Crews maintain a tense focus, wary of sudden\ \ ambushes. The situation feels unstable. -statusUpdateEnemyCritical16.text=%s, advancing cautiously along the route. Terrain analysis\ +statusUpdateEnemyCritical16.text={0}, advancing cautiously along the route. Terrain analysis\ \ suggests the possibility of concealed minefields or improvised barricades ahead. Recon\ \ hovercraft have detected minor signs of recent movement, indicating possible enemy scouting\ \ activity. All convoy units are on high alert, using active sensor sweeps to identify any\ \ immediate threats. Progress is steady but deliberate. -statusUpdateEnemyCritical17.text=%s, made contact with retreating enemy units at the supply depot,\ +statusUpdateEnemyCritical17.text={0}, made contact with retreating enemy units at the supply depot,\ \ with no sustained resistance. The depot remains intact, and enemy forces are pulling back.\ \ Convoy continues to maintain steady progress. -statusUpdateEnemyCritical18.text=%s, light resistance encountered, with most enemy forces\ +statusUpdateEnemyCritical18.text={0}, light resistance encountered, with most enemy forces\ \ neutralized swiftly. Engagements were limited to a small infantry detachment supported by a\ \ single heavily damaged 'Mek, which was promptly disabled. Tactical scans show no remaining\ \ enemy presence in the vicinity, and forward elements continue their advance unhindered.\ \ All systems report green, no delays expected. -statusUpdateEnemyCritical19.text=%s, detecting minimal enemy activity in this sector, with most\ +statusUpdateEnemyCritical19.text={0}, detecting minimal enemy activity in this sector, with most\ \ enemy forces withdrawing rapidly. Progress remains steady, with no deviations from the route. -statusUpdateEnemyCritical20.text=%s, long range scans detected enemy forces collapsing near the\ +statusUpdateEnemyCritical20.text={0}, long range scans detected enemy forces collapsing near the\ \ planned route. Hostile cohesion is minimal, with no coordinated response evident. Convoy\ \ maintains formation, moving steadily past their broken lines. -statusUpdateEnemyCritical21.text=%s, witnessing enemy units in full retreat, with minimal\ +statusUpdateEnemyCritical21.text={0}, witnessing enemy units in full retreat, with minimal\ \ resistance expected. Smoke trails mark their withdrawal, and abandoned vehicles are strewn\ \ across the path. Convoy forces keep pressing, guns hot and crews alert. No sense of victory\ \ yet, but this feels like a turning point. -statusUpdateEnemyCritical22.text=%s, light resistance encountered, %s, marked by scattered\ +statusUpdateEnemyCritical22.text={0}, light resistance encountered, {0}, marked by scattered\ \ autocannon fire from retreating forces. Convoy maintained formation, advancing steadily. -statusUpdateEnemyCritical23.text=%s, enemy forces nearly absent in this sector. The lack\ +statusUpdateEnemyCritical23.text={0}, enemy forces nearly absent in this sector. The lack\ \ of opposition is unsettling, raising concerns of a trap ahead. Crews maintaining high\ \ alert. Sensors continue to sweep for unexpected hostiles. The silence is tense. -statusUpdateEnemyCritical24.text=%s, encountered a rapid enemy retreat in this sector. Enemy\ +statusUpdateEnemyCritical24.text={0}, encountered a rapid enemy retreat in this sector. Enemy\ \ movements suggest a complete breakdown of command and control. Crew remains alert. -statusUpdateEnemyCritical25.text=%s, enemy resistance breaking down across this sector.\ +statusUpdateEnemyCritical25.text={0}, enemy resistance breaking down across this sector.\ \ What was once a stronghold is now a field of wreckage and scattered, fleeing troops.\ \ Convoy units press forward, ready for any last-ditch ambushes, but the air is heavy with\ \ the smell of burning armor. -statusUpdateEnemyCritical26.text=%s, no significant resistance encountered in this sector. Recon\ +statusUpdateEnemyCritical26.text={0}, no significant resistance encountered in this sector. Recon\ \ confirms that enemy forces have likely withdrawn, leaving the area mostly uncontested. Sensor\ \ sweeps show no hidden threats or mines, and forward elements are progressing steadily. Convoy\ \ pace remains steady. -statusUpdateEnemyCritical27.text=%s, steady progress continues, with sensors primed for\ +statusUpdateEnemyCritical27.text={0}, steady progress continues, with sensors primed for\ \ sudden movements from enemy remnants. The air crackles with tension, as all eyes are\ \ on the horizon and every shadow could hide danger. My crews are focused and ready to react at a\ \ moment's notice. -statusUpdateEnemyCritical28.text=%s, proceeding cautiously through the current sector,\ +statusUpdateEnemyCritical28.text={0}, proceeding cautiously through the current sector,\ \ with all units maintaining readiness for sudden enemy attacks from concealed positions.\ \ Sensor sweeps and infrared scans are being conducted regularly to detect\ \ possible hostile movements. So far, no significant contact has been made, and convoy\ \ progression remains on schedule. -statusUpdateEnemyCritical29.text=%s, scattered enemy units encountered in this sector. Engagement\ +statusUpdateEnemyCritical29.text={0}, scattered enemy units encountered in this sector. Engagement\ \ was brief, with limited pushback offered before they withdrew. Convoy armor sustained light\ \ scarring, but all vehicles remain fully operational. -statusUpdateEnemyCritical30.text=%s, crew morale is high as we push through abandoned\ +statusUpdateEnemyCritical30.text={0}, crew morale is high as we push through abandoned\ \ enemy positions. These former strongholds show signs of rapid evacuation, with\ \ supplies and light weaponry left behind. -statusUpdateEnemyCritical31.text=%s, took a few desperate shots from retreating forces at\ +statusUpdateEnemyCritical31.text={0}, took a few desperate shots from retreating forces at\ \ the canyon. Hostiles were disorganized, using mostly light weapons and scattered LRM\ \ volleys. Enemy resistance has dissipated as they pull back further.\ \ Movement through the canyon continues without further incident. -statusUpdateEnemyCritical32.text=%s, advancing smoothly over what used to be a heavily\ +statusUpdateEnemyCritical32.text={0}, advancing smoothly over what used to be a heavily\ \ contested battlefield. Scorched craters and abandoned enemy vehicles mark the path\ \ forward, but no active opposition has been encountered. Crew is maintaining high\ \ readiness despite the lull, with sensors scanning constantly for possible ambushes.\ \ All systems remain at full operational capacity. -statusUpdateEnemyCritical33.text=%s, weak resistance encountered - scattered shots as\ +statusUpdateEnemyCritical33.text={0}, weak resistance encountered - scattered shots as\ \ enemy forces pulled back quickly. The withdrawal feels too easy. Convoy maintains momentum, but\ \ vigilance is high as the retreating units could reposition for a counterattack. -statusUpdateEnemyCritical34.text=%s, minimal hostiles encountered at the river crossing, with most\ +statusUpdateEnemyCritical34.text={0}, minimal hostiles encountered at the river crossing, with most\ \ enemy forces withdrawing rapidly. The swift retreat raises alarms of a possible\ \ regrouping nearby. Convoy advances, but nerves are stretched tight. No hits sustained. -statusUpdateEnemyCritical35.text=%s, minor resistance at the river crossing, with enemy\ +statusUpdateEnemyCritical35.text={0}, minor resistance at the river crossing, with enemy\ \ morale breaking completely. Their retreat was chaotic, but it could be a feint. Convoy\ \ remains on high alert, scanning for signs of a possible regrouping. -statusUpdateEnemyCritical36.text=%s, light resistance faced; most enemy forces were\ +statusUpdateEnemyCritical36.text={0}, light resistance faced; most enemy forces were\ \ neutralized without issue. The engagement was brief but chaotic, adding to the tension.\ \ Sensors remain active, monitoring all directions. -statusUpdateEnemyCritical37.text=%s, a rapid enemy retreat was observed in this sector,\ +statusUpdateEnemyCritical37.text={0}, a rapid enemy retreat was observed in this sector,\ \ with hostile forces offering negligible resistance. Visuals confirm that remaining\ \ enemy units are moving at full speed toward their fallback positions, abandoning or destroying\ \ any remaining equipment. Convoy units continue to advance without impediment, maintaining\ \ formation integrity. Comms chatter suggests minimal enemy coordination, and no\ \ immediate reinforcements detected. Vehicle systems are operating at full capacity,\ \ with no logistical delays reported. -statusUpdateEnemyCritical38.text=%s, minor opposition encountered in this sector, with\ +statusUpdateEnemyCritical38.text={0}, minor opposition encountered in this sector, with\ \ occasional shots from withdrawing infantry. The shots were scattered and ineffective,\ \ doing little to slow our advance. The situation continues to unfold in our favor. -statusUpdateEnemyCritical39.text=%s, desperate shots fired by remnants at a narrow pass,\ +statusUpdateEnemyCritical39.text={0}, desperate shots fired by remnants at a narrow pass,\ \ but no significant offense was encountered. Enemy presence has thinned, with most units already\ \ retreating deeper into their territory. -statusUpdateEnemyCritical40.text=%s, enemy units in full retreat across this sector, with minimal\ +statusUpdateEnemyCritical40.text={0}, enemy units in full retreat across this sector, with minimal\ \ resistance encountered. The few remaining hostiles are offering little more than a token\ \ defense. No delays expected. -statusUpdateEnemyCritical41.text=%s, advancing with heightened alertness, expecting\ +statusUpdateEnemyCritical41.text={0}, advancing with heightened alertness, expecting\ \ possible surprises from concealed enemy positions. Terrain ahead offers several\ \ potential ambush sites. No significant threats have materialized yet, but crews are on edge.\ \ Progress remains steady, with no disruptions reported. -statusUpdateEnemyCritical42.text=%s, convoy elements are maintaining focus while rapidly\ +statusUpdateEnemyCritical42.text={0}, convoy elements are maintaining focus while rapidly\ \ pushing through abandoned enemy outposts. Recent scans indicate that these outposts\ \ were vacated in haste, leaving behind limited supplies and non-functional vehicles.\ \ Comms interference is minimal, allowing uninterrupted coordination among convoy units.\ \ All vehicles green across the board. -statusUpdateEnemyCritical43.text=%s, the area ahead shows signs of recent skirmishes, with heat\ +statusUpdateEnemyCritical43.text={0}, the area ahead shows signs of recent skirmishes, with heat\ \ signatures only now fading. All convoy units are on full alert, weapons hot and ready. We're\ \ going to slow down, so we can scan for potential rear-guard ambushes or hidden 'Meks. -statusUpdateEnemyCritical44.text=%s, the path is rugged, with debris from past battles\ +statusUpdateEnemyCritical44.text={0}, the path is rugged, with debris from past battles\ \ littering the way. Sensors scanning aggressively for mines and hidden infantry along the route.\ \ So far, we've only hit abandoned enemy positions, but the tension is palpable. -statusUpdateEnemyCritical45.text=%s, enemy units were encountered in this sector. Resistance\ +statusUpdateEnemyCritical45.text={0}, enemy units were encountered in this sector. Resistance\ \ minimal, with only a few desperate shots from retreating 'Meks and armor. The chaos\ \ of their withdrawal is evident from the scattered debris and distant smoke plumes.\ \ Convoy continues its advance. -statusUpdateEnemyCritical46.text=%s, morale remains high as convoy units maintain a\ +statusUpdateEnemyCritical46.text={0}, morale remains high as convoy units maintain a\ \ steady advance across former enemy defensive lines. Most hostile positions appear\ \ abandoned, with minimal interference encountered. Initial scans show scattered\ \ remains of defensive installations, primarily unmanned turrets and empty foxholes.\ \ Sensor data suggests a clear path forward, with enemy presence reduced to scattered,\ \ disorganized elements well beyond engagement range. -statusUpdateEnemyCritical47.text=%s, barely any enemy forces left at the river crossing\ +statusUpdateEnemyCritical47.text={0}, barely any enemy forces left at the river crossing\ \ as hostile units pull out of this sector. The water's edge is littered with\ \ abandoned gear and hastily discarded weapons. Convoy maintains a firm push, engines\ \ echoing over the water as crews stay on high alert. -statusUpdateEnemyCritical48.text=%s, minor resistance at the river crossing; enemy morale\ +statusUpdateEnemyCritical48.text={0}, minor resistance at the river crossing; enemy morale\ \ broke rapidly under fire. Hostiles scattered in disarray, abandoning equipment and wounded.\ \ Infantry support weapons were deployed but proved ineffective against the convoy's advance.\ \ Crossing has been secured, and convoy progress remains steady. -statusUpdateEnemyCritical49.text=%s, brief skirmish encountered in this sector as\ +statusUpdateEnemyCritical49.text={0}, brief skirmish encountered in this sector as\ \ retreating enemy troops attempted a final defense. Engagement was limited, primarily\ \ involving small arms fire and shoulder-mounted SRMs. Enemy resistance was quickly broken,\ \ with their remaining forces withdrawing under cover of smoke. Maintaining current pace. -statusUpdateEnemyStalemate0.text=%s, despite ongoing artillery strikes, convoy maintains\ +statusUpdateEnemyStalemate0.text={0}, despite ongoing artillery strikes, convoy maintains\ \ momentum. Artillery impacts have been primarily concentrated on rear positions, with limited\ \ effectiveness. Crews continue operating at full capacity.\ \ Communication lines remain clear, and sensors detect no additional enemy reinforcements at\ \ this stage. Current pace remains steady, with navigation adhering to planned routes despite\ \ ongoing bombardment. -statusUpdateEnemyStalemate1.text=%s, convoy remains intact despite intermittent\ +statusUpdateEnemyStalemate1.text={0}, convoy remains intact despite intermittent\ \ harassment from enemy skirmishers. Scout vehicles primarily employed hit-and-run\ \ tactics, utilizing light autocannons and machine gun fire. Engagements were brief.\ \ No breaches recorded, and operational tempo remains consistent. Sensor sweeps confirm\ \ scouts have retreated to maintain distance. Convoy continues to advance, maintaining\ \ formation integrity. -statusUpdateEnemyStalemate2.text=%s, encountered heavy fire from enemy positions,\ +statusUpdateEnemyStalemate2.text={0}, encountered heavy fire from enemy positions,\ \ primarily autocannon and laser barrages. Engagement lasted approximately ten minutes\ \ before enemy forces initiated a withdrawal. Sensor logs indicate the enemy concentrated\ \ fire on central convoy elements, aiming to disrupt movement. Defensive maneuvers were\ \ executed successfully, preventing major casualties. All vehicles remain operational,\ \ with minor repairs underway. Comms remain functional, and we're resuming our\ \ advance, maintaining speed and formation. -statusUpdateEnemyStalemate3.text=%s, sustained heavy fire left convoy locked in place\ +statusUpdateEnemyStalemate3.text={0}, sustained heavy fire left convoy locked in place\ \ for several hours. Incoming rounds included autocannon bursts and indirect LRM fire.\ \ The enemy maintained pressure but retreated following a prolonged exchange. All\ \ convoy systems are fully operational, and defensive formations remain intact.\ \ Movement has resumed, albeit cautiously. -statusUpdateEnemyStalemate4.text=%s, convoy engaged in a brief firefight at a bridge.\ +statusUpdateEnemyStalemate4.text={0}, convoy engaged in a brief firefight at a bridge.\ \ Both sides exchanged small arms fire and autocannon bursts before disengaging due to\ \ incoming artillery. No decisive advantage gained by either side. Damage\ \ assessment shows superficial impacts to convoy armor, with no breaches.\ \ Bridge remains structurally sound. Sensors indicate sporadic enemy movement in the\ \ vicinity, but no immediate pursuit. Convoy remains on course. -statusUpdateEnemyStalemate5.text=%s, series of brief clashes erupted near convoy route,\ +statusUpdateEnemyStalemate5.text={0}, series of brief clashes erupted near convoy route,\ \ with both sides exchanging fire but failing to gain ground. Engagements primarily\ \ involved small arms fire. Enemy forces retreated to maintain distance, indicating\ \ no sustained offensive intent. Convoy pace remains steady, with minimal deviation\ \ from planned route. -statusUpdateEnemyStalemate6.text=%s, convoy executing detours to avoid long-range\ +statusUpdateEnemyStalemate6.text={0}, convoy executing detours to avoid long-range\ \ autocannon fire from entrenched positions. All vehicles remain in formation,\ \ maintaining steady movement forward. Sensor sweeps confirm that enemy fire is\ \ concentrated on known choke points, necessitating adjusted routes. Crews are\ \ maintaining high readiness, with comms channels open.\ \ Progress is steady. -statusUpdateEnemyStalemate7.text=%s, convoy moving steadily while avoiding entrenched\ +statusUpdateEnemyStalemate7.text={0}, convoy moving steadily while avoiding entrenched\ \ LRM emplacements and sniper fire. Sensor data confirms enemy presence remains active,\ \ targeting key routes with precise fire. Convoy continues forward, maintaining formation\ \ All sensors scanning continuously for concealed threats. -statusUpdateEnemyStalemate8.text=%s, progress is slow but steady as convoy maneuvers\ +statusUpdateEnemyStalemate8.text={0}, progress is slow but steady as convoy maneuvers\ \ around minefields and fortified positions. Minesweeper units are active, clearing\ \ paths to prevent damage. Defensive emplacements have been identified but remain\ \ unengaged, allowing convoy to maintain its current route. Damage reports indicate\ \ only minor wear on transports due to terrain conditions. No direct enemy\ \ engagement recorded. Movement continues, but at a cautious pace to ensure safety and\ \ operational readiness. -statusUpdateEnemyStalemate9.text=%s, convoy remains operational despite slow progress\ +statusUpdateEnemyStalemate9.text={0}, convoy remains operational despite slow progress\ \ through fields of scattered debris and unspent munitions. Terrain is proving\ \ challenging, with potential hazards limiting speed. Sensor sweeps detect sporadic\ \ munitions but no immediate threats from active enemy forces. -statusUpdateEnemyStalemate10.text=%s, a brief firefight erupted, resulting in casualties\ +statusUpdateEnemyStalemate10.text={0}, a brief firefight erupted, resulting in casualties\ \ on both sides. Engagement was sharp and intense, with autocannon bursts and missile\ \ strikes exchanged at close range. After sustaining moderate losses, both sides pulled\ \ back without pressing further. Convoy elements maintained position and resumed forward\ \ movement once the area cleared. All vehicles remain operational, and crews are already\ \ prepared for the next contact. -statusUpdateEnemyStalemate11.text=%s, combat remains active in this sector, with\ +statusUpdateEnemyStalemate11.text={0}, combat remains active in this sector, with\ \ autocannons and lasers trading blows. Hostile fire is consistent but not\ \ overwhelming, indicating an attempt to wear us down rather than achieve a breakthrough.\ \ No decisive advantage gained on either side so far. Convoy continues advancing under\ \ covering fire, maintaining formation despite constant pressure. -statusUpdateEnemyStalemate12.text=%s, current standoff persists, with both sides\ +statusUpdateEnemyStalemate12.text={0}, current standoff persists, with both sides\ \ entrenched and unable to gain ground. Hostile positions are reinforced, delivering\ \ steady fire to delay our movement. Convoy maintains readiness, using counter-fire to\ \ suppress enemy advances. No significant breaches reported on our side, but tension\ \ remains high as both forces await an opening. Movement is steady, with all systems\ \ fully operational despite the ongoing stalemate. -statusUpdateEnemyStalemate13.text=%s, sustained heavy fire from enemy positions, narrowly\ +statusUpdateEnemyStalemate13.text={0}, sustained heavy fire from enemy positions, narrowly\ \ avoiding major damage. Engagement involved concentrated laser and missile fire,\ \ forcing a temporary halt. Enemy units withdrew after a few minutes of intense\ \ exchange, suggesting an attempt to conserve their forces. All convoy elements report green\ \ status, and advance has resumed with increased caution. -statusUpdateEnemyStalemate14.text=%s, another skirmish broke out but failed to produce\ +statusUpdateEnemyStalemate14.text={0}, another skirmish broke out but failed to produce\ \ a decisive outcome. Both sides engaged in heavy fire, with PPC and laser\ \ bursts exchanged before withdrawing to previous positions. Enemy fire was accurate but\ \ lacked sustained pressure. Crews maintain vigilance, expecting additional skirmishes as the\ \ route continues. -statusUpdateEnemyStalemate15.text=%s, fighting continues to threaten our route, as enemy\ +statusUpdateEnemyStalemate15.text={0}, fighting continues to threaten our route, as enemy\ \ forces maintain sporadic but intense autocannon fire. Convoy is taking evasive\ \ maneuvers, keeping moving under pressure. Crews are focused, executing\ \ defensive tactics while maintaining operational speed. Enemy presence suggests\ \ further attempts to disrupt our advance. -statusUpdateEnemyStalemate16.text=%s, repeated skirmishes encountered in this sector,\ +statusUpdateEnemyStalemate16.text={0}, repeated skirmishes encountered in this sector,\ \ with enemy forces retreating after a few autocannon volleys. Convoy maintained\ \ defensive positions, exchanging accurate fire to suppress enemy positions. Engagements\ \ were short-lived, with enemy units choosing to fall back rather than commit. Convoy\ \ continues forward, maintaining readiness for further contacts. -statusUpdateEnemyStalemate17.text=%s, convoy just faced another skirmish. Enemy withdrew\ +statusUpdateEnemyStalemate17.text={0}, convoy just faced another skirmish. Enemy withdrew\ \ after we inflicted light casualties, preferring to conserve forces rather than sustain a\ \ prolonged engagement. The situation remains tense, as enemy elements may attempt another\ \ strike further along the route. Crew discipline remains high, with readiness levels maintained. -statusUpdateEnemyStalemate18.text=%s, combat in this sector remains ongoing, marked by\ +statusUpdateEnemyStalemate18.text={0}, combat in this sector remains ongoing, marked by\ \ autocannon and missile exchanges. No decisive outcomes achieved as both sides are\ \ holding ground. Convoy continues to press forward cautiously, maintaining defensive\ \ formations. -statusUpdateEnemyStalemate19.text=%s, another clash resulted in a stalemate, with the enemy\ +statusUpdateEnemyStalemate19.text={0}, another clash resulted in a stalemate, with the enemy\ \ sustaining moderate damage before withdrawing. Engagement was brief but intense,\ \ featuring coordinated missile strikes and autocannon volleys. Convoy maintained position\ \ until enemy fire subsided, then resumed its advance. Crews are already preparing for the next\ \ contact. -statusUpdateEnemyStalemate20.text=%s, another skirmish erupted at the river crossing.\ +statusUpdateEnemyStalemate20.text={0}, another skirmish erupted at the river crossing.\ \ The exchange was fierce, with lasers lighting up the water's edge.\ \ Mud and debris sprayed everywhere making traction difficult. Enemy forces tried to\ \ press, but their advance was stalled by allied counter-fire. Despite the chaos, the enemy failed\ \ to secure any ground and eventually fell back under sustained pressure. We're still\ \ moving, but the tension in the air is thick. -statusUpdateEnemyStalemate21.text=%s, our convoy was caught in a brief but intense\ +statusUpdateEnemyStalemate21.text={0}, our convoy was caught in a brief but intense\ \ skirmish. Gunfire echoed between the hills, and visibility was low due to smoke and\ \ stirred-up dust. Enemy forces hit us hard initially, but their line cracked after taking\ \ light casualties. Things are still tense, and we're anticipating another ambush. -statusUpdateEnemyStalemate22.text=%s, convoy exchanged heavy fire with enemy forces\ +statusUpdateEnemyStalemate22.text={0}, convoy exchanged heavy fire with enemy forces\ \ along the route. Autocannon bursts and missile trails cut through the air as both\ \ sides clashed. The roar of battle was overwhelming, with each hit rattling the armor.\ \ The enemy pulled back before managing to breach our lines, leaving only smoke and\ \ distant echoes behind. Our forward movement is steady, but the crews are running high on\ \ adrenaline, eyes scanning for the next ambush. -statusUpdateEnemyStalemate23.text=%s, situation in this sector remains unresolved.\ +statusUpdateEnemyStalemate23.text={0}, situation in this sector remains unresolved.\ \ Constant ambushes are keeping everyones' heads down. The terrain is scorched,\ \ with impact craters lining the path forward. Every movement is under threat, with SRM\ \ volleys and autocannon fire coming from unseen positions. The convoy is still enroute,\ \ maintaining formation under relentless pressure. Crews are running on pure resolve,\ \ pushing through the exhaustion as we wait for a moment to break through. -statusUpdateEnemyStalemate24.text=%s, a firefight broke out, sudden and violent. The air\ +statusUpdateEnemyStalemate24.text={0}, a firefight broke out, sudden and violent. The air\ \ was filled with smoke and shrapnel and the sound of metal on metal, with casualties\ \ sustained on both sides. After a few minutes of brutal exchange, both forces pulled\ \ back to regroup. The convoy remains intact, but the wear of constant skirmishes is\ \ beginning to show. -statusUpdateEnemyStalemate25.text=%s, intense fire at a canyon entrance locked the\ +statusUpdateEnemyStalemate25.text={0}, intense fire at a canyon entrance locked the\ \ convoy in place. Incoming rounds slammed into the canyon walls, sending debris raining\ \ down on the vehicles. We almost got trapped in there. The situation was chaotic,\ \ but defensive fire eventually forced the enemy to ease off. All\ \ systems remain nominal, and forward movement has resumed. -statusUpdateEnemyStalemate26.text=%s, engaged in a clash at the river crossing. Enemy\ +statusUpdateEnemyStalemate26.text={0}, engaged in a clash at the river crossing. Enemy\ \ forces, led by a lance of light 'Meks, launched a well-coordinated attack. Laser beams\ \ and missiles crisscrossed the area, turning the riverbank into a battlefield. Despite\ \ their aggressive push, allied counter-fire held them back, and their lines broke after\ \ sustaining moderate damage. We're still moving, but I've told the crews to expect\ \ further strikes. -statusUpdateEnemyStalemate27.text=%s, the deadlock in this sector persists with frequent skirmishes\ +statusUpdateEnemyStalemate27.text={0}, the deadlock in this sector persists with frequent skirmishes\ \ erupting along the route. Autocannon fire and missile bursts continue to rain down as\ \ the convoy inches forward. We're having to make some tough calls here, finding\ \ routes around mines and contested sectors, but the air is heavy with smoke, and each turn\ \ feels like walking into an ambush. Progress is slow. -statusUpdateEnemyStalemate28.text=%s, deadlock continues in this sector, with constant\ +statusUpdateEnemyStalemate28.text={0}, deadlock continues in this sector, with constant\ \ autocannon fire pinning both sides down. The sound of rounds hitting metal is relentless,\ \ and the convoy has struggled to gain ground. Crews are pushing through exhaustion, staying\ \ focused amid the chaos. It's a real mess out here, shell casings and scorched\ \ remains, with no clear winner in sight. The convoy presses on. -statusUpdateEnemyStalemate29.text=%s, minor skirmishes broke out along our route, marked\ +statusUpdateEnemyStalemate29.text={0}, minor skirmishes broke out along our route, marked\ \ by sharp exchanges of fire. No decisive gains have been made, but the situation remains\ \ stable for now. The road ahead is littered with debris, and smoke lingers in the air.\ \ Crews remain vigilant, knowing that each step forward could be another firefight.\ \ Despite the uncertainty, we're still enroute. -statusUpdateEnemyStalemate30.text=%s, situation in this sector remains tense, marked by\ +statusUpdateEnemyStalemate30.text={0}, situation in this sector remains tense, marked by\ \ regular exchanges of fire. Neither side has managed to gain a decisive advantage.\ \ Autocannon bursts and missile volleys punctuate the air, but the convoy maintains\ \ formation and keeps moving forward. While the progress is steady, we're all on\ \ high alert here. -statusUpdateEnemyStalemate31.text=%s, sustained heavy fire left us locked in place for\ +statusUpdateEnemyStalemate31.text={0}, sustained heavy fire left us locked in place for\ \ several hours. The engagement was intense, with incoming rounds forcing us to\ \ go to ground. Despite the pressure, the convoy remains intact, with armor holding against\ \ repeated impacts. It was a close situation, but the escorts ensured our\ \ safety. Crews maintained composure throughout, and no critical systems were\ \ compromised. We're now enroute to nav gamma. -statusUpdateEnemyStalemate32.text=%s, a skirmish erupted at the river crossing, with\ +statusUpdateEnemyStalemate32.text={0}, a skirmish erupted at the river crossing, with\ \ concentrated fire from both sides. The intensity was high, but neither force managed\ \ to establish clear dominance. Convoy elements maintained defensive positions, absorbing\ \ impacts and responding in kind. Despite the lack of decisive outcomes, the convoy\ \ remained undeterred and progress now continues. -statusUpdateEnemyStalemate33.text=%s, we got hit again on our route, but\ +statusUpdateEnemyStalemate33.text={0}, we got hit again on our route, but\ \ we made it through. The exchange of fire was sustained, but the\ \ convoy's defences held firm. Enemy forces eventually withdrew to their\ \ positions, unable to break through. Convoy remains intact, and forward\ \ movement continues. Our crews are focused, displaying calm determination despite the\ \ ongoing challenges. -statusUpdateEnemyStalemate34.text=%s, the situation in this sector remains unresolved, with\ +statusUpdateEnemyStalemate34.text={0}, the situation in this sector remains unresolved, with\ \ continuous exchanges keeping both sides pinned down. The battle lines are clear, but\ \ progress is slow. Convoy units maintain steady fire discipline, holding defensive\ \ formation under pressure. While no major gains have been made, the convoy remains\ \ operational, and we're pressing forward on our planned route. -statusUpdateEnemyStalemate35.text=%s, convoy was caught in an ambush near the river\ +statusUpdateEnemyStalemate35.text={0}, convoy was caught in an ambush near the river\ \ crossing. The skirmish was brief but intense, marked by rapid exchanges of fire and a\ \ couple of LRM volleys from the enemy. Despite the surprise attack, we were able to\ \ maintain formation and repelled the threat. Enemy units withdrew shortly\ \ after, unable to sustain the engagement. Movement has resumed, with crews maintaining a\ \ steady focus on the mission. -statusUpdateEnemyStalemate36.text=%s, minor skirmishes occurred at a narrow pass,\ +statusUpdateEnemyStalemate36.text={0}, minor skirmishes occurred at a narrow pass,\ \ but we got through it okay. The engagement was brief, with scattered\ \ fire exchanged. Convoy crews continue to adapt to the ongoing challenges, keeping\ \ formations tight and ready for sudden threats. Despite the constant skirmishes, the\ \ mission remains on track, with all vehicles reporting green status. -statusUpdateEnemyStalemate37.text=%s, we encountered heavy resistance in this sector,\ +statusUpdateEnemyStalemate37.text={0}, we encountered heavy resistance in this sector,\ \ mainly sustained LRM and PPC fire at long range. Crews remain composed, and we're\ \ maintaining movement despite the opposition. Progress is slower than predicted. -statusUpdateEnemyStalemate38.text=%s, heavy combat persists in this sector, with\ +statusUpdateEnemyStalemate38.text={0}, heavy combat persists in this sector, with\ \ entrenched enemy forces holding their ground. The exchange of fire is continuous, but\ \ convoy elements are maintaining defensive stances and responding effectively. Progress\ \ is methodical, with crews demonstrating disciplined engagement tactics. The situation\ \ remains fluid. -statusUpdateEnemyStalemate39.text=%s, frequent skirmishes continue to prevent clear\ +statusUpdateEnemyStalemate39.text={0}, frequent skirmishes continue to prevent clear\ \ advances in this sector. The convoy maintains its position, absorbing enemy fire while\ \ attempting to press forward. The situation remains challenging, but the overall mission\ \ objective remains in sight. -statusUpdateEnemyStalemate40.text=%s, brief firefight erupted. An intense exchange, but no ground\ +statusUpdateEnemyStalemate40.text={0}, brief firefight erupted. An intense exchange, but no ground\ \ gained. Enemy retreated quickly, but tension is high. Crew remains alert, ready for immediate\ \ response. Pushing forward now. -statusUpdateEnemyStalemate41.text=%s, minor skirmishes erupted along the route. Rapid\ +statusUpdateEnemyStalemate41.text={0}, minor skirmishes erupted along the route. Rapid\ \ exchanges of fire ensued, with both sides attempting to gain ground. Enemy resistance\ \ was consistent but not overwhelming. Movement was slowed as crews adapted to the shifting\ \ engagement zones. The situation remains stable for now, but vigilance is crucial. Convoy maintains\ \ its pace, but alertness remains high as skirmishes continue intermittently. -statusUpdateEnemyStalemate42.text=%s, deadlock persists across the battlefield. Constant\ +statusUpdateEnemyStalemate42.text={0}, deadlock persists across the battlefield. Constant\ \ autocannon fire exchanges keep both sides entrenched, with little room for maneuver.\ \ Convoy is maintaining cover, focusing on suppressive fire to manage enemy pressure.\ \ Progress is slow, but consistent. Despite the standoff, the convoy pushes forward cautiously\ \ whenever possible, with defensive formations intact. -statusUpdateEnemyStalemate43.text=%s, clash at the river crossing was intense. The\ +statusUpdateEnemyStalemate43.text={0}, clash at the river crossing was intense. The\ \ enemy's attack was well-coordinated, led by a lance of light 'Meks delivering precise\ \ autocannon fire. Convoy defensive measures absorbed impacts, maintaining formation under\ \ heavy fire. Despite heavy resistance, forward movement is steady. -statusUpdateEnemyStalemate44.text=%s, heavy resistance encountered at the canyon.\ +statusUpdateEnemyStalemate44.text={0}, heavy resistance encountered at the canyon.\ \ Autocannon fire was exchanged heavily, with neither side managing to gain a decisive\ \ advantage. The convoy pressed forward despite the barrage, maintaining a steady pace\ \ under pressure. The situation remains tense, but the convoy continues forward with caution,\ \ ready for further engagements. -statusUpdateEnemyStalemate45.text=%s, situation remains unresolved in this sector. Constant\ +statusUpdateEnemyStalemate45.text={0}, situation remains unresolved in this sector. Constant\ \ exchanges of fire are keeping both sides pinned down. The convoy holds steady,\ \ with crews scanning for sudden changes in enemy movements. No clear outcome is evident, but the\ \ convoy continues to advance where opportunities arise. Defensive tactics are in place,\ \ adapting to ongoing resistance. -statusUpdateEnemyStalemate46.text=%s, combat persists in this sector with continuous autocannon and\ +statusUpdateEnemyStalemate46.text={0}, combat persists in this sector with continuous autocannon and\ \ SRM fire. No decisive outcome achieved, as both sides maintain entrenched positions.\ \ Convoy maintains formation, responding to sustained attacks while avoiding\ \ major breaches. Progress is gradual. -statusUpdateEnemyStalemate47.text=%s, recent engagement ended inconclusively. Heavy fire was\ +statusUpdateEnemyStalemate47.text={0}, recent engagement ended inconclusively. Heavy fire was\ \ exchanged, but the convoy remains intact, advancing slowly despite lingering\ \ threats. -statusUpdateEnemyStalemate48.text=%s, clashes continue along the route, directly\ +statusUpdateEnemyStalemate48.text={0}, clashes continue along the route, directly\ \ threatening convoy movement. Heavy autocannon fire forced evasive maneuvers, but crews\ \ maintained focus and defensive positions. Progress is steady, though tension remains high\ \ as the convoy presses forward. The situation remains unstable, but the convoy adapts\ \ continuously to enemy pressure. -statusUpdateEnemyStalemate49.text=%s, recent skirmish failed to produce clear results.\ +statusUpdateEnemyStalemate49.text={0}, recent skirmish failed to produce clear results.\ \ Heavy fire was exchanged, but hostiles withdrew after sustaining minor damage. The\ \ convoy continues forward, maintaining operational status under persistent threats. Crews\ \ remain alert, anticipating renewed contact. -statusUpdateEnemyDominating0.text=%s, enemy dominance in this sector is making each\ +statusUpdateEnemyDominating0.text={0}, enemy dominance in this sector is making each\ \ step forward a struggle. Continuous fire rains down, keeping the convoy pinned and\ \ forcing frequent stops. Autocannon bursts, missile volleys, and laser fire crisscross\ \ the route, leaving crews under constant stress. Progress is possible, but no breaks in the\ \ assault yet. -statusUpdateEnemyDominating1.text=%s, convoy still on course, but enemy sniper fire\ +statusUpdateEnemyDominating1.text={0}, convoy still on course, but enemy sniper fire\ \ is taking a toll on speed. Snipers are positioned strategically, forcing evasive\ \ movements and slowing progress. Defensive measures are in place, but the convoy's\ \ pace is erratic due to frequent stops. No significant losses reported, but conditions\ \ remain harsh. -statusUpdateEnemyDominating2.text=%s, pressing through risky terrain, trying to evade\ +statusUpdateEnemyDominating2.text={0}, pressing through risky terrain, trying to evade\ \ skirmishes while maintaining convoy speed. The ground is unstable, with frequent\ \ obstacles slowing movement. Enemy units are active in the area, but no direct contact\ \ has occurred yet. -statusUpdateEnemyDominating3.text=%s, navigating through less-contested terrain. The\ +statusUpdateEnemyDominating3.text={0}, navigating through less-contested terrain. The\ \ aim is to find a safer path beyond enemy long-range sensor reach. The convoy is making\ \ steady progress, though the rough terrain presents challenges. Visibility is limited,\ \ but movement remains steady. -statusUpdateEnemyDominating4.text=%s, enemy dominance in this sector has forced\ +statusUpdateEnemyDominating4.text={0}, enemy dominance in this sector has forced\ \ longer, more hazardous routes. Progress is both slow and dangerous, with risk\ \ increasing as we detour into less secure territory. Enemy pressure is constant,\ \ and maintaining formation under these conditions requires rapid adjustments.\ \ The situation remains critical. -statusUpdateEnemyDominating5.text=%s, relentless attacks forced a withdrawal to a\ +statusUpdateEnemyDominating5.text={0}, relentless attacks forced a withdrawal to a\ \ backup route. Enemy pressure was too intense, with LRM and SRM fire was overwhelming\ \ convoy defenses. We're adapting, but the pressure remains high. -statusUpdateEnemyDominating6.text=%s, enemy forces are deploying significant resources\ +statusUpdateEnemyDominating6.text={0}, enemy forces are deploying significant resources\ \ into this sector. Allied units have begun a tactical retreat, unable to sustain\ \ defensive positions under current pressure. Without reinforcements, allied forces will\ \ not hold for long. Current analysis suggests that maintaining this route is not viable\ \ for subsequent operations. Alternative routing is advised for future movements. -statusUpdateEnemyDominating7.text=%s, attempted to hold ground at the depot, but enemy\ +statusUpdateEnemyDominating7.text={0}, attempted to hold ground at the depot, but enemy\ \ artillery strikes came down relentlessly. Shells landed close, shaking the vehicles and\ \ forcing rapid repositioning. Despite strong defensive efforts, allied units were forced\ \ to abandon the depot and retreat swiftly. -statusUpdateEnemyDominating8.text=%s, rerouting to avoid enemy airstrikes. Assault 'Meks\ +statusUpdateEnemyDominating8.text={0}, rerouting to avoid enemy airstrikes. Assault 'Meks\ \ advancing rapidly, forcing constant adjustments. Defensive measures are in place, with\ \ crews focused on evasion. The convoy is maintaining speed, but the threat from air\ \ support and 'Meks remains immediate. -statusUpdateEnemyDominating9.text=%s, entrenched enemy positions are forcing the convoy\ +statusUpdateEnemyDominating9.text={0}, entrenched enemy positions are forcing the convoy\ \ into longer, riskier routes. Moving through rough terrain under fire is taking a toll on\ \ both speed and morale. Defensive fire keeps the enemy at bay, but the danger of sudden\ \ ambushes remains high. -statusUpdateEnemyDominating10.text=%s, heavy enemy momentum at the resupply point left\ +statusUpdateEnemyDominating10.text={0}, heavy enemy momentum at the resupply point left\ \ no choice but to abandon supplies to maintain speed. Crew focus was on survival,\ \ making the hard call. We're moving fast, but the loss of resources will hurt. -statusUpdateEnemyDominating11.text=%s, enemy control over this sector has forced a\ +statusUpdateEnemyDominating11.text={0}, enemy control over this sector has forced a\ \ tactical retreat. Infantry and 'Meks hold the high ground, using it to deliver sustained\ \ fire on our position. Defensive measures were enacted, but the enemy's elevated position\ \ gave them a clear advantage. The convoy remains intact, but forward movement is currently\ \ impeded. Alternate routes are under consideration to avoid further exposure. -statusUpdateEnemyDominating12.text=%s, enemy control of the canyon has necessitated\ +statusUpdateEnemyDominating12.text={0}, enemy control of the canyon has necessitated\ \ longer detours. Entrenched 'Meks block all main routes, preventing direct movement\ \ through the area. Convoy integrity is maintained, but progress is slow due to rerouting\ \ requirements. Tactical evaluations suggest limited options for regaining momentum without\ \ reinforcements. -statusUpdateEnemyDominating13.text=%s, moving cautiously to avoid ambushes from\ +statusUpdateEnemyDominating13.text={0}, moving cautiously to avoid ambushes from\ \ entrenched SRM carriers. Sensor sweeps are frequent, aiming to detect potential threats\ \ before engagement. The terrain favors ambushes, but convoy formations are tight and\ \ prepared. -statusUpdateEnemyDominating14.text=%s, enemy advance forced retreat from the checkpoint.\ +statusUpdateEnemyDominating14.text={0}, enemy advance forced retreat from the checkpoint.\ \ Precision AC/2 fire created conditions unsuitable for defense, prompting withdrawal.\ \ The checkpoint remains under enemy control. Convoy operations continue, but route\ \ adjustments are now essential. -statusUpdateEnemyDominating15.text=%s, navigating hazardous terrain to avoid enemy\ +statusUpdateEnemyDominating15.text={0}, navigating hazardous terrain to avoid enemy\ \ ambushes. The convoy is maintaining speed and momentum, though the path is challenging.\ \ Rocks and debris complicate movement, but crews remain disciplined and calm. Defensive\ \ measures are active, scanning for potential threats. -statusUpdateEnemyDominating16.text=%s, coordinated enemy strikes hit the resupply point\ +statusUpdateEnemyDominating16.text={0}, coordinated enemy strikes hit the resupply point\ \ hard. Initial defense was strong, but the sheer volume of fire broke our lines. Rapid\ \ artillery strikes, backed by precise autocannon volleys, forced a retreat. The convoy\ \ managed to pull back, but supplies were left behind in the chaos. -statusUpdateEnemyDominating17.text=%s, pressing through rocky terrain to avoid sudden\ +statusUpdateEnemyDominating17.text={0}, pressing through rocky terrain to avoid sudden\ \ skirmishes with enemy light 'Meks. Movement is cautious but continuous, with crews\ \ remaining vigilant. Scout units are visible on the periphery, but defensive measures are\ \ holding. -statusUpdateEnemyDominating18.text=%s, using alternate routes to stay ahead of advancing\ +statusUpdateEnemyDominating18.text={0}, using alternate routes to stay ahead of advancing\ \ forces. The terrain is rough, filled with potential ambush sites. Crews are scanning\ \ continuously, searching for safe passage, but enemy units are closing in, creating constant\ \ pressure. The situation remains unstable, with enemy pursuit ongoing. -statusUpdateEnemyDominating19.text=%s, taking risky detours to evade advancing heavy\ +statusUpdateEnemyDominating19.text={0}, taking risky detours to evade advancing heavy\ \ 'Mek units. The terrain is rough, but morale is steady. -statusUpdateEnemyDominating20.text=%s, moving swiftly to avoid contact with enemy 'Meks\ +statusUpdateEnemyDominating20.text={0}, moving swiftly to avoid contact with enemy 'Meks\ \ fortifying strategic positions. The terrain is manageable, and we're prepared for potential\ \ encounters, maintaining formation and focus. Enemy presence is noted but distant. -statusUpdateEnemyDominating21.text=%s, progressing quickly through rough terrain. Enemy\ +statusUpdateEnemyDominating21.text={0}, progressing quickly through rough terrain. Enemy\ \ BattleMeks are closing in, slowing our advance, but not halting it. Defensive measures\ \ are active, keeping enemy fire at bay. -statusUpdateEnemyDominating22.text=%s, barely escaped an ambush at the river crossing.\ +statusUpdateEnemyDominating22.text={0}, barely escaped an ambush at the river crossing.\ \ Light 'Meks harassed the convoy relentlessly. Defensive responses were rapid,\ \ keeping convoy intact, but the ambush was well-timed, suggesting they knew\ \ when would arrive. -statusUpdateEnemyDominating23.text=%s, convoy encountered heavy fire as enemy forces\ +statusUpdateEnemyDominating23.text={0}, convoy encountered heavy fire as enemy forces\ \ secured high ground at a blind canyon. AC/20s were deployed effectively, dominating the\ \ route and preventing further advance. Defensive measures absorbed some impacts, but\ \ retreat was necessary to prevent losses. The canyon remains under enemy control, making\ \ this path untenable for continued movement. Rerouting. -statusUpdateEnemyDominating24.text=%s, relentless enemy strikes made reaching the depot\ +statusUpdateEnemyDominating24.text={0}, relentless enemy strikes made reaching the depot\ \ nearly impossible. Autocannon volleys were precise, pinning the convoy down repeatedly.\ \ The intensity of the assault shows no signs of letting up. -statusUpdateEnemyDominating25.text=%s, taking dangerous paths to maintain progress while\ +statusUpdateEnemyDominating25.text={0}, taking dangerous paths to maintain progress while\ \ avoiding entrenched autocannon positions. Enemy fire is consistent, with bursts forcing\ \ rapid shifts in route. The terrain is challenging, slowing forward momentum. -statusUpdateEnemyDominating26.text=%s, convoy nearly surrounded at the river crossing.\ +statusUpdateEnemyDominating26.text={0}, convoy nearly surrounded at the river crossing.\ \ Enemy 'Meks moved fast, cutting off key paths with precision. The convoy struggled\ \ to maintain formation as debris and splashes from near hits filled the air.\ \ Crews pushed through under immense pressure, barely managing to find a gap before being\ \ fully encircled. The situation remains dire, with escape routes still under threat. -statusUpdateEnemyDominating27.text=%s, heavy assault encountered along the current route.\ +statusUpdateEnemyDominating27.text={0}, heavy assault encountered along the current route.\ \ Enemy SRM Carriers initiated a coordinated push, forcing a tactical retreat. The carriers\ \ attempted to pin the convoy down with sustained fire. Operational integrity is\ \ preserved, but enemy presence remains significant. SRM carriers are still in the\ \ vicinity, indicating a strong enemy foothold in this area. -statusUpdateEnemyDominating28.text=%s, heavy resistance encountered along the route. Enemy forces are\ +statusUpdateEnemyDominating28.text={0}, heavy resistance encountered along the route. Enemy forces are\ \ using entrenched positions to hold ground, deploying continuous fire to halt our advance.\ \ Convoy managed to push through initial bursts, but sustained attacks are slowing progress.\ \ Defensive measures are in place, and vehicles remain operational. Moving forward will\ \ require persistence and increased vigilance. -statusUpdateEnemyDominating29.text=%s, enemy launched a concentrated push at a narrow\ +statusUpdateEnemyDominating29.text={0}, enemy launched a concentrated push at a narrow\ \ pass. Heavy autocannon fire created a lethal barrage that slowedconvoy movement.\ \ Defensive formations held firm, but progress was severely impacted.\ \ Crews remain focused, ready for another potential surge as we regroup. -statusUpdateEnemyDominating30.text=%s, convoy took heavy hits from enemy 'Meks blocking\ +statusUpdateEnemyDominating30.text={0}, convoy took heavy hits from enemy 'Meks blocking\ \ our planned route. PPC blasts and autocannon fire rocked the vehicles, forcing immediate\ \ evasive maneuvers. The path ahead is unclear, with enemy units still holding key choke\ \ points. Defensive formations remain intact, but the pressure is unrelenting. -statusUpdateEnemyDominating31.text=%s, despite intense pressure, the convoy continues to\ +statusUpdateEnemyDominating31.text={0}, despite intense pressure, the convoy continues to\ \ push forward. Enemy fire is sustained, creating a high-risk environment. Crews are\ \ adapting quickly, using evasive tactics to minimize damage. Speed is maintained where\ \ possible, but caution is necessary. -statusUpdateEnemyDominating32.text=%s, convoy hit hard at the narrow pass. Artillery fire\ +statusUpdateEnemyDominating32.text={0}, convoy hit hard at the narrow pass. Artillery fire\ \ was intense, creating significant delays. Defensive maneuvers kept the convoy intact, but\ \ forward momentum was severely affected. Progress remains slow, but movement has resumed. -statusUpdateEnemyDominating33.text=%s, navigating hostile terrain occupied by enemy\ +statusUpdateEnemyDominating33.text={0}, navigating hostile terrain occupied by enemy\ \ infantry. Engagements are frequent, with small arms fire disrupting movement. The convoy\ \ is keeping pace, but speed is reduced to maintain control over rough ground. Crews are\ \ on constant alert, responding to sudden attacks. Enemy positions are scattered but\ \ persistent, requiring continuous adjustments. Progress remains steady, but the pressure\ \ from enemy infantry is increasing. Evasion tactics are in use. -statusUpdateEnemyDominating34.text=%s, enemy control over key routes is forcing risky\ +statusUpdateEnemyDominating34.text={0}, enemy control over key routes is forcing risky\ \ detours. Formation is hard to maintain as we navigate unfamiliar terrain under constant\ \ fire. Defensive adjustments are ongoing, but pressure is building. -statusUpdateEnemyDominating35.text=%s, convoy remains operational despite immense pressure\ +statusUpdateEnemyDominating35.text={0}, convoy remains operational despite immense pressure\ \ from enemy skirmishers. Constant attacks slow progress, with frequent stops to avoid\ \ concentrated fire. No critical damage reported, but the situation remains tense. -statusUpdateEnemyDominating36.text=%s, convoy remains operational. However, scattered\ +statusUpdateEnemyDominating36.text={0}, convoy remains operational. However, scattered\ \ LRM fire complicates route planning. Incoming rounds are not concentrated but create\ \ unpredictable threats across the path. Current conditions suggest continued movement will\ \ be slow, with increased risk from sustained indirect fire. Monitoring of hostile\ \ positions continues. -statusUpdateEnemyDominating37.text=%s, forced into a hasty retreat at the depot after it\ +statusUpdateEnemyDominating37.text={0}, forced into a hasty retreat at the depot after it\ \ came under continuous artillery bombardment. Shells landed close, creating chaos and\ \ forcing immediate withdrawal. Crew maintained discipline, executing the retreat under\ \ intense pressure. Enemy artillery is still active, making a return unfeasible. -statusUpdateEnemyDominating38.text=%s, enemy pressure is mounting, with heavy LRM fire\ +statusUpdateEnemyDominating38.text={0}, enemy pressure is mounting, with heavy LRM fire\ \ targeting the convoy. Defensive formations are holding strong, absorbing impacts while\ \ maintaining momentum. Armor is strained but intact. The situation demands patience and resilience,\ \ both of which remain unwavering among all personnel. -statusUpdateEnemyDominating39.text=%s, sudden strike at the narrow pass has heightened risk\ +statusUpdateEnemyDominating39.text={0}, sudden strike at the narrow pass has heightened risk\ \ levels. Enemy fire was concentrated but brief, aimed at disrupting the convoy's pace.\ \ Forward momentum is being cautiously resumed, though the narrow terrain increases vulnerability\ \ to repeat attacks. Tactical assessments are underway to determine immediate next steps. -statusUpdateEnemyDominating40.text=%s, coordinated attacks drove the convoy back. Enemy\ +statusUpdateEnemyDominating40.text={0}, coordinated attacks drove the convoy back. Enemy\ \ forces unleashed a rapid assault under intense PPC fire. Defensive fire was returned,\ \ but the intensity forced a quick withdrawal. The retreat was hasty, leaving no time for\ \ regrouping, and we are currently waiting for all units to report in. -statusUpdateEnemyDominating41.text=%s, held position at the checkpoint initially, but enemy\ +statusUpdateEnemyDominating41.text={0}, held position at the checkpoint initially, but enemy\ \ LRMs launched in coordinated waves. Explosions rattled the convoy, showering the area with\ \ debris and creating disarray. Forced withdrawal was the only option, as further attempts to\ \ hold ground would have resulted in severe losses. Morale remains strained, but crews are\ \ ready to press forward again. -statusUpdateEnemyDominating42.text=%s, we just narrowly escaped a well-laid ambush. Enemy forces hit\ +statusUpdateEnemyDominating42.text={0}, we just narrowly escaped a well-laid ambush. Enemy forces hit\ \ hard, but convoy units rallied quickly, breaking through the attack. Enemy positions remain\ \ active, suggesting more ambushes ahead. -statusUpdateEnemyDominating43.text=%s, taking every available detour to bypass enemy-\ +statusUpdateEnemyDominating43.text={0}, taking every available detour to bypass enemy-\ \ controlled zones. Enemy fire is heavy, with autocannons and missile volleys creating\ \ constant hazards. Detours are risky, often passing through contested ground and potential\ \ ambush sites. Convoy pace is inconsistent, with stops to avoid concentrated fire. Urgency is\ \ high, as enemy units continue to pursue from multiple directions. -statusUpdateEnemyDominating44.text=%s, situation is dire, with heavy enemy presence ahead.\ +statusUpdateEnemyDominating44.text={0}, situation is dire, with heavy enemy presence ahead.\ \ Withdrawal is not an option. Crews are aware of the stakes and remain committed to the\ \ mission. Progress is challenging, but every effort is made to keep moving forward. -statusUpdateEnemyDominating45.text=%s, faced a series of well-timed strikes at the river\ +statusUpdateEnemyDominating45.text={0}, faced a series of well-timed strikes at the river\ \ crossing. Enemy assault 'Meks gained control of key paths, deploying heavy fire that forced\ \ multiple detours. All vehicles remain functional, but the enemy's dominance at the crossing\ \ poses a significant threat to further movement. Current assessment indicates a high likelihood\ \ of additional attacks in this sector. -statusUpdateEnemyDominating46.text=%s, convoy remains intact. Enemy fire from elevated\ +statusUpdateEnemyDominating46.text={0}, convoy remains intact. Enemy fire from elevated\ \ positions is increasing rapidly, but defensive responses are immediate and effective.\ \ Progress is slow but steady. -statusUpdateEnemyDominating47.text=%s, navigating now enemy-held territory. Fire from all sides\ +statusUpdateEnemyDominating47.text={0}, navigating now enemy-held territory. Fire from all sides\ \ is constant, forcing evasive maneuvers. Despite heavy fire, the convoy keeps moving,\ \ maintaining momentum. No major losses reported, but the situation remains critical. -statusUpdateEnemyDominating48.text=%s, enemy launched a strong push at the river crossing,\ +statusUpdateEnemyDominating48.text={0}, enemy launched a strong push at the river crossing,\ \ using entrenched positions to disrupt movement. Convoy units maintained defensive positions,\ \ but forward movement was significantly slowed. All systems remain operational, but enemy\ \ presence is effectively blocking the route. Rerouting. -statusUpdateEnemyDominating49.text=%s, heavy resistance encountered in this sector. Enemy\ +statusUpdateEnemyDominating49.text={0}, heavy resistance encountered in this sector. Enemy\ \ units are entrenched, holding key positions with precision fire. Missile trails and\ \ mortar bursts are creating a wall of steel that slows every advance.\ \ The convoy is pushing forward, but each meter is earned under fire. -statusUpdateEnemyOverwhelming0.text=%s, situation is dire, with heavy enemy\ +statusUpdateEnemyOverwhelming0.text={0}, situation is dire, with heavy enemy\ \ presence ahead. Defensive measures are active, adapting to enemy positions\ \ as they are revealed. Progress is challenging, but we're going to keep moving. -statusUpdateEnemyOverwhelming1.text=%s, retreating rapidly, trying to maintain\ +statusUpdateEnemyOverwhelming1.text={0}, retreating rapidly, trying to maintain\ \ momentum as enemy 'Meks close in. The situation is desperate, with heavy\ \ laser fire threatening convoy integrity. We're taking evasive\ \ maneuvers under pressure. The convoy is not defeated,\ \ but the enemy pursuit is relentless. -statusUpdateEnemyOverwhelming2.text=%s, enemy forces closing in rapidly;\ +statusUpdateEnemyOverwhelming2.text={0}, enemy forces closing in rapidly;\ \ immediate action required to avoid encirclement. Crews are moving fast,\ \ adjusting routes to escape encirclement. The escorts are\ \ active, keeping advancing forces at bay. The convoy is pushing forward, aiming\ \ to break through the tightening perimeter. -statusUpdateEnemyOverwhelming3.text=%s, narrowly escaped an ambush. Enemy\ +statusUpdateEnemyOverwhelming3.text={0}, narrowly escaped an ambush. Enemy\ \ forces launched a sudden, coordinated attack, targeting vulnerable sections\ \ of the convoy. Defensive actions were swift, enabling a rapid withdrawal.\ \ Crews maintained composure under fire, keeping formation intact. Convoy\ \ remains operational and pressing forward. -statusUpdateEnemyOverwhelming4.text=%s, situation remains desperate, but we\ +statusUpdateEnemyOverwhelming4.text={0}, situation remains desperate, but we\ \ broke through a wall of SRM Carriers. Crews pushed hard, navigating narrow paths\ \ amidst heavy fire from all sides. We're struggling here, but the terrain opens up\ \ ahead - we might get through this. -statusUpdateEnemyOverwhelming5.text=%s, convoy witnessed an allied force\ +statusUpdateEnemyOverwhelming5.text={0}, convoy witnessed an allied force\ \ suffer catastrophic losses at the river crossing checkpoint. Visuals confirm\ \ heavy enemy fire, including concentrated LRM strikes, overwhelming allied defenses.\ \ Initial estimates indicate significant casualties among friendly units,\ \ with remaining forces in disarray. Enemy 'Meks seized control of the crossing within minutes.\ \ The convoy maintained a safe distance, monitoring the situation without\ \ engaging. Current assessment shows no viable support options at this time. -statusUpdateEnemyOverwhelming6.text=%s, enemy positions remain strong in this sector,\ +statusUpdateEnemyOverwhelming6.text={0}, enemy positions remain strong in this sector,\ \ forcing rapid adjustments in convoy speed and formation. The situation is critical,\ \ with continued enemy fire creating constant pressure. Defensive measures are\ \ focused on minimizing damage, maintaining full operational capacity. -statusUpdateEnemyOverwhelming7.text=%s, moving at full speed, barely holding\ +statusUpdateEnemyOverwhelming7.text={0}, moving at full speed, barely holding\ \ formation as LRMs rain down. Crews are pushing hard, keeping vehicles\ \ on the road under heavy fire. The situation remains critical. -statusUpdateEnemyOverwhelming8.text=%s, overwhelming enemy pressure forced\ +statusUpdateEnemyOverwhelming8.text={0}, overwhelming enemy pressure forced\ \ abandonment of resupply attempts. Initial engagement involved concentrated\ \ artillery and missile fire, rapidly escalating to a full-scale assault on the checkpoint.\ \ Supplies were left behind to maintain speed and maneuverability. Movement\ \ continues toward secondary navpoints. -statusUpdateEnemyOverwhelming9.text=%s, enemy launched an ambush at the\ +statusUpdateEnemyOverwhelming9.text={0}, enemy launched an ambush at the\ \ river crossing, using entrenched positions to disrupt movement. Dug-in\ \ troops provided cover for their forward units, limiting convoy progress.\ \ Convoy units maintained defensive positions, but forward movement was\ \ significantly slowed. All systems remain operational, but enemy presence is\ \ effectively blocking the route. -statusUpdateEnemyOverwhelming10.text=%s, coordinated attacks drove the convoy\ +statusUpdateEnemyOverwhelming10.text={0}, coordinated attacks drove the convoy\ \ back from the supply depot. Enemy forces unleashed a rapid assault with heavy\ \ laser fire, scorching the surrounding terrain. Defensive fire was returned,\ \ but the intensity forced a quick withdrawal. -statusUpdateEnemyOverwhelming11.text=%s, retreating without pause under continuous\ +statusUpdateEnemyOverwhelming11.text={0}, retreating without pause under continuous\ \ artillery bombardment. We're keeping our heads down and pushing forward\ \ despite heavy impacts. The mission continues, but each moment feels like it\ \ could be our last. -statusUpdateEnemyOverwhelming12.text=%s, we watched as the resupply depot\ +statusUpdateEnemyOverwhelming12.text={0}, we watched as the resupply depot\ \ got hit hard. Explosions ripped through its defenses as\ \ enemy forces broke through, turning it into a scene of utter devastation.\ \ Fire and smoke filled the air, masking our withdrawal. The convoy continues its\ \ advance, but the cost has been high, with allied support largely compromised. -statusUpdateEnemyOverwhelming13.text=%s, the enemy's relentless pressure nearly\ +statusUpdateEnemyOverwhelming13.text={0}, the enemy's relentless pressure nearly\ \ broke the convoy's resolve near the river crossing. Sustained autocannon fire\ \ forced rapid evasive action. Convoy continues to advance, but the way forward is uncertain. -statusUpdateEnemyOverwhelming14.text=%s, narrowly escaped a well-laid ambush.\ +statusUpdateEnemyOverwhelming14.text={0}, narrowly escaped a well-laid ambush.\ \ Enemy forces hit hard, but convoy units rallied quickly, breaking through the\ \ attack. We're getting more sensor pings, suggesting more ambushes ahead. -statusUpdateEnemyOverwhelming15.text=%s, moving rapidly through a storm of SRM\ +statusUpdateEnemyOverwhelming15.text={0}, moving rapidly through a storm of SRM\ \ fire. Enemy salvos are continuous, targeting critical convoy elements. Defensive\ \ measures are active, but there's no time to recover or regroup. The\ \ situation is dire, but the mission remains intact against overwhelming odds.\ \ We're heads-down and pedal to the metal here! -statusUpdateEnemyOverwhelming16.text=%s, convoy's route in this sector was\ +statusUpdateEnemyOverwhelming16.text={0}, convoy's route in this sector was\ \ nearly overrun during the enemy's final push. Enemy units pressed hard, breaking\ \ through defensive lines with heavy firepower. Drivers struggled to hold\ \ formation amid chaotic conditions, executing rapid evasive maneuvers to prevent\ \ full encirclement. Enemy presence remains significant,\ \ suggesting possible follow-up attacks. Crews are maintaining focus, with defensive\ \ measures active. -statusUpdateEnemyOverwhelming17.text=%s, attempted to regroup in this sector,\ +statusUpdateEnemyOverwhelming17.text={0}, attempted to regroup in this sector,\ \ but overwhelming enemy firepower forced a rapid retreat. Artillery shells rained\ \ down, creating craters and debris that blocked potential escape routes. Crews\ \ struggled to maintain formation as enemy 'Meks pressed hard, their cannons and\ \ lasers tearing into our position. Defensive measures are active, with crews\ \ ready to respond to further attacks. -statusUpdateEnemyOverwhelming18.text=%s, convoy hit hard at a narrow pass.\ +statusUpdateEnemyOverwhelming18.text={0}, convoy hit hard at a narrow pass.\ \ Enemy fire was intense, targeting key vehicles and breaking formation briefly.\ \ Morale among crews is declining due to sustained pressure. Defensive measures\ \ are holding. Movement is slow, with evasive tactics in use to prevent losses. -statusUpdateEnemyOverwhelming19.text=%s, pressing through rocky terrain, aiming\ +statusUpdateEnemyOverwhelming19.text={0}, pressing through rocky terrain, aiming\ \ to avoid sudden skirmishes with enemy light 'Meks. Dust clouds obscure\ \ visibility, making navigation hazardous. Crews are tense, expecting an ambush\ \ at any moment. We're still moving though. -statusUpdateEnemyOverwhelming20.text=%s, moving swiftly to avoid encirclement by\ +statusUpdateEnemyOverwhelming20.text={0}, moving swiftly to avoid encirclement by\ \ skirmishers. Enemy units are actively attempting to cut off escape routes,\ \ forcing rapid changes in direction. Vehicles are maneuvering through rough\ \ terrain, engines straining under the pressure. The convoy keeps pushing\ \ forward, but it's a race against time here. -statusUpdateEnemyOverwhelming21.text=%s, navigating now enemy-held territory. Fire\ +statusUpdateEnemyOverwhelming21.text={0}, navigating now enemy-held territory. Fire\ \ from all sides is constant, forcing evasive maneuvers. Despite heavy fire,\ \ the convoy keeps moving, maintaining momentum. The situation remains critical. -statusUpdateEnemyOverwhelming22.text=%s, withdrawing rapidly while dodging\ +statusUpdateEnemyOverwhelming22.text={0}, withdrawing rapidly while dodging\ \ incoming enemy aircraft. The situation remains desperate, with aerial attacks\ \ disrupting movement. Convoy integrity is maintained, but defensive actions\ \ are at their limit. -statusUpdateEnemyOverwhelming23.text=%s, navigating through enemy-held territory\ +statusUpdateEnemyOverwhelming23.text={0}, navigating through enemy-held territory\ \ under continuous fire. Crews maintain focus, responding swiftly to sniper and\ \ autocannon bursts. Progress is slower than anticipated, but all systems are\ \ operational. The situation is tense, but we're still enroute. -statusUpdateEnemyOverwhelming24.text=%s, convoy remains operational despite heavy\ +statusUpdateEnemyOverwhelming24.text={0}, convoy remains operational despite heavy\ \ skirmisher pressure. We're doing our best, but every moment counts. -statusUpdateEnemyOverwhelming25.text=%s, faced a devastating ambush at the river\ +statusUpdateEnemyOverwhelming25.text={0}, faced a devastating ambush at the river\ \ crossing. Heavy fire hit the flanks hard, forcing rapid evasive action. Crews\ \ maintained defensive formations, but the intensity of the assault caused\ \ significant disruption. We're still enroute, but it's touch and go here. -statusUpdateEnemyOverwhelming26.text=%s, the convoy is intact but was nearly crippled by\ +statusUpdateEnemyOverwhelming26.text={0}, the convoy is intact but was nearly crippled by\ \ relentless enemy fighter attacks. SRMs and machine gun fire rained down from\ \ above, keeping crews pinned and unable to respond effectively. Armor is scarred,\ \ and engines are overheating, but we're still on our way. -statusUpdateEnemyOverwhelming27.text=%s, pulling back rapidly to create distance\ +statusUpdateEnemyOverwhelming27.text={0}, pulling back rapidly to create distance\ \ from advancing assault 'Meks. Enemy units are maintaining steady pressure.\ \ The situation is critical, but convoy movement continues under full alert.\ \ Crews are executing rapid maneuvers to evade direct confrontation. -statusUpdateEnemyOverwhelming28.text=%s, morale is low, impacted by sustained\ +statusUpdateEnemyOverwhelming28.text={0}, morale is low, impacted by sustained\ \ enemy fire. We've still got everyone, but it's tough going.\ \ Progress is slower than expected, but the convoy remains intact. -statusUpdateEnemyOverwhelming29.text=%s, maintaining formation under heavy fire is\ +statusUpdateEnemyOverwhelming29.text={0}, maintaining formation under heavy fire is\ \ proving difficult. Enemy units are probing the perimeter, forcing rapid\ \ defensive shifts. -statusUpdateEnemyOverwhelming30.text=%s, situation is critical; defensive positions\ +statusUpdateEnemyOverwhelming30.text={0}, situation is critical; defensive positions\ \ must be re-established immediately in this sector. The enemy is everywhere,\ \ and while we've remained unnoticed for now, that won't last.\ \ I don't know what the situation is like at your end, but it's a real\ \ rat's nest here! -statusUpdateEnemyOverwhelming31.text=%s, convoy's position in this sector was\ +statusUpdateEnemyOverwhelming31.text={0}, convoy's position in this sector was\ \ nearly overrun during the latest push. Assault 'Meks broke through\ \ defensive lines, forcing crews to make split-second decisions under intense\ \ pressure. Vehicles maneuvered desperately, dodging autocannon bursts and PPC\ \ volleys, and we were able to break free at the last moment. -statusUpdateEnemyOverwhelming32.text=%s, held position at the checkpoint initially,\ +statusUpdateEnemyOverwhelming32.text={0}, held position at the checkpoint initially,\ \ but enemy LRMs launched while we were refueling. Forced withdrawal was the only\ \ option. -statusUpdateEnemyOverwhelming33.text=%s, attempted to regroup in this sector, but\ +statusUpdateEnemyOverwhelming33.text={0}, attempted to regroup in this sector, but\ \ enemy fire proved too intense. Initial repositioning efforts were disrupted by\ \ concentrated autocannon bursts, forcing another rapid retreat. Enemy units\ \ maintain a strong foothold, leaving limited options for immediate re-engagement.\ \ Convoy continues to retreat toward secondary routes. -statusUpdateEnemyOverwhelming34.text=%s, despite relentless pressure, the convoy\ +statusUpdateEnemyOverwhelming34.text={0}, despite relentless pressure, the convoy\ \ continues forward. Enemy fire is concentrated, targeting key vehicles in an\ \ attempt to disrupt formation. Crews maintain defensive posture, responding\ \ promptly to incoming attacks. Morale is under strain, but the mission remains\ \ the top priority. No halts possible. -statusUpdateEnemyOverwhelming35.text=%s, retreating to a safer position under\ +statusUpdateEnemyOverwhelming35.text={0}, retreating to a safer position under\ \ sustained fire. Enemy pressure is intense, forcing rapid withdrawals while\ \ maintaining tight formation. Crews are managing vehicle integrity despite the\ \ barrage. We're currently regrouping, stabilizing our position before the next\ \ move forward. -statusUpdateEnemyOverwhelming36.text=%s, the convoy is barely moving, pinned down\ +statusUpdateEnemyOverwhelming36.text={0}, the convoy is barely moving, pinned down\ \ by relentless PPC fire from entrenched enemy positions. Crews are working frantically,\ \ trying to keep engines running and maintain any semblance of formation. The odds\ \ are heavily stacked against us, but we're going to make a break for it. -statusUpdateEnemyOverwhelming37.text=%s, retreating at full speed. Enemy 'Meks,\ +statusUpdateEnemyOverwhelming37.text={0}, retreating at full speed. Enemy 'Meks,\ \ including heavies and fast scouts, are closing quickly. PPC blasts and missile trails\ \ are coming at us. We're pushing the vehicles to their absolute limits,\ \ fighting to maintain control over rough terrain. There's no time to regroup or\ \ assess damages; it's a desperate race to create distance from relentless pursuit. -statusUpdateEnemyOverwhelming38.text=%s, convoy remains intact. Enemy fire from\ +statusUpdateEnemyOverwhelming38.text={0}, convoy remains intact. Enemy fire from\ \ elevated positions is increasing rapidly. Progress is slow but steady. -statusUpdateEnemyOverwhelming39.text=%s, taking detours to bypass enemy-controlled zones.\ +statusUpdateEnemyOverwhelming39.text={0}, taking detours to bypass enemy-controlled zones.\ \ Enemy units are harassing us from multiple directions,\ \ and we're struggling to keep moving. -statusUpdateEnemyOverwhelming40.text=%s, we're pulling back, no time to\ +statusUpdateEnemyOverwhelming40.text={0}, we're pulling back, no time to\ \ regroup. Enemy pressure remains high, with advancing 'Meks closing in rapidly.\ \ Convoy speed is critical, with evasive tactics prioritized to maintain distance.\ \ Situation remains unstable, but the convoy is still together. -statusUpdateEnemyOverwhelming41.text=%s, enemy's relentless assault at the river\ +statusUpdateEnemyOverwhelming41.text={0}, enemy's relentless assault at the river\ \ crossing nearly broke our formation. Heavy fire targeted the front and rear\ \ elements, forcing rapid adjustments. -statusUpdateEnemyOverwhelming42.text=%s, drivers are pushing hard, doing their best\ +statusUpdateEnemyOverwhelming42.text={0}, drivers are pushing hard, doing their best\ \ to maintain movement. LRMs and autocannon fire create constant hazards, making\ \ every moment a struggle. We're doing what we can. -statusUpdateEnemyOverwhelming43.text=%s, the convoy was nearly overrun just moments ago.\ +statusUpdateEnemyOverwhelming43.text={0}, the convoy was nearly overrun just moments ago.\ \ Enemy forces launched a coordinated assault, closing in rapidly from multiple directions.\ \ Defensive measures were stretched to their limits, but crews managed to repel\ \ the initial wave. Convoy remains intact, pushing forward under heavy fire. -statusUpdateEnemyOverwhelming44.text=%s, the convoy narrowly escaped a massive\ +statusUpdateEnemyOverwhelming44.text={0}, the convoy narrowly escaped a massive\ \ assault. Initial contact involved heavy SRM fire, forcing evasive maneuvers.\ \ Enemy 'Meks, primarily heavies, attempted encirclement but failed due to a\ \ rapid breakout. Enemy forces are regrouping, but convoy continues\ \ to advance toward safer ground. statusUpdateEnemyOverwhelming45.text=[Heavy static is audible, punctuated with\ - \ yells and the sound of battle] %s, we're on the move. Enemy VTOLs are on us,\ + \ yells and the sound of battle] {0}, we're on the move. Enemy VTOLs are on us,\ \ we're going to try and shake them in the canyon. This is going to be close,\ \ but I think we can make it. -statusUpdateEnemyOverwhelming46.text=%s, the last enemy assault in this sector was\ +statusUpdateEnemyOverwhelming46.text={0}, the last enemy assault in this sector was\ \ devastating, inflicting heavy casualties among allied forces. Enemy units,\ \ primarily heavy 'Meks and infantry support, launched a coordinated attack,\ \ rapidly breaking through defensive positions. Allied losses are substantial,\ \ with multiple vehicles and personnel incapacitated. Enemy fire remains\ \ concentrated, limiting movement options for remaining allied units. The convoy\ \ is maintaining distance, operating under full alert. -statusUpdateEnemyOverwhelming47.text=%s, enemy's assault at the depot has\ +statusUpdateEnemyOverwhelming47.text={0}, enemy's assault at the depot has\ \ inflicted heavy losses among allied forces. Enemy fire was precise,\ \ targeting supply lines and command vehicles. Convoy units maintained a defensive\ \ posture but could not prevent significant allied casualties. We're on the move,\ \ and maintaining speed despite the loss of allied support. -statusUpdateEnemyOverwhelming48.text=%s, rerouting immediately to avoid incoming\ +statusUpdateEnemyOverwhelming48.text={0}, rerouting immediately to avoid incoming\ \ airstrikes. Assault 'Meks advancing rapidly, forcing fast adjustments. Crews\ \ are moving with urgency, trying to maintain formation while navigating rugged\ \ terrain. Defensive measures are fully active, with sensors sweeping for\ \ incoming threats. Speed is critical; the convoy is pushing forward. -statusUpdateEnemyOverwhelming49.text=%s, we faced a series of well-timed strikes at\ +statusUpdateEnemyOverwhelming49.text={0}, we faced a series of well-timed strikes at\ \ the last checkpoint. Enemy assault 'Meks gained control of key routes.\ \ The convoy was able to break through, but I think the checkpoint has been lost.\ \ Current assessment indicates a high likelihood of additional attacks in this sector. -statusUpdateIntercepted.boilerplate=%s, hostile interception underway. Incoming contacts unrelenting.\ - \ Reinforcements needed urgently. They're closing in fast.%s -statusUpdateIntercepted0.text=%s, rear assault underway. Hostile 'Meks penetrating our lines.\ +statusUpdateIntercepted.boilerplate={0}, hostile interception underway. Incoming contacts unrelenting.\ + \ Reinforcements needed urgently. They're closing in fast.{0} +statusUpdateIntercepted0.text={0}, rear assault underway. Hostile 'Meks penetrating our lines.\ \ Transports are fully exposed; formation in disarray. Incoming contacts on sensors.\ \ Reinforcements needed urgently. Attempting to fall back, but we're in serious trouble. They're closing in\ - \ fast - holding this position much longer isn't viable, we're going to try regrouping.%s -statusUpdateIntercepted1.text=%s, losing ground rapidly. Encircled by fast-moving 'Meks. Regroup\ + \ fast - holding this position much longer isn't viable, we're going to try regrouping.{0} +statusUpdateIntercepted1.text={0}, losing ground rapidly. Encircled by fast-moving 'Meks. Regroup\ \ attempt failed; transports are under siege. Last call for reinforcements - our line is\ - \ collapsing, and command structure is breaking down.%s -statusUpdateIntercepted2.text=%s, convoy disintegrating under heavy fire. Hostile armor units\ + \ collapsing, and command structure is breaking down.{0} +statusUpdateIntercepted2.text={0}, convoy disintegrating under heavy fire. Hostile armor units\ \ advancing at high speed. Reinforcements required immediately - total defeat imminent. Enemy\ - \ overwhelming all attempts to break free. We're going to make one final attempt to withdraw.%s -statusUpdateIntercepted3.text=%s, we're surrounded. Our perimeter has collapsed. Crew making a final\ + \ overwhelming all attempts to break free. We're going to make one final attempt to withdraw.{0} +statusUpdateIntercepted3.text={0}, we're surrounded. Our perimeter has collapsed. Crew making a final\ \ stand, barely holding. Supplies are exposed, no fallback route. Immediate reinforcements needed\ - \ to prevent total loss.%s -statusUpdateIntercepted4.text=%s, perimeter has been breached by enemy 'Meks. Convoy overrun, cargo\ + \ to prevent total loss.{0} +statusUpdateIntercepted4.text={0}, perimeter has been breached by enemy 'Meks. Convoy overrun, cargo\ \ being seized. If reinforcements are inbound, they must arrive now. Regroup efforts faltering\ - \ under intense fire.%s -statusUpdateIntercepted5.text=%s, checkpoint lost. Final defenses shattered by artillery. Command\ + \ under intense fire.{0} +statusUpdateIntercepted5.text={0}, checkpoint lost. Final defenses shattered by artillery. Command\ \ and control severely compromised. Immediate support required to hold the line, but position is\ - \ barely stable. We're going to try and withdraw.%s -statusUpdateIntercepted6.text=%s, convoy breaking apart. Heavy fire shredding our defenses. Supplies\ + \ barely stable. We're going to try and withdraw.{0} +statusUpdateIntercepted6.text={0}, convoy breaking apart. Heavy fire shredding our defenses. Supplies\ \ falling into enemy hands. Defensive positions have been compromised.\ - \ Urgent need for reinforcements to make a last stand.%s -statusUpdateIntercepted7.text=%s, we're at breaking point. Hostiles control the main supply route,\ + \ Urgent need for reinforcements to make a last stand.{0} +statusUpdateIntercepted7.text={0}, we're at breaking point. Hostiles control the main supply route,\ \ regroup attempts have failed. Supplies dwindling, morale fading. Reinforcements must arrive now,\ - \ or total defeat is inevitable.%s -statusUpdateIntercepted8.text=%s, surrounded on all sides. Hostile forces closing in rapidly.\ + \ or total defeat is inevitable.{0} +statusUpdateIntercepted8.text={0}, surrounded on all sides. Hostile forces closing in rapidly.\ \ Immediate reinforcements essential - without them, complete collapse is certain. Situation\ - \ critical.%s -statusUpdateIntercepted9.text=%s, convoy devastated. Sustained bombardment by enemy 'Meks. Supplies\ + \ critical.{0} +statusUpdateIntercepted9.text={0}, convoy devastated. Sustained bombardment by enemy 'Meks. Supplies\ \ fully exposed - crew attempting to destroy remaining stock. Reinforcements required urgently;\ - \ convoy is coming apart.%s -statusUpdateIntercepted10.text=%s, convoy in disarray. Defensive lines breached in all sectors. Crew\ + \ convoy is coming apart.{0} +statusUpdateIntercepted10.text={0}, convoy in disarray. Defensive lines breached in all sectors. Crew\ \ falling back, supplies being seized. Reinforcements requested urgently.\ - \ Situation dire.%s -statusUpdateIntercepted11.text=%s, critical state. Hostiles advancing quickly. Supplies\ - \ compromised. Reinforcements essential - total defeat imminent without immediate support.%s -statusUpdateIntercepted12.text=%s, surrounded with no escape. Crew attempting fallback\ - \ but with no clear route available. Immediate support required to avoid total annihilation.%s -statusUpdateIntercepted13.text=%s, overwhelmed on all sides. Final line breached.\ - \ Immediate support required, but time is running out.%s -statusUpdateIntercepted14.text=%s, we're at the edge. Convoy in ruins, crew desperately holding\ - \ ground. Reinforcements needed urgently, or this is the end.%s -statusUpdateIntercepted15.text=%s, defenses completely breached. Convoy collapsing under enemy\ - \ pressure. Reinforcements urgently needed to have any chance of survival.%s -statusUpdateIntercepted16.text=%s, enemy closing in, convoy overrun. Regroup efforts have failed.\ - \ Reinforcements required urgently to prevent complete collapse.%s -statusUpdateIntercepted17.text=%s, enemy advancing uncontested. Supplies exposed and\ - \ unable to secure or destroy. Reinforcements required urgently - time is critical.%s -statusUpdateIntercepted18.text=%s, under relentless fire. Supplies compromised, crew taking heavy\ - \ losses. Reinforcements needed immediately - situation critical.%s -statusUpdateIntercepted19.text=%s, situation hopeless. Last line of defense lost. Convoy collapsing,\ - \ all units breaking down. Reinforcements needed urgently.%s + \ Situation dire.{0} +statusUpdateIntercepted11.text={0}, critical state. Hostiles advancing quickly. Supplies\ + \ compromised. Reinforcements essential - total defeat imminent without immediate support.{0} +statusUpdateIntercepted12.text={0}, surrounded with no escape. Crew attempting fallback\ + \ but with no clear route available. Immediate support required to avoid total annihilation.{0} +statusUpdateIntercepted13.text={0}, overwhelmed on all sides. Final line breached.\ + \ Immediate support required, but time is running out.{0} +statusUpdateIntercepted14.text={0}, we're at the edge. Convoy in ruins, crew desperately holding\ + \ ground. Reinforcements needed urgently, or this is the end.{0} +statusUpdateIntercepted15.text={0}, defenses completely breached. Convoy collapsing under enemy\ + \ pressure. Reinforcements urgently needed to have any chance of survival.{0} +statusUpdateIntercepted16.text={0}, enemy closing in, convoy overrun. Regroup efforts have failed.\ + \ Reinforcements required urgently to prevent complete collapse.{0} +statusUpdateIntercepted17.text={0}, enemy advancing uncontested. Supplies exposed and\ + \ unable to secure or destroy. Reinforcements required urgently - time is critical.{0} +statusUpdateIntercepted18.text={0}, under relentless fire. Supplies compromised, crew taking heavy\ + \ losses. Reinforcements needed immediately - situation critical.{0} +statusUpdateIntercepted19.text={0}, situation hopeless. Last line of defense lost. Convoy collapsing,\ + \ all units breaking down. Reinforcements needed urgently.{0} interceptionInstructions.text=

A special scenario has been generated. Failure to complete\ \ this scenario will result in the loss of all units, personnel, and cargo. @@ -2261,94 +2262,94 @@ interceptionInstructions.text=

A special scenario has been generated. -statusUpdateAbandoned0.text=%s, we're being overwhelmed! Enemy 'Meks are breaching our lines, and\ +statusUpdateAbandoned0.text={0}, we're being overwhelmed! Enemy 'Meks are breaching our lines, and\ \ half the transports are lost! Supplies are about to be captured, and the crew\ \ is down. No reinforcements in sight! They're breaking through the barricade -\ \ damn it, they're inside the vehicle! We need immediate - [unintelligible shouting] - I\ \ don't think we can hold any - [burst of static, then silence]. -statusUpdateAbandoned1.text=%s, the convoy is in disarray! Heavy autocannon fire has cut off our\ +statusUpdateAbandoned1.text={0}, the convoy is in disarray! Heavy autocannon fire has cut off our\ \ last escape route! The crew is making a last stand, but morale is\ \ gone, and supplies are nearly lost! I can't believe this is the end! They're\ \ breaching - [explosion drowns out transmission] - if anyone's there, we need\ \ help - [static]. -statusUpdateAbandoned2.text=%s, we're pinned down! Hostiles everywhere, and no chance to regroup!\ +statusUpdateAbandoned2.text={0}, we're pinned down! Hostiles everywhere, and no chance to regroup!\ \ Supplies are exposed, and ammo is gone! The crew is making a final stand, but\ \ it won't last! Reinforcements were supposed to save us, but - [sudden gunfire\ \ and shouting] - casualties are mounting! They're at the doors! This is -\ \ [abrupt cut-off]. -statusUpdateAbandoned3.text=%s, we're under relentless assault! The enemy has broken every\ +statusUpdateAbandoned3.text={0}, we're under relentless assault! The enemy has broken every\ \ defensive line, and the crew is scattered! Supplies are in enemy hands, and\ \ there's no time left! Tried to rally for a last stand, but it's hopeless!\ \ We're - [screams in the background] - I don't know how much longer we can -\ \ [gunfire, then abrupt silence]. -statusUpdateAbandoned4.text=%s, there's no escape! The convoy is collapsing, and supplies are\ +statusUpdateAbandoned4.text={0}, there's no escape! The convoy is collapsing, and supplies are\ \ lost! Fighting for every inch, but the enemy is inside the perimeter! How\ \ could you leave us here?! Crew is using knives and makeshift weapons to hold the last\ \ positions. If we don't get - [loud explosion] - I'm not sure anyone will make it out of -\ \ [static, then silence]. -statusUpdateAbandoned5.text=%s, we're barely holding! The central line is breached, and we're\ +statusUpdateAbandoned5.text={0}, we're barely holding! The central line is breached, and we're\ \ down to our last shots! Supplies are almost gone, and we can't hold them off!\ \ Without reinforcements, it's a massacre! I don't think - [explosions and\ \ gunfire] - we're not going to last much longer! They're closing in fast! We\ \ need - [static, then silence]. -statusUpdateAbandoned6.text=%s, completely pinned down! The enemy is relentless, and supplies are\ +statusUpdateAbandoned6.text={0}, completely pinned down! The enemy is relentless, and supplies are\ \ exposed! The crew is fighting desperately, but we're overwhelmed from all\ \ sides! Reinforcements are nowhere, and we're - [loud gunfire and shouting] -\ \ morale is broken, and we're losing men fast! They've broken through! We're -\ \ [abrupt silence]. -statusUpdateAbandoned7.text=%s, chaos everywhere! Supplies exposed, and the crew is struggling to\ +statusUpdateAbandoned7.text={0}, chaos everywhere! Supplies exposed, and the crew is struggling to\ \ hold ground! Facing overwhelming firepower with no backup! The enemy is\ \ breaching the final defenses! Damn it, we need help! Crew is - [static,\ \ followed by screams] - they're inside the vehicles! We can't - [cut-off\ \ abruptly]. -statusUpdateAbandoned8.text=%s, this is it! Enemy forces have broken through everywhere, and the\ +statusUpdateAbandoned8.text={0}, this is it! Enemy forces have broken through everywhere, and the\ \ convoy is collapsing! Crew is down to the last weapons, trying to protect\ \ supplies, but it's hopeless! We're being torn apart, and - [explosions shake transmission] -\ \ without help soon, we're - [gunfire interrupts]. -statusUpdateAbandoned9.text=%s, we're out of time! Enemy has breached our position, and we're\ +statusUpdateAbandoned9.text={0}, we're out of time! Enemy has breached our position, and we're\ \ losing everything! Supplies are being seized, and there's no stopping it\ \ without support! Morale is shattered, and everyone is just fighting to\ \ survive! If you hear this, we're not going to last - [sudden gunfire and\ \ shouting] - they've breached the main cabin! We're - [silence]. -statusUpdateAbandoned10.text=%s, surrounded and taking heavy fire! Most of the convoy is lost,\ +statusUpdateAbandoned10.text={0}, surrounded and taking heavy fire! Most of the convoy is lost,\ \ and crew is trying to destroy supplies before capture! Down to hand-to-hand combat!\ \ Supplies are gone, and - [gunfire drowns out voice] - if anyone's listening,\ \ we need - [static] - this is it! -statusUpdateAbandoned11.text=%s, we're beyond desperate! We're completely cut off, and enemy forces are\ +statusUpdateAbandoned11.text={0}, we're beyond desperate! We're completely cut off, and enemy forces are\ \ seizing the last of our supplies! We're out of ammo, and crew is using\ \ anything they can find! Morale is gone, and we can't hold much longer! The\ \ convoy is - [loud explosion] - reinforcements aren't coming, are they?! We're\ \ finished here! They're breaching - [cut-off abruptly]. -statusUpdateAbandoned12.text=%s, breaking point reached! Supplies are gone, and enemy 'Meks are\ +statusUpdateAbandoned12.text={0}, breaking point reached! Supplies are gone, and enemy 'Meks are\ \ pushing through! We tried to make a stand, but it's impossible without\ \ support! The crew is scattered, and we're out of time! We're - [shouting and\ \ gunfire] - they're taking everything! If you hear this, send help or -\ \ [static, then silence]. -statusUpdateAbandoned13.text=%s, down to the last man! Enemy is everywhere, and supplies are\ +statusUpdateAbandoned13.text={0}, down to the last man! Enemy is everywhere, and supplies are\ \ lost! Wounded everywhere, and no more ammo! Crew is fighting to the end, but\ \ it's hopeless! We never had a chance without reinforcements! If you hear this,\ \ know that we - [gunfire and screams] - this is the end! We're not going to -\ \ [cut-off]. -statusUpdateAbandoned14.text=%s, convoy is in ruins! Supplies in enemy hands, and all vehicles are\ +statusUpdateAbandoned14.text={0}, convoy is in ruins! Supplies in enemy hands, and all vehicles are\ \ lost! Crew is fighting to survive, but without reinforcements, it's over!\ \ Trying to regroup, but the enemy is too strong! They've broken through -\ \ [explosions and gunfire] - We're - [silence]. -statusUpdateAbandoned15.text=%s, enemy breached every line! Down to the last few men, and\ +statusUpdateAbandoned15.text={0}, enemy breached every line! Down to the last few men, and\ \ supplies are being seized! No backup, no options left! Morale is broken, and\ \ crew is fighting with desperation! If anyone is out there, we need -\ \ [explosion] - they're closing in! We can't hold - [cut-off]. -statusUpdateAbandoned16.text=%s, fire from all directions! Supplies nearly captured, and crew is\ +statusUpdateAbandoned16.text={0}, fire from all directions! Supplies nearly captured, and crew is\ \ falling back! No reinforcements, no retreat possible! Situation critical, and\ \ we're - [gunfire drowns out voice] - there's no more time! We're -\ \ [message ends]. -statusUpdateAbandoned17.text=%s, chaos everywhere! Enemy breached final defenses, and crew is\ +statusUpdateAbandoned17.text={0}, chaos everywhere! Enemy breached final defenses, and crew is\ \ barely holding! Supplies lost, and we're out of ammo! Convoy is collapsing, and - [gunfire\ \ and shouting] - they're taking everything! This is - [ends abruptly]. -statusUpdateAbandoned18.text=%s, this is the end! The enemy has full control, and convoy is falling\ +statusUpdateAbandoned18.text={0}, this is the end! The enemy has full control, and convoy is falling\ \ apart! Supplies exposed, and crews are surrendering! Morale is\ \ gone, and we've got wounded everywhere! We're - [explosion] - if anyone can\ \ hear this, we need - [ends]. -statusUpdateAbandoned19.text=%s, surrounded, and convoy is collapsing! Supplies gone, and crew\ +statusUpdateAbandoned19.text={0}, surrounded, and convoy is collapsing! Supplies gone, and crew\ \ retreating under fire! No more options! Critical situation, and we've got -\ \ [gunfire] - this is our last stand! We need - [static, then silence]. @@ -2357,829 +2358,829 @@ statusUpdateAbandoned19.text=%s, surrounded, and convoy is collapsing! Supplies confirmDud.text=Understood, Return to Base # getCacheDescriptionDud -dudGeneric0.text=%s, we've scoured the valley below %s for hours now. The canyon walls are steep,\ - \ littered with fallen debris and old scorch marks - possibly from a %s skirmish long before our time.\ +dudGeneric0.text={0}, we've scoured the valley below {0} for hours now. The canyon walls are steep,\ + \ littered with fallen debris and old scorch marks - possibly from a {0} skirmish long before our time.\ \ Low visibility's a problem; dense fog keeps sensors from getting clean reads. We picked up faint\ \ energy spikes three times, but each time it flickered out, almost as if it's designed to lure us\ \ deeper. I've got a bad feeling about this place. The team's morale is dipping; some swear they\ \ saw movement in the shadows, but I've found no tracks or heat signatures. Continuing the sweep,\ - \ but so far, no sign of the %s depot. -dudGeneric1.text=%s, we're in the eastern ridgeline now, along the edge of %s. Rain is coming down hard,\ + \ but so far, no sign of the {0} depot. +dudGeneric1.text={0}, we're in the eastern ridgeline now, along the edge of {0}. Rain is coming down hard,\ \ turning the ground into mud and gumming up our treads. Signal interference is off the charts -\ - \ comms are breaking up, and sensors are giving ghost returns, as if something from the %s\ + \ comms are breaking up, and sensors are giving ghost returns, as if something from the {0}\ \ is jamming us. The depot's supposed coordinates have been checked twice, but all we've found are\ \ old bunkers, stripped bare. I'm not sure if we're missing something or if the intel's bad,\ \ but nothing here feels right. We'll push a little further before pulling back to base. -dudGeneric2.text=%s, we're at the foothills in %s, and this place is like a graveyard. Twisted metal and\ - \ wreckage from old %s 'Meks litter the ground, half-buried in snowdrifts. There's a chill in the\ +dudGeneric2.text={0}, we're at the foothills in {0}, and this place is like a graveyard. Twisted metal and\ + \ wreckage from old {0} 'Meks litter the ground, half-buried in snowdrifts. There's a chill in the\ \ air that's not just the cold - the men are restless. Sensors keep pinging with minor seismic\ \ tremors, but no sign of structures beneath the surface. I ordered a manual check, but all\ \ we're digging up are shattered armor plates and frozen corpses. We're running low on supplies,\ \ but I'll give it one more pass before retreating. The depot remains elusive. -dudGeneric3.text=%s, we've reached the marshlands near %s. The terrain's worse than expected;\ +dudGeneric3.text={0}, we've reached the marshlands near {0}. The terrain's worse than expected;\ \ it's a mire of sucking mud and stagnant pools, hard on the legs and the 'Meks' joints. Visibility\ \ is near-zero - constant mist and roiling fog make thermal scans unreliable. We picked up what\ \ might have been a burst transmission earlier, but it cut out before we could trace it back to a\ \ possible source. The team is growing wary; strange sounds are coming from deeper in the swamp,\ - \ but no visual or sensor confirmation. It could be wildlife... or something else. No sign of the %s\ + \ but no visual or sensor confirmation. It could be wildlife... or something else. No sign of the {0}\ \ cache yet. -dudGeneric4.text=%s, we've crossed the plateau and are closing in on the last set of coordinates in %s.\ +dudGeneric4.text={0}, we've crossed the plateau and are closing in on the last set of coordinates in {0}.\ \ The high winds up here are tearing at our comms, and static is becoming a constant companion.\ \ I've seen strange lights on the horizon - some kind of electrical discharge, maybe from old tech,\ \ but the sensors can't make heads or tails of it. We've found more abandoned gear, some bearing\ - \ faint %s insignia, but no sign of anything functional. Morale is low, and the crew's starting to\ + \ faint {0} insignia, but no sign of anything functional. Morale is low, and the crew's starting to\ \ think this depot might be nothing more than a rumor. Requesting permission to withdraw soon. -dudGeneric5.text=%s, we're moving through the deep woods in %s. Thick underbrush is slowing\ +dudGeneric5.text={0}, we're moving through the deep woods in {0}. Thick underbrush is slowing\ \ our advance, and sensors are picking up strange heat anomalies, but they vanish just as quickly\ \ as they appear. We found remnants of a resupply - old crates, half-buried in moss - but no\ \ signs of anything more significant. The atmosphere here is heavy, and some of the crew report\ - \ hearing distorted voices over the comms, but no clear signals from a %s frequency. Still no sight\ + \ hearing distorted voices over the comms, but no clear signals from a {0} frequency. Still no sight\ \ of the depot. -dudGeneric6.text=%s, we've reached the desert flats near %s. Sandstorms are fierce, creating\ - \ interference across all sensor bands. We've come across what looks like the ruins of a %s outpost,\ +dudGeneric6.text={0}, we've reached the desert flats near {0}. Sandstorms are fierce, creating\ + \ interference across all sensor bands. We've come across what looks like the ruins of a {0} outpost,\ \ walls partially buried in dunes, but no clear leads on the depot's location. The wind carries\ \ a strange whistling sound, almost like a distant scream, but no signatures detected. The team\ \ is growing weary; supplies are running thin, but we'll press on a bit longer. -dudGeneric7.text=%s, we've pushed into the rocky canyons near %s. The cliffs are steep, and\ +dudGeneric7.text={0}, we've pushed into the rocky canyons near {0}. The cliffs are steep, and\ \ strange markings along the rock face hint at a possible local presence. However, every cave we've\ - \ searched so far has been empty, save for a few damaged scout cars with %s insignia. Scans suggest\ + \ searched so far has been empty, save for a few damaged scout cars with {0} insignia. Scans suggest\ \ faint energy trails deeper in, but they're erratic, almost like decoys. It's as if the depot\ \ was never here - or has been moved elsewhere entirely. -dudGeneric8.text=%s, the swamp here in %s is a nightmare. Deep, murky waters make it nearly\ - \ impossible to maneuver, and sensors are plagued by thick bio-signatures. We found a rusted %s\ +dudGeneric8.text={0}, the swamp here in {0} is a nightmare. Deep, murky waters make it nearly\ + \ impossible to maneuver, and sensors are plagued by thick bio-signatures. We found a rusted {0}\ \ DropShip hull partially submerged, but it's long dead. No signs of recent activity, no depot.\ \ Strange lights dance across the surface of the water, but readings suggest they're natural\ \ phenomena. The crew's resolve is wavering. -dudGeneric9.text=%s, we're in the abandoned urban sector at %s. These ruins bear %s \ +dudGeneric9.text={0}, we're in the abandoned urban sector at {0}. These ruins bear {0} \ \ markings, likely from the last conflict here. Most buildings are gutted, with walls blackened by\ \ fires. We found a few leftover data terminals, but they're too damaged to provide intel on the\ \ depot. Sporadic sensor pings suggest faint power sources, but they're too weak to be the target.\ \ We'll continue the sweep. -dudGeneric10.text=%s, we've arrived at the ice fields near %s. Blizzards are brutal, and the\ - \ cold is affecting our equipment. We found a partially collapsed %s bunker under the ice, but it's\ +dudGeneric10.text={0}, we've arrived at the ice fields near {0}. Blizzards are brutal, and the\ + \ cold is affecting our equipment. We found a partially collapsed {0} bunker under the ice, but it's\ \ been abandoned for years. A few sensor ghosts suggested movement beneath the ice sheets, but we\ \ can't confirm if it's related to the depot. The search is feeling more futile with every passing\ \ hour. -dudGeneric11.text=%s, we've entered the gorge in %s. It's dark and narrow, with jagged rocks\ +dudGeneric11.text={0}, we've entered the gorge in {0}. It's dark and narrow, with jagged rocks\ \ making navigation difficult. The echoes here play tricks on the sensors - everything feels off.\ - \ We located a rusted %s APC, but it's been stripped clean. Still no sign of the depot, just a lot\ + \ We located a rusted {0} APC, but it's been stripped clean. Still no sign of the depot, just a lot\ \ of eerie silence. Morale is dipping, but we'll keep moving. -dudGeneric12.text=%s, we're in the dense fog zone around %s. Visibility is nearly zero, and\ +dudGeneric12.text={0}, we're in the dense fog zone around {0}. Visibility is nearly zero, and\ \ our thermal scans are useless. There are remnants of a communication array here, but it's been\ - \ inactive for ages. We attempted a few broadcasts on %s frequencies, hoping for a response, but\ + \ inactive for ages. We attempted a few broadcasts on {0} frequencies, hoping for a response, but\ \ nothing came back. The depot's location remains elusive. -dudGeneric13.text=%s, we've reached the river crossing at %s. The water is fast and deep, with\ - \ fallen %s equipment littering the banks. We tried scanning for possible underwater installations,\ +dudGeneric13.text={0}, we've reached the river crossing at {0}. The water is fast and deep, with\ + \ fallen {0} equipment littering the banks. We tried scanning for possible underwater installations,\ \ but the currents make it too risky for a deeper search. No depot detected. The crew is exhausted,\ \ and the constant roar of the river is fraying nerves. -dudGeneric14.text=%s, we're combing through the craters at %s. The ground is pockmarked from past\ - \ bombardments, making traversal treacherous. We found a cache of old %s munitions, but they're\ +dudGeneric14.text={0}, we're combing through the craters at {0}. The ground is pockmarked from past\ + \ bombardments, making traversal treacherous. We found a cache of old {0} munitions, but they're\ \ degraded beyond use. No power sources, no shelter, and certainly no depot. It's starting to feel\ \ like a wild goose chase, but we'll give it one last push before calling it off. -dudGeneric15.text=%s, we're at the edge of the wasteland in %s. The ground here is blackened,\ +dudGeneric15.text={0}, we're at the edge of the wasteland in {0}. The ground here is blackened,\ \ possibly from an earlier scorched-earth tactic. Radiation spikes occasionally flare up, throwing\ - \ off our sensors. We found scattered remains of %s recon vehicles, but the depot itself is nowhere\ + \ off our sensors. We found scattered remains of {0} recon vehicles, but the depot itself is nowhere\ \ to be found. The deeper we go, the more desolate it becomes. -dudGeneric16.text=%s, we've entered a cavern network at %s. It's a twisting maze down here, full\ - \ of stale air and lingering darkness. We found a few %s insignias etched into the rock, but no signs\ +dudGeneric16.text={0}, we've entered a cavern network at {0}. It's a twisting maze down here, full\ + \ of stale air and lingering darkness. We found a few {0} insignias etched into the rock, but no signs\ \ of recent activity. Sensors keep picking up false positives, likely echoes from the rocks. No depot,\ \ no progress. -dudGeneric17.text=%s, we're deep in the ravine at %s. The ground is unstable, and minor landslides\ - \ have slowed us down. We found an old %s MekBay, buried under debris, but it's been picked clean.\ +dudGeneric17.text={0}, we're deep in the ravine at {0}. The ground is unstable, and minor landslides\ + \ have slowed us down. We found an old {0} MekBay, buried under debris, but it's been picked clean.\ \ No signs of life, no depot, and only the occasional creak of shifting rocks to accompany us. We're\ \ preparing to exit the area. -dudGeneric18.text=%s, we're scanning the old mining site at %s. Rusted machinery and broken %s\ +dudGeneric18.text={0}, we're scanning the old mining site at {0}. Rusted machinery and broken {0}\ \ loaders dot the landscape, but the depot remains elusive. The tunnels are partially collapsed,\ \ making it too risky to venture deeper. We'll mark this location as a potential future salvage site,\ - \ but as for the %s depot, there's no trace of it here. -dudGeneric19.text=%s, we've reached the frozen ridge at %s. Snow drifts are high, and visibility\ - \ is minimal. We detected what appeared to be a %s signal burst, but it was gone before we could\ + \ but as for the {0} depot, there's no trace of it here. +dudGeneric19.text={0}, we've reached the frozen ridge at {0}. Snow drifts are high, and visibility\ + \ is minimal. We detected what appeared to be a {0} signal burst, but it was gone before we could\ \ triangulate its source. No depot, just more barren snowfields. The team is growing weary, and\ \ supplies are dwindling. Requesting permission to retreat. -dudGeneric20.text=%s, we're backtracking through %s. The ground is scarred by ancient shell\ +dudGeneric20.text={0}, we're backtracking through {0}. The ground is scarred by ancient shell\ \ craters, now filled with stagnant pools of water. It's strange - seeing the relics of battles\ - \ fought long ago, with no one left to remember what it was even for. The %s depot, if it ever existed,\ + \ fought long ago, with no one left to remember what it was even for. The {0} depot, if it ever existed,\ \ is lost to time, much like the warriors who once bled for it. I fear we're searching for ghosts. -dudGeneric21.text=%s, we've reached the abandoned fortress at %s. %s emblems still adorn the\ +dudGeneric21.text={0}, we've reached the abandoned fortress at {0}. {0} emblems still adorn the\ \ crumbling walls, faded and weathered by time. It feels more like a mausoleum than a depot now.\ \ The corridors echo with emptiness, reminding us of those who must have once called this home. No\ \ sign of life or supplies - only dust and the lingering sorrow of battles lost. -dudGeneric22.text=%s, we're in the overgrown ruins at %s. Nature has reclaimed the old outpost\ +dudGeneric22.text={0}, we're in the overgrown ruins at {0}. Nature has reclaimed the old outpost\ \ here, vines wrapping around shattered gun emplacements and shattered buildings. We found rusted\ - \ ammo crates with %s insignia, untouched for decades. It's like searching through the remains of\ + \ ammo crates with {0} insignia, untouched for decades. It's like searching through the remains of\ \ a forgotten dream. I wonder if anyone even remembers why we're still looking. -dudGeneric23.text=%s, we've combed through the wasteland at %s. The ground is cracked and barren,\ - \ scorched by a %s retreat that left nothing but ash. It's as if the land itself is trying to forget\ +dudGeneric23.text={0}, we've combed through the wasteland at {0}. The ground is cracked and barren,\ + \ scorched by a {0} retreat that left nothing but ash. It's as if the land itself is trying to forget\ \ what happened here. The depot is nowhere to be found, and the crew can't shake the feeling that\ \ our efforts mean little in the grand scheme of endless war. -dudGeneric24.text=%s, we've reached the mountain pass in %s. Snow covers the remnants of a %s\ +dudGeneric24.text={0}, we've reached the mountain pass in {0}. Snow covers the remnants of a {0}\ \ base, the white blanket making everything feel eerily peaceful, despite the chaos that must have\ \ raged here once. No depot, only shattered bunkers and the hollow remnants of a cause that seems\ \ distant now, even as we pursue it. -dudGeneric25.text=%s, we're in the canyon at %s. Faded %s banners still hang from some of the\ +dudGeneric25.text={0}, we're in the canyon at {0}. Faded {0} banners still hang from some of the\ \ rocky outcroppings, flapping gently in the wind. We found a few empty crates and a rusted comm\ \ relay, both long since abandoned. It's clear that whatever purpose this depot once served has\ \ been abandoned, much like the ideals that built it. -dudGeneric26.text=%s, we've reached the outskirts of an old %s encampment at %s. Everything here\ +dudGeneric26.text={0}, we've reached the outskirts of an old {0} encampment at {0}. Everything here\ \ feels forgotten, like a chapter left unfinished in a book no one reads anymore. There's no depot,\ \ only the scattered remains of tents and supply crates. Sometimes I wonder if our search is driven\ \ more by stubbornness than reason. -dudGeneric27.text=%s, we're deep in the forest at %s. The trees are thick, their branches heavy\ - \ with a quiet sadness, as if they've seen too much. We found a rusted %s Scout VTOL, fallen among\ +dudGeneric27.text={0}, we're deep in the forest at {0}. The trees are thick, their branches heavy\ + \ with a quiet sadness, as if they've seen too much. We found a rusted {0} Scout VTOL, fallen among\ \ the roots. No sign of the depot, just the faint memory of battles that seemed important once,\ \ but now feel pointless. -dudGeneric28.text=%s, we're navigating the swamps of %s. This place is a graveyard of old %s\ +dudGeneric28.text={0}, we're navigating the swamps of {0}. This place is a graveyard of old {0}\ \ vehicles, slowly being consumed by the mud. It's hard to say if this depot was ever real, or\ \ just another mirage in a war full of them. The men seem tired, not just from the search, but from\ \ the futility of finding meaning in any of this. -dudGeneric29.text=%s, we've arrived at the abandoned %s airfield in %s. The runways are cracked,\ +dudGeneric29.text={0}, we've arrived at the abandoned {0} airfield in {0}. The runways are cracked,\ \ and the hangars are empty. It's a scene of a past defeat, frozen in time. We found nothing but\ \ broken Meks and hollow shells. No depot, just the echoes of strategies that failed and lives that\ \ were lost chasing glory. -dudGeneric30.text=%s, we've entered the trench network at %s. These %s trenches are deep, filled\ +dudGeneric30.text={0}, we've entered the trench network at {0}. These {0} trenches are deep, filled\ \ with rusted weapons and faded uniforms, but no depot. The mud here is heavy, as if it's trying to\ \ pull us down into the past with every step. It's hard not to feel like we're just retracing old\ \ failures, destined to repeat them. -dudGeneric31.text=%s, we're among the ruins of a city in %s. The buildings are skeletal,\ - \ shadows of a once-thriving population now reduced to rubble. No %s depot, only the sadness of a city\ +dudGeneric31.text={0}, we're among the ruins of a city in {0}. The buildings are skeletal,\ + \ shadows of a once-thriving population now reduced to rubble. No {0} depot, only the sadness of a city\ \ that dreamed of prosperity but fell under the weight of war. We're finding nothing of value here,\ \ only a lingering sense of loss. -dudGeneric32.text=%s, we've reached the fields of %s. It's a plain covered in overgrown grass\ - \ and rusting %s weapon emplacements. There's a haunting calm here, as if even nature has grown\ +dudGeneric32.text={0}, we've reached the fields of {0}. It's a plain covered in overgrown grass\ + \ and rusting {0} weapon emplacements. There's a haunting calm here, as if even nature has grown\ \ tired of conflict. No sign of the depot, just remnants of another battle that history has all\ \ but forgotten. -dudGeneric33.text=%s, we've arrived at the edge of a %s battlefield at %s. Scorched Meks and\ +dudGeneric33.text={0}, we've arrived at the edge of a {0} battlefield at {0}. Scorched Meks and\ \ shattered tanks are all that remain, their wreckage a testament to how quickly war can erase\ \ everything. No depot in sight, just a grim reminder of how futile it all seems when the metal\ \ stops moving. -dudGeneric34.text=%s, we've reached the lakeshore at %s. The water is still, reflecting the\ - \ gray sky above. We found a few %s ration crates near the bank, but they're long expired, much\ +dudGeneric34.text={0}, we've reached the lakeshore at {0}. The water is still, reflecting the\ + \ gray sky above. We found a few {0} ration crates near the bank, but they're long expired, much\ \ like the hopes that once fueled this depot's creation. We're not finding anything of use, just\ \ reminders of a war that seems determined to outlast us all. -dudGeneric35.text=%s, we're at the border of the %s encampment in %s. The camp is deserted, its\ +dudGeneric35.text={0}, we're at the border of the {0} encampment in {0}. The camp is deserted, its\ \ tents torn and its fires cold. I can't help but feel a deep sadness as we sift through the remnants\ \ - once part of a grand strategy, now just meaningless fragments. No depot, only shadows of what was. -dudGeneric36.text=%s, we've reached the foothills of Sector 29-R. The landscape here is scarred by %s\ +dudGeneric36.text={0}, we've reached the foothills of Sector 29-R. The landscape here is scarred by {0}\ \ artillery strikes, and the air is thick with melancholy. We found an old command post, but it's\ \ stripped bare, like the hollow remains of hope itself. No sign of the depot, just the sense that\ \ we're chasing something that's long since slipped away. -dudGeneric37.text=%s, we're moving through the plains at %s. We came across a %s outpost, little\ +dudGeneric37.text={0}, we're moving through the plains at {0}. We came across a {0} outpost, little\ \ more than ruins now. The wind carries a sad, empty sound, as if the land itself is mourning what\ \ was lost here. No depot, just more evidence futility of this endless conflict. -dudGeneric38.text=%s, we're in the desert at %s. The ground is hard, and the air is heavy with\ - \ the weight of old battles. We found rusted %s Meks buried in the sand, their pilots long gone.\ +dudGeneric38.text={0}, we're in the desert at {0}. The ground is hard, and the air is heavy with\ + \ the weight of old battles. We found rusted {0} Meks buried in the sand, their pilots long gone.\ \ The depot is nowhere to be found - only silence and the feeling that every step forward is a step\ \ deeper into the past. -dudGeneric39.text=%s, we've reached the ruins of a %s HQ at %s. The walls are covered in bullet\ +dudGeneric39.text={0}, we've reached the ruins of a {0} HQ at {0}. The walls are covered in bullet\ \ holes, and the floor is littered with spent casings. It's as if time itself has moved on, leaving\ \ this place behind. No depot here, just the quiet futility of trying to hold on to something that\ \ war has already claimed. -dudGeneric40.text=%s, we're back at %s, scanning for the fifth time. There's nothing but sand\ - \ and rocks - again. No sign of a %s depot, just the same dusty horizon we've been staring at for\ +dudGeneric40.text={0}, we're back at {0}, scanning for the fifth time. There's nothing but sand\ + \ and rocks - again. No sign of a {0} depot, just the same dusty horizon we've been staring at for\ \ hours. The crew's getting restless, and honestly, so am I. It's hard to keep pushing when it feels\ \ like we're chasing our own shadows. -dudGeneric41.text=%s, we've entered the marshlands around %s. The mud is thick, the humidity\ - \ is unbearable, and we haven't found so much as a single %s crate. Sensors keep glitching, but\ +dudGeneric41.text={0}, we've entered the marshlands around {0}. The mud is thick, the humidity\ + \ is unbearable, and we haven't found so much as a single {0} crate. Sensors keep glitching, but\ \ we're almost certain it's just the local wildlife messing with the readings. The men are\ \ complaining about everything - from the mosquitoes to the mission itself. I can't blame them. -dudGeneric42.text=%s, we're trudging through the forest at %s. Every step feels the same -\ - \ trees, mud, more trees. We found an old %s sensor tower, but it's useless now, just like the\ +dudGeneric42.text={0}, we're trudging through the forest at {0}. Every step feels the same -\ + \ trees, mud, more trees. We found an old {0} sensor tower, but it's useless now, just like the\ \ past eight hours of searching. The crew is barely responding to orders; it's like their minds\ \ are already back at base. -dudGeneric43.text=%s, we're in %s, checking yet another empty canyon. No %s signals, no depot,\ +dudGeneric43.text={0}, we're in {0}, checking yet another empty canyon. No {0} signals, no depot,\ \ no point. I don't know what the intel team was thinking sending us here. This place is just\ \ another dead end, and we're starting to wonder if the depot even exists. -dudGeneric44.text=%s, we're skirting the edge of an old battlefield at %s. Just more %s wreckage,\ +dudGeneric44.text={0}, we're skirting the edge of an old battlefield at {0}. Just more {0} wreckage,\ \ more silence, and more of the same stale air. No depot in sight, not even a hint of recent activity.\ \ Morale is as low as I've ever seen it; half the team isn't even pretending to look anymore. -dudGeneric45.text=%s, we've reached the plains at %s. It's flat, empty, and utterly\ - \ unremarkable. We found some %s equipment scattered around, but it's just rusted junk -\ +dudGeneric45.text={0}, we've reached the plains at {0}. It's flat, empty, and utterly\ + \ unremarkable. We found some {0} equipment scattered around, but it's just rusted junk -\ \ nothing worth hauling back. I think we've reached the point where everyone's just going through\ \ the motions. -dudGeneric46.text=%s, we're at the river in %s. It's too wide to cross, and the water looks as\ - \ brown as our chances of finding anything %s-related here. We've been staring at our maps for\ +dudGeneric46.text={0}, we're at the river in {0}. It's too wide to cross, and the water looks as\ + \ brown as our chances of finding anything {0}-related here. We've been staring at our maps for\ \ hours, but every potential lead just seems to be another wild goose chase. No depot, just a\ \ bunch of wet boots and wasted time. -dudGeneric47.text=%s, we're stuck in %s, waiting for the fog to clear. We've been sitting here\ - \ so long that the crew has started playing cards just to pass the time. No signals, no %s depot,\ +dudGeneric47.text={0}, we're stuck in {0}, waiting for the fog to clear. We've been sitting here\ + \ so long that the crew has started playing cards just to pass the time. No signals, no {0} depot,\ \ just the same old haze. I think some of the team are starting to wonder why we're even bothering. -dudGeneric48.text=%s, we're combing through the hills at %s. Nothing but rocks and more rocks.\ - \ We found a half-buried %s vehicle, but it's been picked clean - just like every other so-called\ +dudGeneric48.text={0}, we're combing through the hills at {0}. Nothing but rocks and more rocks.\ + \ We found a half-buried {0} vehicle, but it's been picked clean - just like every other so-called\ \ lead. The crew's too frustrated to even argue about it anymore; they just shrug and move on. -dudGeneric49.text=%s, we've entered the swamp at %s. It's all mud and stale water, and our\ - \ sensors are useless here. The %s depot was supposed to be nearby, but I'm starting to think\ +dudGeneric49.text={0}, we've entered the swamp at {0}. It's all mud and stale water, and our\ + \ sensors are useless here. The {0} depot was supposed to be nearby, but I'm starting to think\ \ this whole mission is just someone's idea of a cruel joke. Morale is at rock bottom. -dudGeneric50.text=%s, we're trudging through %s again. This is our third sweep, and we've\ - \ found exactly nothing %s-related - unless you count a few rusted ammo casings. The men are\ +dudGeneric50.text={0}, we're trudging through {0} again. This is our third sweep, and we've\ + \ found exactly nothing {0}-related - unless you count a few rusted ammo casings. The men are\ \ grumbling constantly, and frankly, I'm just as tired of this as they are. It feels like a waste\ \ of time. -dudGeneric51.text=%s, we're back at %s. There's no sign of a %s depot, not even a false-positive\ +dudGeneric51.text={0}, we're back at {0}. There's no sign of a {0} depot, not even a false-positive\ \ on the sensors. I think the crew are starting to doubt the existence of this depot entirely.\ \ They're moving slower with each passing hour, as if the weight of futility is sinking in. -dudGeneric52.text=%s, we're in the ruins of an old %s outpost at %s. It's just more of the\ +dudGeneric52.text={0}, we're in the ruins of an old {0} outpost at {0}. It's just more of the\ \ same - empty shells, rusted parts, and nothing of value. The crew's too bored to even feign\ \ interest at this point. I'm not sure how much longer we can keep up this charade. -dudGeneric53.text=%s, we're in the desert of %s. It's hot, dry, and utterly devoid of anything\ - \ useful. We've searched every likely spot for a %s depot, but it's just sand, sand, and more sand.\ +dudGeneric53.text={0}, we're in the desert of {0}. It's hot, dry, and utterly devoid of anything\ + \ useful. We've searched every likely spot for a {0} depot, but it's just sand, sand, and more sand.\ \ The team's growing tired of my orders, and I can't say I blame them. -dudGeneric54.text=%s, we're at the cliffs in %s. The terrain is rough, but what's rougher is\ - \ the lack of results. We've been looking for this %s depot for hours, and all we've got to show for\ +dudGeneric54.text={0}, we're at the cliffs in {0}. The terrain is rough, but what's rougher is\ + \ the lack of results. We've been looking for this {0} depot for hours, and all we've got to show for\ \ it is a few bruised egos. Morale is dragging, and so is this mission. -dudGeneric55.text=%s, we've reached %s. There's nothing here - just more empty plains and more\ - \ disappointment. The crew isn't even trying to hide their frustration anymore. This %s depot, if\ +dudGeneric55.text={0}, we've reached {0}. There's nothing here - just more empty plains and more\ + \ disappointment. The crew isn't even trying to hide their frustration anymore. This {0} depot, if\ \ it ever existed, must be laughing at us from the shadows. -dudGeneric56.text=%s, we're checking the caves at %s. These %s tunnels are as empty as our\ +dudGeneric56.text={0}, we're checking the caves at {0}. These {0} tunnels are as empty as our\ \ hopes of finding anything useful. The crew's getting sick of crawling through the dirt for no\ \ reason. At this point, I think even I'd settle for a solid lead on a canteen. -dudGeneric57.text=%s, we've reached the abandoned %s camp at %s. It's the same story as always\ +dudGeneric57.text={0}, we've reached the abandoned {0} camp at {0}. It's the same story as always\ \ - empty crates, broken weapons, and nothing else. The men have stopped asking questions; they\ \ just keep moving with a blank look in their eyes. I think this mission is breaking us slowly. -dudGeneric58.text=%s, we're back at the ridge in %s. The view's nice, but that's about it. No\ - \ depot, no %s signals, just another false lead in a series of false leads. I think I've seen this\ +dudGeneric58.text={0}, we're back at the ridge in {0}. The view's nice, but that's about it. No\ + \ depot, no {0} signals, just another false lead in a series of false leads. I think I've seen this\ \ ridge so many times, I could draw it from memory. The crew isn't even pretending to care anymore. -dudGeneric59.text=%s, we're in the marshes of %s. We've slogged through knee-deep mud for hours\ - \ with nothing to show for it. The %s depot is still just a rumor, and the crew's patience ran out\ +dudGeneric59.text={0}, we're in the marshes of {0}. We've slogged through knee-deep mud for hours\ + \ with nothing to show for it. The {0} depot is still just a rumor, and the crew's patience ran out\ \ three grids ago. At this point, it feels like the mud is dragging us down more than the mission. -dudGeneric60.text=%s, we're back at %s, staring at the same rocks we saw earlier. I'm starting\ - \ to think these %s folks buried their depot in a parallel dimension. I'll check under one more\ +dudGeneric60.text={0}, we're back at {0}, staring at the same rocks we saw earlier. I'm starting\ + \ to think these {0} folks buried their depot in a parallel dimension. I'll check under one more\ \ suspicious-looking pebble before we move on. Spirits are surprisingly high, though; I think the\ \ crew's betting on who'll find the most useless piece of scrap today. -dudGeneric61.text=%s, we've hit the swamp at %s. The mud here is so thick, I think it's trying\ - \ to steal our boots. No %s depot yet, but we did find a very angry-looking toad. The crew named\ +dudGeneric61.text={0}, we've hit the swamp at {0}. The mud here is so thick, I think it's trying\ + \ to steal our boots. No {0} depot yet, but we did find a very angry-looking toad. The crew named\ \ it "General Sludge," and it's currently our most promising lead. Morale's good - though I think\ \ they like the challenge of the swamp more than the mission. -dudGeneric62.text=%s, we're in the forest at %s. It's so dense that the trees must have grown to\ - \ hide something, right? So far, no luck on the %s depot, but we did find a tree with some suspicious\ +dudGeneric62.text={0}, we're in the forest at {0}. It's so dense that the trees must have grown to\ + \ hide something, right? So far, no luck on the {0} depot, but we did find a tree with some suspicious\ \ bark. The crew's decided to start a "Most Bizarre Discovery" contest, and the current leader is\ \ a rock that vaguely resembles a 'Mek pilot flipping us off. -dudGeneric63.text=%s, we're in %s, still searching for that mythical %s depot. All we've found\ +dudGeneric63.text={0}, we're in {0}, still searching for that mythical {0} depot. All we've found\ \ is an ancient can of rations. The label reads "Best Before 2550," but Private Randall says\ \ he's willing to give it a try if we get desperate. Spirits are weirdly high; it seems the absurdity\ \ of it all is keeping everyone entertained. -dudGeneric64.text=%s, we're at %s, and it's official - this is the most boring stretch of land\ - \ in the Inner Sphere. No depot, no %s activity, just miles of nothing. The crew's taken to naming\ +dudGeneric64.text={0}, we're at {0}, and it's official - this is the most boring stretch of land\ + \ in the Inner Sphere. No depot, no {0} activity, just miles of nothing. The crew's taken to naming\ \ the rocks we pass. We've got "Old Grumpy," "The Jagged Lady," and "Sir Pebbleton III" so far. At\ \ least they're keeping busy. -dudGeneric65.text=%s, we've reached %s. We haven't found a %s depot, but we did find a very\ +dudGeneric65.text={0}, we've reached {0}. We haven't found a {0} depot, but we did find a very\ \ determined squirrel trying to break into one of our ration packs. I think it might be our most\ \ formidable opponent yet. The crew's morale is good - turns out, nothing bonds a team like trying\ \ to outsmart wildlife. -dudGeneric66.text=%s, we're at the river in %s. Still no %s depot, but we did manage to have a\ +dudGeneric66.text={0}, we're at the river in {0}. Still no {0} depot, but we did manage to have a\ \ nice little "who can skim the most rocks" competition. The winner got to wear an improvised crown\ - \ made from twigs. If only %s tech was as easy to find as things to make fun of. -dudGeneric67.text=%s, we're stuck in %s again. The fog's so thick that I think I just tried to\ + \ made from twigs. If only {0} tech was as easy to find as things to make fun of. +dudGeneric67.text={0}, we're stuck in {0} again. The fog's so thick that I think I just tried to\ \ have a conversation with a tree. We're laughing about it, though - it's not every day you mistake\ - \ a pine for your comms officer. Still no %s depot, unless it's disguised as a very stealthy squirrel. -dudGeneric68.text=%s, we're at the hills in %s. The only %s-related thing we've found is an\ + \ a pine for your comms officer. Still no {0} depot, unless it's disguised as a very stealthy squirrel. +dudGeneric68.text={0}, we're at the hills in {0}. The only {0}-related thing we've found is an\ \ old helmet with a dent so big it could serve as a soup bowl. The crew's making jokes about how\ \ the depot's probably cloaked with "super-stealth tech," a.k.a. "just plain missing." -dudGeneric69.text=%s, we've entered the swamp at %s. It's like nature's version of a bad\ - \ joke: every step is squishy, and every scan is a false alarm. No %s depot, but we did discover\ +dudGeneric69.text={0}, we've entered the swamp at {0}. It's like nature's version of a bad\ + \ joke: every step is squishy, and every scan is a false alarm. No {0} depot, but we did discover\ \ that we have an impressive talent for swamp-related limericks. Spirits are oddly high,\ \ probably because it's impossible to be sad while rhyming 'slog' with 'bog.' -dudGeneric70.text=%s, we're back at %s. I think I saw the same bush three times today - it's\ - \ either following us, or we're lost. No %s depot yet, but we think we've found "the meaning\ +dudGeneric70.text={0}, we're back at {0}. I think I saw the same bush three times today - it's\ + \ either following us, or we're lost. No {0} depot yet, but we think we've found "the meaning\ \ of life" in the shape of a weirdly shaped tree root. Morale's still solid - probably because\ \ we've all lost our minds a little bit. -dudGeneric71.text=%s, we're in the ruins at %s. Found some old %s propaganda posters that are\ +dudGeneric71.text={0}, we're in the ruins at {0}. Found some old {0} propaganda posters that are\ \ now being repurposed as a dartboard. No depot, but the crew's having a good laugh at the slogans\ \ - turns out "Victory Through Perseverance" is ironically hilarious when you're lost in the middle\ \ of nowhere. -dudGeneric72.text=%s, we're at %s in the desert. It's so hot here that we've taken to telling\ - \ "dry" jokes. You know the type: "Why don't %s depots like the desert? Because they're afraid of\ +dudGeneric72.text={0}, we're at {0} in the desert. It's so hot here that we've taken to telling\ + \ "dry" jokes. You know the type: "Why don't {0} depots like the desert? Because they're afraid of\ \ being found." Spirits are surprisingly up, mostly thanks to our terrible sense of humor. -dudGeneric73.text=%s, we're at the cliffs in %s. It's rocky, dangerous, and completely devoid\ - \ of anything remotely %s-related. On the bright side, the crew discovered that yelling "Echo!"\ +dudGeneric73.text={0}, we're at the cliffs in {0}. It's rocky, dangerous, and completely devoid\ + \ of anything remotely {0}-related. On the bright side, the crew discovered that yelling "Echo!"\ \ off the cliffs is an excellent way to pass the time. No depot, but plenty of echoes to keep us\ \ entertained. -dudGeneric74.text=%s, we've reached %s. This empty plain has become our least favorite spot,\ +dudGeneric74.text={0}, we've reached {0}. This empty plain has become our least favorite spot,\ \ but we did manage to build a pretty impressive sandcastle in the downtime. The crew's named it\ - \ "Fort %s," in honor of the depot we'll never find. Morale's surprisingly high - who knew building\ + \ "Fort {0}," in honor of the depot we'll never find. Morale's surprisingly high - who knew building\ \ sandcastles was so therapeutic? -dudGeneric75.text=%s, we're in the tunnels at %s. Found some %s graffiti that reads "Kilroy Was\ +dudGeneric75.text={0}, we're in the tunnels at {0}. Found some {0} graffiti that reads "Kilroy Was\ \ Here." No depot, but it's nice to know we're not the first to get stuck in this mess. The crew's\ \ taken to singing campfire songs, even without a fire. Gallows humor is the only thing keeping us\ \ sane. -dudGeneric76.text=%s, we're at the %s camp ruins in %s. There's nothing here but busted tents\ +dudGeneric76.text={0}, we're at the {0} camp ruins in {0}. There's nothing here but busted tents\ \ and empty cans, but the crew's decided to reenact a dramatic play about "The Depot That Never\ \ Was." If you hear loud applause over the comms, don't be alarmed - it's just the sound of our\ \ own despair. -dudGeneric77.text=%s, we're at the ridge in %s. I'm starting to think this whole %s depot\ +dudGeneric77.text={0}, we're at the ridge in {0}. I'm starting to think this whole {0} depot\ \ hunt is a cosmic joke, and we're the punchline. The crew's betting on who can find the most\ \ absurd object out here. So far, the front-runner is a rock shaped like a duck. Spirits are still\ \ high, probably due to the absurdity of it all. -dudGeneric78.text=%s, we're back in the marshes of %s. The mud here is trying to steal our sanity,\ - \ one boot at a time. Still no %s depot, but we did manage to build a raft from some fallen branches.\ +dudGeneric78.text={0}, we're back in the marshes of {0}. The mud here is trying to steal our sanity,\ + \ one boot at a time. Still no {0} depot, but we did manage to build a raft from some fallen branches.\ \ We're calling it the "WarShip Lost Cause." Morale remains shockingly good, considering the circumstances. -dudGeneric79.text=%s, we're in the ice fields at %s. We found nothing %s-related, but the crew\ +dudGeneric79.text={0}, we're in the ice fields at {0}. We found nothing {0}-related, but the crew\ \ did discover that sliding down icy slopes is a surprisingly fun way to kill time. No depot, but\ \ plenty of laughter. I suppose if we're going to be lost, we might as well enjoy the ride. -dudGeneric80.text=%s, we're deep in %s, surrounded by fog so thick it feels like it's alive.\ +dudGeneric80.text={0}, we're deep in {0}, surrounded by fog so thick it feels like it's alive.\ \ The sensors keep flickering - false signals that seem to move closer, then vanish. No sign of the\ - \ %s depot, but I can't shake the feeling we're being watched. The crew is jumpy; even the slightest\ + \ {0} depot, but I can't shake the feeling we're being watched. The crew is jumpy; even the slightest\ \ sound is making us reach for weapons. -dudGeneric81.text=%s, we're in the marshlands of %s. Strange lights hover just beyond our\ - \ vision - flickering, almost playful, but gone the moment we try to focus. No %s depot, only a\ +dudGeneric81.text={0}, we're in the marshlands of {0}. Strange lights hover just beyond our\ + \ vision - flickering, almost playful, but gone the moment we try to focus. No {0} depot, only a\ \ growing sense of unease. It's not just the swamp; it's like the air itself is charged with\ \ something hostile, something old. -dudGeneric82.text=%s, we've reached the forest in %s. The trees here are unnaturally silent\ - \ - no wind, no birds, not even the rustle of leaves. We found a rusted %s 'Mek half-buried in\ +dudGeneric82.text={0}, we've reached the forest in {0}. The trees here are unnaturally silent\ + \ - no wind, no birds, not even the rustle of leaves. We found a rusted {0} 'Mek half-buried in\ \ the ground, its cockpit open and empty. The crew swears they heard whispers coming from inside,\ \ but the scanners showed nothing. -dudGeneric83.text=%s, we're at %s, exploring what seems to be a forgotten %s bunker. The walls\ +dudGeneric83.text={0}, we're at {0}, exploring what seems to be a forgotten {0} bunker. The walls\ \ are covered in strange, indecipherable marks, as if scratched by claws. We detected faint heat\ \ signatures earlier, but they disappeared without a trace. The men are scared - some say they've\ \ seen shadows that move when no one else does. -dudGeneric84.text=%s, we're back at %s. We found a clearing littered with %s remains - helmets,\ +dudGeneric84.text={0}, we're back at {0}. We found a clearing littered with {0} remains - helmets,\ \ scattered bones, and rusted weapons. The strange thing is, there's no record of a battle here. No\ \ depot, but an unsettling feeling that we're standing in a place where something awful happened,\ \ something not recorded in the history books. -dudGeneric85.text=%s, we're in the plains of %s. There's a strange hum in the air - low,\ +dudGeneric85.text={0}, we're in the plains of {0}. There's a strange hum in the air - low,\ \ constant, and impossible to trace. It's unsettling, like the sound is coming from the ground\ - \ itself. No sign of a %s depot, but the crew's starting to act strangely. Some are whispering to\ + \ itself. No sign of a {0} depot, but the crew's starting to act strangely. Some are whispering to\ \ themselves, claiming they hear voices beneath the hum. -dudGeneric86.text=%s, we're at the river in %s. The water's surface is black and still, reflecting\ +dudGeneric86.text={0}, we're at the river in {0}. The water's surface is black and still, reflecting\ \ only darkness. We saw strange ripples earlier, like something large moving beneath, but no signs\ - \ of life or %s tech. The crew is spooked - one of the men claims he saw eyes staring up from the\ + \ of life or {0} tech. The crew is spooked - one of the men claims he saw eyes staring up from the\ \ depths, but they were gone in an instant. -dudGeneric87.text=%s, we're stuck in %s, waiting for the fog to lift. But this isn't normal fog\ - \ - it's cold, dense, and seems to pulse as if breathing. No %s depot, just a chilling sense that\ +dudGeneric87.text={0}, we're stuck in {0}, waiting for the fog to lift. But this isn't normal fog\ + \ - it's cold, dense, and seems to pulse as if breathing. No {0} depot, just a chilling sense that\ \ something here doesn't want us to leave. Even the comms are full of static, but every now and\ \ then, there's a faint voice in the interference. -dudGeneric88.text=%s, we're in the hills of %s. We came across a %s campsite that looks recent,\ +dudGeneric88.text={0}, we're in the hills of {0}. We came across a {0} campsite that looks recent,\ \ but scans indicate it's been abandoned for decades. There's dried blood on the ground, but no bodies.\ \ The men are nervous, saying it feels like we've stepped into a place trapped between times. -dudGeneric89.text=%s, we're in the swamp at %s. There's an eerie stillness here, like the world\ +dudGeneric89.text={0}, we're in the swamp at {0}. There's an eerie stillness here, like the world\ \ is holding its breath. Strange shapes drift in and out of the mist, but disappear when we approach.\ - \ No %s depot, just an overwhelming sense that we're trespassing somewhere we shouldn't be. -dudGeneric90.text=%s, we're pushing through %s, and the fog here is unnaturally dense,\ + \ No {0} depot, just an overwhelming sense that we're trespassing somewhere we shouldn't be. +dudGeneric90.text={0}, we're pushing through {0}, and the fog here is unnaturally dense,\ \ almost suffocating. Sensors are erratic, picking up strange energy spikes - like a signature, then\ - \ gone. No sign of the %s depot, but there's been talk among the crew about a shape in the mist.\ + \ gone. No sign of the {0} depot, but there's been talk among the crew about a shape in the mist.\ \ They say it looked like a Marauder, pitch-black, watching from the edge of visibility before\ \ vanishing into the haze. The men are spooked. Even the air feels colder, like a warning. We're\ \ staying alert, but it feels like we're not alone in this cursed place. -dudGeneric91.text=%s, we're in the ruins of %s. The buildings are hollowed out, but there's\ +dudGeneric91.text={0}, we're in the ruins of {0}. The buildings are hollowed out, but there's\ \ a persistent sound of distant footsteps echoing from the empty corridors. We've searched thoroughly\ - \ - no %s depot, but it feels like we're being stalked by something unseen, something that knows\ + \ - no {0} depot, but it feels like we're being stalked by something unseen, something that knows\ \ this place better than we do. -dudGeneric92.text=%s, we're in the desert at %s. It's as silent as a tomb out here, save for\ - \ the occasional gust that sounds like a faint whisper. No %s depot, but we keep hearing strange\ +dudGeneric92.text={0}, we're in the desert at {0}. It's as silent as a tomb out here, save for\ + \ the occasional gust that sounds like a faint whisper. No {0} depot, but we keep hearing strange\ \ noises - like metal grinding, far off but distinct. The crew's on edge; even the bravest are\ \ keeping their weapons drawn. -dudGeneric93.text=%s, we're at the cliffs in %s. We found a %s observation post built into\ +dudGeneric93.text={0}, we're at the cliffs in {0}. We found a {0} observation post built into\ \ the rock, but it's abandoned and smells of decay. There's a weird marking on the wall - a symbol\ \ we don't recognize, as if it was burned into the stone itself. No depot, just a sense that we're\ \ being watched from the shadows. -dudGeneric94.text=%s, we've reached %s. The plain stretches endlessly, but we found a single %s\ +dudGeneric94.text={0}, we've reached {0}. The plain stretches endlessly, but we found a single {0}\ \ helmet lying in the grass. It's polished, unnaturally clean, almost as if it was left here recently.\ \ The crew is spooked; some say it's bait, others think it's a warning. The air feels thick with\ \ something unseen. -dudGeneric95.text=%s, we're in the tunnels at %s. They're dark and damp, but it's the sounds\ +dudGeneric95.text={0}, we're in the tunnels at {0}. They're dark and damp, but it's the sounds\ \ that are the worst - scraping, scratching, and the occasional echo of something that doesn't\ - \ belong. We've found no signs of a %s depot, only unsettling noises and shadows that seem to move\ + \ belong. We've found no signs of a {0} depot, only unsettling noises and shadows that seem to move\ \ on their own. -dudGeneric96.text=%s, we're at the ruins of the %s camp in %s. It's silent, too silent, like\ +dudGeneric96.text={0}, we're at the ruins of the {0} camp in {0}. It's silent, too silent, like\ \ everything here was drained of life. The crew claims to see figures at the edges of their vision\ \ - always gone when they turn. No depot, just the overwhelming feeling that something dark lingers\ \ here, unseen but present. -dudGeneric97.text=%s, we're back at the ridge in %s. We found a small %s structure, barely\ +dudGeneric97.text={0}, we're back at the ridge in {0}. We found a small {0} structure, barely\ \ standing, with a door that opened on its own. No one can explain it, and no one wants to go inside.\ \ The crew's getting paranoid; some are talking about "the spirits of the lost," and I can't even\ \ laugh it off. -dudGeneric98.text=%s, we're in the marshes at %s. It's dark now, and there are strange, distant\ +dudGeneric98.text={0}, we're in the marshes at {0}. It's dark now, and there are strange, distant\ \ lights floating over the water. We've tried to approach, but they vanish before we can get close.\ - \ No %s depot, just a feeling of dread that's settled over the team. The silence is broken only by\ + \ No {0} depot, just a feeling of dread that's settled over the team. The silence is broken only by\ \ occasional distant cries, almost human. -dudGeneric99.text=%s, we're in the ice fields at %s. It's bitterly cold, but the strangest part\ +dudGeneric99.text={0}, we're in the ice fields at {0}. It's bitterly cold, but the strangest part\ \ is the whispers - faint, almost unintelligible, but unmistakable. They seem to come from nowhere\ - \ and everywhere at once. No %s depot, just a haunting reminder that some places weren't meant to\ + \ and everywhere at once. No {0} depot, just a haunting reminder that some places weren't meant to\ \ be found. -dudStarLeague0.text=%s, we've reached what remains of an old SLDF supply depot at %s. The facility\ +dudStarLeague0.text={0}, we've reached what remains of an old SLDF supply depot at {0}. The facility\ \ is mostly rubble now, scorched during the Amaris Coup. Some of the walls still bear the blast marks\ \ from Kerensky's siege. Sensors picked up faint power readings - remnants of long-dead systems\ \ trying to hum back to life. No clear signs of functional tech or the rumored Star League cache,\ \ just the bitter scent of decay. The air feels heavy, as if the ghosts of those last, desperate\ \ defenders linger. The crew's nerves are fraying; some say they hear echoes of distant gunfire in\ \ the wind. We're proceeding with caution. -dudStarLeague1.text=%s, we're at the ruins of a Terran Hegemony installation in %s, buried deep\ +dudStarLeague1.text={0}, we're at the ruins of a Terran Hegemony installation in {0}, buried deep\ \ in the forest. This base was one of the last to fall during the Amaris Coup. The bunker entrance\ \ is blocked by debris, likely the result of self-destruct protocols triggered by the SLDF. The air\ \ inside is stale and cold, and a few of the men claim to have seen faint blue lights flickering in\ \ the corridors - emergency systems that shouldn't have power after two centuries. No Star League\ \ artifacts uncovered yet, but something about this place feels alive... and hostile. -dudStarLeague2.text=%s, we've reached a massive, half-collapsed SLDF logistics hub in %s. The\ +dudStarLeague2.text={0}, we've reached a massive, half-collapsed SLDF logistics hub in {0}. The\ \ place is eerie - massive hangars filled with rows of rusted unusable 'Mek parts, abandoned when\ \ Kerensky ordered the Exodus. Old bloodstains still mark the walls, silent witnesses to the final\ \ chaos of the Hegemony's collapse. The crew's on edge; sensors are giving off strange interference,\ \ as if something in the wreckage is trying to communicate. No confirmed depot yet, just a lingering\ \ feeling of betrayal, as if the past itself resents our intrusion. -dudStarLeague3.text=%s, we're deep inside a crumbling SLDF command center at %s. The central\ +dudStarLeague3.text={0}, we're deep inside a crumbling SLDF command center at {0}. The central\ \ hall is filled with the dusty remains of command consoles, likely used during Kerensky's final\ \ campaigns against Amaris. The walls are covered with screens still showing deployments from the\ \ last days of the Star League. Scanners picked up encrypted data bursts - ancient transmissions,\ \ perhaps, looping endlessly since the coup. Some of the crew refuse to enter the deeper halls,\ \ saying the air is too cold, too full of dread. No depot detected, but this place feels like a tomb. -dudStarLeague4.text=%s, we've reached what seems to be an abandoned SLDF weapons cache in %s.\ +dudStarLeague4.text={0}, we've reached what seems to be an abandoned SLDF weapons cache in {0}.\ \ The structure is intact, but something feels off. There's a persistent, low hum that's hard to\ \ pinpoint, as if the building itself is resonating with some hidden energy. The crew's uneasy -\ \ one of the men swears he saw a figure in an old SLDF uniform, though scans show no life signs.\ \ This site fell during one of Kerensky's final assaults; it's said the defenders knew they were\ \ doomed, but fought to the last anyway. No Star League tech uncovered yet, only the eerie sense\ \ that something from those days never left. -dudStarLeague5.text=%s, we're inside the remains of an old SLDF research outpost at %s. The\ +dudStarLeague5.text={0}, we're inside the remains of an old SLDF research outpost at {0}. The\ \ labs are silent, littered with debris and the scent of decay. The facility fell during the Amaris\ \ Coup, and the walls seem to resonate with the last desperate moments of the defenders. No depot\ \ found, only the unsettling stillness that makes even the bravest among us hesitate. It's as if\ \ the ghosts of Star League's failures linger here, watching. -dudStarLeague6.text=%s, we've reached what was once a Terran Hegemony garrison in %s. The\ +dudStarLeague6.text={0}, we've reached what was once a Terran Hegemony garrison in {0}. The\ \ structures are in ruins, collapsed during Kerensky's final retribution. The crew senses something\ \ here - a presence, perhaps, or just the oppressive weight of defeat. No sign of a Star League depot,\ \ only a gnawing feeling of dread. Some of the men are muttering that LosTech sites like these are\ \ cursed, best left undisturbed. -dudStarLeague7.text=%s, we're standing outside a sealed bunker at %s. The door is massive,\ +dudStarLeague7.text={0}, we're standing outside a sealed bunker at {0}. The door is massive,\ \ reinforced, and covered in scorched marks - likely from Amaris's forces trying to breach it.\ \ It's silent here, deathly so, and the air feels unnaturally cold. We haven't found anything of\ \ value yet, just a sense that we're trespassing on a battlefield lost long ago. The men say this\ \ place is cursed, that the tech within is better left untouched. -dudStarLeague8.text=%s, we're in the wreckage of an old SLDF airfield at %s. The winds howl\ +dudStarLeague8.text={0}, we're in the wreckage of an old SLDF airfield at {0}. The winds howl\ \ through empty hangars, carrying a faint, distant sound that's hard to identify. The site has been\ \ abandoned since the Exodus, and it feels like the memories of that era still haunt the place. No\ \ sign of a depot, just whispers in the wind - if one believes in such things. The crew's spirits\ \ are sinking; they say the place feels cursed, as if the very ground rejects us. -dudStarLeague9.text=%s, we're exploring the charred remains of an SLDF staging ground at %s. The\ +dudStarLeague9.text={0}, we're exploring the charred remains of an SLDF staging ground at {0}. The\ \ buildings are gutted, the walls blackened by fire and riddled with bullet holes. We haven't\ \ uncovered any signs of a depot, only the remnants of a brutal last stand. Some of the crew believe\ \ that sites like this one are haunted, cursed by Amaris' hate and by those who fought for a cause\ \ that ultimately failed. -dudStarLeague10.text=%s, we've reached a ruined SLDF bunker in %s. The entrance is half-collapsed,\ +dudStarLeague10.text={0}, we've reached a ruined SLDF bunker in {0}. The entrance is half-collapsed,\ \ its steel door twisted beyond recognition. The interior is dark, and our lights barely penetrate\ \ the dense shadows within. We haven't found anything yet, but the feeling of being watched is\ \ stronger than ever. The men are convinced that the depot, if it exists, is cursed - like all lost\ \ Star League secrets. -dudStarLeague11.text=%s, we're in a decrepit SLDF command center at %s. The silence here is thick,\ +dudStarLeague11.text={0}, we're in a decrepit SLDF command center at {0}. The silence here is thick,\ \ broken only by the occasional drip of water from the ceiling. No depot in sight, just rows of\ \ dead consoles that once controlled mighty armies. The crew's uneasy - some say this place feels\ \ alive, like it's waiting for something. The darkness here feels tangible, and the whispers of a\ \ cursed past are hard to ignore. -dudStarLeague12.text=%s, we've reached a derelict SLDF outpost in %s. The base is eerily quiet,\ +dudStarLeague12.text={0}, we've reached a derelict SLDF outpost in {0}. The base is eerily quiet,\ \ save for the creaking of old metal in the wind. We haven't seen any signs of a depot, only the\ \ oppressive remnants of a battle long lost. The crew is growing paranoid, claiming to hear footsteps\ \ behind them when no one's there. They say the tech here is cursed, echoing the despair of those\ \ who never made it out. -dudStarLeague13.text=%s, we're at what appears to be a forgotten SLDF logistics hub at %s. The\ +dudStarLeague13.text={0}, we're at what appears to be a forgotten SLDF logistics hub at {0}. The\ \ facility is massive, but most of it is inaccessible, sealed off behind collapsed walls and twisted\ \ beams. No depot yet, only dark hallways that seem to stretch endlessly. The men are on edge, as\ \ if the very shadows are moving around them. Some whisper that sites like this are haunted by the\ \ cursed remnants of the past, a warning to those who seek what's been lost. -dudStarLeague14.text=%s, we're inside the crumbling remains of an SLDF stronghold at %s. The\ +dudStarLeague14.text={0}, we're inside the crumbling remains of an SLDF stronghold at {0}. The\ \ air here is cold, with a strange metallic tang that clings to the back of the throat. We've\ \ searched for hours, but no depot has been found. The crew is jumpy, saying that places like this\ \ are cursed - trapped in time, haunted by the failure of a once-great Star League. The walls\ \ themselves seem to hum with an eerie energy. -dudStarLeague15.text=%s, we're exploring a massive SLDF command center in %s. The facility is\ +dudStarLeague15.text={0}, we're exploring a massive SLDF command center in {0}. The facility is\ \ vast and filled with empty command consoles that haven't seen use since Kerensky's Exodus. No\ \ depot has been found, only a suffocating sense of dread. The crew swears they hear faint whispers\ \ coming from the dark corners of the room, like distant voices that shouldn't exist. It feels like\ \ the cursed legacy of the Star League is all that remains here. -dudStarLeague16.text=%s, we've reached the ruins of an SLDF fortress at %s. The exterior is\ +dudStarLeague16.text={0}, we've reached the ruins of an SLDF fortress at {0}. The exterior is\ \ pitted with craters from heavy bombardment, likely from Amaris's forces. We've found no depot,\ \ but the place feels heavy with a sense of old anger and betrayal. The men say the darkness here\ \ is unnatural, that cursed tech lies somewhere deeper within, waiting to ensnare anyone foolish\ \ enough to seek it. -dudStarLeague17.text=%s, we're inside an abandoned SLDF listening post at %s. It's been dead\ +dudStarLeague17.text={0}, we're inside an abandoned SLDF listening post at {0}. It's been dead\ \ silent, save for occasional pings on our sensors that vanish without explanation. No depot has\ \ been located yet, but some of the crew claim they've seen shadows moving in the reflection of\ \ the old radar screens. They say this place is cursed - haunted by the Star League's failures. -dudStarLeague18.text=%s, we're in the depths of a ruined SLDF research lab at %s. The air is\ +dudStarLeague18.text={0}, we're in the depths of a ruined SLDF research lab at {0}. The air is\ \ stale and tinged with a strange, acrid smell that makes breathing difficult. We've found nothing\ \ resembling a depot, just endless halls filled with eerie quiet. Some of the crew have begun\ \ muttering about the cursed legacy of LosTech, saying it's tainted by the ghosts of those who\ \ tried to wield it. -dudStarLeague19.text=%s, we've reached the edge of an SLDF encampment in %s. The camp is empty,\ +dudStarLeague19.text={0}, we've reached the edge of an SLDF encampment in {0}. The camp is empty,\ \ save for the rusted remains of tents and barricades. No depot in sight, only a pervasive sense of\ \ doom that seems to cling to everything here. The men are convinced the site is cursed, as if the\ \ ghosts of the Star League's final defenders are still here, trying to keep us from uncovering their\ \ secrets. -dudStarLeague20.text=%s, we're inside the ruins of an SLDF testing facility at %s. The labs are\ +dudStarLeague20.text={0}, we're inside the ruins of an SLDF testing facility at {0}. The labs are\ \ silent, filled with broken equipment and shattered glass. We haven't found any signs of a depot,\ \ only a chilling emptiness. The crew is growing uneasy, saying that cursed LosTech haunts this\ \ place, keeping it lost for a reason. It feels as if the shadows themselves want to swallow us whole. -dudStarLeague21.text=%s, we're at what remains of an SLDF command post in %s. The place is a shell\ +dudStarLeague21.text={0}, we're at what remains of an SLDF command post in {0}. The place is a shell\ \ of its former glory - crumbling walls covered in faded insignias. No depot in sight, just a sense\ \ of abandonment. It's hard not to feel the weight of what's been lost here; it's as if the dreams\ \ of the Star League withered alongside the men who died defending it. We keep moving, but the\ \ futility of it all is beginning to sink in. -dudStarLeague22.text=%s, we're at the ruins of a once-thriving SLDF logistics hub in %s. Empty\ +dudStarLeague22.text={0}, we're at the ruins of a once-thriving SLDF logistics hub in {0}. Empty\ \ storage bays stretch for miles, now home only to dust and silence. No depot, only the echo of a\ \ once-unified Inner Sphere that fell apart under its own ambitions. The crew moves slowly, as if\ \ the sadness of the place has seeped into their bones. We press on, but it feels like chasing a\ \ lost cause. -dudStarLeague23.text=%s, we're trudging through an old SLDF garrison at %s. The barracks are\ +dudStarLeague23.text={0}, we're trudging through an old SLDF garrison at {0}. The barracks are\ \ empty, the bunks still neatly made, as if waiting for soldiers who will never return. No depot\ \ found, only a profound stillness that feels like grief itself. The men seem quieter here, as\ \ if even speaking aloud would disturb the lingering sorrow of an era that died here long before us. -dudStarLeague24.text=%s, we've reached a ruined SLDF hospital in %s. The halls are lined with\ +dudStarLeague24.text={0}, we've reached a ruined SLDF hospital in {0}. The halls are lined with\ \ empty beds and broken medical equipment, remnants of a time when the Star League tried to heal\ \ rather than destroy. No depot, just the feeling that those who were abandoned here never truly\ \ left. The sense of futility is overwhelming - war promises glory, but it seems to leave only ghosts. -dudStarLeague25.text=%s, we're at an SLDF communications center in %s. The consoles are dead,\ +dudStarLeague25.text={0}, we're at an SLDF communications center in {0}. The consoles are dead,\ \ covered in layers of dust. We haven't found the depot, just rows of silent screens that once\ \ coordinated fleets and armies across the stars. Now, they're just relics of ambition gone cold,\ \ a reminder that even the greatest empires can crumble into irrelevance. -dudStarLeague26.text=%s, we're inside a derelict SLDF armory at %s. The halls are filled with\ +dudStarLeague26.text={0}, we're inside a derelict SLDF armory at {0}. The halls are filled with\ \ empty racks where weapons once stood ready to defend the ideals of the Star League. No depot in\ \ sight, only a feeling of profound loss. The crew seems weighed down by the futility of finding\ \ anything of value here - it's like searching through the ashes of hope itself. -dudStarLeague27.text=%s, we're in a deserted SLDF command center at %s. The battle maps still\ +dudStarLeague27.text={0}, we're in a deserted SLDF command center at {0}. The battle maps still\ \ cover the walls, detailing campaigns that are now just faded memories. No depot has been found,\ \ only the haunting realization that the grand strategies of the Star League ultimately meant\ \ nothing. The crew's morale is sinking; it's hard to stay hopeful when every step seems to echo\ \ with failure. -dudStarLeague28.text=%s, we're among the ruins of an SLDF outpost in %s. The outpost was likely\ +dudStarLeague28.text={0}, we're among the ruins of an SLDF outpost in {0}. The outpost was likely\ \ abandoned during Kerensky's final campaigns, its defenders leaving behind only silent halls and\ \ unanswered questions. No depot found, only a sense of regret that lingers in the air. It's as if\ \ the ghosts of those who once stood here are still waiting for orders that will never come. -dudStarLeague29.text=%s, we're at the remnants of a once-mighty SLDF fortress in %s. The walls\ +dudStarLeague29.text={0}, we're at the remnants of a once-mighty SLDF fortress in {0}. The walls\ \ are cracked, and the turrets are silent. No depot, just the feeling of a grand dream that was\ \ shattered by ambition and betrayal. The crew's growing weary; they know that searching here is\ \ like sifting through the ruins of lost ideals. The hope of finding something meaningful is fading\ \ fast. -dudStarLeague30.text=%s, we're in the ruins of an SLDF headquarters at %s. The command table\ +dudStarLeague30.text={0}, we're in the ruins of an SLDF headquarters at {0}. The command table\ \ stands empty, as if waiting for leaders long dead. No depot found, just a sense of lost authority.\ \ The crew is silent, perhaps out of respect for the failed ambitions that echo through these walls.\ \ It's hard not to wonder if we're just repeating history - fighting for something that doesn't\ \ even matter anymore. -dudStarLeague31.text=%s, we're inside an abandoned SLDF research facility at %s. The labs are\ +dudStarLeague31.text={0}, we're inside an abandoned SLDF research facility at {0}. The labs are\ \ empty, their experiments long forgotten. No depot has been uncovered, only the faded symbols of\ \ a Star League that once promised unity. The crew's mood is somber; this place feels like a\ \ graveyard of forgotten dreams, a testament to the futility of trying to hold onto power through\ \ war. -dudStarLeague32.text=%s, we're at an SLDF barracks in %s. The beds are unmade, as if the\ +dudStarLeague32.text={0}, we're at an SLDF barracks in {0}. The beds are unmade, as if the\ \ soldiers left in a hurry, never to return. No depot has been found, only the oppressive silence\ \ of lives cut short by battles that couldn't be won. The crew is tired; each room we enter feels\ \ like stepping into another sad story that has no ending. -dudStarLeague33.text=%s, we're exploring a hollow SLDF outpost at %s. The corridors are dark,\ +dudStarLeague33.text={0}, we're exploring a hollow SLDF outpost at {0}. The corridors are dark,\ \ and the only sound is the wind howling through broken windows. No depot located, only a lingering\ \ sense of despair from those who once thought they were fighting for a better future. The crew's\ \ morale is fading - it's hard to believe in glory when all you find are ruins. -dudStarLeague34.text=%s, we're at the crumbling remains of an SLDF citadel in %s. The\ +dudStarLeague34.text={0}, we're at the crumbling remains of an SLDF citadel in {0}. The\ \ structure was once a symbol of Star League power, now reduced to rubble by time and war. No\ \ depot here, just the melancholy of lost grandeur. The crew is quiet, as if the realization has\ \ set in that we're searching for something that died long ago. -dudStarLeague35.text=%s, we're inside an abandoned SLDF hangar at %s. The bay doors are rusted\ +dudStarLeague35.text={0}, we're inside an abandoned SLDF hangar at {0}. The bay doors are rusted\ \ shut, and the air smells stale. No depot has been found, only the empty spaces where war machines\ \ once stood ready. The crew seems haunted by the thought that everything we're searching for might\ \ just be echoes of an era that was doomed from the start. -dudStarLeague36.text=%s, we've reached the remains of an SLDF command hub in %s. The control\ +dudStarLeague36.text={0}, we've reached the remains of an SLDF command hub in {0}. The control\ \ room is filled with shattered monitors and scattered paperwork - records of battles that ended\ \ in failure. No depot in sight, only the realization that even the most organized plans can end\ \ in chaos. The men's spirits are low; this place feels like a monument to the futility of trying\ \ to control the uncontrollable. -dudStarLeague37.text=%s, we're inside an old SLDF depot at %s, but it's long abandoned. The\ +dudStarLeague37.text={0}, we're inside an old SLDF depot at {0}, but it's long abandoned. The\ \ crates are empty, and the walls bear the marks of hasty retreat. No depot found, only the\ \ remnants of what might have been a grand storehouse of power. The crew's mood is bleak; the\ \ feeling here is one of missed opportunities, of efforts that ultimately amounted to nothing. -dudStarLeague38.text=%s, we're in the hollow halls of an SLDF citadel at %s. The banners of\ +dudStarLeague38.text={0}, we're in the hollow halls of an SLDF citadel at {0}. The banners of\ \ the Star League still hang, tattered and faded. No depot has been found, only the sad remnants\ \ of a once-united Inner Sphere. The crew's morale is dwindling; the more we search, the more it\ \ feels like we're walking through a graveyard of ideals that were doomed from the start. -dudStarLeague39.text=%s, we're at the edge of an old SLDF supply base in %s. The base is silent,\ +dudStarLeague39.text={0}, we're at the edge of an old SLDF supply base in {0}. The base is silent,\ \ its storage facilities empty and forgotten. No depot in sight, only the empty shells of what was\ \ meant to sustain a glorious future. The crew is despondent; it's clear that what we're chasing\ \ might never have existed in the first place - just another sad chapter in a history of broken\ \ promises. -dudStarLeague40.text=%s, we're inside the remains of an old SLDF command bunker at %s. It's pitch\ +dudStarLeague40.text={0}, we're inside the remains of an old SLDF command bunker at {0}. It's pitch\ \ dark, and our lights don't seem to penetrate the shadows well. There's a strange, rhythmic sound\ \ - almost like a heartbeat - coming from somewhere deep below. No sign of the depot, but the air\ \ feels heavy, oppressive. Some of the crew say they can hear faint whispers, but there's no one\ \ else here. -dudStarLeague41.text=%s, we've reached %s, where the ruins of an SLDF staging post lie in eerie\ +dudStarLeague41.text={0}, we've reached {0}, where the ruins of an SLDF staging post lie in eerie\ \ silence. The walls are covered in strange markings - perhaps scrawled by soldiers during the final\ \ days of the Amaris Coup. The symbols make no sense, but they seem... angry. No depot found, just\ \ a pervasive sense of dread, like the shadows are watching us. -dudStarLeague42.text=%s, we're at an abandoned SLDF fort in %s. The halls echo with distant\ +dudStarLeague42.text={0}, we're at an abandoned SLDF fort in {0}. The halls echo with distant\ \ clanging, even though we've confirmed there's no one else here. Some of the crew are getting\ \ unnerved, saying the echoes sound like old battle cries. We haven't found the depot, just a cold,\ \ unsettling feeling that something is trying to drive us away. -dudStarLeague43.text=%s, we've entered a collapsed SLDF research base at %s. The air is stale,\ +dudStarLeague43.text={0}, we've entered a collapsed SLDF research base at {0}. The air is stale,\ \ and the walls seem to sweat moisture, despite the dry surroundings outside. We've found no depot,\ \ but several crew members reported seeing movement at the edge of their vision. It feels like\ \ we're being watched by unseen eyes in the darkness. -dudStarLeague44.text=%s, we're in the ruins of an SLDF logistics center in %s. The facility is\ +dudStarLeague44.text={0}, we're in the ruins of an SLDF logistics center in {0}. The facility is\ \ massive, but dead quiet - too quiet. The only sound is a faint, repetitive ticking, like a clock\ \ that stopped ages ago but refuses to die completely. No depot in sight, only a growing sense of\ \ fear among the crew. Some say it's the spirits of those who never made it out. -dudStarLeague45.text=%s, we're inside an SLDF communications hub at %s. The consoles are dark,\ +dudStarLeague45.text={0}, we're inside an SLDF communications hub at {0}. The consoles are dark,\ \ but the static crackles sporadically, almost like whispers trying to form words. No depot found,\ \ but the crew's nerves are fraying. The longer we stay, the colder it gets, as if the very walls\ \ are trying to expel us. -dudStarLeague46.text=%s, we've reached %s, at the remnants of an SLDF barracks. The bunks are\ +dudStarLeague46.text={0}, we've reached {0}, at the remnants of an SLDF barracks. The bunks are\ \ still made, as if expecting soldiers to return, but there's no sign of life. Some of the crew\ \ claim to hear footsteps in the halls, heavy and deliberate, but there's no one there. No depot,\ \ only an overwhelming sense of being unwelcome. -dudStarLeague47.text=%s, we're at an SLDF forward base in %s. The air is thick with a strange\ +dudStarLeague47.text={0}, we're at an SLDF forward base in {0}. The air is thick with a strange\ \ metallic scent, and the walls are stained with a dark, rusty substance. We haven't found the\ \ depot, just a disturbing feeling that something terrible happened here. The crew's jumpy, and a\ \ few refuse to go deeper into the base. -dudStarLeague48.text=%s, we're exploring an SLDF airfield at %s. The hangars are empty, but\ +dudStarLeague48.text={0}, we're exploring an SLDF airfield at {0}. The hangars are empty, but\ \ we've been hearing faint, distant cries - like the sound of pilots calling for evac that never\ \ came. The crew's morale is sinking fast; some of the men believe the airfield is haunted by those\ \ who died without hope. No depot found, just unsettling echoes that refuse to die. -dudStarLeague49.text=%s, we're in the ruins of an SLDF intelligence center at %s. It's dark\ +dudStarLeague49.text={0}, we're in the ruins of an SLDF intelligence center at {0}. It's dark\ \ and cold, colder than it should be. The air seems to carry distant whispers, and the crew reports\ \ feeling like they're being watched. We haven't found the depot, only an oppressive atmosphere\ \ that makes every step feel like a mistake. -dudStarLeague50.text=%s, we're at %s, in what was once an SLDF hospital. The walls are smeared\ +dudStarLeague50.text={0}, we're at {0}, in what was once an SLDF hospital. The walls are smeared\ \ with faded, bloody handprints, as if patients tried to claw their way out during the chaos of\ \ the Amaris Coup. No depot here, only the horrifying sense that the souls of the desperate still\ \ linger, trapped in a place that offered no salvation. -dudStarLeague51.text=%s, we're inside an old SLDF bunker in %s. The main hallway stretches\ +dudStarLeague51.text={0}, we're inside an old SLDF bunker in {0}. The main hallway stretches\ \ into darkness, with old warning signs flickering faintly in red. No depot found, but some of the\ \ crew say they feel a strange pull deeper into the bunker, as if something is calling to them.\ \ It's unsettling - like a trap set by the shadows of the past. -dudStarLeague52.text=%s, we're in the remains of an SLDF command outpost at %s. The base is empty,\ +dudStarLeague52.text={0}, we're in the remains of an SLDF command outpost at {0}. The base is empty,\ \ save for a faint, eerie hum that seems to vibrate through the floor. We haven't found any trace\ \ of the depot, but some of the men are complaining of headaches and hearing faint cries for help\ \ that echo from nowhere. -dudStarLeague53.text=%s, we're inside a collapsed SLDF depot at %s. The walls are covered in\ +dudStarLeague53.text={0}, we're inside a collapsed SLDF depot at {0}. The walls are covered in\ \ strange, unintelligible symbols, scratched into the metal with something sharp. The air feels\ \ thick, like we're breathing the despair of those who never left. No depot found, just a growing\ \ sense that we're digging up something that was meant to stay buried. -dudStarLeague54.text=%s, we're at an SLDF staging ground in %s. The campfires are long cold, but\ +dudStarLeague54.text={0}, we're at an SLDF staging ground in {0}. The campfires are long cold, but\ \ it feels as if someone was here just moments ago. No depot found, only strange shadows that seem\ \ to shift and move on their own. The crew's morale is shot; they say the place feels cursed, like\ \ it's trying to drive us mad. -dudStarLeague55.text=%s, we're exploring an SLDF forward command post in %s. The walls are\ +dudStarLeague55.text={0}, we're exploring an SLDF forward command post in {0}. The walls are\ \ adorned with faded battle maps, but the air is filled with a strange, sulfuric smell. No depot\ \ in sight, but the feeling of dread is palpable. Some of the crew have started praying, saying\ \ they can feel the despair of the fallen soldiers here. -dudStarLeague56.text=%s, we're in the ruins of an SLDF research lab at %s. The facility is eerily\ +dudStarLeague56.text={0}, we're in the ruins of an SLDF research lab at {0}. The facility is eerily\ \ intact, almost as if it's been waiting for us. No depot has been located, but the crew reports\ \ feeling watched, and there's a sense of something ancient and hostile in the air. It's hard to\ \ shake the feeling that we're intruding where we shouldn't. -dudStarLeague57.text=%s, we're at an SLDF listening post in %s. The base is silent, but the\ +dudStarLeague57.text={0}, we're at an SLDF listening post in {0}. The base is silent, but the\ \ sensors occasionally pick up garbled transmissions - like echoes from the past that refuse to\ \ die. We haven't found the depot, only a sense that this place is haunted by voices that were\ \ never heard. The men are rattled; they say the whispers are warnings. -dudStarLeague58.text=%s, we're at the edge of an SLDF stronghold in %s. The walls are covered in\ +dudStarLeague58.text={0}, we're at the edge of an SLDF stronghold in {0}. The walls are covered in\ \ old blast marks, and the floors are littered with empty shell casings. No depot has been found,\ \ but some of the men claim to hear voices in the darkness, urging us to leave. The darkness here\ \ feels tangible, as if it's closing in on us. -dudStarLeague59.text=%s, we're in the depths of an SLDF citadel at %s. The corridors are filled\ +dudStarLeague59.text={0}, we're in the depths of an SLDF citadel at {0}. The corridors are filled\ \ with an unnatural mist that shouldn't be here. No depot found, but the temperature keeps dropping,\ \ and some of the crew swear they hear footsteps behind them. It feels like we're being hunted by\ \ something old, something that wants to keep its secrets hidden. -dudStarLeague60.text=%s, we're inside an SLDF listening post at %s. The facility is dark, but\ +dudStarLeague60.text={0}, we're inside an SLDF listening post at {0}. The facility is dark, but\ \ our sensors keep picking up faint signals - brief, encrypted bursts that stop when we try to\ \ trace them. It's as if someone's watching and adjusting to our movements. No depot located, but\ \ the crew is on edge. We're not alone out here. -dudStarLeague61.text=%s, we're at an abandoned SLDF staging area in %s. The ground is covered\ +dudStarLeague61.text={0}, we're at an abandoned SLDF staging area in {0}. The ground is covered\ \ in scattered debris, but there are fresh tracks in the dust - too fresh for a place supposedly\ \ untouched for centuries. No sign of the depot, but it's clear someone has been here recently.\ \ The crew's getting paranoid; it feels like we're being followed. -dudStarLeague62.text=%s, we're exploring an SLDF command center in %s. The terminals are dead,\ +dudStarLeague62.text={0}, we're exploring an SLDF command center in {0}. The terminals are dead,\ \ but our systems keep detecting incoming pings - like someone's trying to hack our comms. We\ \ haven't found the depot, but its clear someone is trying to disrupt our search. The crew is\ \ uneasy; it feels like we're being watched through the very walls. -dudStarLeague63.text=%s, we're inside the ruins of an SLDF logistics hub at %s. The air is still,\ +dudStarLeague63.text={0}, we're inside the ruins of an SLDF logistics hub at {0}. The air is still,\ \ but our sensors detected movement in the upper levels - small, fleeting heat signatures that vanish\ \ as soon as we focus on them. No depot in sight, just the unsettling feeling that someone - or\ \ something - is watching us from the shadows. -dudStarLeague64.text=%s, we're in an old SLDF supply depot at %s. The base appears deserted, but\ +dudStarLeague64.text={0}, we're in an old SLDF supply depot at {0}. The base appears deserted, but\ \ there's a strange static in our comms that only started when we entered. It's almost like someone's\ \ trying to jam our signals, just enough to cause confusion. No depot found, but the crew is\ \ whispering among themselves, convinced we're under surveillance. -dudStarLeague65.text=%s, we're at an SLDF research lab in %s. The facility is dark, but our\ +dudStarLeague65.text={0}, we're at an SLDF research lab in {0}. The facility is dark, but our\ \ scanners have picked up faint traces of motion - too quick to lock onto. We haven't found the\ \ depot, but the feeling of being watched is impossible to ignore. It's as if someone is observing\ \ our every move, waiting for us to make a mistake. -dudStarLeague66.text=%s, we're at the edge of an SLDF airfield in %s. The place is dead quiet,\ +dudStarLeague66.text={0}, we're at the edge of an SLDF airfield in {0}. The place is dead quiet,\ \ but our sensors keep detecting small, flickering signatures at the perimeter. It feels like we're\ \ being circled, but whoever it is never comes close enough for a clear scan. No depot here, only\ \ a deepening sense of unease. -dudStarLeague67.text=%s, we're inside an abandoned SLDF barracks at %s. The crew reports seeing\ +dudStarLeague67.text={0}, we're inside an abandoned SLDF barracks at {0}. The crew reports seeing\ \ glints of light at a distance, almost like the reflection from binoculars. No depot in sight, but\ \ it's clear someone is keeping tabs on us. The air feels charged, and even the shadows seem to\ \ shift when we're not looking. -dudStarLeague68.text=%s, we're in an SLDF bunker at %s. Our sensors picked up a weak transmission\ +dudStarLeague68.text={0}, we're in an SLDF bunker at {0}. Our sensors picked up a weak transmission\ \ on an encrypted frequency - one that stopped the moment we tried to respond. It's as if someone's\ \ testing us, probing our presence. No depot found, but the feeling of being watched grows stronger\ \ with each passing moment. -dudStarLeague69.text=%s, we're inside an old SLDF communications hub at %s. The consoles are\ +dudStarLeague69.text={0}, we're inside an old SLDF communications hub at {0}. The consoles are\ \ offline, but our systems detected a sudden spike in electronic interference - like someone nearby\ \ is actively scanning us. No sign of the depot, only the sense that we're not alone. The crew's\ \ getting anxious; it's like we're being studied. -dudStarLeague70.text=%s, we're at an SLDF forward base in %s. We've seen strange lights flickering\ +dudStarLeague70.text={0}, we're at an SLDF forward base in {0}. We've seen strange lights flickering\ \ in the distance, but they vanish whenever we try to get a closer look. It's not just the darkness;\ \ it feels like someone is intentionally leading us away. No depot here, just a growing sense that\ \ someone is manipulating us. -dudStarLeague71.text=%s, we're in the remains of an SLDF intelligence post at %s. Our equipment\ +dudStarLeague71.text={0}, we're in the remains of an SLDF intelligence post at {0}. Our equipment\ \ keeps malfunctioning - static on the comms, sudden power fluctuations. No depot found, but it's\ \ clear someone is trying to interfere with our search. The crew's starting to get jittery, convinced\ \ we're being hunted by an unseen observer. -dudStarLeague72.text=%s, we're inside an SLDF fort at %s. The place is silent, but our scanners\ +dudStarLeague72.text={0}, we're inside an SLDF fort at {0}. The place is silent, but our scanners\ \ detected what might be surveillance scans - brief pings that vanish when we try to triangulate\ \ them. No depot located, but the feeling of being monitored is becoming unbearable. The crew is\ \ whispering that we've walked into a trap. -dudStarLeague73.text=%s, we're at an old SLDF depot in %s. The facility is mostly rubble, No\ +dudStarLeague73.text={0}, we're at an old SLDF depot in {0}. The facility is mostly rubble, No\ \ depot in sight, only the unsettling suspicion that someone is documenting our every move. The\ \ crew's growing paranoid, convinced we're part of someone else's plan. -dudStarLeague74.text=%s, we're in an SLDF command post at %s. The sensors are acting up - picking\ +dudStarLeague74.text={0}, we're in an SLDF command post at {0}. The sensors are acting up - picking\ \ up multiple small heat signatures, but they fade away as soon as we get closer. No depot found,\ \ just the unsettling feeling that someone is tracking us, adjusting their position to stay just\ \ out of sight. -dudStarLeague75.text=%s, we're at an SLDF stronghold in %s. The corridors are filled with\ +dudStarLeague75.text={0}, we're at an SLDF stronghold in {0}. The corridors are filled with\ \ shadows that seem to stretch and twist as we pass. We've seen flashes of movement, but nothing\ \ shows up on our sensors. No depot here, only a sense of unseen eyes following us, observing our\ \ every step. -dudStarLeague76.text=%s, we're exploring the ruins of an SLDF depot at %s. The crew reports\ +dudStarLeague76.text={0}, we're exploring the ruins of an SLDF depot at {0}. The crew reports\ \ seeing faint lights in the distance, but they're too erratic to be natural. No depot found, but\ \ it feels like someone's setting up a perimeter around us, watching, waiting. The crew's getting\ \ restless, convinced that whoever it is wants us to stop searching - for reasons unknown. -dudStarLeague77.text=%s, we're inside an SLDF logistics hub at %s. We've seen strange flashes,\ +dudStarLeague77.text={0}, we're inside an SLDF logistics hub at {0}. We've seen strange flashes,\ \ like camera flashes, in the darkness. No depot located, only a creeping suspicion that we're being\ \ recorded - watched like rats in a maze. The crew's morale is slipping; they feel exposed, vulnerable. -dudStarLeague78.text=%s, we're in an abandoned SLDF base at %s. The facility is deserted, but\ +dudStarLeague78.text={0}, we're in an abandoned SLDF base at {0}. The facility is deserted, but\ \ we've picked up faint infrared signals from the surrounding hills - like distant observers trying\ \ to stay hidden. No depot found, just a deepening sense that we're part of someone's game. -dudStarLeague79.text=%s, we're at an SLDF control center in %s. Our comms have been glitching\ +dudStarLeague79.text={0}, we're at an SLDF control center in {0}. Our comms have been glitching\ \ non-stop, and there's a strange clicking sound on the encrypted channel. No depot has been located,\ \ but the crew's sure someone is eavesdropping on us. It feels like every word, every step, is being\ \ cataloged by unseen eyes. -dudStarLeague80.text=%s, we're inside a ruined SLDF command bunker at %s. The walls still bear\ +dudStarLeague80.text={0}, we're inside a ruined SLDF command bunker at {0}. The walls still bear\ \ screens showing flickering maps of Kerensky's last campaigns. It's eerie, as if the General\ \ himself left these plans behind for us to find. No depot yet, just the feeling that we're walking\ \ among the echoes of his final orders - a legacy that's long been lost to the stars. -dudStarLeague81.text=%s, we're at an SLDF forward base in %s. There's a worn plaque here,\ +dudStarLeague81.text={0}, we're at an SLDF forward base in {0}. There's a worn plaque here,\ \ commemorating one of Kerensky's pivotal battles during the Hegemony Campaign. It's covered in\ \ rust, but his name is still visible, a reminder of the ideals he fought for. No depot found,\ \ just the sense that his presence lingers, urging us forward despite the odds. -dudStarLeague82.text=%s, we're in the ruins of an SLDF supply hub at %s. The crew found an old\ +dudStarLeague82.text={0}, we're in the ruins of an SLDF supply hub at {0}. The crew found an old\ \ banner bearing Kerensky's name, torn but still recognizable. No sign of the depot, but the place\ \ feels sacred - like we're treading on ground that once resounded with the General's commands.\ \ Morale is high, as if the spirit of Kerensky himself is watching over us. -dudStarLeague83.text=%s, we're inside an abandoned SLDF stronghold in %s. The walls are lined\ +dudStarLeague83.text={0}, we're inside an abandoned SLDF stronghold in {0}. The walls are lined\ \ with crumbling quotes from Kerensky's speeches, still legible despite the decay. The crew is silent,\ \ moved by his words. No depot found, just a profound sense of honor mixed with sorrow. It's hard\ \ not to feel like we're living in the shadow of greatness. -dudStarLeague84.text=%s, we're at an SLDF base in %s. The crew found an old comms console engraved\ +dudStarLeague84.text={0}, we're at an SLDF base in {0}. The crew found an old comms console engraved\ \ with a simple line: "For the General." No depot here, just a palpable sense of reverence for the\ \ man who tried to save the Star League from its own destruction. The team feels a renewed\ \ determination, driven by Kerensky's spirit to keep searching. -dudStarLeague85.text=%s, we're at an SLDF logistics post at %s. There's a statue here,\ +dudStarLeague85.text={0}, we're at an SLDF logistics post at {0}. There's a statue here,\ \ partially collapsed but unmistakably depicting Kerensky. It's covered in grime, but his eyes\ \ seem to watch us with a mix of hope and disappointment. No depot yet, but the crew is visibly\ \ humbled by his enduring legacy. -dudStarLeague86.text=%s, we're inside an old SLDF headquarters at %s. The central command table\ +dudStarLeague86.text={0}, we're inside an old SLDF headquarters at {0}. The central command table\ \ still bears the symbol of Kerensky's forces. It feels like a memorial to a lost era, a silent\ \ tribute to the man who led the Exodus. No depot found, only a sense of duty to honor what he\ \ stood for - even in failure. -dudStarLeague87.text=%s, we're at an SLDF training camp in %s. The crew stumbled upon an old\ +dudStarLeague87.text={0}, we're at an SLDF training camp in {0}. The crew stumbled upon an old\ \ insignia bearing the motto: "Strength through unity." No depot in sight, but the words seem\ \ to echo through the empty halls, a reminder of the ideals that died when the Star League fell\ \ apart. -dudStarLeague88.text=%s, we're exploring a remote SLDF bunker at %s. The walls are covered in\ +dudStarLeague88.text={0}, we're exploring a remote SLDF bunker at {0}. The walls are covered in\ \ graffiti from soldiers who once served under Kerensky, some of it pleading for his return. No\ \ depot found, only a lingering sense of loyalty that stretches across the centuries. -dudStarLeague89.text=%s, we're at an SLDF comms station in %s. The crew found a plaque\ +dudStarLeague89.text={0}, we're at an SLDF comms station in {0}. The crew found a plaque\ \ commemorating Kerensky's victory over Amaris, now rusted and nearly unreadable. No depot, just\ \ the remnants of glory that faded with his departure. The men are unusually quiet, as if they can\ \ feel the weight of Kerensky's unfulfilled dream. -dudStarLeague90.text=%s, we're in an abandoned SLDF outpost at %s. Half written letters to family\ +dudStarLeague90.text={0}, we're in an abandoned SLDF outpost at {0}. Half written letters to family\ \ lie scattered, a quick look through them speaks to a time of war, a desire to return to peace,\ \ and fear of the coming war to retake Terra. No depot, just a haunting sense of apprehension that\ \ still clings to the air, as if even the General's followers were unsure of their fate. -dudStarLeague91.text=%s, we're in an SLDF barracks at %s. The crew found a dusty old portrait\ +dudStarLeague91.text={0}, we're in an SLDF barracks at {0}. The crew found a dusty old portrait\ \ of Kerensky, half-covered by fallen debris. His gaze seems to pierce through the centuries, a\ \ silent witness to the search that continues long after his departure. No depot, just a deep sense\ \ of loss. -dudStarLeague92.text=%s, we're at an SLDF armory in %s. The walls bear the symbol of Kerensky's\ +dudStarLeague92.text={0}, we're at an SLDF armory in {0}. The walls bear the symbol of Kerensky's\ \ command - now faded, but still potent. No depot located, but the feeling of walking in the footsteps\ \ of the General is unmistakable. The crew feels a mix of inspiration and sadness, as if the weight\ \ of history itself is upon them. -dudStarLeague93.text=%s, we're inside an SLDF depot at %s. The base's main hall has a mural\ +dudStarLeague93.text={0}, we're inside an SLDF depot at {0}. The base's main hall has a mural\ \ dedicated to Kerensky's final speech before the Exodus. It's faded, but the words still carry weight.\ \ No depot found, only a sense of melancholy as we realize that his dream of unity is more distant\ \ than ever. -dudStarLeague94.text=%s, we're at an SLDF memorial site in %s. The plaque here reads: "For General\ +dudStarLeague94.text={0}, we're at an SLDF memorial site in {0}. The plaque here reads: "For General\ \ Kerensky - last hope of the Star League." No depot located, but the crew is unusually somber, as\ \ if the place itself demands respect. It's a painful reminder that even the greatest leaders\ \ couldn't prevent the collapse. -dudStarLeague95.text=%s, we're in the ruins of an SLDF command center at %s. The central console\ +dudStarLeague95.text={0}, we're in the ruins of an SLDF command center at {0}. The central console\ \ still bears the symbol of Kerensky's forces, now corroded but visible. No depot here, just the\ \ feeling that we're walking among the last vestiges of a legacy that ultimately led to self-exile. -dudStarLeague96.text=%s, we're at an SLDF base in %s. The crew found a handwritten note from one\ +dudStarLeague96.text={0}, we're at an SLDF base in {0}. The crew found a handwritten note from one\ \ of Kerensky's officers, addressed to "the General" and left unfinished. No depot found, only the\ \ lingering sense of promises unfulfilled. The team is subdued, as if they're feeling the weight\ \ of a history that never had a happy ending. -dudStarLeague97.text=%s, we're inside an old SLDF staging area at %s. The base is filled with\ +dudStarLeague97.text={0}, we're inside an old SLDF staging area at {0}. The base is filled with\ \ broken gear, but there's a large mural of Kerensky in the main hall, now barely recognizable. No\ \ depot located, only a profound sense of reverence for the man who once led these soldiers. -dudStarLeague98.text=%s, we're at an SLDF fort in %s. The crew discovered an old battle standard\ +dudStarLeague98.text={0}, we're at an SLDF fort in {0}. The crew discovered an old battle standard\ \ bearing Kerensky's name, untouched for centuries. No depot, but the sense of honor and loss is\ \ overwhelming. It's hard not to feel like we're chasing the remnants of a dream that died with him. -dudStarLeague99.text=%s, we're in an SLDF depot at %s. There's a statue of Kerensky here, toppled\ +dudStarLeague99.text={0}, we're in an SLDF depot at {0}. There's a statue of Kerensky here, toppled\ \ and broken. It's a painful sight, a symbol of the Star League's fall and the General's departure.\ \ No depot found, only the reminder that his vision, though noble, couldn't prevent the inevitable\ \ collapse. @@ -3193,319 +3194,319 @@ warning.text=Are you sure? Refusing this offer will have consequences. # saleDialog propositionAccept.text=Accept Transaction propositionRefuse.text=Enter the Depot -proposition0.text=%s, I understand your team is actively pursuing a Star League depot, a relic of immense\ +proposition0.text={0}, I understand your team is actively pursuing a Star League depot, a relic of immense\ \ historical significance. I represent a party with considerable interest in its precise location,\ \ especially any ties it may have to the SLDF. We are prepared to offer a substantial sum of C-bills\ \ in exchange for this information. Consider this a rare opportunity to secure significant resources.\ \ Declining, however, could have unintended consequences - others may not be as patient or generous. -proposition1.text=%s, time is of the essence. The Star League depot is more than just a trove of lost\ +proposition1.text={0}, time is of the essence. The Star League depot is more than just a trove of lost\ \ technology; it's a piece of history tied directly to the SLDF's final days. My associates are\ \ eager to secure its exact location and will provide a generous payment in C-bills for this\ \ information. Refusal, however, could lead to increased interest from other, less diplomatic parties. -proposition2.text=%s, your pursuit of the Star League depot has not gone unnoticed. My principals are\ +proposition2.text={0}, your pursuit of the Star League depot has not gone unnoticed. My principals are\ \ interested in its coordinates, especially due to its rumored connection to the SLDF's legacy.\ \ We are prepared to offer an immediate transfer of C-bills in return. Keeping such a discovery\ \ secret carries its own risks. Accepting our terms ensures both financial security and the avoidance\ \ of complications. -proposition3.text=%s, I commend your efforts in locating the Star League depot, a site potentially\ +proposition3.text={0}, I commend your efforts in locating the Star League depot, a site potentially\ \ linked to the SLDF's lost glory. We propose a straightforward exchange: a significant amount of\ \ C-bills for the depot's coordinates. Refusing this offer may attract the attention of others who\ \ are less inclined toward negotiation. Accept the funds, and avoid unnecessary complications. -proposition4.text=%s, consider this a final offer concerning the Star League depot. We can provide a\ +proposition4.text={0}, consider this a final offer concerning the Star League depot. We can provide a\ \ substantial amount of C-bills in exchange for its location, particularly given its rumored ties\ \ to the SLDF. Refusal could invite scrutiny from factions less concerned with diplomacy. A quick,\ \ discreet transaction benefits both sides. Choose wisely, as time is limited. -proposition5.text=%s, the Star League depot you're pursuing is of significant interest to my associates,\ +proposition5.text={0}, the Star League depot you're pursuing is of significant interest to my associates,\ \ particularly due to its potential ties to the SLDF's storied history. We are ready to transfer a\ \ considerable sum of C-bills in exchange for its exact location. This offer is both discreet and\ \ beneficial. However, failing to respond could attract less favorable attention from other factions. -proposition6.text=%s, I must reiterate the urgency of securing the Star League depot's coordinates. It\ +proposition6.text={0}, I must reiterate the urgency of securing the Star League depot's coordinates. It\ \ holds immense strategic and historical value, particularly for those who respect the SLDF's legacy.\ \ My principals are prepared to make a swift, substantial payment in C-bills. Be aware that delaying\ \ could draw interest from parties who might not negotiate as fairly. -proposition7.text=%s, I represent a group that considers the Star League depot, and any links to the SLDF,\ +proposition7.text={0}, I represent a group that considers the Star League depot, and any links to the SLDF,\ \ to be invaluable. We offer a generous sum of C-bills for its coordinates. This is an opportunity\ \ to secure immediate resources. A refusal could, however, alter the nature of future negotiations\ \ - possibly not to your advantage. -proposition8.text=%s, we both know the Star League depot has implications beyond mere salvage. It's a\ +proposition8.text={0}, we both know the Star League depot has implications beyond mere salvage. It's a\ \ relic of the SLDF's strength and influence. We are ready to provide a significant transfer of\ \ C-bills for its location. Consider this a strategic alliance. Declining may invite attention\ \ from those who prefer force over finance. -proposition9.text=%s, the Star League depot represents more than just lost technology; it's a piece of\ +proposition9.text={0}, the Star League depot represents more than just lost technology; it's a piece of\ \ SLDF heritage. My associates wish to acquire its location and will provide immediate compensation\ \ in C-bills. Refusal to cooperate could have broader implications for your operations, especially\ \ if others decide to pursue this more aggressively. -proposition10.text=%s, the Star League depot holds more than just salvage - it holds history, specifically\ +proposition10.text={0}, the Star League depot holds more than just salvage - it holds history, specifically\ \ that of the SLDF. We are prepared to offer a substantial sum of C-bills in exchange for its\ \ precise location. Declining this offer may lead to less pleasant inquiries from other interested\ \ factions. The choice is yours, but time is running short. -proposition11.text=%s, your pursuit of the Star League depot has reached the attention of those who\ +proposition11.text={0}, your pursuit of the Star League depot has reached the attention of those who\ \ hold the SLDF in high regard. We wish to negotiate the exchange of its coordinates for C-bills.\ \ The offer is fair, immediate, and advantageous. A refusal could complicate matters with parties\ \ more focused on the depot's strategic value than on monetary compensation. -proposition12.text=%s, securing the location of the Star League depot is of paramount importance to my\ +proposition12.text={0}, securing the location of the Star League depot is of paramount importance to my\ \ principals, particularly given its ties to the SLDF's legacy. We offer substantial C-bills for\ \ this information, with no strings attached. However, withholding the location could lead to\ \ unexpected confrontations from other interested parties. -proposition13.text=%s, the Star League depot is of immense value to my associates, especially given its\ +proposition13.text={0}, the Star League depot is of immense value to my associates, especially given its\ \ historical link to the SLDF. We propose a significant sum of C-bills for the coordinates. Be\ \ aware that delays or refusals could prompt actions from less patient factions. Your cooperation\ \ ensures both profit and peace. -proposition14.text=%s, the Star League depot's connection to the SLDF is well understood by my principals.\ +proposition14.text={0}, the Star League depot's connection to the SLDF is well understood by my principals.\ \ We offer a substantial transfer of C-bills in exchange for its location. Declining may draw\ \ interest from other factions with fewer concerns about diplomacy and more about possession. -proposition15.text=%s, the Star League depot you seek is a critical piece of SLDF history. We are\ +proposition15.text={0}, the Star League depot you seek is a critical piece of SLDF history. We are\ \ prepared to offer immediate compensation in C-bills for its coordinates. Be aware that others\ \ may soon seek it by force, making this offer one of the safer paths forward. -proposition16.text=%s, the Star League depot holds vital historical insights related to the SLDF's past.\ +proposition16.text={0}, the Star League depot holds vital historical insights related to the SLDF's past.\ \ We are interested only in the coordinates and will offer C-bills in return. It's a straightforward\ \ transaction, but declining it may lead to a more aggressive pursuit from other factions. -proposition17.text=%s, the Star League depot is of significant interest due to its ties to the SLDF's\ +proposition17.text={0}, the Star League depot is of significant interest due to its ties to the SLDF's\ \ final days. We wish to acquire the coordinates and are prepared to offer a considerable sum of\ \ C-bills. Your cooperation ensures a smooth exchange. Refusal could, however, attract the wrong\ \ kind of attention. -proposition18.text=%s, I represent a group deeply invested in Star League history, specifically that of\ +proposition18.text={0}, I represent a group deeply invested in Star League history, specifically that of\ \ the SLDF. We propose a discreet exchange of C-bills for the depot's location. Declining this\ \ offer could complicate your current mission, as others may choose a more direct approach. -proposition19.text=%s, the Star League depot's link to the SLDF makes it uniquely valuable. My principals\ +proposition19.text={0}, the Star League depot's link to the SLDF makes it uniquely valuable. My principals\ \ are prepared to offer a generous sum of C-bills for its coordinates. Accepting ensures mutual\ \ benefit, while refusal could draw the interest of parties less inclined toward peaceful negotiations. -proposition20.text=%s, some things are not meant to be found - yet here you are, on the trail of a Star\ +proposition20.text={0}, some things are not meant to be found - yet here you are, on the trail of a Star\ \ League depot tied to the SLDF. We seek its location and offer C-bills in exchange. This knowledge\ \ is not without its burdens, but the rewards can be... enlightening. Choose wisely. -proposition21.text=%s, the past holds many secrets, as does the Star League depot you seek. We are\ +proposition21.text={0}, the past holds many secrets, as does the Star League depot you seek. We are\ \ prepared to transfer C-bills for its coordinates, but understand: knowledge can bring shadows.\ \ Refuse, and the shadows may grow longer than you expect. -proposition22.text=%s, you are walking in the footsteps of the SLDF, tracing paths they once tread in\ +proposition22.text={0}, you are walking in the footsteps of the SLDF, tracing paths they once tread in\ \ secrecy. We seek the same location as you, and offer C-bills to lighten your burden. Know that\ \ roads not taken often have watchers. -proposition23.text=%s, the Star League depot's secrets are old, but our interest in them is new. The\ +proposition23.text={0}, the Star League depot's secrets are old, but our interest in them is new. The\ \ coordinates, exchanged for C-bills, could secure your future - if you choose to share them. Just\ \ remember, history is often written by those who find it first. -proposition24.text=%s, we sense you are close to the Star League depot, an echo of the SLDF's lost\ +proposition24.text={0}, we sense you are close to the Star League depot, an echo of the SLDF's lost\ \ legacy. We offer C-bills for its location, and our offer stands for a short time. Remember, eyes\ \ are always watching those who seek the past. -proposition25.text=%s, the location you seek has seen more than battles; it has seen betrayal and\ +proposition25.text={0}, the location you seek has seen more than battles; it has seen betrayal and\ \ ambition. The Star League depot's coordinates are valuable to us. C-bills are offered in exchange.\ \ Be cautious - persistence can attract the gaze of those who move in silence. -proposition26.text=%s, what you chase is both tangible and elusive. We seek only the coordinates of\ +proposition26.text={0}, what you chase is both tangible and elusive. We seek only the coordinates of\ \ the Star League depot, and we offer C-bills in return. Time is a fickle ally; those who linger\ \ too long may find themselves pursued. -proposition27.text=%s, the SLDF left many things behind - this depot is just one of them. We are willing\ +proposition27.text={0}, the SLDF left many things behind - this depot is just one of them. We are willing\ \ to compensate you in C-bills for its location. But be aware: old ghosts often find new hunters.\ \ Delay may not be wise. -proposition28.text=%s, some legacies were never meant to be uncovered, yet here you are. The Star League\ +proposition28.text={0}, some legacies were never meant to be uncovered, yet here you are. The Star League\ \ depot holds value, and so do C-bills. We offer them in exchange for the coordinates, but whispers\ \ travel fast. Silence has a cost. -proposition29.text=%s, you approach a place where the SLDF's intentions are frozen in time. We wish to\ +proposition29.text={0}, you approach a place where the SLDF's intentions are frozen in time. We wish to\ \ acquire the coordinates, offering C-bills for what you find. But tread lightly - there are those\ \ who would prefer that such locations remain forgotten. -proposition30.text=%s, the Star League depot is a key - one that unlocks both opportunity and danger.\ +proposition30.text={0}, the Star League depot is a key - one that unlocks both opportunity and danger.\ \ We seek its location, offering C-bills for your cooperation. But keys can turn both ways; keep\ \ that in mind as you consider our offer. -proposition31.text=%s, knowledge of the SLDF's remnants carries weight, as does the Star League depot's\ +proposition31.text={0}, knowledge of the SLDF's remnants carries weight, as does the Star League depot's\ \ location. We offer C-bills in exchange, but understand this: you are not the only seeker. The\ \ quietest hunters are often the deadliest. -proposition32.text=%s, the past calls to those who listen, and it seems you have answered. The Star\ +proposition32.text={0}, the past calls to those who listen, and it seems you have answered. The Star\ \ League depot holds truths that should be uncovered, for the right price. We offer C-bills, but\ \ others may offer something far less forgiving. -proposition33.text=%s, the SLDF once guarded secrets with zeal, and this Star League depot is no\ +proposition33.text={0}, the SLDF once guarded secrets with zeal, and this Star League depot is no\ \ exception. We seek its coordinates and will provide C-bills in return. Be wary - secrets long\ \ buried can attract dangerous interest. -proposition34.text=%s, you walk a path few dare to tread. The Star League depot is closer than you\ +proposition34.text={0}, you walk a path few dare to tread. The Star League depot is closer than you\ \ think. We offer C-bills for its location, but haste is advised. The past has many eyes, and not\ \ all are friendly. -proposition35.text=%s, the SLDF's legacy is fraught with hidden agendas. The Star League depot is part\ +proposition35.text={0}, the SLDF's legacy is fraught with hidden agendas. The Star League depot is part\ \ of that tale. We offer C-bills for its coordinates, but know that even shadows have their own\ \ purposes. -proposition36.text=%s, some say the Star League depot is cursed, others that it is guarded. We are\ +proposition36.text={0}, some say the Star League depot is cursed, others that it is guarded. We are\ \ interested only in its location, offering C-bills in exchange. But consider this: doors opened\ \ too soon can let in more than just opportunity. -proposition37.text=%s, you seek what the SLDF could not protect forever. The Star League depot's\ +proposition37.text={0}, you seek what the SLDF could not protect forever. The Star League depot's\ \ coordinates are what we desire, and C-bills are what we offer. However, hesitation can be\ \ dangerous when others are also searching. -proposition38.text=%s, the Star League depot you pursue is part of a larger web, one spun long ago.\ +proposition38.text={0}, the Star League depot you pursue is part of a larger web, one spun long ago.\ \ We wish to know its location, and C-bills are our currency of exchange. Be warned, however -\ \ some webs have more than one spider. -proposition39.text=%s, the depot's coordinates are more than just a location; they are a piece of the\ +proposition39.text={0}, the depot's coordinates are more than just a location; they are a piece of the\ \ SLDF's forgotten war. We offer C-bills in return, but the clock is ticking. Know that the past\ \ is a powerful force, and it does not always forgive. -proposition40.text=%s, the Star League depot you seek is a dangerous prize. We demand the coordinates\ +proposition40.text={0}, the Star League depot you seek is a dangerous prize. We demand the coordinates\ \ in exchange for C-bills. Refuse, and others - far less diplomatic - will come calling. You don't\ \ want to be in their crosshairs. -proposition41.text=%s, time is not your ally. The SLDF left behind many secrets, and this Star League\ +proposition41.text={0}, time is not your ally. The SLDF left behind many secrets, and this Star League\ \ depot is one of the most coveted. Provide the coordinates, accept the C-bills, and avoid\ \ complications. Refuse, and those complications will find you. -proposition42.text=%s, your pursuit of the Star League depot is attracting attention beyond your control.\ +proposition42.text={0}, your pursuit of the Star League depot is attracting attention beyond your control.\ \ We offer C-bills in exchange for the coordinates, but consider this your final chance. The next\ \ contact may not be as patient - or as generous. -proposition43.text=%s, we know you're close to the Star League depot. Deliver the coordinates, or face\ +proposition43.text={0}, we know you're close to the Star League depot. Deliver the coordinates, or face\ \ the consequences of keeping such knowledge hidden. Our offer of C-bills is the only peaceful\ \ resolution you'll receive. -proposition44.text=%s, history's secrets are not meant for everyone. The Star League depot is no exception.\ +proposition44.text={0}, history's secrets are not meant for everyone. The Star League depot is no exception.\ \ We offer C-bills for its location, but decline at your own peril. Others will be less interested\ \ in negotiation and more interested in results. -proposition45.text=%s, you may believe you have time, but that is a dangerous illusion. The Star League\ +proposition45.text={0}, you may believe you have time, but that is a dangerous illusion. The Star League\ \ depot's coordinates are needed now. Accept the C-bills offered, or expect a less pleasant approach\ \ from others already en route. -proposition46.text=%s, this is not merely an offer - it's a warning. The Star League depot has many eyes\ +proposition46.text={0}, this is not merely an offer - it's a warning. The Star League depot has many eyes\ \ upon it, some less forgiving than others. Provide the coordinates, take the C-bills, and leave\ \ the shadows behind. Refusal will only invite them closer. -proposition47.text=%s, the SLDF's secrets come with a heavy cost. You stand at a crossroads: accept our\ +proposition47.text={0}, the SLDF's secrets come with a heavy cost. You stand at a crossroads: accept our\ \ C-bills for the Star League depot's location, or face the inevitable interest of those who won't\ \ ask twice. -proposition48.text=%s, we both know the value of the Star League depot. We also know the danger of delay.\ +proposition48.text={0}, we both know the value of the Star League depot. We also know the danger of delay.\ \ We offer C-bills for its coordinates. Decline, and the next visitor may not be inclined to negotiate. -proposition49.text=%s, we understand the risks you've taken in finding the Star League depot. But risks\ +proposition49.text={0}, we understand the risks you've taken in finding the Star League depot. But risks\ \ have a way of multiplying when they're ignored. Provide the coordinates and accept our C-bills,\ \ or suffer the consequences of silence. -proposition50.text=%s, the Star League depot's location is no longer a secret. Others are closing in,\ +proposition50.text={0}, the Star League depot's location is no longer a secret. Others are closing in,\ \ and their intentions are not as peaceful as ours. This is your final opportunity to exchange it\ \ for C-bills. Refuse, and you may find yourself hunted. -proposition51.text=%s, you are walking a dangerous path. The SLDF left behind many ghosts, and the Star\ +proposition51.text={0}, you are walking a dangerous path. The SLDF left behind many ghosts, and the Star\ \ League depot is one of them. Give us the coordinates and take the C-bills, or find yourself\ \ haunted by more than just history. -proposition52.text=%s, the past can be a cruel teacher. The Star League depot's location is of great\ +proposition52.text={0}, the past can be a cruel teacher. The Star League depot's location is of great\ \ interest to many, not all of whom care for your well-being. Deliver the coordinates and secure\ \ your C-bills. The alternative is far less forgiving. -proposition53.text=%s, we are not alone in our pursuit of the Star League depot. Provide the coordinates,\ +proposition53.text={0}, we are not alone in our pursuit of the Star League depot. Provide the coordinates,\ \ accept the C-bills, and avoid the chaos that follows. Delay will only attract the wolves. -proposition54.text=%s, you must understand: the Star League depot is a beacon, and you are standing too\ +proposition54.text={0}, you must understand: the Star League depot is a beacon, and you are standing too\ \ close. Offer the coordinates in exchange for C-bills, or expect the arrival of those who prefer to\ \ take what they want by force. -proposition55.text=%s, knowledge of the SLDF depot's location is a dangerous thing. We offer C-bills for\ +proposition55.text={0}, knowledge of the SLDF depot's location is a dangerous thing. We offer C-bills for\ \ the coordinates, but time is running out. Those who do not act quickly often become collateral. -proposition56.text=%s, we will not wait forever. The Star League depot is too valuable, and your hesitation\ +proposition56.text={0}, we will not wait forever. The Star League depot is too valuable, and your hesitation\ \ is drawing dangerous attention. Provide the coordinates, take the C-bills, and avoid the fate of\ \ those who are too slow to act. -proposition57.text=%s, the Star League depot has become a target, and so have you. Deliver the coordinates\ +proposition57.text={0}, the Star League depot has become a target, and so have you. Deliver the coordinates\ \ now, receive your C-bills, and walk away. Refuse, and you may find your options rapidly disappearing. -proposition58.text=%s, this is the last offer you'll receive from us regarding the Star League depot.\ +proposition58.text={0}, this is the last offer you'll receive from us regarding the Star League depot.\ \ Accept the C-bills for its coordinates, or face those who have less patience for negotiation. You\ \ do not want to test their resolve. -proposition59.text=%s, the SLDF depot is a prize worth fighting for - and dying for. We offer C-bills\ +proposition59.text={0}, the SLDF depot is a prize worth fighting for - and dying for. We offer C-bills\ \ for its location, but be warned: others are not far behind, and they are far less concerned with\ \ your survival. -proposition60.text=%s, I hear you're getting close to that Star League depot. I've got some serious C-bills\ +proposition60.text={0}, I hear you're getting close to that Star League depot. I've got some serious C-bills\ \ waiting for you if you're willing to share the location. Let's keep this simple and beneficial for\ \ both of us. After all, who needs unnecessary complications, right? -proposition61.text=%s, you've done some impressive work tracking that Star League depot! I've got a good\ +proposition61.text={0}, you've done some impressive work tracking that Star League depot! I've got a good\ \ feeling about this. How about we make a deal? A generous sum of C-bills in exchange for the coordinates\ \ - no strings attached. Let's make this a win-win, shall we? -proposition62.text=%s, I've always admired your tenacity. Finding a Star League depot is no small feat!\ +proposition62.text={0}, I've always admired your tenacity. Finding a Star League depot is no small feat!\ \ I'm willing to pay a fair amount of C-bills for the location. No need to make things difficult -\ \ just a straightforward deal between two professionals. -proposition63.text=%s, it's not every day someone stumbles across a Star League depot. Lucky for you,\ +proposition63.text={0}, it's not every day someone stumbles across a Star League depot. Lucky for you,\ \ I happen to be in the market for that exact kind of information. I'll make it worth your while -\ \ C-bills, quick and easy. What do you say? -proposition64.text=%s, I'll be honest - this Star League depot is the stuff of legend. I'm sure you're\ +proposition64.text={0}, I'll be honest - this Star League depot is the stuff of legend. I'm sure you're\ \ ready for a good payday, so how about we swap the coordinates for a nice pile of C-bills? No fuss,\ \ no hassle - just a friendly exchange. -proposition65.text=%s, you're on the verge of a big find! I'm talking about that Star League depot, of\ +proposition65.text={0}, you're on the verge of a big find! I'm talking about that Star League depot, of\ \ course. I've got a hefty sum of C-bills ready to go, just for the coordinates. Think of it as a\ \ friendly favor that pays off handsomely. -proposition66.text=%s, it's not every day someone uncovers a piece of SLDF history like this depot. I'd\ +proposition66.text={0}, it's not every day someone uncovers a piece of SLDF history like this depot. I'd\ \ love to be a part of your success story. Let's swap the coordinates for a good amount of C-bills,\ \ and you'll walk away with a heavy wallet. -proposition67.text=%s, I've been following your progress with this Star League depot, and I've got to\ +proposition67.text={0}, I've been following your progress with this Star League depot, and I've got to\ \ say, I like your style! How about a friendly deal - coordinates for C-bills? It's a simple trade\ \ that'll put a smile on your face. -proposition68.text=%s, you've done the hard work of finding the Star League depot, and now I'd like to\ +proposition68.text={0}, you've done the hard work of finding the Star League depot, and now I'd like to\ \ help you cash in on it. I've got C-bills with your name on them, ready to be transferred as soon\ \ as we get the coordinates. Easy money! -proposition69.text=%s, you and I both know how rare it is to find something tied to the SLDF. I've got\ +proposition69.text={0}, you and I both know how rare it is to find something tied to the SLDF. I've got\ \ the C-bills to make this worth your while. Let's keep things simple - coordinates for cash. No\ \ need to make this more complicated than it has to be! -proposition70.text=%s, you've got a nose for finding lost treasures, and that Star League depot is a\ +proposition70.text={0}, you've got a nose for finding lost treasures, and that Star League depot is a\ \ prime example. I'm offering a nice pile of C-bills for the location. Let's make this a friendly,\ \ profitable exchange. After all, who doesn't like a good payday? -proposition71.text=%s, you're on a hot streak with this Star League depot, and I want to be a part of\ +proposition71.text={0}, you're on a hot streak with this Star League depot, and I want to be a part of\ \ it. I'm offering a generous sum of C-bills for the coordinates - easy money for you, and no strings\ \ attached. Let's shake hands, so to speak. -proposition72.text=%s, finding a Star League depot is no small feat, but getting paid for it? That's the\ +proposition72.text={0}, finding a Star League depot is no small feat, but getting paid for it? That's the\ \ fun part. I've got the C-bills ready for you. All I need is the location, and we both walk away\ \ happy. What do you say? -proposition73.text=%s, you've got yourself a golden opportunity with that Star League depot. I've got\ +proposition73.text={0}, you've got yourself a golden opportunity with that Star League depot. I've got\ \ C-bills burning a hole in my pocket, and I'd love to share them with you in exchange for the\ \ coordinates. Let's make this a smooth, friendly deal. -proposition74.text=%s, let's make this a story we can both smile about. You give me the Star League\ +proposition74.text={0}, let's make this a story we can both smile about. You give me the Star League\ \ depot's coordinates, and I'll make sure you're paid handsomely in C-bills. No drama, no\ \ complications - just a friendly exchange. -proposition75.text=%s, I'm sure you're used to these kinds of negotiations, so let me be clear: you\ +proposition75.text={0}, I'm sure you're used to these kinds of negotiations, so let me be clear: you\ \ provide the Star League depot's location, and you'll have more C-bills than you know what to do\ \ with. No hard feelings, just business. -proposition76.text=%s, you've got yourself a fine opportunity with that SLDF depot. But let's be honest\ +proposition76.text={0}, you've got yourself a fine opportunity with that SLDF depot. But let's be honest\ \ - turning coordinates into C-bills is even better. I'm offering just that, with no strings\ \ attached. Friends keep things simple, after all. -proposition77.text=%s, I'm not here to complicate things. I just want the Star League depot's coordinates,\ +proposition77.text={0}, I'm not here to complicate things. I just want the Star League depot's coordinates,\ \ and I'm more than happy to pay you well for them in C-bills. Let's keep this simple, friend - no\ \ need to let others get involved -proposition78.text=%s, let's not beat around the bush. The Star League depot you've found is impressive,\ +proposition78.text={0}, let's not beat around the bush. The Star League depot you've found is impressive,\ \ but I'm sure you'd prefer the C-bills I'm offering. A fair trade, wouldn't you say? We can keep\ \ this all friendly and profitable. -proposition79.text=%s, we both know that finding a Star League depot is no small feat. But selling the\ +proposition79.text={0}, we both know that finding a Star League depot is no small feat. But selling the\ \ info? Now that's easy. Let me take it off your hands for a good price in C-bills. We both get\ \ what we want, and no one gets hurt. -proposition80.text=%s, you're a real pro at this, I can tell. So let's keep it professional, but friendly:\ +proposition80.text={0}, you're a real pro at this, I can tell. So let's keep it professional, but friendly:\ \ you hand over the Star League depot's location, and I'll hand over the C-bills. No drama, just a\ \ clean deal. -proposition81.text=%s, I've heard you're closing in on an SLDF treasure. How about we make a deal? You\ +proposition81.text={0}, I've heard you're closing in on an SLDF treasure. How about we make a deal? You\ \ share the coordinates, and I'll make sure you're enjoying a nice payday with a generous amount of\ \ C-bills. Just good business, friend. -proposition82.text=%s, I know how these things go - sometimes you're the one who finds the Star League\ +proposition82.text={0}, I know how these things go - sometimes you're the one who finds the Star League\ \ depot, and sometimes you're the one who buys the info. This time, I'm buying. Let's make a friendly\ \ exchange for C-bills, shall we? -proposition83.text=%s, you're onto something good with that Star League depot. I can help make it even\ +proposition83.text={0}, you're onto something good with that Star League depot. I can help make it even\ \ better - with a pile of C-bills, of course. Just share the coordinates, and we'll both walk away\ \ smiling. -proposition84.text=%s, I admire your tenacity in tracking down that SLDF depot. But you know what's better\ +proposition84.text={0}, I admire your tenacity in tracking down that SLDF depot. But you know what's better\ \ than hard work? Getting paid for it. Give me the location, and you'll have more C-bills than you\ \ can count. What do you say? -proposition85.text=%s, let's cut to the chase. I want the Star League depot's coordinates, and you want\ +proposition85.text={0}, let's cut to the chase. I want the Star League depot's coordinates, and you want\ \ C-bills. I'm here to make sure you get exactly what you deserve, with a little extra for your\ \ troubles. No need to complicate things. -proposition86.text=%s, you've always struck me as someone who knows a good deal when they see one. Here\ +proposition86.text={0}, you've always struck me as someone who knows a good deal when they see one. Here\ \ it is: you give me the Star League depot's location, and I'll fill your coffers with C-bills.\ \ Let's make it easy on both of us. -proposition87.text=%s, you've got yourself a real find with that Star League depot. I'd love to take it\ +proposition87.text={0}, you've got yourself a real find with that Star League depot. I'd love to take it\ \ off your hands - just the coordinates, of course. I'll make sure you're rolling in C-bills before\ \ you know it. Friends help friends, right? -proposition88.text=%s, chasing down a Star League depot isn't easy work. I respect that. But why do it\ +proposition88.text={0}, chasing down a Star League depot isn't easy work. I respect that. But why do it\ \ the hard way? You share the location, and I'll make sure you're flush with C-bills. We can both\ \ walk away happy, no strings attached. -proposition89.text=%s, I've got a feeling we're both after the same thing - the Star League depot. But\ +proposition89.text={0}, I've got a feeling we're both after the same thing - the Star League depot. But\ \ hey, I'm offering C-bills for the coordinates, fair and square. Why not cash in now and save\ \ yourself the trouble of unwanted company? -proposition90.text=%s, it's not every day you get this close to an SLDF depot, is it? I can make it\ +proposition90.text={0}, it's not every day you get this close to an SLDF depot, is it? I can make it\ \ worth your while. Just a friendly deal between us: you give me the coordinates, and I give you\ \ a nice stack of C-bills. No fuss, no muss. -proposition91.text=%s, I hear you're hot on the trail of a Star League depot. Let's not make things\ +proposition91.text={0}, I hear you're hot on the trail of a Star League depot. Let's not make things\ \ too complicated: hand over the coordinates, and I'll make sure you're swimming in C-bills before\ \ the day's out. Just between friends, yeah? -proposition92.text=%s, our terms are clear: C-bills in exchange for the Star League depot's coordinates.\ +proposition92.text={0}, our terms are clear: C-bills in exchange for the Star League depot's coordinates.\ \ This transaction is purely business, aimed at delivering prompt compensation for actionable\ \ intelligence. We anticipate your cooperation. -proposition93.text=%s, as you near the Star League depot, we wish to formalize an agreement. We offer\ +proposition93.text={0}, as you near the Star League depot, we wish to formalize an agreement. We offer\ \ C-bills for the coordinates, in full and with immediate payment. We trust that this approach\ \ aligns with your own professional standards. -proposition94.text=%s, our interest in the Star League depot is strictly professional. We offer a\ +proposition94.text={0}, our interest in the Star League depot is strictly professional. We offer a\ \ significant transfer of C-bills for the location. This is an efficient solution for both sides\ \ - minimal risk, maximum benefit. -proposition95.text=%s, we propose a clear-cut deal: the coordinates of the Star League depot for a\ +proposition95.text={0}, we propose a clear-cut deal: the coordinates of the Star League depot for a\ \ substantial amount of C-bills. This is a straightforward exchange, free of any further obligations\ \ or complications. We await your response. -proposition96.text=%s, we understand the value of the Star League depot you've uncovered. We offer\ +proposition96.text={0}, we understand the value of the Star League depot you've uncovered. We offer\ \ C-bills in direct exchange for the location, ensuring prompt payment. This is a clean,\ \ business-focused transaction that benefits all parties. -proposition97.text=%s, our offer remains firm: a substantial amount of C-bills for the precise\ +proposition97.text={0}, our offer remains firm: a substantial amount of C-bills for the precise\ \ coordinates of the Star League depot. This is a professional exchange of information for\ \ compensation, designed to maximize efficiency for both parties involved. -proposition98.text=%s, the Star League depot's coordinates are of high value to our interests. We\ +proposition98.text={0}, the Star League depot's coordinates are of high value to our interests. We\ \ propose a clear transaction: C-bills in exchange for the location. The terms are simple and\ \ direct, with no additional stipulations. We look forward to a swift resolution. -proposition99.text=%s, you've made significant progress in locating the Star League depot. We are\ +proposition99.text={0}, you've made significant progress in locating the Star League depot. We are\ \ prepared to facilitate an immediate transfer of C-bills upon receipt of the coordinates. This\ \ offer is straightforward and beneficial for both parties. We recommend acting promptly. -propositionValue.text=We're offering %s for the depot's location, sealed and unsullied. +propositionValue.text=We're offering {0} for the depot's location, sealed and unsullied. senderUnknown.text=Sender Unknown \ No newline at end of file diff --git a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/PerformResupply.java b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/PerformResupply.java index 6db3ff71f9d..5fd477fc436 100644 --- a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/PerformResupply.java +++ b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/PerformResupply.java @@ -61,6 +61,7 @@ import static mekhq.campaign.stratcon.StratconRulesManager.generateExternalScenario; import static mekhq.gui.dialog.resupplyAndCaches.DialogItinerary.itineraryDialog; import static mekhq.utilities.EntityUtilities.getEntityFromUnitId; +import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; import static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG; import static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor; @@ -75,7 +76,7 @@ public class PerformResupply { private static final double INTERCEPTION_LOAD_INFLUENCE = 50; public static final String RESUPPLY_LOOT_BOX_NAME = "Resupply"; - private static final ResourceBundle resources = ResourceBundle.getBundle("mekhq.resources.Resupply"); + private static final String RESOURCE_BUNDLE = "mekhq.resources.Resupply"; private static final MMLogger logger = MMLogger.create(PerformResupply.class); @@ -177,12 +178,12 @@ public static void performResupply(Resupply resupply, AtBContract contract, int } } - logger.info("totalTonnage: " + totalTonnage); + logger.info(String.format("totalTonnage: %s", totalTonnage)); // This shouldn't occur, but we include it as insurance. if (resupply.getConvoyContents().isEmpty()) { - campaign.addReport(String.format(resources.getString("convoyUnsuccessful.text"), + campaign.addReport(getFormattedTextAt(RESOURCE_BUNDLE, "convoyUnsuccessful.text", spanOpeningWithCustomColor(MekHQ.getMHQOptions().getFontColorNegativeHexColor()), CLOSING_SPAN_TAG)); return; @@ -246,7 +247,7 @@ public static void makeSmugglerDelivery(Resupply resupply) { } else { final Campaign campaign = resupply.getCampaign(); - campaign.addReport(String.format(resources.getString("convoySuccessfulSmuggler.text"), + campaign.addReport(getFormattedTextAt(RESOURCE_BUNDLE, "convoySuccessfulSmuggler.text", spanOpeningWithCustomColor(MekHQ.getMHQOptions().getFontColorPositiveHexColor()), CLOSING_SPAN_TAG)); makeDelivery(resupply, null); @@ -305,7 +306,7 @@ public static void loadPlayerConvoys(Resupply resupply) { convoyContents.removeAll(convoyItems); - campaign.addReport(String.format(resources.getString("convoyDispatched.text"), + campaign.addReport(getFormattedTextAt(RESOURCE_BUNDLE, "convoyDispatched.text", convoy.getName())); processConvoy(resupply, convoyItems, convoy); } @@ -419,11 +420,12 @@ private static void generateInterceptionOrConvoyEvent(Resupply resupply, @Nullab final String STATUS_AFTERWARD = ".text"; AtBMoraleLevel morale = contract.getMoraleLevel(); + String commanderAddress = campaign.getCommanderAddress(false); String eventText; if (Compute.d6() <= 2) { - eventText = resources.getString(STATUS_FORWARD + Compute.randomInt(100) - + STATUS_AFTERWARD); + eventText = getFormattedTextAt(RESOURCE_BUNDLE, + STATUS_FORWARD + Compute.randomInt(100) + STATUS_AFTERWARD); } else { int roll = Compute.randomInt(2); @@ -431,8 +433,9 @@ private static void generateInterceptionOrConvoyEvent(Resupply resupply, @Nullab morale = roll == 0 ? (morale.isAdvancing() ? DOMINATING : CRITICAL) : STALEMATE; } - eventText = resources.getString(STATUS_FORWARD + "Enemy" + morale - + Compute.randomInt(50) + STATUS_AFTERWARD); + eventText = getFormattedTextAt(RESOURCE_BUNDLE, + STATUS_FORWARD + "Enemy" + morale + Compute.randomInt(50) + STATUS_AFTERWARD, + commanderAddress); } new DialogRoleplayEvent(campaign, convoy, eventText); @@ -450,7 +453,7 @@ private static void generateInterceptionOrConvoyEvent(Resupply resupply, @Nullab private static void completeSuccessfulDelivery(Resupply resupply, List convoyContents) { final Campaign campaign = resupply.getCampaign(); - campaign.addReport(String.format(resources.getString("convoySuccessful.text"), + campaign.addReport(getFormattedTextAt(RESOURCE_BUNDLE, "convoySuccessful.text", spanOpeningWithCustomColor(MekHQ.getMHQOptions().getFontColorPositiveHexColor()), CLOSING_SPAN_TAG)); @@ -509,7 +512,7 @@ private static void processConvoyInterception(Resupply resupply, @Nullable Force // We report the error in this fashion, instead of hiding it in the log, as we want to // increase the likelihood the player is aware an error has occurred. if (template == null) { - campaign.addReport(String.format(resources.getString("convoyErrorTemplate.text"), + campaign.addReport(getFormattedTextAt(RESOURCE_BUNDLE, "convoyErrorTemplate.text", spanOpeningWithCustomColor(MekHQ.getMHQOptions().getFontColorNegativeHexColor()), templateAddress, CLOSING_SPAN_TAG)); @@ -525,7 +528,7 @@ private static void processConvoyInterception(Resupply resupply, @Nullable Force List tracks = campaignState.getTracks(); track = ObjectUtility.getRandomItem(tracks); } catch (NullPointerException e) { - campaign.addReport(String.format(resources.getString("convoyErrorTracks.text"), + campaign.addReport(getFormattedTextAt(RESOURCE_BUNDLE, "convoyErrorTracks.text", spanOpeningWithCustomColor(MekHQ.getMHQOptions().getFontColorNegativeHexColor()), templateAddress, CLOSING_SPAN_TAG)); @@ -566,14 +569,14 @@ private static void processConvoyInterception(Resupply resupply, @Nullable Force backingScenario.addLoot(loot); // Announce the situation to the player - campaign.addReport(String.format(resources.getString("convoyInterceptedStratCon.text"), + campaign.addReport(getFormattedTextAt(RESOURCE_BUNDLE, "convoyInterceptedStratCon.text", spanOpeningWithCustomColor(MekHQ.getMHQOptions().getFontColorNegativeHexColor()), CLOSING_SPAN_TAG)); } else { // If we failed to generate a scenario, for whatever reason, we don't // want the player confused why there isn't a scenario, so we offer // this fluffy response. - campaign.addReport(String.format(resources.getString("convoyEscaped.text"), + campaign.addReport(getFormattedTextAt(RESOURCE_BUNDLE, "convoyEscaped.text", spanOpeningWithCustomColor(MekHQ.getMHQOptions().getFontColorNegativeHexColor()), CLOSING_SPAN_TAG)); diff --git a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogAbandonedConvoy.java b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogAbandonedConvoy.java index e714cec0e13..65b3a72a59f 100644 --- a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogAbandonedConvoy.java +++ b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogAbandonedConvoy.java @@ -27,13 +27,13 @@ import javax.swing.*; import java.awt.*; -import java.util.ResourceBundle; import java.util.UUID; import static megamek.common.Compute.randomInt; import static mekhq.gui.baseComponents.MHQDialogImmersive.getSpeakerDescription; import static mekhq.gui.baseComponents.MHQDialogImmersive.getSpeakerIcon; import static mekhq.utilities.ImageUtilities.scaleImageIconToWidth; +import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; /** * This class provides a utility method to display a custom dialog related to abandoned convoys @@ -44,10 +44,10 @@ public class DialogAbandonedConvoy extends JDialog { final int LEFT_WIDTH = UIUtil.scaleForGUI(200); final int RIGHT_WIDTH = UIUtil.scaleForGUI(400); - private static final ResourceBundle resources = ResourceBundle.getBundle("mekhq.resources.Resupply"); + private static final String RESOURCE_BUNDLE = "mekhq.resources.Resupply"; public DialogAbandonedConvoy(Campaign campaign, AtBContract contract, @Nullable Force targetConvoy) { - setTitle(resources.getString("incomingTransmission.title")); + setTitle(getFormattedTextAt(RESOURCE_BUNDLE, "incomingTransmission.title")); final int INSERT_SIZE = UIUtil.scaleForGUI(10); @@ -75,7 +75,7 @@ public DialogAbandonedConvoy(Campaign campaign, AtBContract contract, @Nullable speakerName = speaker.getFullTitle(); } else { if (targetConvoy == null) { - speakerName = String.format(resources.getString("dialogBorderConvoySpeakerDefault.text"), + speakerName = getFormattedTextAt(RESOURCE_BUNDLE, "dialogBorderConvoySpeakerDefault.text", contract.getEmployerName(campaign.getGameYear())); } else { speakerName = campaign.getName(); @@ -113,8 +113,8 @@ public DialogAbandonedConvoy(Campaign campaign, AtBContract contract, @Nullable JPanel rightBox = new JPanel(new BorderLayout()); rightBox.setBorder(BorderFactory.createEtchedBorder()); - String message = String.format( - resources.getString("statusUpdateAbandoned" + randomInt(20) + ".text"), + String message = getFormattedTextAt(RESOURCE_BUNDLE, + "statusUpdateAbandoned" + randomInt(20) + ".text", campaign.getCommanderAddress(false)); JLabel rightDescription = new JLabel( @@ -135,7 +135,7 @@ public DialogAbandonedConvoy(Campaign campaign, AtBContract contract, @Nullable // Buttons panel JPanel buttonPanel = new JPanel(); - JButton confirmButton = new JButton(resources.getString("logisticsDestroyed.text")); + JButton confirmButton = new JButton(getFormattedTextAt(RESOURCE_BUNDLE, "logisticsDestroyed.text")); confirmButton.addActionListener(e -> dispose()); buttonPanel.add(confirmButton); @@ -149,7 +149,8 @@ public DialogAbandonedConvoy(Campaign campaign, AtBContract contract, @Nullable JLabel newPanelLabel = new JLabel( String.format("
%s
", - LEFT_WIDTH + RIGHT_WIDTH, resources.getString("documentation.prompt"))); + LEFT_WIDTH + RIGHT_WIDTH, + getFormattedTextAt(RESOURCE_BUNDLE, "documentation.prompt"))); infoPanel.add(newPanelLabel, BorderLayout.CENTER); // Add the new panel to the container (below the button panel) diff --git a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogContractStart.java b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogContractStart.java index b8f50045dda..d793c9f407b 100644 --- a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogContractStart.java +++ b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogContractStart.java @@ -29,7 +29,6 @@ import javax.swing.*; import java.awt.*; -import java.util.ResourceBundle; import java.util.UUID; import static mekhq.campaign.mission.resupplyAndCaches.Resupply.isProhibitedUnitType; @@ -37,6 +36,7 @@ import static mekhq.gui.baseComponents.MHQDialogImmersive.getSpeakerDescription; import static mekhq.gui.baseComponents.MHQDialogImmersive.getSpeakerIcon; import static mekhq.utilities.ImageUtilities.scaleImageIconToWidth; +import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; /** * This class provides utility methods to display dialogs related to the beginning of a contract. @@ -48,7 +48,7 @@ public class DialogContractStart extends JDialog { final int RIGHT_WIDTH = UIUtil.scaleForGUI(400); final int INSERT_SIZE = UIUtil.scaleForGUI(10); - private static final ResourceBundle resources = ResourceBundle.getBundle("mekhq.resources.Resupply"); + private static final String RESOURCE_BUNDLE = "mekhq.resources.Resupply"; /** * Displays a dialog at the start of a contract, providing summarized details about the mission @@ -64,7 +64,7 @@ public class DialogContractStart extends JDialog { * @param contract the active contract. */ public DialogContractStart(Campaign campaign, AtBContract contract) { - setTitle(resources.getString("incomingTransmission.title")); + setTitle(getFormattedTextAt(RESOURCE_BUNDLE, "incomingTransmission.title")); // Main Panel to hold both boxes JPanel mainPanel = new JPanel(new GridBagLayout()); @@ -139,7 +139,7 @@ public DialogContractStart(Campaign campaign, AtBContract contract) { // Buttons panel JPanel buttonPanel = new JPanel(); - JButton confirmButton = new JButton(resources.getString("convoyConfirm.text")); + JButton confirmButton = new JButton(getFormattedTextAt(RESOURCE_BUNDLE, "convoyConfirm.text")); confirmButton.addActionListener(e -> dispose()); buttonPanel.add(confirmButton); @@ -151,7 +151,7 @@ public DialogContractStart(Campaign campaign, AtBContract contract) { JLabel lblInfo = new JLabel( String.format("
%s
", RIGHT_WIDTH + LEFT_WIDTH, - String.format(resources.getString("documentation.prompt")))); + getFormattedTextAt(RESOURCE_BUNDLE, "documentation.prompt"))); lblInfo.setHorizontalAlignment(SwingConstants.CENTER); infoPanel.add(lblInfo, BorderLayout.CENTER); infoPanel.setBorder(BorderFactory.createEtchedBorder()); @@ -239,17 +239,17 @@ private static String generateContractStartMessage(Campaign campaign, AtBContrac String commanderTitle = campaign.getCommanderAddress(false); if (contract.getContractType().isGuerrillaWarfare()) { - String convoyMessageTemplate = resources.getString("contractStartMessageGuerrilla.text"); - convoyMessage = String.format(convoyMessageTemplate, commanderTitle); + String convoyMessageTemplate = "contractStartMessageGuerrilla.text"; + convoyMessage = getFormattedTextAt(RESOURCE_BUNDLE, convoyMessageTemplate, commanderTitle); } else { - String convoyMessageTemplate = resources.getString("contractStartMessageGeneric.text"); + String convoyMessageTemplate = "contractStartMessageGeneric.text"; if (contract.getCommandRights().isIndependent()) { - convoyMessageTemplate = resources.getString("contractStartMessageIndependent.text"); + convoyMessageTemplate = "contractStartMessageIndependent.text"; } - convoyMessage = String.format(convoyMessageTemplate, commanderTitle, - estimateCargoRequirements(campaign, contract), totalPlayerCargoCapacity, - playerConvoys, playerConvoys != 1 ? "s" : ""); + convoyMessage = getFormattedTextAt(RESOURCE_BUNDLE, convoyMessageTemplate, commanderTitle, + estimateCargoRequirements(campaign, contract), totalPlayerCargoCapacity, playerConvoys, + playerConvoys != 1 ? "s" : ""); } int width = UIUtil.scaleForGUI(500); diff --git a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogInterception.java b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogInterception.java index 9085d306a8e..b233a8700aa 100644 --- a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogInterception.java +++ b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogInterception.java @@ -37,6 +37,7 @@ import static mekhq.gui.baseComponents.MHQDialogImmersive.getSpeakerDescription; import static mekhq.gui.baseComponents.MHQDialogImmersive.getSpeakerIcon; import static mekhq.utilities.ImageUtilities.scaleImageIconToWidth; +import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; /** * The {@code DialogInterception} class is responsible for displaying a UI dialog when an @@ -49,7 +50,7 @@ public class DialogInterception extends JDialog{ final int RIGHT_WIDTH = UIUtil.scaleForGUI(400); final int INSERT_SIZE = UIUtil.scaleForGUI(10); - private static final ResourceBundle resources = ResourceBundle.getBundle("mekhq.resources.Resupply"); + private static final String RESOURCE_BUNDLE = "mekhq.resources.Resupply"; /** * Displays a dialog for an interception event that occurs during a resupply operation. @@ -63,7 +64,7 @@ public class DialogInterception extends JDialog{ * generated from the resources. * 4. Builds a GUI with Swing components: * - A description panel containing a localized, HTML-styled message. - * - An image panel displaying the speaker's icon (sized to a width of 100px). + * - An image panel displaying the speaker's icon (sized to a width of 100 px). * 5. Adds confirmation buttons to the dialog, allowing users to close it. * 6. Displays the dialog as modal to block further user interaction until dismissed. * @@ -78,7 +79,7 @@ public DialogInterception(Resupply resupply, @Nullable Force targetConvoy) { final Campaign campaign = resupply.getCampaign(); final AtBContract contract = resupply.getContract(); - setTitle(resources.getString("incomingTransmission.title")); + setTitle(getFormattedTextAt(RESOURCE_BUNDLE, "incomingTransmission.title")); // Main Panel to hold both boxes JPanel mainPanel = new JPanel(new GridBagLayout()); @@ -104,7 +105,7 @@ public DialogInterception(Resupply resupply, @Nullable Force targetConvoy) { speakerName = speaker.getFullTitle(); } else { if (targetConvoy == null) { - speakerName = String.format(resources.getString("dialogBorderConvoySpeakerDefault.text"), + speakerName = getFormattedTextAt(RESOURCE_BUNDLE, "dialogBorderConvoySpeakerDefault.text", contract.getEmployerName(campaign.getGameYear())); } else { speakerName = campaign.getName(); @@ -147,18 +148,16 @@ public DialogInterception(Resupply resupply, @Nullable Force targetConvoy) { if (targetConvoy != null) { if (forceContainsOnlyVTOLForces(campaign, targetConvoy) || forceContainsOnlyAerialForces(campaign, targetConvoy)) { - message = String.format( - resources.getString("statusUpdateIntercepted.boilerplate"), + message = getFormattedTextAt(RESOURCE_BUNDLE, "statusUpdateIntercepted.boilerplate", campaign.getCommanderAddress(false), - resources.getString("interceptionInstructions.text")); + getFormattedTextAt(RESOURCE_BUNDLE,"interceptionInstructions.text")); } } if (message.isBlank()) { - message = String.format( - resources.getString("statusUpdateIntercepted" + randomInt(20) + ".text"), + message = getFormattedTextAt(RESOURCE_BUNDLE, "statusUpdateIntercepted" + randomInt(20) + ".text", campaign.getCommanderAddress(false), - resources.getString("interceptionInstructions.text")); + getFormattedTextAt(RESOURCE_BUNDLE, "interceptionInstructions.text")); } JLabel rightDescription = new JLabel( @@ -179,7 +178,7 @@ public DialogInterception(Resupply resupply, @Nullable Force targetConvoy) { // Buttons panel JPanel buttonPanel = new JPanel(); - JButton confirmButton = new JButton(resources.getString("logisticsReceived.text")); + JButton confirmButton = new JButton(getFormattedTextAt(RESOURCE_BUNDLE, "logisticsReceived.text")); confirmButton.addActionListener(e -> dispose()); buttonPanel.add(confirmButton); @@ -191,7 +190,7 @@ public DialogInterception(Resupply resupply, @Nullable Force targetConvoy) { JLabel lblInfo = new JLabel( String.format("
%s
", RIGHT_WIDTH + LEFT_WIDTH, - String.format(resources.getString("documentation.prompt")))); + getFormattedTextAt(RESOURCE_BUNDLE, "documentation.prompt"))); lblInfo.setHorizontalAlignment(SwingConstants.CENTER); infoPanel.add(lblInfo, BorderLayout.CENTER); infoPanel.setBorder(BorderFactory.createEtchedBorder()); diff --git a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogItinerary.java b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogItinerary.java index d0b7c4aa851..c4fe537abfb 100644 --- a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogItinerary.java +++ b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogItinerary.java @@ -32,7 +32,6 @@ import javax.swing.*; import java.awt.*; import java.util.List; -import java.util.ResourceBundle; import static javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE; import static megamek.common.Compute.randomInt; @@ -50,13 +49,14 @@ import static mekhq.gui.dialog.resupplyAndCaches.ResupplyDialogUtilities.formatColumnData; import static mekhq.gui.dialog.resupplyAndCaches.ResupplyDialogUtilities.getEnemyFactionReference; import static mekhq.utilities.ImageUtilities.scaleImageIconToWidth; +import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; /** * The {@code DialogItinerary} class generates and displays dialogs related to resupply operations. * These include normal resupply, looting, contract-ending resupply, and smuggler-related resupplies. */ public class DialogItinerary { - private static final ResourceBundle resources = ResourceBundle.getBundle("mekhq.resources.Resupply"); + private static final String RESOURCE_BUNDLE = "mekhq.resources.Resupply"; /** * Displays a detailed itinerary dialog based on the type of resupply operation. The dialog @@ -86,7 +86,7 @@ public static void itineraryDialog(Resupply resupply) { final int DIALOG_WIDTH = UIUtil.scaleForGUI(700); // Retrieves the title from the resources - String title = resources.getString("dialog.title"); + String title = getFormattedTextAt(RESOURCE_BUNDLE, "dialog.title"); // Create a custom dialog JDialog dialog = new JDialog(); @@ -111,7 +111,7 @@ public static void itineraryDialog(Resupply resupply) { speakerIcon = getSpeakerIcon(campaign, speaker); speakerIcon = scaleImageIconToWidth(speakerIcon, 100); } else if (resupplyType.equals(RESUPPLY_SMUGGLER)) { - speakerName = resources.getString("guerrillaSpeaker.text"); + speakerName = getFormattedTextAt(RESOURCE_BUNDLE, "guerrillaSpeaker.text"); speakerIcon = getFactionLogo(campaign, "PIR", true); speakerIcon = scaleImageIconToWidth(speakerIcon, 200); @@ -147,7 +147,7 @@ public static void itineraryDialog(Resupply resupply) { JPanel descriptionPanel = new JPanel(); descriptionPanel.setBorder(BorderFactory.createTitledBorder( - String.format(resources.getString("dialogBorderTitle.text"), speakerName))); + getFormattedTextAt(RESOURCE_BUNDLE, "dialogBorderTitle.text", speakerName))); descriptionPanel.add(description); // Create the main panel to hold the description and image @@ -163,11 +163,11 @@ public static void itineraryDialog(Resupply resupply) { scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); // Create the buttons and add their action listeners - JButton confirmButton = new JButton(resources.getString("confirmAccept.text")); + JButton confirmButton = new JButton(getFormattedTextAt(RESOURCE_BUNDLE, "confirmAccept.text")); confirmButton.addActionListener(e -> { dialog.dispose(); campaign.getFinances().debit(EQUIPMENT_PURCHASE, campaign.getLocalDate(), - resupply.getConvoyContentsValueCalculated(), resources.getString("smugglerFee.text")); + resupply.getConvoyContentsValueCalculated(), getFormattedTextAt(RESOURCE_BUNDLE, "smugglerFee.text")); if (resupplyType.equals(RESUPPLY_SMUGGLER)) { makeSmugglerDelivery(resupply); @@ -177,7 +177,7 @@ public static void itineraryDialog(Resupply resupply) { final List convoyContents = resupply.getConvoyContents(); if (!convoyContents.isEmpty()) { - campaign.addReport(String.format(resources.getString("convoyInsufficientSize.text"))); + campaign.addReport(getFormattedTextAt(RESOURCE_BUNDLE, "convoyInsufficientSize.text")); for (Part part : convoyContents) { campaign.addReport("- " + part.getName()); @@ -189,10 +189,10 @@ public static void itineraryDialog(Resupply resupply) { } }); - JButton refuseButton = new JButton(resources.getString("confirmRefuse.text")); + JButton refuseButton = new JButton(getFormattedTextAt(RESOURCE_BUNDLE, "confirmRefuse.text")); refuseButton.addActionListener(evt -> dialog.dispose()); - JButton okButton = new JButton(resources.getString("confirmReceipt.text")); + JButton okButton = new JButton(getFormattedTextAt(RESOURCE_BUNDLE, "confirmReceipt.text")); okButton.addActionListener(evt -> { dialog.dispose(); makeDelivery(resupply, null); @@ -214,9 +214,8 @@ public static void itineraryDialog(Resupply resupply) { infoPanel.setBorder(BorderFactory.createEtchedBorder()); JLabel lblInfo = new JLabel( String.format("
%s
%s
", - DIALOG_WIDTH, - String.format(resources.getString("roleplayItems.prompt")), - String.format(resources.getString("documentation.prompt")))); + DIALOG_WIDTH, getFormattedTextAt(RESOURCE_BUNDLE, "roleplayItems.prompt"), + getFormattedTextAt(RESOURCE_BUNDLE, "documentation.prompt"))); infoPanel.add(lblInfo); // Create a container panel to hold both buttonPanel and infoPanel @@ -271,16 +270,16 @@ private static void generateRoleplayItems(Campaign campaign, List partsR // These are all roleplay items that have no tangible benefit if (rationPacks > 0) { - partsReport.add("" + resources.getString("resourcesRations.text") + partsReport.add("" + getFormattedTextAt(RESOURCE_BUNDLE, "resourcesRations.text") + " x" + rationPacks + ""); } if (medicalSupplies > 0) { - partsReport.add("" + resources.getString("resourcesMedical.text") + partsReport.add("" + getFormattedTextAt(RESOURCE_BUNDLE, "resourcesMedical.text") + " x" + medicalSupplies + ""); } - partsReport.add("" + resources.getString("resourcesRoleplay" + randomInt(50) + partsReport.add("" + getFormattedTextAt(RESOURCE_BUNDLE, "resourcesRoleplay" + randomInt(50) + ".text") + " x" + (randomInt((int) Math.ceil((double) rationPacks / 5)) + 1) + ""); } @@ -314,38 +313,26 @@ private static String getInitialDescription(Resupply resupply) { AtBContract contract = resupply.getContract(); AtBMoraleLevel morale = contract.getMoraleLevel(); - String message = resources.getString(morale.toString().toLowerCase() + "Supplies" - + randomInt(20) + ".text"); - - String value = String.format(resources.getString("supplyCostFull.text"), - resupply.getConvoyContentsValueCalculated().toAmountAndSymbolString(), - resupply.getConvoyContentsValueBase().toAmountAndSymbolString()); - - yield String.format(message, value); - } - case RESUPPLY_LOOT -> { - String message = resources.getString("salvaged" + randomInt(10) + ".text"); - - String value = String.format(resources.getString("supplyCostAbridged.text"), - resupply.getConvoyContentsValueBase().toAmountAndSymbolString()); - - yield String.format(message, value); - } - case RESUPPLY_CONTRACT_END -> { - String message = resources.getString("looted" + randomInt(10) + ".text"); - - String value = String.format(resources.getString("supplyCostAbridged.text"), - resupply.getConvoyContentsValueBase().toAmountAndSymbolString()); - - yield String.format(message, value); + yield getFormattedTextAt(RESOURCE_BUNDLE, + morale.toString().toLowerCase() + "Supplies" + randomInt(20) + ".text", + getFormattedTextAt(RESOURCE_BUNDLE, "supplyCostFull.text", + resupply.getConvoyContentsValueCalculated().toAmountAndSymbolString(), + resupply.getConvoyContentsValueBase().toAmountAndSymbolString())); } + case RESUPPLY_LOOT -> getFormattedTextAt(RESOURCE_BUNDLE, + "salvaged" + randomInt(10) + ".text", + getFormattedTextAt(RESOURCE_BUNDLE, "supplyCostAbridged.text", + resupply.getConvoyContentsValueBase().toAmountAndSymbolString())); + case RESUPPLY_CONTRACT_END -> getFormattedTextAt(RESOURCE_BUNDLE, + "looted" + randomInt(10) + ".text", + getFormattedTextAt(RESOURCE_BUNDLE, "supplyCostAbridged.text", + resupply.getConvoyContentsValueBase().toAmountAndSymbolString())); case RESUPPLY_SMUGGLER -> { - String value = String.format(resources.getString("supplyCostFull.text"), + String value = getFormattedTextAt(RESOURCE_BUNDLE, "supplyCostFull.text", resupply.getConvoyContentsValueCalculated().toAmountAndSymbolString(), resupply.getConvoyContentsValueBase().toAmountAndSymbolString()); - yield String.format( - resources.getString("guerrillaSupplies" + randomInt(25) + ".text"), + yield getFormattedTextAt(RESOURCE_BUNDLE, "guerrillaSupplies" + randomInt(25) + ".text", campaign.getCommanderAddress(true), getEnemyFactionReference(resupply), resupply.getConvoyContentsValueCalculated().toAmountAndSymbolString(), value); } diff --git a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogPlayerConvoyOption.java b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogPlayerConvoyOption.java index efa53c36f93..f16100b759b 100644 --- a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogPlayerConvoyOption.java +++ b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogPlayerConvoyOption.java @@ -32,6 +32,7 @@ import static mekhq.gui.baseComponents.MHQDialogImmersive.getSpeakerDescription; import static mekhq.gui.baseComponents.MHQDialogImmersive.getSpeakerIcon; import static mekhq.utilities.ImageUtilities.scaleImageIconToWidth; +import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; /** * The {@code DialogPlayerConvoyOption} class provides functionality to display a dialog @@ -44,7 +45,7 @@ public class DialogPlayerConvoyOption extends JDialog { final int RIGHT_WIDTH = UIUtil.scaleForGUI(400); final int INSERT_SIZE = UIUtil.scaleForGUI(10); - private static final ResourceBundle resources = ResourceBundle.getBundle("mekhq.resources.Resupply"); + private static final String RESOURCE_BUNDLE = "mekhq.resources.Resupply"; /** * Displays a dialog that allows the player to decide if they want to use their own convoy for resupply. @@ -87,7 +88,7 @@ public class DialogPlayerConvoyOption extends JDialog { public DialogPlayerConvoyOption(Resupply resupply, boolean forcedUseOfPlayerConvoy) { final Campaign campaign = resupply.getCampaign(); - setTitle(resources.getString("incomingTransmission.title")); + setTitle(getFormattedTextAt(RESOURCE_BUNDLE, "incomingTransmission.title")); // Main Panel to hold both boxes JPanel mainPanel = new JPanel(new GridBagLayout()); @@ -148,17 +149,18 @@ public DialogPlayerConvoyOption(Resupply resupply, boolean forcedUseOfPlayerConv String message; if (forcedUseOfPlayerConvoy) { - messageResource = resources.getString("usePlayerConvoyForced.text"); + messageResource = "usePlayerConvoyForced.text"; - message = String.format(messageResource, campaign.getCommanderAddress(false), - resupply.getTargetCargoTonnagePlayerConvoy(), resupply.getTotalPlayerCargoCapacity(), - playerConvoyCount, pluralizer, pluralizer); + message = getFormattedTextAt(RESOURCE_BUNDLE, messageResource, + campaign.getCommanderAddress(false), resupply.getTargetCargoTonnagePlayerConvoy(), + resupply.getTotalPlayerCargoCapacity(), playerConvoyCount, pluralizer); } else { - messageResource = resources.getString("usePlayerConvoyOptional.text"); + messageResource = "usePlayerConvoyOptional.text"; - message = String.format(messageResource, campaign.getCommanderAddress(false), - resupply.getTargetCargoTonnagePlayerConvoy(), resupply.getTotalPlayerCargoCapacity(), - playerConvoyCount, pluralizer, resupply.getTargetCargoTonnage(), pluralizer); + message = getFormattedTextAt(RESOURCE_BUNDLE, messageResource, + campaign.getCommanderAddress(false), resupply.getTargetCargoTonnagePlayerConvoy(), + resupply.getTotalPlayerCargoCapacity(), playerConvoyCount, pluralizer, + resupply.getTargetCargoTonnage()); } JLabel rightDescription = new JLabel( @@ -181,7 +183,7 @@ public DialogPlayerConvoyOption(Resupply resupply, boolean forcedUseOfPlayerConv JPanel buttonPanel = new JPanel(); // Create the buttons and add their action listener. - JButton acceptButton = new JButton(resources.getString("confirmAccept.text")); + JButton acceptButton = new JButton(getFormattedTextAt(RESOURCE_BUNDLE,"confirmAccept.text")); acceptButton.addActionListener(e -> { dispose(); resupply.setUsePlayerConvoy(true); @@ -189,7 +191,7 @@ public DialogPlayerConvoyOption(Resupply resupply, boolean forcedUseOfPlayerConv acceptButton.setEnabled(playerConvoyCount > 0); buttonPanel.add(acceptButton); - JButton refuseButton = new JButton(resources.getString("confirmRefuse.text")); + JButton refuseButton = new JButton(getFormattedTextAt(RESOURCE_BUNDLE,"confirmRefuse.text")); refuseButton.addActionListener(e -> { dispose(); resupply.setUsePlayerConvoy(false); @@ -204,7 +206,7 @@ public DialogPlayerConvoyOption(Resupply resupply, boolean forcedUseOfPlayerConv JLabel lblInfo = new JLabel( String.format("
%s
", RIGHT_WIDTH + LEFT_WIDTH, - String.format(resources.getString("documentation.prompt")))); + getFormattedTextAt(RESOURCE_BUNDLE, "documentation.prompt"))); lblInfo.setHorizontalAlignment(SwingConstants.CENTER); infoPanel.add(lblInfo, BorderLayout.CENTER); infoPanel.setBorder(BorderFactory.createEtchedBorder()); diff --git a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogResupplyFocus.java b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogResupplyFocus.java index 9c6d881ede7..b60db449720 100644 --- a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogResupplyFocus.java +++ b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogResupplyFocus.java @@ -26,11 +26,11 @@ import javax.swing.*; import java.awt.*; -import java.util.ResourceBundle; import static mekhq.gui.baseComponents.MHQDialogImmersive.getSpeakerDescription; import static mekhq.gui.baseComponents.MHQDialogImmersive.getSpeakerIcon; import static mekhq.utilities.ImageUtilities.scaleImageIconToWidth; +import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; /** * The {@code DialogResupplyFocus} class is responsible for displaying a dialog that allows @@ -43,7 +43,7 @@ public class DialogResupplyFocus extends JDialog { final int RIGHT_WIDTH = UIUtil.scaleForGUI(400); final int INSERT_SIZE = UIUtil.scaleForGUI(10); - private static final ResourceBundle resources = ResourceBundle.getBundle("mekhq.resources.Resupply"); + private static final String RESOURCE_BUNDLE = "mekhq.resources.Resupply"; /** * Displays a dialog to let the player select a resupply focus. The available options include: @@ -80,7 +80,7 @@ public class DialogResupplyFocus extends JDialog { public DialogResupplyFocus(Resupply resupply) { final Campaign campaign = resupply.getCampaign(); - setTitle(resources.getString("incomingTransmission.title")); + setTitle(getFormattedTextAt(RESOURCE_BUNDLE, "incomingTransmission.title")); // Main Panel to hold both boxes JPanel mainPanel = new JPanel(new GridBagLayout()); @@ -135,7 +135,7 @@ public DialogResupplyFocus(Resupply resupply) { JPanel rightBox = new JPanel(new BorderLayout()); rightBox.setBorder(BorderFactory.createEtchedBorder()); - String message = String.format(resources.getString("focusDescription.text"), + String message = getFormattedTextAt(RESOURCE_BUNDLE, "focusDescription.text", campaign.getCommanderAddress(false)); JLabel rightDescription = new JLabel( @@ -156,8 +156,8 @@ public DialogResupplyFocus(Resupply resupply) { // Buttons panel JPanel buttonPanel = new JPanel(); - JButton optionBalanced = new JButton(resources.getString("optionBalanced.text")); - optionBalanced.setToolTipText(resources.getString("optionBalanced.tooltip")); + JButton optionBalanced = new JButton(getFormattedTextAt(RESOURCE_BUNDLE, "optionBalanced.text")); + optionBalanced.setToolTipText(getFormattedTextAt(RESOURCE_BUNDLE, "optionBalanced.tooltip")); optionBalanced.addActionListener(e -> { dispose(); // The Resupply class initialization assumes a balanced approach @@ -167,8 +167,8 @@ public DialogResupplyFocus(Resupply resupply) { // The player should not be able to focus on parts for game balance reasons. // If the player could pick parts, the optimum choice would be to always pick parts. - JButton optionArmor = new JButton(resources.getString("optionArmor.text")); - optionArmor.setToolTipText(resources.getString("optionArmor.tooltip")); + JButton optionArmor = new JButton(getFormattedTextAt(RESOURCE_BUNDLE, "optionArmor.text")); + optionArmor.setToolTipText(getFormattedTextAt(RESOURCE_BUNDLE, "optionArmor.tooltip")); optionArmor.addActionListener(e -> { dispose(); resupply.setFocusAmmo(0); @@ -177,8 +177,8 @@ public DialogResupplyFocus(Resupply resupply) { }); buttonPanel.add(optionArmor); - JButton optionAmmo = new JButton(resources.getString("optionAmmo.text")); - optionAmmo.setToolTipText(resources.getString("optionAmmo.tooltip")); + JButton optionAmmo = new JButton(getFormattedTextAt(RESOURCE_BUNDLE, "optionAmmo.text")); + optionAmmo.setToolTipText(getFormattedTextAt(RESOURCE_BUNDLE, "optionAmmo.tooltip")); optionAmmo.addActionListener(e -> { dispose(); resupply.setFocusAmmo(0.75); @@ -195,7 +195,7 @@ public DialogResupplyFocus(Resupply resupply) { JLabel lblInfo = new JLabel( String.format("
%s
", RIGHT_WIDTH + LEFT_WIDTH, - String.format(resources.getString("documentation.prompt")))); + getFormattedTextAt(RESOURCE_BUNDLE, "documentation.prompt"))); lblInfo.setHorizontalAlignment(SwingConstants.CENTER); infoPanel.add(lblInfo, BorderLayout.CENTER); infoPanel.setBorder(BorderFactory.createEtchedBorder()); diff --git a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogRoleplayEvent.java b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogRoleplayEvent.java index 3aed64a44ca..645107bb5cd 100644 --- a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogRoleplayEvent.java +++ b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogRoleplayEvent.java @@ -25,12 +25,12 @@ import javax.swing.*; import java.awt.*; -import java.util.ResourceBundle; import java.util.UUID; import static mekhq.gui.baseComponents.MHQDialogImmersive.getSpeakerDescription; import static mekhq.gui.baseComponents.MHQDialogImmersive.getSpeakerIcon; import static mekhq.utilities.ImageUtilities.scaleImageIconToWidth; +import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; /** * The {@code DialogRoleplayEvent} class handles the creation and display of roleplay event dialogs @@ -42,7 +42,7 @@ public class DialogRoleplayEvent extends JDialog { final int RIGHT_WIDTH = UIUtil.scaleForGUI(400); final int INSERT_SIZE = UIUtil.scaleForGUI(10); - private static final ResourceBundle resources = ResourceBundle.getBundle("mekhq.resources.Resupply"); + private static final String RESOURCE_BUNDLE = "mekhq.resources.Resupply"; /** * Displays a roleplay event dialog for a player convoy. The dialog is used to present messages related @@ -81,7 +81,7 @@ public class DialogRoleplayEvent extends JDialog { * placeholders ({@code %s}) to dynamically incorporate campaign-specific details. */ public DialogRoleplayEvent(Campaign campaign, Force playerConvoy, String eventText) { - setTitle(resources.getString("incomingTransmission.title")); + setTitle(getFormattedTextAt(RESOURCE_BUNDLE, "incomingTransmission.title")); // Main Panel to hold both boxes JPanel mainPanel = new JPanel(new GridBagLayout()); @@ -137,11 +137,9 @@ public DialogRoleplayEvent(Campaign campaign, Force playerConvoy, String eventTe JPanel rightBox = new JPanel(new BorderLayout()); rightBox.setBorder(BorderFactory.createEtchedBorder()); - String message = String.format(eventText, campaign.getCommanderAddress(false)); - JLabel rightDescription = new JLabel( String.format("
%s
", - RIGHT_WIDTH, message)); + RIGHT_WIDTH, eventText)); rightBox.add(rightDescription); // Add rightBox to mainPanel @@ -157,7 +155,7 @@ public DialogRoleplayEvent(Campaign campaign, Force playerConvoy, String eventTe // Buttons panel JPanel buttonPanel = new JPanel(); - JButton confirmButton = new JButton(resources.getString("convoyConfirm.text")); + JButton confirmButton = new JButton(getFormattedTextAt(RESOURCE_BUNDLE, "convoyConfirm.text")); confirmButton.addActionListener(e -> dispose()); buttonPanel.add(confirmButton); @@ -169,7 +167,7 @@ public DialogRoleplayEvent(Campaign campaign, Force playerConvoy, String eventTe JLabel lblInfo = new JLabel( String.format("
%s
", RIGHT_WIDTH + LEFT_WIDTH, - String.format(resources.getString("documentation.prompt")))); + getFormattedTextAt(RESOURCE_BUNDLE, "documentation.prompt"))); lblInfo.setHorizontalAlignment(SwingConstants.CENTER); infoPanel.add(lblInfo, BorderLayout.CENTER); infoPanel.setBorder(BorderFactory.createEtchedBorder()); diff --git a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogSwindled.java b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogSwindled.java index 682d854854d..5ee99906a0b 100644 --- a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogSwindled.java +++ b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogSwindled.java @@ -26,11 +26,11 @@ import javax.swing.*; import java.awt.*; -import java.util.ResourceBundle; import static mekhq.campaign.universe.Factions.getFactionLogo; import static mekhq.gui.dialog.resupplyAndCaches.ResupplyDialogUtilities.getEnemyFactionReference; import static mekhq.utilities.ImageUtilities.scaleImageIconToWidth; +import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; /** * The {@code DialogSwindled} class provides functionality to display a dialog related to swindling events @@ -42,7 +42,7 @@ public class DialogSwindled extends JDialog { final int RIGHT_WIDTH = UIUtil.scaleForGUI(400); final int INSERT_SIZE = UIUtil.scaleForGUI(10); - private static final ResourceBundle resources = ResourceBundle.getBundle("mekhq.resources.Resupply"); + private static final String RESOURCE_BUNDLE = "mekhq.resources.Resupply"; /** * Displays a dialog notifying the player that they have been swindled during a resupply. @@ -85,7 +85,7 @@ public class DialogSwindled extends JDialog { public DialogSwindled(Resupply resupply) { final Campaign campaign = resupply.getCampaign(); - setTitle(resources.getString("incomingTransmission.title")); + setTitle(getFormattedTextAt(RESOURCE_BUNDLE, "incomingTransmission.title")); // Main Panel to hold both boxes JPanel mainPanel = new JPanel(new GridBagLayout()); @@ -102,7 +102,7 @@ public DialogSwindled(Resupply resupply) { // Get speaker details final RandomCallsignGenerator callsignGenerator = RandomCallsignGenerator.getInstance(); String smugglerCallSign = callsignGenerator.generate(); - String smugglerTitle = resources.getString("guerrillaSpeaker.text"); + String smugglerTitle = getFormattedTextAt(RESOURCE_BUNDLE, "guerrillaSpeaker.text"); String speakerName = String.format("'%s'
%s", smugglerCallSign, smugglerTitle); ImageIcon speakerIcon = getFactionLogo(campaign, "PIR", true); @@ -133,8 +133,8 @@ public DialogSwindled(Resupply resupply) { rightBox.setBorder(BorderFactory.createEtchedBorder()); String enemyFactionReference = getEnemyFactionReference(resupply); - String message = String.format( - resources.getString("guerrillaSwindled" + Compute.randomInt(25) + ".text"), + String message = getFormattedTextAt(RESOURCE_BUNDLE, + "guerrillaSwindled" + Compute.randomInt(25) + ".text", campaign.getCommanderAddress(true), enemyFactionReference); @@ -156,7 +156,7 @@ public DialogSwindled(Resupply resupply) { // Buttons panel JPanel buttonPanel = new JPanel(); - JButton confirmButton = new JButton(resources.getString("logisticsDestroyed.text")); + JButton confirmButton = new JButton(getFormattedTextAt(RESOURCE_BUNDLE, "logisticsDestroyed.text")); confirmButton.addActionListener(e -> dispose()); buttonPanel.add(confirmButton); @@ -168,7 +168,7 @@ public DialogSwindled(Resupply resupply) { JLabel lblInfo = new JLabel( String.format("
%s
", RIGHT_WIDTH + LEFT_WIDTH, - String.format(resources.getString("documentation.prompt")))); + getFormattedTextAt(RESOURCE_BUNDLE, "documentation.prompt"))); lblInfo.setHorizontalAlignment(SwingConstants.CENTER); infoPanel.add(lblInfo, BorderLayout.CENTER); infoPanel.setBorder(BorderFactory.createEtchedBorder()); From d851dfab9ecb62cb7576099218fb9e97b538b60e Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sun, 26 Jan 2025 19:09:52 -0600 Subject: [PATCH 018/112] Incorporated edits from #5888 - Credit: UlyssesSockdrawer --- .../mekhq/resources/Resupply.properties | 153 +++++++++--------- 1 file changed, 75 insertions(+), 78 deletions(-) diff --git a/MekHQ/resources/mekhq/resources/Resupply.properties b/MekHQ/resources/mekhq/resources/Resupply.properties index 5d02e825f6d..c71c6d5a4e8 100644 --- a/MekHQ/resources/mekhq/resources/Resupply.properties +++ b/MekHQ/resources/mekhq/resources/Resupply.properties @@ -912,13 +912,13 @@ statusUpdate5.text=Sorry for the blackout, {0}, radio antenna was bent by a low- \ additional issues. All units are maintaining contact, with regular check-ins confirmed. The\ \ convoy is proceeding as planned, with no deviation from the established route. No delays are\ \ expected at this time. -statusUpdate6.text=Detour forced by fallen trees, {0}. The arrangement seems too neat to be mere\ - \ chance - perhaps it's nature, or perhaps it's enemy sappers. Crew members swear they've seen\ - \ movement in the treeline, dark shapes that vanish before they can be confirmed. We're advancing\ - \ cautiously, weapons ready, each rustle of leaves amplified by the silence that follows. There's\ - \ a growing sense that we're being funneled somewhere, like prey unaware of the hunter. Spirits\ - \ are uneasy, but discipline holds for now. ETA still stands at 18 hours, but the sense of dread\ - \ is growing. +statusUpdate6.text=We've had to take a detour due to trees blocking the road, {0}.\ + \ Way too suspicious to just be a freak of nature. I've sent patrols out into the woods after\ + \ crew members reported they saw movement in the treeline.\ + \ I'm going to go out with the next patrol whilst the techs keep clearing the road.\ + \ If I had to take a guess, {0}, the OpFor is trying to canalize us onto an ambush route further\ + \ ahead. I've ordered all escort forces to extend the perimeter and switch through all sensor bands.\ + \ ETA still stands at 18 hours, provided we get the blockage cleared on time. statusUpdate7.text=Water levels have reached critical, {0}. Hydration rations have been adjusted to\ \ extend remaining supplies. The crew is feeling the strain, but they understand the necessity.\ \ We're pushing forward, with all personnel adhering to updated water protocols. An adjusted ETA\ @@ -930,10 +930,10 @@ statusUpdate8.text={0}, we just left the remains of a bombed out village. We saw \ through a world that's lost its meaning. There's no room for compassion here, only survival.\ \ ETA to delivery is 18 hours, but the emptiness remains. statusUpdate9.text=Arrived at an allied field camp recently hit by enemy 'Meks, {0}. The MedTechs\ - \ were busy with fresh casualties, working swiftly in the smoky air filled with urgency. We\ - \ dropped off our supplies, before pushing ahead. The crew is visibly shaken by what they saw,\ - \ the 'Meks were using Infernos. but their focus is unbroken. We press on. ETA to delivery is 17\ - \ hours. + \ were busy with fresh casualties. These guys got messed up here, {0}. I got out and had a talk\ + \ with their commander whilst we dropped off our supplies. Some real nasty work here -\ + \ the 'Meks were using Infernos. Even the chatty guys on the crew are quiet now. ETA to delivery is 17\ + \ hours. statusUpdate10.text=A small group of refugees tried to approach, {0}, but we had to speed up for\ \ security reasons. We're not running a charity service, after all. The crew's gone quiet again,\ \ trying to swallow the bitter truth that survival trumps sympathy out here. The guilt is heavy.\ @@ -952,47 +952,44 @@ statusUpdate13.text=Reached an allied checkpoint, {0}. The soldiers took the sup \ countless times - quick exchanges under grim circumstances. We're moving on, but the mood is\ \ heavy. The crew feels it, a sense of helplessness that lingers long after we leave each\ \ checkpoint behind. ETA to delivery is 17 hours. -statusUpdate14.text=Recon vehicle experienced a sudden sensor glitch, {0} - a brief but total blind\ - \ spot that left us vulnerable. Cause is unknown, and the crew is on edge, suspecting sabotage.\ - \ The momentary blindness felt like more than just a malfunction; it was a gap, an invitation for\ - \ disaster. The techs patched it up, but the fear of another unexpected failure lingers. We're\ - \ moving carefully, with extra scans and sensors active. ETA remains 18 hours, but the atmosphere\ - \ is heavy with paranoia. +statusUpdate14.text=Our lead vehicle experienced a sudden sensor glitch, {0} - a brief but total brown\ + \ out that left us vulnerable. Cause is unknown, and the crew is on edge, suspecting sabotage.\ + \ One of the guys started mouthing off over the comm about gremlins in the machinery - had to\ + \ put him on a charge to keep discipline. The techs patched it up, but its spooked the crew. We're\ + \ moving carefully, with extra scans and sensors active. ETA remains 18 hours, {0}\ statusUpdate15.text=Coolant levels dropped suddenly, {0}, forcing an unexpected halt. Diagnostics\ - \ traced the issue to a minor breach, likely caused by a stray AC/2 round from a previous\ - \ skirmish. Techs patched it up efficiently, using reinforced seals to prevent recurrence. Crew\ - \ is alert but remains relaxed - these sorts of issues are routine in active combat zones. Extra\ - \ coolant has been distributed among vehicles, and sensors are calibrated for closer monitoring.\ - \ There's even some banter about past coolant leaks during more intense battles. Morale is\ - \ strong, and we're pushing forward without any major delays. Crew knows this is all part of the\ - \ convoy grind. + \ traced the issue to a minor breach, likely caused by bottoming out on the rough roads here.\ + \ Techs patched it up efficiently, using some reinforced seals - fingers crossed. Crew\ + \ is alert but it's not too bad out here - everything feels pretty routine. Extra\ + \ coolant has been distributed among vehicles, and the techs put some extra diagnostics in.\ + \ There's even some banter about what the coolant does to your chance of having kids. Morale is\ + \ strong, and we're pushing forward without any major delays. Convoy is on schedule, {0}. statusUpdate16.text=Fuel's dropping rapidly, {0}. Rough terrain caught us off-guard, and retreating\ - \ enemy 'Meks left craters along the roads, damaging key routes. We're making adjustments to\ - \ stretch our reserves, but it's critical now. We've implemented lower-speed settings to maximize\ - \ efficiency, but that won't last if more combat breaks out. Crew's maintaining focus, with\ - \ everyone aware of the urgency. There's some chatter about potential fuel resupply options,\ - \ but we're far from any friendly base. We've sent a recon hovercraft ahead to scout for possible\ - \ emergency caches or uncharted fueling points. No delays projected yet, but the situation is\ - \ precarious. Crew's prepared for defensive actions if we encounter hostiles during fuel foraging. + \ enemy 'Meks left craters along the roads to try and cut key routes. We're making adjustments to\ + \ stretch our reserves, but it's critical now. I've slowed our pace to maximize\ + \ efficiency, but that won't last if more combat breaks out.\ + \ There's some chatter amongst the crews about potential fuel resupply options,\ + \ but we're far from any friendly base. We've sent a recon patrol ahead to scout for possible\ + \ emergency caches or civilian fuel stations. No delays projected yet, but the situation is\ + \ precarious. Crew's prepared for defensive actions if we encounter hostiles during fuel foraging. statusUpdate17.text=Civilians waved us down, as we left through the last checkpoint, {0}. They were\ - \ begging for medical supplies, but we didn't stop. Their disappointment was clear, but the crew\ - \ remained resolute - our orders leave no room for deviations. Morale is affected, but the\ - \ mission's priority remains unchanged. ETA to delivery is 18 hours, and the convoy maintains\ - \ operational efficiency. -statusUpdate18.text=An allied logistics unit requested spare parts, {0}. The convoy maintained its\ - \ course, adhering strictly to the schedule. The crew knows the importance of keeping pace, and\ - \ there's no room for deviations. ETA to delivery is 17 hours, with no expected delays. + \ begging for medical supplies, but we didn't stop. Their disappointment was clear, and a few\ + \ tried to block the route. The checkpoint guards moved them off with warning shots. It shook up\ + \ my guys but we're still en route. ETA to delivery is 18 hours, {0} +statusUpdate18.text=An allied logistics unit requested spare parts, {0}. I refused the request,\ + \ my guess is they were trying to shark us out of our gear. We're still en route, and\ + \ they've got their own supply lines. ETA to delivery is 17 hours, with no expected delays. statusUpdate19.text=Missing fuel traced to a punctured tank, {0} - likely damage from stray laser\ \ fire during a previous skirmish. We're redistributing remaining fuel reserves across the convoy\ \ to maintain movement. The situation is tight, but so far, no delays are projected. Crew's\ \ prepping for possible emergency refueling if needed. -statusUpdate20.text=Located missing crates near an abandoned checkpoint, {0}. Evidence suggests a\ - \ previous supply convoy came under heavy fire and had to abandon cargo during their retreat. We\ - \ secured what we could, including ammunition crates and medical supplies. Techs believe some\ - \ crates contain Class-C Coolant, which could come in useful. The team's moving steadily, though\ - \ the discovery has sparked concerns over enemy activity in the area. We've added patrols to\ - \ cover the rear in case of ambush. We'll assess the contents of the salvaged crates at the next\ - \ checkpoint. For now, we're staying cautious. +statusUpdate20.text=We came across some crates near an abandoned checkpoint, {0}. Evidence suggests a\ + \ previous supply convoy came under heavy fire and had to abandon cargo during their retreat. We\ + \ secured what we could, including ammo and medical supplies. Techs believe some\ + \ crates contain Class-C Coolant, which could come in useful. We're back en route now, though\ + \ the discovery has sparked concerns over enemy activity in the area. We've added patrols to\ + \ cover the rear in case of ambush. We'll assess the contents of the salvaged crates at the next\ + \ checkpoint. Convoy, out. statusUpdate21.text=Blocked pass ahead, {0} - fallen trees and debris, likely remnants of a recent\ \ skirmish. Clearing it took longer than expected, with scouts maintaining a constant watch for\ \ potential ambushes. It's a bleak routine - clearing, advancing, expecting the worst, and\ @@ -1026,49 +1023,49 @@ statusUpdate27.text=We linked up with an allied logistics convoy that needed bas \ feels pretty tense; everybody seems to feel like we could get jumped out here.\ \ ETA to delivery is 17 hours. statusUpdate28.text=Reached an allied outpost that showed clear signs of a recent skirmish, {0}.\ - \ Torn sandbags, bullet holes in the walls, and soldiers with dusty uniforms greeted us. Their\ - \ readiness was high despite the signs of battle still fresh around them. The supply handoff was\ - \ fast before we resumed our route. The crew is staying alert, eyes scanning the horizon for\ - \ potential threats. We maintain speed, knowing that vigilance is as important as delivery. ETA\ - \ to delivery remains 18 hours. + \ Torn sandbags, bullet holes in the walls, and soldiers with dusty uniforms greeted us. Their\ + \ readiness was high despite the signs of battle still fresh around them. The supply handoff was\ + \ fast before we resumed our route. The crew is staying alert, eyes scanning the horizon for\ + \ potential threats. Pretty easy going, but the crew are sleeping by their weapons on breaks. ETA\ + \ to delivery remains 18 hours. statusUpdate29.text=GPS signal cut out near a known ambush site, {0}. This could be a result of\ \ residual jamming from previous encounters. All units are proceeding with heightened caution,\ \ maintaining full situational awareness. Sensors have been recalibrated to account for potential\ \ interference, and the crew is prepared for ambush. No delays are expected, but all personnel\ \ will remain on alert until we clear the area. statusUpdate30.text=Civilians just crowded the convoy, {0}, desperate for water or some such. We had\ - \ to keep moving, because "rationing" doesn't include charity drops. The crew's trying to stay\ - \ focused, but despair has a way of seeping in when you least expect it. At least we're\ - \ efficient, if not empathetic. ETA to delivery holds at 17 hours. -statusUpdate31.text=Met an allied recon team, {0}. They were well-prepared, with gear in order and\ - \ weapons ready, but the fatigue was clear in their eyes. It's a burden we all share, each of us\ - \ carrying the same weariness that has become part of the mission. The encounter was brief, but\ - \ the crew feels the shared weight of it all, but the path ahead demands focus. ETA to delivery\ - \ is 17 hours, with no expected delays. -statusUpdate32.text=Saw children scavenging in the ruins of an old market as we passed, {0}. It was\ - \ hard to ignore, but the crew tried their best to focus on the mission. After all, emotional\ - \ detachment is the closest thing we've got to armor these days. Spirits are low, but the convoy\ - \ maintains speed. At least the engine hum drowns out the emptiness. ETA to delivery remains 17 hours. + \ to keep moving, because "rationing" doesn't include charity drops. The crew's trying to stay\ + \ focused, but a few of the guys think we should've done more for them. At least we're\ + \ efficient, if not empathetic. ETA to delivery holds at 17 hours. +statusUpdate31.text=We met an allied recon team, {0}, near Nav Point Gamma. They were patrolling our sector\ + \ and looked like they'd been out for some time. They told us they'd lost a few guys back down the road\ + \ and we shared a drink with them before moving on. The encounter was brief, but\ + \ the guys appreciated the chance to catch up with some fellow fighters out here. ETA to delivery\ + \ is 17 hours, with no expected delays, {0}. +statusUpdate32.text=We saw some children scavenging in the ruins of an old market as we passed, {0}.\ + \ It was hard to ignore, but the crews obeyed the NO STOP orders and kept moving through the town.\ + \ We had to stop the lead vehicle when a few of them ran out into the road - the guys didn't\ + \ like clearing them off. We're back moving now, {0}; ETA to delivery remains 17 hours. statusUpdate33.text=Landslides have hit the main cargo road, {0}, forcing us to reroute to a path\ \ closer to contested zones. It's a necessary risk, but one that adds to the sense of futility\ \ - rerouting, retreating, always adapting, yet never truly advancing. It's as if the road itself\ \ resists our passage. ETA remains at 18 hours, but the sense of inevitable confrontation looms. -statusUpdate34.text=Intercepted a faint distress signal, {0}. It's weak, barely a whisper among the\ - \ static. Could be an ally trapped, or it could be a trap set by the enemy. The tone of the\ - \ signal has an eerie, desperate quality, impossible to ignore. We've opened all channels, hoping\ - \ to verify its origin, but no joy. We're proceeding cautiously, weapons primed. No delays\ - \ expected, but the air is heavy with uncertainty. -statusUpdate35.text=Civilians tried to flag us down, {0}, hoping for help. We didn't stop, as the\ - \ risk to the convoy was too great. The crew barely reacted, their expressions indifferent.\ - \ Desperate faces lingered in the rear-cam for a moment before disappearing. It's a familiar\ - \ scene - lots of refugees on this route. It's shaken the crews a bit.\ - \ ETA to delivery holds at 17 hours, with no significant delays expected. -statusUpdate36.text=A woman carrying a sick child approached the convoy, hoping for help, {0}. We\ - \ had to keep moving, leaving her behind like so many others. It felt like another failure,\ - \ another weight added to the growing burden of this war. The crew fell silent, the reality of\ - \ our choices weighing heavily on everyone. It's a stark reminder of what we've become in the\ - \ name of duty - just another part of the machine, indifferent to the suffering left in its wake.\ - \ ETA to delivery is 17 hours, but the sense of loss remains. +statusUpdate34.text=My comm's guy intercepted a faint distress signal, {0}. It's weak, barely a whisper\ + \ in the static. Could be downed ally, or it could be a trap set by the enemy. Weird tone too,\ + \ it doesn't match up to any of the frequencies in the recent codebooks. We've focused the source, trying\ + \ to verify its origin, but no joy. We're proceeding at alert status. No delays\ + \ expected, but I've got a few jokers talking about ghosts on the crew now. +statusUpdate35.text=We had a few civilians try to flag us down, {0}, hoping for help. We didn't stop, as the\ + \ risk to the convoy was too great. With the way it's been going out here, it could've been an ambush.\ + \ The civvies are desperate out here, {0}. It's getting to be an all too familiar\ + \ scene on this route - lots of refugees. It's shaken the crews a bit.\ + \ ETA to delivery holds at 17 hours, with no significant delays expected. +statusUpdate36.text={0}, there's women and kids on the road through this town asking for help. I've\ + \ issued NO STOP orders to the convoy but it's rough out here. My crews are all pretty rattled\ + \ and complaining about not being able to halt and help these folks.\ + \ When we get back I need to speak to the logi' guys about our convoy route planning;\ + \ we've got to avoid these kinds of places or we'll start having real trouble in the crews.\ + \ Still on for our ETA, {0}, but it'll be tight. statusUpdate37.text=Reached an allied checkpoint that had just repelled an attack, {0}. The soldiers\ \ were still on high alert, eyes scanning the surroundings for any lingering threats. We were\ \ passed through the checkpoint quickly. ETA to delivery remains 18 hours, with all systems\ From 466fae4ce1da3172a249f4263e64902a2ff369d9 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Mon, 27 Jan 2025 13:26:51 -0600 Subject: [PATCH 019/112] Refactor currency handling and transition to generic "money" Replaced hardcoded C-Bill references with currency-agnostic terminology across the codebase, enabling support for multiple currencies. Improved the finance system by handling currency changes and implementing mechanisms to manage historical data compatibility. Added flexibility with new currency data and conversion logic. --- MekHQ/data/universe/currencies.xml | 39 +++++-- .../resources/CampaignExportWizard.properties | 2 +- .../CampaignOptionsDialog.properties | 8 +- .../mekhq/resources/Finances.properties | 2 +- .../mekhq/resources/FinancesTab.properties | 4 +- .../resources/mekhq/resources/GUI.properties | 16 +-- .../RetirementDefectionDialog.properties | 2 +- MekHQ/src/mekhq/campaign/Campaign.java | 19 +++- .../src/mekhq/campaign/finances/Currency.java | 4 + .../campaign/finances/CurrencyManager.java | 100 ++++++++++-------- .../src/mekhq/campaign/finances/Finances.java | 45 ++++++-- .../gui/dialog/PersonnelMarketDialog.java | 51 +++------ 12 files changed, 180 insertions(+), 112 deletions(-) diff --git a/MekHQ/data/universe/currencies.xml b/MekHQ/data/universe/currencies.xml index 19dc68280a4..8d4c0665db2 100644 --- a/MekHQ/data/universe/currencies.xml +++ b/MekHQ/data/universe/currencies.xml @@ -25,20 +25,47 @@ defaultEnd - Indicates which year the currency ended being the default currency - Star League Dollar + Kerensky + KSK + 0 + K + 2807 + 3152 + + + Fox Credit + SFC + 0 + Fox + 3133 + 999999 + true + + + Hegemony Dollar + HGD + 0 + HG$ + 1 + 2570 + true + + + Star Dollar SLD - 2 - SL$ + 0 + S$ 2571 2785 + true - ComStar bill + ComStar Bill CSB 0 C-Bill - 1 - 999999 + 2572 + 3132 true true diff --git a/MekHQ/resources/mekhq/resources/CampaignExportWizard.properties b/MekHQ/resources/mekhq/resources/CampaignExportWizard.properties index e83ec68e0d8..e240842280e 100644 --- a/MekHQ/resources/mekhq/resources/CampaignExportWizard.properties +++ b/MekHQ/resources/mekhq/resources/CampaignExportWizard.properties @@ -16,5 +16,5 @@ lblInstructions.PartCountSelection.text = Confirm Part Counts for Export lblInstructions.MiscSelection.text = Miscellaneous Export Settings lblInstructions.Finalize.text = Choose Export Destination lblStatus.Error.text = Part count should be an integer -lblMoney.text = Export C-Bills +lblMoney.text = Export Money btnUpdatePartCount.text = Update diff --git a/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties b/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties index 16f68d0e556..6c508b9d381 100644 --- a/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties +++ b/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties @@ -1328,14 +1328,14 @@ lblFinancesGeneralTab.text=General Options # createPaymentsPanel lblPaymentsPanel.text=Payments lblPayForPartsBox.text=Pay For Parts -lblPayForPartsBox.tooltip=Pay the C-Bill cost of any replacement parts +lblPayForPartsBox.tooltip=Pay the cost of any replacement parts lblPayForRepairsBox.text=Pay For Repairs lblPayForRepairsBox.tooltip=Repairs cost 20% of a part's list price. This is for equipment repairs\ \ only and doesn't count armor repairs.\
\
This is reimbursed by Battle Loss Compensation. lblPayForUnitsBox.text=Pay For Units -lblPayForUnitsBox.tooltip=Pay the C-Bill cost for any new units. +lblPayForUnitsBox.tooltip=Pay the cost for any new units. lblPayForSalariesBox.text=Pay For Salaries lblPayForSalariesBox.tooltip=Pay the monthly salaries of all personnel. lblPayForOverheadBox.text=Pay For Overhead (FM:Mr) @@ -1372,9 +1372,9 @@ lblNewFinancialYearFinancesToCSVExportBox.tooltip=This writes the finance table # createSalesPanel lblSalesPanel.text=Sales lblSellUnitsBox.text=Enable the Sale of Units -lblSellUnitsBox.tooltip=Units can be sold for C-bills. +lblSellUnitsBox.tooltip=Units can be sold. lblSellPartsBox.text=Enable the Sale of Parts -lblSellPartsBox.tooltip=Parts can be sold for C-bills. +lblSellPartsBox.tooltip=Parts can be sold. # createTaxesPanel lblTaxesPanel.text=Taxes \u270E diff --git a/MekHQ/resources/mekhq/resources/Finances.properties b/MekHQ/resources/mekhq/resources/Finances.properties index e980d2eeaec..ce3af86d451 100644 --- a/MekHQ/resources/mekhq/resources/Finances.properties +++ b/MekHQ/resources/mekhq/resources/Finances.properties @@ -87,7 +87,7 @@ TransactionType.UNIT_PURCHASE.toolTipText=A financial transaction where a unit w TransactionType.UNIT_SALE.text=Unit Sale(s) TransactionType.UNIT_SALE.toolTipText=A financial transaction where a unit was or multiple units were sold. TransactionType.BONUS_EXCHANGE.text=Bonus Exchange -TransactionType.BONUS_EXCHANGE.toolTipText=A financial transaction where Bonus Parts were exchanged for c-bills. +TransactionType.BONUS_EXCHANGE.toolTipText=A financial transaction where Bonus Parts were exchanged for money. ## Finances Files # Peacetime Operating Costs diff --git a/MekHQ/resources/mekhq/resources/FinancesTab.properties b/MekHQ/resources/mekhq/resources/FinancesTab.properties index 9d6df0ccfaf..7750f53340f 100644 --- a/MekHQ/resources/mekhq/resources/FinancesTab.properties +++ b/MekHQ/resources/mekhq/resources/FinancesTab.properties @@ -1,7 +1,7 @@ graphMonthlyRevenue.text=Monthly Revenue graphMonthlyExpenditures.text=Monthly Expenditures graphDate.text=Date -graphCBills.text=C-Bills +graphCBills.text=Money activeLoans.text=Active Loans -cbillsBalanceTime.text=C-Bills Balance Over Time +cbillsBalanceTime.text=Financial Balance Over Time monthlyRevenueExpenditures.text=Monthly Revenue and Expenditures \ No newline at end of file diff --git a/MekHQ/resources/mekhq/resources/GUI.properties b/MekHQ/resources/mekhq/resources/GUI.properties index b9400436f62..02439f0914c 100644 --- a/MekHQ/resources/mekhq/resources/GUI.properties +++ b/MekHQ/resources/mekhq/resources/GUI.properties @@ -1301,18 +1301,18 @@ financesPanel.title=Finances chkProcessFinances.text=Process Finances chkProcessFinances.toolTipText=Process finances during startup.
The company will always be left with a minimum of the starting float, although they may have to take out a significant loan if that option is enabled.
If disabled, the initial contract payment will be paid out normally. financialCreditsPanel.title=Credits -lblStartingCash.text=Starting C-Bills -lblStartingCash.toolTipText=The number of C-Bills to start with, minus expenses, if not randomizing the starting cash. -chkRandomizeStartingCash.text=Randomize Starting C-Bills -chkRandomizeStartingCash.toolTipText=This overrides the starting C-Bills with a random roll of nd6 million C-Bills, with the n specified below. -lblRandomStartingCashDiceCount.text=Random Starting C-Bills d6 Count -lblRandomStartingCashDiceCount.toolTipText=This is the number of d6s rolled to generate random starting C-Bills when randomizing starting funds.
The base value of 18 will generate approximately 63m C-Bills, which is enough for a company in 3025 with a small float.
For later eras 22 to 30 per company is recommended. +lblStartingCash.text=Starting Money +lblStartingCash.toolTipText=The amount of money to start with, minus expenses, if not randomizing the starting cash. +chkRandomizeStartingCash.text=Randomize Starting Funds +chkRandomizeStartingCash.toolTipText=This overrides the starting funds with a random roll of nd6 million, with the n specified below. +lblRandomStartingCashDiceCount.text=Random Starting Funds d6 Count +lblRandomStartingCashDiceCount.toolTipText=This is the number of d6s rolled to generate random starting funds when randomizing starting funds.
The base value of 18 will generate approximately 63m, which is enough for a company in 3025 with a small float.
For later eras 22 to 30 per company is recommended. lblMinimumStartingFloat.text=Minimum Starting Float -lblMinimumStartingFloat.toolTipText=This is the minimum number of available C-Bills the company will start with following generation. The minimum value to start with is 0 C-Bills. +lblMinimumStartingFloat.toolTipText=This is the minimum number of available funds the company will start with following generation. The minimum value to start with is 0. chkIncludeInitialContractPayment.text=Include Initial Contract Selection Payment In Calculations (Unimplemented) chkIncludeInitialContractPayment.toolTipText=Include the payment from the selected contract as part of the starting funds. This will mean the force may spend this cash during force creation, up to the limit of the minimum starting float.

If disabled, the initial contract payment will be paid out normally. chkStartingLoan.text=Starting Loan -chkStartingLoan.toolTipText=Take a loan containing the remaining cost of the unit after the starting cash has been expended, leaving the starting float as available C-Bills.
The loan will be 2 years long, monthly payments, 100% collateral, and at 15% interest. +chkStartingLoan.toolTipText=Take a loan containing the remaining cost of the unit after the starting cash has been expended, leaving the starting float as available funds.
The loan will be 2 years long, monthly payments, 100% collateral, and at 15% interest. financialDebitsPanel.title=Debits chkPayForSetup.text=Pay for Setup chkPayForSetup.toolTipText=Pay for the generated unit from the starting cash, to a minimum of the starting float. diff --git a/MekHQ/resources/mekhq/resources/RetirementDefectionDialog.properties b/MekHQ/resources/mekhq/resources/RetirementDefectionDialog.properties index 86c8a0295f1..61db3b43d28 100644 --- a/MekHQ/resources/mekhq/resources/RetirementDefectionDialog.properties +++ b/MekHQ/resources/mekhq/resources/RetirementDefectionDialog.properties @@ -17,7 +17,7 @@ txtInstructions.Overview.text=The turnover check involves rolling 2d6 and compar \nPaying a retention bonus decreases an individual's target number by 2, but this can get expensive, so should be used sparingly.\ \n\ \nIf this is your first time seeing this screen, please take a moment to read the documentation in 'docs/personnel modules/Turnover & Retention Module.pdf'. -txtInstructions.Results.text=Combat personnel who brought their units with them should be given a unit upon departure. If no unit of the appropriate weight class and technology is available, the retiree is compensated in C-bills.\ +txtInstructions.Results.text=Combat personnel who brought their units with them should be given a unit upon departure. If no unit of the appropriate weight class and technology is available, the retiree is compensated in funds.\ \n\ \nAny pilots with a final payout value of zero have either been compensated with a unit that meets or exceeds the payout amount, or are breaking their employment contract. diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index 609721ffd49..5a9de326828 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -52,6 +52,7 @@ import mekhq.campaign.againstTheBot.AtBConfiguration; import mekhq.campaign.enums.CampaignTransportType; import mekhq.campaign.event.*; +import mekhq.campaign.finances.Currency; import mekhq.campaign.finances.*; import mekhq.campaign.finances.enums.TransactionType; import mekhq.campaign.force.CombatTeam; @@ -340,17 +341,17 @@ public Campaign() { game.addPlayer(0, player); currentDay = LocalDate.ofYearDay(3067, 1); campaignStartDate = null; + campaignOptions = new CampaignOptions(); + setFaction(Factions.getInstance().getDefaultFaction()); + techFactionCode = ITechnology.F_MERC; CurrencyManager.getInstance().setCampaign(this); location = new CurrentLocation(Systems.getInstance().getSystems().get("Outreach"), 0); - campaignOptions = new CampaignOptions(); currentReport = new ArrayList<>(); currentReportHTML = ""; newReports = new ArrayList<>(); name = randomMercenaryCompanyNameGenerator(null); overtime = false; gmMode = false; - setFaction(Factions.getInstance().getDefaultFaction()); - techFactionCode = ITechnology.F_MERC; retainerEmployerCode = null; retainerStartDate = null; reputation = null; @@ -4740,6 +4741,7 @@ public boolean newDay() { // Advance the day by one final LocalDate yesterday = currentDay; + final Currency oldCurrency = CurrencyManager.getInstance().getDefaultCurrency(); currentDay = currentDay.plusDays(1); // Determine if we have an active contract or not, as this can get used @@ -4810,7 +4812,7 @@ public boolean newDay() { setShoppingList(goShopping(getShoppingList())); // check for anything in finances - finances.newDay(this, yesterday, getLocalDate()); + finances.newDay(this, yesterday, getLocalDate(), oldCurrency); // process removal of old personnel data on the last day of each month if ((campaignOptions.isUsePersonnelRemoval()) @@ -9138,4 +9140,13 @@ public ImageIcon getCampaignFactionIcon() { } return icon; } + + /** + * Retrieves the symbol of the default currency from the {@link CurrencyManager}. + * + * @return the symbol of the default currency as a {@link String}. + */ + public String getCurrencyString() { + return CurrencyManager.getInstance().getDefaultCurrency().getSymbol(); + } } diff --git a/MekHQ/src/mekhq/campaign/finances/Currency.java b/MekHQ/src/mekhq/campaign/finances/Currency.java index 1b0af0c2452..f05511fd26c 100644 --- a/MekHQ/src/mekhq/campaign/finances/Currency.java +++ b/MekHQ/src/mekhq/campaign/finances/Currency.java @@ -75,6 +75,10 @@ boolean getIsDefault() { return this.isDefault; } + public String getSymbol() { + return this.symbol; + } + @Override public String toString() { return this.wrapped.toString(); diff --git a/MekHQ/src/mekhq/campaign/finances/CurrencyManager.java b/MekHQ/src/mekhq/campaign/finances/CurrencyManager.java index 6482c9b22f8..99c3c6aacc6 100644 --- a/MekHQ/src/mekhq/campaign/finances/CurrencyManager.java +++ b/MekHQ/src/mekhq/campaign/finances/CurrencyManager.java @@ -20,17 +20,12 @@ */ package mekhq.campaign.finances; -import java.io.FileInputStream; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; - -import javax.xml.parsers.DocumentBuilder; - +import megamek.logging.MMLogger; +import mekhq.campaign.Campaign; +import mekhq.campaign.universe.Faction; +import mekhq.campaign.universe.Factions; +import mekhq.campaign.universe.PlanetarySystem; +import mekhq.utilities.MHQXMLUtility; import org.joda.money.CurrencyUnitDataProvider; import org.joda.money.format.MoneyFormatter; import org.joda.money.format.MoneyFormatterBuilder; @@ -39,14 +34,10 @@ import org.w3c.dom.Node; import org.w3c.dom.NodeList; -import megamek.logging.MMLogger; -import mekhq.campaign.Campaign; -import mekhq.campaign.mission.AtBContract; -import mekhq.campaign.mission.Contract; -import mekhq.campaign.universe.Faction; -import mekhq.campaign.universe.Factions; -import mekhq.campaign.universe.PlanetarySystem; -import mekhq.utilities.MHQXMLUtility; +import javax.xml.parsers.DocumentBuilder; +import java.io.FileInputStream; +import java.time.LocalDate; +import java.util.*; /** * Main class used to handle all money and currency information. @@ -136,7 +127,7 @@ MoneyFormatter getUiAmountAndNamePrinter() { return this.uiAmountAndNamePrinter; } - synchronized Currency getDefaultCurrency() { + public synchronized Currency getDefaultCurrency() { if (this.campaign == null) { return this.backupCurrency; } @@ -153,46 +144,65 @@ synchronized Currency getDefaultCurrency() { this.lastSystem = currentSystem; this.defaultCurrency = this.backupCurrency; - Map possibleCurrencies = new HashMap<>(); +// Map possibleCurrencies = new HashMap<>(); - // Use the default currency in this time period, if it exists + // Use the default currency in this time period if it exists int year = date.getYear(); for (Currency currency : this.currencies) { - if ((year >= currency.getStartYear()) && (year <= currency.getEndYear())) { + boolean isWithinYearRange = (year >= currency.getStartYear()) && (year <= currency.getEndYear()); + boolean isKSKorSFC = campaign.getFaction().isClan() && + ("KSK".equals(String.valueOf(currency)) || "SFC".equals(String.valueOf(currency))); + if (isWithinYearRange) { + // Special case for Clan factions + if (isKSKorSFC) { + return defaultCurrency = currency; + } + + // Check if the current currency is default if (currency.getIsDefault()) { return defaultCurrency = currency; } - possibleCurrencies.put(currency.getCode(), currency); + // Add currency to possible options + // This is where we'd construct a list for the commented code to use. +// possibleCurrencies.put(currency.getCode(), currency); } } + + + // The next two options have been disabled until we have a way to easily communicate to + // the user why their funds are being changed. This is especially true for H-Bills, as + // they are undesirable compared to the C-Bill so players should be given a choice to + // change. The code is good, though, so shouldn't be deleted as it could prove useful + // later. + // Use the currency of the Faction in any of our contracts, if it exists - for (Contract contract : this.campaign.getActiveContracts()) { - if (contract instanceof AtBContract) { - Currency currency = possibleCurrencies.getOrDefault( - Factions.getInstance().getFaction(((AtBContract) contract).getEmployerCode()) - .getCurrencyCode(), - null); - - if (currency != null) { - return defaultCurrency = currency; - } - } - } +// for (Contract contract : this.campaign.getActiveContracts()) { +// if (contract instanceof AtBContract) { +// Currency currency = possibleCurrencies.getOrDefault( +// Factions.getInstance().getFaction(((AtBContract) contract).getEmployerCode()) +// .getCurrencyCode(), +// null); +// +// if (currency != null) { +// return defaultCurrency = currency; +// } +// } +// } // Use the currency of one of the factions in the planet where the unit is // deployed, if it exists - if (currentSystem != null) { - Set factions = currentSystem.getFactionSet(date); - for (Faction faction : factions) { - Currency currency = possibleCurrencies.getOrDefault(faction.getCurrencyCode(), null); - if (currency != null) { - return defaultCurrency = currency; - } - } - } +// if (currentSystem != null) { +// Set factions = currentSystem.getFactionSet(date); +// for (Faction faction : factions) { +// Currency currency = possibleCurrencies.getOrDefault(faction.getCurrencyCode(), null); +// if (currency != null) { +// return defaultCurrency = currency; +// } +// } +// } } return defaultCurrency; diff --git a/MekHQ/src/mekhq/campaign/finances/Finances.java b/MekHQ/src/mekhq/campaign/finances/Finances.java index cc28f4f5980..c9488ac9e62 100644 --- a/MekHQ/src/mekhq/campaign/finances/Finances.java +++ b/MekHQ/src/mekhq/campaign/finances/Finances.java @@ -35,21 +35,20 @@ import mekhq.utilities.ReportingUtilities; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVPrinter; +import org.joda.money.CurrencyMismatchException; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import java.io.BufferedWriter; import java.io.File; import java.io.PrintWriter; +import java.math.BigDecimal; import java.nio.file.Files; import java.nio.file.Paths; import java.time.LocalDate; import java.time.Period; import java.time.temporal.ChronoUnit; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.ResourceBundle; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -128,7 +127,21 @@ public void setWentIntoDebt(final @Nullable LocalDate wentIntoDebt) { public Money getBalance() { Money balance = Money.zero(); - return balance.plus(transactions.stream().map(Transaction::getAmount).collect(Collectors.toList())); + + Currency currency = CurrencyManager.getInstance().getDefaultCurrency(); + + for (Transaction transaction : transactions) { + try { + balance.plus(transaction.getAmount()); + } catch (CurrencyMismatchException e) { + // This means the finances were logged in the wrong currency. + // This can be caused by data mismatch or by legacy campaigns. + // The fix is easy: convert the transaction to the right currency + convertFinances(transaction, currency); + } + } + + return balance; } public Money getLoanBalance() { @@ -258,7 +271,15 @@ public void addLoan(Loan loan) { loans.add(loan); } - public void newDay(final Campaign campaign, final LocalDate yesterday, final LocalDate today) { + public void newDay(final Campaign campaign, final LocalDate yesterday, final LocalDate today, Currency oldCurrency) { + // check for currency change + Currency newCurrency = CurrencyManager.getInstance().getDefaultCurrency(); + if (!Objects.equals(newCurrency, oldCurrency)) { + for (Transaction transaction : transactions) { + convertFinances(transaction, newCurrency); + } + } + // check for a new fiscal year if (campaign.getCampaignOptions().getFinancialYearDuration().isEndOfFinancialYear(campaign.getLocalDate())) { // calculate profits @@ -446,6 +467,18 @@ public void newDay(final Campaign campaign, final LocalDate yesterday, final Loc loans = newLoans; } + private static void convertFinances(Transaction transaction, Currency newCurrency) { + Money currentMoney = transaction.getAmount(); + + // Perform the currency conversion (amount * conversionRate) + // TODO currency specific conversion rates. Replace hardcoded '1' + double newAmount = currentMoney.getAmount().multiply(BigDecimal.valueOf(1)).doubleValue(); + + Money convertedMoney = Money.of(newAmount, newCurrency); + + transaction.setAmount(convertedMoney); + } + /** * Calculates the profits made by the campaign based on the transactions * recorded. diff --git a/MekHQ/src/mekhq/gui/dialog/PersonnelMarketDialog.java b/MekHQ/src/mekhq/gui/dialog/PersonnelMarketDialog.java index ab93be87a68..5ce1bba2bed 100644 --- a/MekHQ/src/mekhq/gui/dialog/PersonnelMarketDialog.java +++ b/MekHQ/src/mekhq/gui/dialog/PersonnelMarketDialog.java @@ -18,42 +18,11 @@ */ package mekhq.gui.dialog; -import java.awt.BorderLayout; -import java.awt.Component; -import java.awt.Dimension; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.GridLayout; -import java.awt.Insets; -import java.awt.event.ActionEvent; -import java.awt.event.WindowAdapter; -import java.awt.event.WindowEvent; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.ResourceBundle; -import java.util.UUID; - -import javax.swing.*; -import javax.swing.RowSorter.SortKey; -import javax.swing.event.ListSelectionEvent; -import javax.swing.table.TableCellRenderer; -import javax.swing.table.TableColumn; -import javax.swing.table.TableRowSorter; - import megamek.client.ui.models.XTableColumnModel; -import megamek.client.ui.preferences.JComboBoxPreference; -import megamek.client.ui.preferences.JTablePreference; -import megamek.client.ui.preferences.JToggleButtonPreference; -import megamek.client.ui.preferences.JWindowPreference; -import megamek.client.ui.preferences.PreferencesNode; +import megamek.client.ui.preferences.*; import megamek.client.ui.swing.MekViewPanel; import megamek.codeUtilities.StringUtility; -import megamek.common.Aero; -import megamek.common.Compute; -import megamek.common.Entity; -import megamek.common.Mek; -import megamek.common.Tank; +import megamek.common.*; import megamek.logging.MMLogger; import mekhq.MekHQ; import mekhq.campaign.Campaign; @@ -73,6 +42,19 @@ import mekhq.gui.utilities.JScrollPaneWithSpeed; import mekhq.gui.view.PersonViewPanel; +import javax.swing.*; +import javax.swing.RowSorter.SortKey; +import javax.swing.event.ListSelectionEvent; +import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableColumn; +import javax.swing.table.TableRowSorter; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.util.List; +import java.util.*; + /** * @author Jay Lawson (jaylawson39 at yahoo.com) * (code borrowed heavily from MegaMekLab UnitSelectorDialog @@ -196,7 +178,8 @@ public void windowClosing(WindowEvent e) { gridBagConstraints.anchor = GridBagConstraints.WEST; panelFilterBtns.add(radioNormalRoll, gridBagConstraints); - radioPaidRecruitment.setText("Make paid recruitment roll next week (100,000 C-bills)"); + radioPaidRecruitment.setText(String.format("Make paid recruitment roll next week (%s)", + campaign.getCurrencyString())); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 2; gridBagConstraints.gridwidth = 2; From 9f57723fd39a91fc640c2e482843dcc5baff15a5 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Mon, 27 Jan 2025 13:28:46 -0600 Subject: [PATCH 020/112] Added JavaDocs --- .../campaign/finances/CurrencyManager.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/MekHQ/src/mekhq/campaign/finances/CurrencyManager.java b/MekHQ/src/mekhq/campaign/finances/CurrencyManager.java index 99c3c6aacc6..5ca99c25e1c 100644 --- a/MekHQ/src/mekhq/campaign/finances/CurrencyManager.java +++ b/MekHQ/src/mekhq/campaign/finances/CurrencyManager.java @@ -2,6 +2,7 @@ * CurrencyManager.java * * Copyright (c) 2019 Vicente Cartas Espinel (vicente.cartas at outlook.com). All rights reserved. + * Copyright (c) 2025 The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * @@ -127,6 +128,26 @@ MoneyFormatter getUiAmountAndNamePrinter() { return this.uiAmountAndNamePrinter; } + /** + * Retrieves the default currency for the current campaign, based on the campaign's + * date, planetary system, and faction details. + *

+ * This method ensures the default currency is updated if the campaign's date or + * planetary system has changed since the last check. It uses various conditions + * to determine the appropriate default currency, including: + *

+ *
    + *
  • The year range validity for each currency
  • + *
  • Special cases for Clan factions (e.g., "KSK" or "SFC" currencies)
  • + *
  • The "default" status of available currencies
  • + *
+ * + * If no campaign is active, the backup currency is returned. Certain decisions + * regarding currency selection (e.g., based on contracts or planetary factions) + * are currently disabled. + * + * @return the default {@link Currency} for the campaign. + */ public synchronized Currency getDefaultCurrency() { if (this.campaign == null) { return this.backupCurrency; From 9d381a5b833c7dfe8879be9fe2945b21da8b2f77 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Mon, 27 Jan 2025 13:46:28 -0600 Subject: [PATCH 021/112] Added "Simulate Gray Monday" option to Campaign Options - Implemented a new campaign option to simulate the effects of Gray Monday, including checkbox UI elements, persistence in XML, and related functionality. --- .../resources/CampaignOptionsDialog.properties | 2 ++ MekHQ/src/mekhq/campaign/CampaignOptions.java | 13 +++++++++++++ .../gui/campaignOptions/contents/FinancesTab.java | 10 ++++++++++ 3 files changed, 25 insertions(+) diff --git a/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties b/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties index 16f68d0e556..330beaa899b 100644 --- a/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties +++ b/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties @@ -1368,6 +1368,8 @@ lblFinancialYearDuration.tooltip=This changes the Financial Term, which is when lblNewFinancialYearFinancesToCSVExportBox.text=Export Finances as CSV Table on Term End lblNewFinancialYearFinancesToCSVExportBox.tooltip=This writes the finance table to a CSV file on\ \ the first day of a new financial term, right before the table is carried over to the next period. +lblSimulateGrayMonday.text=Simulate Gray Monday +lblSimulateGrayMonday.tooltip=Simulate the economic and social upheaval of Gray Monday. # createSalesPanel lblSalesPanel.text=Sales diff --git a/MekHQ/src/mekhq/campaign/CampaignOptions.java b/MekHQ/src/mekhq/campaign/CampaignOptions.java index 020609bf6d0..df694fe69d4 100644 --- a/MekHQ/src/mekhq/campaign/CampaignOptions.java +++ b/MekHQ/src/mekhq/campaign/CampaignOptions.java @@ -444,6 +444,7 @@ public static String getTechLevelName(final int techLevel) { private boolean showPeacetimeCost; private FinancialYearDuration financialYearDuration; private boolean newFinancialYearFinancesToCSVExport; + private boolean simulateGrayMonday; // Price Multipliers private double commonPartPriceMultiplier; @@ -1071,6 +1072,7 @@ public CampaignOptions() { showPeacetimeCost = false; setFinancialYearDuration(FinancialYearDuration.ANNUAL); newFinancialYearFinancesToCSVExport = false; + simulateGrayMonday = false; // Price Multipliers setCommonPartPriceMultiplier(1.0); @@ -3398,6 +3400,14 @@ public void setNewFinancialYearFinancesToCSVExport(final boolean newFinancialYea this.newFinancialYearFinancesToCSVExport = newFinancialYearFinancesToCSVExport; } + public boolean isSimulateGrayMonday() { + return simulateGrayMonday; + } + + public void setSimulateGrayMonday(final boolean simulateGrayMonday) { + this.simulateGrayMonday = simulateGrayMonday; + } + // region Price Multipliers public double getCommonPartPriceMultiplier() { return commonPartPriceMultiplier; @@ -5170,6 +5180,7 @@ public void writeToXml(final PrintWriter pw, int indent) { MHQXMLUtility.writeSimpleXMLTag(pw, indent, "financialYearDuration", financialYearDuration.name()); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "newFinancialYearFinancesToCSVExport", newFinancialYearFinancesToCSVExport); + MHQXMLUtility.writeSimpleXMLTag(pw, indent, "simulateGrayMonday", simulateGrayMonday); // region Price Multipliers MHQXMLUtility.writeSimpleXMLTag(pw, indent, "commonPartPriceMultiplier", getCommonPartPriceMultiplier()); @@ -6126,6 +6137,8 @@ public static CampaignOptions generateCampaignOptionsFromXml(Node wn, Version ve retVal.setFinancialYearDuration(FinancialYearDuration.parseFromString(wn2.getTextContent().trim())); } else if (wn2.getNodeName().equalsIgnoreCase("newFinancialYearFinancesToCSVExport")) { retVal.newFinancialYearFinancesToCSVExport = Boolean.parseBoolean(wn2.getTextContent().trim()); + } else if (wn2.getNodeName().equalsIgnoreCase("simulateGrayMonday")) { + retVal.simulateGrayMonday = Boolean.parseBoolean(wn2.getTextContent().trim()); // region Price Multipliers } else if (wn2.getNodeName().equalsIgnoreCase("commonPartPriceMultiplier")) { diff --git a/MekHQ/src/mekhq/gui/campaignOptions/contents/FinancesTab.java b/MekHQ/src/mekhq/gui/campaignOptions/contents/FinancesTab.java index 02b5260ab84..efba6d0e6c9 100644 --- a/MekHQ/src/mekhq/gui/campaignOptions/contents/FinancesTab.java +++ b/MekHQ/src/mekhq/gui/campaignOptions/contents/FinancesTab.java @@ -58,6 +58,7 @@ public class FinancesTab { private JLabel lblFinancialYearDuration; private MMComboBox comboFinancialYearDuration; private JCheckBox newFinancialYearFinancesToCSVExportBox; + private JCheckBox simulateGrayMonday; private JPanel pnlPayments; private JCheckBox payForPartsBox; @@ -167,6 +168,8 @@ private void initializeGeneralOptionsTab() { newFinancialYearFinancesToCSVExportBox = new JCheckBox(); + simulateGrayMonday = new JCheckBox(); + // Payments pnlPayments = new JPanel(); payForPartsBox = new JCheckBox(); @@ -352,6 +355,8 @@ private JPanel createGeneralOptionsPanel() { newFinancialYearFinancesToCSVExportBox = new CampaignOptionsCheckBox("NewFinancialYearFinancesToCSVExportBox"); + simulateGrayMonday = new CampaignOptionsCheckBox("SimulateGrayMonday"); + // Layout the Panel final JPanel panel = new CampaignOptionsStandardPanel("GeneralOptionsPanel"); final GridBagConstraints layout = new CampaignOptionsGridBagConstraints(panel); @@ -384,6 +389,9 @@ private JPanel createGeneralOptionsPanel() { layout.gridwidth = 2; panel.add(newFinancialYearFinancesToCSVExportBox, layout); + layout.gridy++; + panel.add(simulateGrayMonday, layout); + return panel; } @@ -757,6 +765,7 @@ public void applyCampaignOptionsToCampaign(@Nullable CampaignOptions presetCampa options.setShowPeacetimeCost(showPeacetimeCostBox.isSelected()); options.setFinancialYearDuration(comboFinancialYearDuration.getSelectedItem()); options.setNewFinancialYearFinancesToCSVExport(newFinancialYearFinancesToCSVExportBox.isSelected()); + options.setSimulateGrayMonday(simulateGrayMonday.isSelected()); options.setPayForParts(payForPartsBox.isSelected()); options.setPayForRepairs(payForRepairsBox.isSelected()); options.setPayForUnits(payForUnitsBox.isSelected()); @@ -822,6 +831,7 @@ public void loadValuesFromCampaignOptions(@Nullable CampaignOptions presetCampai showPeacetimeCostBox.setSelected(options.isShowPeacetimeCost()); comboFinancialYearDuration.setSelectedItem(options.getFinancialYearDuration()); newFinancialYearFinancesToCSVExportBox.setSelected(options.isNewFinancialYearFinancesToCSVExport()); + simulateGrayMonday.setSelected(options.isSimulateGrayMonday()); payForPartsBox.setSelected(options.isPayForParts()); payForRepairsBox.setSelected(options.isPayForRepairs()); payForUnitsBox.setSelected(options.isPayForUnits()); From 12a92726a017c26ef5bd8cc2488ab9c592fa6c93 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Mon, 27 Jan 2025 16:26:56 -0600 Subject: [PATCH 022/112] Introduce Gray Monday event and dialog infrastructure - Added the Gray Monday event system including its triggers, properties, and dialogs to simulate the catastrophic HPG failure scenario. --- .../mekhq/resources/GrayMonday.properties | 97 ++++++++++++ MekHQ/src/mekhq/campaign/Campaign.java | 9 ++ .../src/mekhq/campaign/mission/Contract.java | 4 +- .../campaign/randomEvents/GrayMonday.java | 117 +++++++++++++++ .../baseComponents/MHQDialogImmersive.java | 21 +-- .../dialog/randomEvents/GrayMondayDialog.java | 142 ++++++++++++++++++ 6 files changed, 378 insertions(+), 12 deletions(-) create mode 100644 MekHQ/resources/mekhq/resources/GrayMonday.properties create mode 100644 MekHQ/src/mekhq/campaign/randomEvents/GrayMonday.java create mode 100644 MekHQ/src/mekhq/gui/dialog/randomEvents/GrayMondayDialog.java diff --git a/MekHQ/resources/mekhq/resources/GrayMonday.properties b/MekHQ/resources/mekhq/resources/GrayMonday.properties new file mode 100644 index 00000000000..dbb2bd07f29 --- /dev/null +++ b/MekHQ/resources/mekhq/resources/GrayMonday.properties @@ -0,0 +1,97 @@ +confirm.button=Understood +dialog.ooc=This is a canonical event. Expect the unexpected. +transaction.message=Gray Monday + +# REPORTS +employer.report=All employers have withdrawn their contract offers. + +# CLARION NOTE +clarionNoteEvent0.message=

{0}, I wanted to bring something odd to your attention. The local HPG relay\ + \ station has gone dark. Initial diagnostics suggest it''s not just us - our neighboring systems\ + \ aren''t responding either. At first, I thought this might be a routine system outage or\ + \ scheduled maintenance, but there''s been no notice from ComStar or the Republic.

\ +

I'm attempting to use alternate communication methods to confirm the extent of the disruption.\ + \ I''ll keep you updated. For now, it''s an inconvenience, but nothing we can''t manage +clarionNoteEvent1.message=

{0}, this situation is far worse than we thought. Our attempts to\ + \ re-establish communication have failed, and reports are coming in through secondary channels\ + \ that this is happening across the sector. Some JumpShip captains we''ve contacted say they''ve\ + \ been unable to send or receive any transmissions from the HPGs on their routes.

\ +

The scale of this outage is unprecedented. Entire trade lanes are effectively cut off. Local\ + \ stations are scrambling, and the lack of centralized updates is breeding chaos. This is no\ + \ ordinary technical failure, {0}. Something deliberate or catastrophic has occurred.

+clarionNoteEvent2.message=

{0}, I must stress the urgency of this situation. Reports are coming in\ + \ from merchant and military channels that nearly every HPG station across the Inner Sphere has\ + \ gone silent. What little information we''re gathering suggests this isn''t a regional issue but\ + \ galaxy-wide. Entire star systems are isolated.

\ +

We''ve already started to see the ripple effects - panicked citizens, disrupted supply chains,\ + \ and opportunists exploiting the confusion. Without HPGs, the very foundation of our logistics\ + \ and command structure is crumbling. Resupply requests from neighboring systems are coming in by\ + \ JumpShip courier! The time delay is going to cripple our operations.

\ +

This situation is quickly spiraling out of contro.

+clarionNoteEvent3.message=

{0}, it''s worse than anyone could imagine. JumpShip captains are now\ + \ reporting signs of sabotage at several HPG sites. Some claim entire facilities are inoperable,\ + \ equipment burned out beyond repair. There are whispers of coordinated attacks, but no one has\ + \ solid answers.

\ +

The civilian population is in panic. Markets have shut down, transport contracts are failing,\ + \ and rumors of piracy are already circulating. Systems that relied on regular HPG updates to\ + \ coordinate food shipments or medical supplies are completely vulnerable. Our logistical chain\ + \ is fragmenting with every hour that passes.

\ +

This is no temporary outage. The Inner Sphere as we know it has stopped. We''ve entered a new\ + \ reality - disconnected, fragmented chaos. I''m recommending we secure local resources and prepare\ + \ for the possibility that external support won''t arrive anytime soon.

\ +

{0}, what are we going to do?

+ +# GREY MONDAY +grayMondayEvent1.message=

{0}, the situation has escalated beyond sanity. We have confirmation - direct\ + \ attacks on HPG facilities have occurred across the Inner Sphere. Reports are flooding in from\ + \ JumpShip captains and emergency couriers: sabotage, armed assaults, and even viral intrusions\ + \ targeting the systems themselves.

\ +

This is no accident, sir. This is a deliberate and widespread operation. Entire stations have\ + \ been destroyed or rendered inoperable. One captain reported seeing the remnants of a station\ + \ near Terra - scorched to the frame, with no survivors. A courier claims another was\ + \ overwhelmed by a precision strike from an unidentified force.

\ +

Whoever is behind this has killed us. Logistics are breaking down faster than we can adapt,\ + \ and rumors are spreading like wildfire. Some are even claiming the Republic itself may be\ + \ involved. I don''t know what''s true anymore, sir, but the scale of this is staggering. We''re\ + \ talking about an attack that could only have been planned and executed over years, maybe\ + \ decades.

\ +

Morale is starting to falter. People are looking to us for answers, but I don''t have them. I\ + \ need to know what we''re going to do, Commander. I''m losing control, and I don''t know how much\ + \ longer we can hold things together.

+grayMondayEvent2.message=

{0}, it''s over. Everything''s gone. The banks... they''ve collapsed.

\ +

I don''t know how to say this without sounding insane, but every account, every credit, every\ + \ fund we had tied to the banks or electronic systems - it''s just gone. It''s not just us. I''ve\ + \ spoken to couriers and merchants. This is widespread. Whole planetary economies have been wiped\ + \ out overnight.

\ +

What wasn''t in physical currency doesn''t exist anymore. No explanations, no warning, nothing.\ + \ All we have left is what''s in our coffers right now - and let''s be honest, that''s not going to\ + \ last. Trade is collapsing. People are panicking, rioting, killing for supplies.

\ +

I''ve already had our men demanding answers, demanding their pay. What do I tell them, {0}?\ + \ That their life savings are gone? That their families back home might already be starving? Do I\ + \ tell them we''re no better off?

\ +

I... I don''t know what to do. I don''t know how to fix this. {0}, this is beyond us. We''re\ + \ watching everything we''ve built fall apart, and all I can do is report it while the galaxy\ + \ burns.

\ +

Tell me you have a plan. Please. Just... tell me what to do.

+grayMondayEvent3.message=

{0}, I... I don''t know how to say this. I don''t have any good way to\ + \ explain what''s happening, but we can''t pay you anymore. The accounts are gone - everything is\ + \ gone. The banks have collapsed, the markets are dead, and every contract we had is worthless\ + \ now.

\ +

But please, I am begging you - don''t abandon us. We''re barely holding things together here. If\ + \ you and your unit leave, we''re finished. We have no defense, no chance. The riots have started\ + \ already. People are dying in the streets, and there''s no order left.

\ +

I know this isn''t fair. I know what I''m asking of you, but I have nothing else to give. I''ve\ + \ bumped your salvage rights to 100%. Whatever you can take, whatever you can carry - it''s yours.\ + \ Every last scrap. Just stay. Help us keep what little we still have.

\ +

Please, Commander. I am on my knees right now. You''re our last hope. If you leave, we won''t\ + \ survive. I''ll do anything to keep you here. Just tell me what you need. Tell me what it will\ + \ take.

+grayMondayEvent4.message=

{0}, we just received a message. At least, we think it was a message.\ + \ The terminal lit up for a moment - just for a moment - but there was no voice, no text. Just\ + \ static. Harsh, crackling static.

\ +

We tried everything to clear the signal, but there was nothing. No identifiers, no content, no\ + \ life. And now the terminal is dead again. Completely dark. It''s like it was trying to speak,\ + \ but the system couldn''t hold on long enough to get the words through.

\ +

I think... I think that''s it. The HPG is gone. Whatever faint pulse was still out there is gone\ + \ now. We''re cut off. Completely. There''s nothing more coming, {0}.

\ +

Nothing.

\ No newline at end of file diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index 609721ffd49..c8d0208c9ca 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -102,6 +102,7 @@ import mekhq.campaign.personnel.ranks.Ranks; import mekhq.campaign.personnel.turnoverAndRetention.Fatigue; import mekhq.campaign.personnel.turnoverAndRetention.RetirementDefectionTracker; +import mekhq.campaign.randomEvents.GrayMonday; import mekhq.campaign.rating.CamOpsReputation.ReputationController; import mekhq.campaign.rating.FieldManualMercRevDragoonsRating; import mekhq.campaign.rating.IUnitRating; @@ -165,6 +166,8 @@ import static mekhq.campaign.personnel.education.EducationController.getAcademy; import static mekhq.campaign.personnel.education.TrainingCombatTeams.processTrainingCombatTeams; import static mekhq.campaign.personnel.turnoverAndRetention.RetirementDefectionTracker.Payout.isBreakingContract; +import static mekhq.campaign.randomEvents.GrayMonday.EVENT_DATE_CLARION_NOTE; +import static mekhq.campaign.randomEvents.GrayMonday.EVENT_DATE_GRAY_MONDAY; import static mekhq.campaign.stratcon.SupportPointNegotiation.negotiateAdditionalSupportPoints; import static mekhq.campaign.unit.Unit.SITE_FACILITY_BASIC; import static mekhq.campaign.universe.Factions.getFactionLogo; @@ -4832,6 +4835,12 @@ public boolean newDay() { addReport(String.format(resources.getString("weeklyStockCheck.text"), bought)); } + // Random Events + if (currentDay.equals(EVENT_DATE_CLARION_NOTE) || + (currentDay.isBefore(EVENT_DATE_GRAY_MONDAY.plusDays(4)))) { + new GrayMonday(this, currentDay); + } + // This must be the last step before returning true MekHQ.triggerEvent(new NewDayEvent(this)); return true; diff --git a/MekHQ/src/mekhq/campaign/mission/Contract.java b/MekHQ/src/mekhq/campaign/mission/Contract.java index 7af8dbdb769..398575ec81b 100644 --- a/MekHQ/src/mekhq/campaign/mission/Contract.java +++ b/MekHQ/src/mekhq/campaign/mission/Contract.java @@ -2,7 +2,7 @@ * Contract.java * * Copyright (c) 2011 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved. - * Copyright (c) 2024 The MegaMek Team. All Rights Reserved. + * Copyright (c) 2024-2025 The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * @@ -383,7 +383,7 @@ public Money getTransitAmount() { return transitAmount; } - protected void setTransitAmount(Money amount) { + public void setTransitAmount(Money amount) { transitAmount = amount; } diff --git a/MekHQ/src/mekhq/campaign/randomEvents/GrayMonday.java b/MekHQ/src/mekhq/campaign/randomEvents/GrayMonday.java new file mode 100644 index 00000000000..da70a09e877 --- /dev/null +++ b/MekHQ/src/mekhq/campaign/randomEvents/GrayMonday.java @@ -0,0 +1,117 @@ +/* + * 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 as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with MekHQ. If not, see . + */ +package mekhq.campaign.randomEvents; + +import megamek.common.annotations.Nullable; +import mekhq.campaign.Campaign; +import mekhq.campaign.finances.Finances; +import mekhq.campaign.finances.Money; +import mekhq.campaign.mission.AtBContract; +import mekhq.campaign.personnel.Person; +import mekhq.gui.dialog.randomEvents.GrayMondayDialog; + +import java.time.LocalDate; + +import static java.time.temporal.ChronoUnit.DAYS; +import static mekhq.campaign.Campaign.AdministratorSpecialization.LOGISTICS; +import static mekhq.campaign.finances.enums.TransactionType.STARTING_CAPITAL; +import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; + +public class GrayMonday { + private static final String RESOURCE_BUNDLE = "mekhq.resources.GrayMonday"; + + public final static LocalDate EVENT_DATE_CLARION_NOTE = LocalDate.of(3132, 8,4); + public final static LocalDate EVENT_DATE_GRAY_MONDAY = LocalDate.of(3132, 8,7); + + private final Campaign campaign; + + public GrayMonday(Campaign campaign, LocalDate today) { + this.campaign = campaign; + Person speaker = getSpeaker(); + + int daysAfterClarionNote = (int) DAYS.between(EVENT_DATE_CLARION_NOTE, today); + if (daysAfterClarionNote >= 0 && daysAfterClarionNote <= 3) { + new GrayMondayDialog(campaign, speaker, true, daysAfterClarionNote); + } + + int daysAfterGrayMonday = (int) DAYS.between(EVENT_DATE_GRAY_MONDAY, today); + if (daysAfterGrayMonday > 0 && daysAfterGrayMonday <= 4) { + boolean shouldShowDialog = daysAfterGrayMonday != 2; + + if (daysAfterGrayMonday == 2) { + for (AtBContract contract : campaign.getAtBContracts()) { + LocalDate startDate = contract.getStartDate(); + if (!startDate.isBefore(today)) { + shouldShowDialog = true; + break; + } + } + } + + if (shouldShowDialog) { + new GrayMondayDialog(campaign, speaker, false, daysAfterGrayMonday); + } + } + + if (daysAfterGrayMonday == 1) { + Finances finances = campaign.getFinances(); + Money balance = finances.getBalance(); + Money adjustedBalance = balance.multipliedBy(0.01); + + finances.getTransactions().clear(); + + finances.getLoans().clear(); + + finances.credit(STARTING_CAPITAL, today, adjustedBalance, + getFormattedTextAt(RESOURCE_BUNDLE, "transaction.message")); + + } + + if (daysAfterGrayMonday == 3) { + for (AtBContract contract : campaign.getAtBContracts()) { + LocalDate startDate = contract.getStartDate(); + if (!startDate.isBefore(today)) { + contract.setBaseAmount(Money.of(0)); + contract.setOverheadComp(0); + contract.setBattleLossComp(0); + contract.setStraightSupport(0); + contract.setTransportComp(0); + contract.setTransitAmount(Money.of(0)); + } + } + + campaign.getContractMarket().getContracts().clear(); + + getFormattedTextAt(RESOURCE_BUNDLE, "employer.report"); + } + } + + + /** + * Retrieves the speaker for the dialogs. + * + *

The speaker is determined as the senior administrator personnel with the Logistics + * specialization within the campaign. If no such person exists, this method returns {@code null}.

+ * + * @return a {@link Person} representing the left speaker, or {@code null} if no suitable speaker is available + */ + private @Nullable Person getSpeaker() { + return campaign.getSeniorAdminPerson(LOGISTICS); + } +} diff --git a/MekHQ/src/mekhq/gui/baseComponents/MHQDialogImmersive.java b/MekHQ/src/mekhq/gui/baseComponents/MHQDialogImmersive.java index bcad687a643..a4bb991609e 100644 --- a/MekHQ/src/mekhq/gui/baseComponents/MHQDialogImmersive.java +++ b/MekHQ/src/mekhq/gui/baseComponents/MHQDialogImmersive.java @@ -58,7 +58,7 @@ public class MHQDialogImmersive extends JDialog { private int CENTER_WIDTH = UIUtil.scaleForGUI(400); private final int INSERT_SIZE = UIUtil.scaleForGUI(5); - private final int IMAGE_WIDTH = 125; // This is scaled to GUI by 'scaleImageIconToWidth' + protected final int IMAGE_WIDTH = 125; // This is scaled to GUI by 'scaleImageIconToWidth' private JPanel northPanel; private JPanel southPanel; @@ -123,7 +123,7 @@ public MHQDialogImmersive(Campaign campaign, @Nullable Person leftSpeaker, // Left box for speaker details if (leftSpeaker != null) { - JPanel pnlLeftSpeaker = buildSpeakerPanel(true); + JPanel pnlLeftSpeaker = buildSpeakerPanel(leftSpeaker, campaign); // Add pnlLeftSpeaker to mainPanel constraints.gridx = gridx; @@ -145,7 +145,7 @@ public MHQDialogImmersive(Campaign campaign, @Nullable Person leftSpeaker, // Right box for speaker details if (rightSpeaker != null) { - JPanel pnlRightSpeaker = buildSpeakerPanel(false); + JPanel pnlRightSpeaker = buildSpeakerPanel(rightSpeaker, campaign); // Add pnlRightSpeaker to mainPanel constraints.gridx = gridx; @@ -226,7 +226,7 @@ private JPanel createCenterBox(String centerMessage, List buttons) { * This method creates a vertically stacked panel that includes the person's icon, title, * and any additional descriptive information (e.g., roles, forces, or campaign affiliations). * - * @param isLeftSpeaker Indicates if the individual is displayed on the left side of the dialog. - * This affects alignment and panel width. + * @param speaker The character shown in the dialog, can be {@code null} for no speaker + * @param campaign The current campaign. * @return A {@link JPanel} forming the speaker's dialog box. */ - private JPanel buildSpeakerPanel(boolean isLeftSpeaker) { - final Person speaker = isLeftSpeaker ? leftSpeaker : rightSpeaker; - + protected JPanel buildSpeakerPanel(@Nullable Person speaker, Campaign campaign) { JPanel speakerBox = new JPanel(); speakerBox.setLayout(new BoxLayout(speakerBox, BoxLayout.Y_AXIS)); speakerBox.setAlignmentX(Component.CENTER_ALIGNMENT); speakerBox.setMaximumSize(new Dimension(IMAGE_WIDTH, Integer.MAX_VALUE)); // Get speaker details - String speakerName = speaker.getFullTitle(); + String speakerName = campaign.getName(); + if (speaker != null) { + speakerName = speaker.getFullTitle(); + } // Add speaker image (icon) ImageIcon speakerIcon = getSpeakerIcon(campaign, speaker); diff --git a/MekHQ/src/mekhq/gui/dialog/randomEvents/GrayMondayDialog.java b/MekHQ/src/mekhq/gui/dialog/randomEvents/GrayMondayDialog.java new file mode 100644 index 00000000000..e507bce6937 --- /dev/null +++ b/MekHQ/src/mekhq/gui/dialog/randomEvents/GrayMondayDialog.java @@ -0,0 +1,142 @@ +/* + * 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 as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with MekHQ. If not, see . + */ +package mekhq.gui.dialog.randomEvents; + +import megamek.common.annotations.Nullable; +import mekhq.campaign.Campaign; +import mekhq.campaign.mission.AtBContract; +import mekhq.campaign.personnel.Person; +import mekhq.campaign.universe.Factions; +import mekhq.gui.baseComponents.MHQDialogImmersive; + +import javax.swing.*; +import java.awt.*; +import java.time.LocalDate; +import java.util.List; + +import static mekhq.campaign.Campaign.AdministratorSpecialization.LOGISTICS; +import static mekhq.campaign.randomEvents.GrayMonday.EVENT_DATE_GRAY_MONDAY; +import static mekhq.utilities.ImageUtilities.scaleImageIconToWidth; +import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; + +public class GrayMondayDialog extends MHQDialogImmersive { + private static final String RESOURCE_BUNDLE = "mekhq.resources.GrayMonday"; + + public GrayMondayDialog(Campaign campaign, Person speaker, boolean isClarionNote, int eventIndex) { + super(campaign, speaker, null, + createInCharacterMessage(campaign, isClarionNote, eventIndex), + createButtons(), createOutOfCharacterMessage(), null); + } + + /** + * Creates the list of buttons to be displayed in the dialog. + * + *

The dialog includes only a confirmation button for this purpose, allowing + * the user to acknowledge the information provided.

+ * + * @return a list of {@link ButtonLabelTooltipPair} representing the dialog's buttons + */ + private static List createButtons() { + ButtonLabelTooltipPair btnConfirm = new ButtonLabelTooltipPair( + getFormattedTextAt(RESOURCE_BUNDLE, "confirm.button"), null); + + return List.of(btnConfirm); + } + + /** + * Retrieves the left-side speaker for the dialog. + * + *

The speaker is determined as the senior administrator personnel with the Logistics + * specialization within the campaign. If no such person exists, this method returns {@code null}.

+ * + * @param campaign the {@link Campaign} containing personnel data + * @return a {@link Person} representing the left speaker, or {@code null} if no suitable speaker is available + */ + private static @Nullable Person getSpeaker(Campaign campaign) { + return campaign.getSeniorAdminPerson(LOGISTICS); + } + + private static String createInCharacterMessage(Campaign campaign, boolean isClarionNote, int eventIndex) { + String commanderAddress = campaign.getCommanderAddress(false); + String eventType = isClarionNote ? "clarionNote" : "grayMonday"; + + return getFormattedTextAt(RESOURCE_BUNDLE, eventType + "Event" + eventIndex + ".message", + commanderAddress); + } + + + private static String createOutOfCharacterMessage() { + return getFormattedTextAt(RESOURCE_BUNDLE, "dialog.ooc"); + } + + @Override + protected JPanel buildSpeakerPanel(@Nullable Person speaker, Campaign campaign) { + JPanel speakerBox = new JPanel(); + speakerBox.setLayout(new BoxLayout(speakerBox, BoxLayout.Y_AXIS)); + speakerBox.setAlignmentX(Component.CENTER_ALIGNMENT); + speakerBox.setMaximumSize(new Dimension(IMAGE_WIDTH, Integer.MAX_VALUE)); + + AtBContract chosenContract = null; + for (AtBContract contract : campaign.getAtBContracts()) { + LocalDate startDate = contract.getStartDate(); + if (!startDate.isBefore(campaign.getLocalDate())) { + chosenContract = contract; + break; + } + } + + // Get speaker details + String speakerName = speaker.getFullTitle(); + if (campaign.getLocalDate().equals(EVENT_DATE_GRAY_MONDAY.plusDays(2))) { + if (chosenContract != null) { + speakerName = chosenContract.getEmployerName(campaign.getGameYear()); + } + } + + // Add speaker image (icon) + ImageIcon speakerIcon = getSpeakerIcon(campaign, speaker); + + if (campaign.getLocalDate().equals(EVENT_DATE_GRAY_MONDAY.plusDays(2))) { + if (chosenContract != null) { + String employerCode = chosenContract.getEmployerCode(); + speakerIcon = Factions.getFactionLogo(campaign, employerCode, true); + } + } + + if (speakerIcon != null) { + speakerIcon = scaleImageIconToWidth(speakerIcon, IMAGE_WIDTH); + } + JLabel imageLabel = new JLabel(); + imageLabel.setIcon(speakerIcon); + imageLabel.setAlignmentX(Component.CENTER_ALIGNMENT); + + // Speaker description (below the icon) + StringBuilder speakerDescription = getSpeakerDescription(campaign, speaker, speakerName); + JLabel leftDescription = new JLabel( + String.format("
%s
", + IMAGE_WIDTH, speakerDescription)); + leftDescription.setAlignmentX(Component.CENTER_ALIGNMENT); + + // Add the image and description to the speakerBox + speakerBox.add(imageLabel); + speakerBox.add(leftDescription); + + return speakerBox; + } +} From b61650278f174f8de7bdc5b4d3bad77b67731182 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Mon, 27 Jan 2025 16:27:35 -0600 Subject: [PATCH 023/112] Fix Gray Monday event date check off-by-one error Adjusted the date comparison to include an additional day for the Gray Monday event. This ensures the proper handling of events occurring on the intended time frame. --- MekHQ/src/mekhq/campaign/Campaign.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index c8d0208c9ca..8da9e043f6d 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -4837,7 +4837,7 @@ public boolean newDay() { // Random Events if (currentDay.equals(EVENT_DATE_CLARION_NOTE) || - (currentDay.isBefore(EVENT_DATE_GRAY_MONDAY.plusDays(4)))) { + (currentDay.isBefore(EVENT_DATE_GRAY_MONDAY.plusDays(5)))) { new GrayMonday(this, currentDay); } From 27e10bbc7c2bf1cc6a874295ccca6d37e6448181 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Mon, 27 Jan 2025 16:28:00 -0600 Subject: [PATCH 024/112] Fix whitespace issue in GrayMonday.java Removed unnecessary trailing whitespace to improve code readability and maintain consistent formatting. This change has no functional impact on the application's behavior. --- MekHQ/src/mekhq/campaign/randomEvents/GrayMonday.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MekHQ/src/mekhq/campaign/randomEvents/GrayMonday.java b/MekHQ/src/mekhq/campaign/randomEvents/GrayMonday.java index da70a09e877..a98d6469c07 100644 --- a/MekHQ/src/mekhq/campaign/randomEvents/GrayMonday.java +++ b/MekHQ/src/mekhq/campaign/randomEvents/GrayMonday.java @@ -97,7 +97,7 @@ public GrayMonday(Campaign campaign, LocalDate today) { } campaign.getContractMarket().getContracts().clear(); - + getFormattedTextAt(RESOURCE_BUNDLE, "employer.report"); } } From 0b1575a21e5a919f4919cb428fae605d24fa5718 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Mon, 27 Jan 2025 16:40:08 -0600 Subject: [PATCH 025/112] Handle Gray Monday event in contract generation logic - Added checks to simulate the Gray Monday event, affecting the generation and availability of contracts on the specified date. --- .../AtbMonthlyContractMarket.java | 24 ++++++++++++++++--- .../contractMarket/CamOpsContractMarket.java | 19 ++++++++++++++- .../campaign/randomEvents/GrayMonday.java | 23 +++++++++++++++++- 3 files changed, 61 insertions(+), 5 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/market/contractMarket/AtbMonthlyContractMarket.java b/MekHQ/src/mekhq/campaign/market/contractMarket/AtbMonthlyContractMarket.java index 0e8d0407603..52aba7aaf9e 100644 --- a/MekHQ/src/mekhq/campaign/market/contractMarket/AtbMonthlyContractMarket.java +++ b/MekHQ/src/mekhq/campaign/market/contractMarket/AtbMonthlyContractMarket.java @@ -2,6 +2,7 @@ * ContractMarket.java * * Copyright (c) 2014 Carl Spain. All rights reserved. + * Copyright (c) 2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * @@ -41,6 +42,9 @@ import java.util.ArrayList; import java.util.Set; +import static megamek.common.Compute.d6; +import static mekhq.campaign.randomEvents.GrayMonday.isIsGrayMonday; + /** * Contract offers that are generated monthly under AtB rules. * @@ -66,6 +70,8 @@ public AtBContract addAtBContract(Campaign campaign) { @Override public void generateContractOffers(Campaign campaign, boolean newCampaign) { + boolean isGrayMonday = isIsGrayMonday(campaign); + if (((campaign.getLocalDate().getDayOfMonth() == 1)) || newCampaign) { // need to copy to prevent concurrent modification errors new ArrayList<>(contracts).forEach(this::removeContract); @@ -76,7 +82,19 @@ public void generateContractOffers(Campaign campaign, boolean newCampaign) { checkForSubcontracts(campaign, contract, unitRatingMod); } - int numContracts = Compute.d6() - 4 + unitRatingMod; + int numContracts = d6() - 4 + unitRatingMod; + + if (isGrayMonday) { + for (int i = 0; i < numContracts; i++) { + if (d6() <= 2) { + numContracts--; + } + } + } + + if (numContracts == 0) { + return; + } Set currentFactions = campaign.getCurrentSystem().getFactionSet(campaign.getLocalDate()); final boolean inMinorFaction = currentFactions.stream() @@ -180,7 +198,7 @@ private void checkForSubcontracts(Campaign campaign, AtBContract contract, int u } } for (int i = numSubcontracts; i < unitRatingMod - 1; i++) { - int roll = Compute.d6(2); + int roll = d6(2); if (roll >= 10) { AtBContract sub = generateAtBSubcontract(campaign, contract, unitRatingMod); if (sub.getEndingDate().isBefore(contract.getEndingDate())) { @@ -513,7 +531,7 @@ public void checkForFollowup(Campaign campaign, AtBContract contract) { AtBContractType type = contract.getContractType(); if (type.isDiversionaryRaid() || type.isReconRaid() || type.isRiotDuty()) { - int roll = Compute.d6(); + int roll = d6(); if (roll == 6) { addFollowup(campaign, contract); campaign.addReport( diff --git a/MekHQ/src/mekhq/campaign/market/contractMarket/CamOpsContractMarket.java b/MekHQ/src/mekhq/campaign/market/contractMarket/CamOpsContractMarket.java index fbdd73ae22c..d56c023e777 100644 --- a/MekHQ/src/mekhq/campaign/market/contractMarket/CamOpsContractMarket.java +++ b/MekHQ/src/mekhq/campaign/market/contractMarket/CamOpsContractMarket.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2024-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * @@ -38,6 +38,9 @@ import java.time.format.DateTimeFormatter; import java.util.*; +import static megamek.common.Compute.d6; +import static mekhq.campaign.randomEvents.GrayMonday.isIsGrayMonday; + /** * Contract Market as described in Campaign Operations, 4th printing. */ @@ -65,6 +68,8 @@ public AtBContract addAtBContract(Campaign campaign) { @Override public void generateContractOffers(Campaign campaign, boolean newCampaign) { + boolean isGrayMonday = isIsGrayMonday(campaign); + if (!(campaign.getLocalDate().getDayOfMonth() == 1) && !newCampaign) { return; } @@ -81,6 +86,18 @@ public void generateContractOffers(Campaign campaign, boolean newCampaign) { int numOffers = getNumberOfOffers( rollNegotiation(negotiationSkill, ratingMod + hiringHallModifiers.offersMod) - BASE_NEGOTIATION_TARGET); + if (isGrayMonday) { + for (int i = 0; i < numOffers; i++) { + if (d6() <= 2) { + numOffers--; + } + } + } + + if (numOffers == 0) { + return; + } + for (int i = 0; i < numOffers; i++) { addAtBContract(campaign); } diff --git a/MekHQ/src/mekhq/campaign/randomEvents/GrayMonday.java b/MekHQ/src/mekhq/campaign/randomEvents/GrayMonday.java index a98d6469c07..205879c4510 100644 --- a/MekHQ/src/mekhq/campaign/randomEvents/GrayMonday.java +++ b/MekHQ/src/mekhq/campaign/randomEvents/GrayMonday.java @@ -102,7 +102,6 @@ public GrayMonday(Campaign campaign, LocalDate today) { } } - /** * Retrieves the speaker for the dialogs. * @@ -114,4 +113,26 @@ public GrayMonday(Campaign campaign, LocalDate today) { private @Nullable Person getSpeaker() { return campaign.getSeniorAdminPerson(LOGISTICS); } + + /** + * Determines whether it is within the Gray Monday event period. + * + *

This method checks if the campaign is configured to simulate the Gray Monday event + * and whether the current in-game date falls within a certain period around the + * predefined Gray Monday date. Specifically, it verifies that today is after one day + * prior to the Gray Monday event date and before three months after the event date.

+ * + * @param campaign the {@link Campaign} object containing the campaign data, including + * the current date and campaign options + * @return {@code true} if the Gray Monday event should be active based on the campaign + * configuration and current date, {@code false} otherwise + */ + public static boolean isIsGrayMonday(Campaign campaign) { + LocalDate today = campaign.getLocalDate(); + boolean isGrayMonday = campaign.getCampaignOptions().isSimulateGrayMonday(); + isGrayMonday = isGrayMonday + && today.isAfter(EVENT_DATE_GRAY_MONDAY.minusDays(1)) + && EVENT_DATE_GRAY_MONDAY.isBefore(today.plusMonths(3)); + return isGrayMonday; + } } From f21bf6c0f607cdde008de281b761fea6c8faec90 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Mon, 27 Jan 2025 17:34:55 -0600 Subject: [PATCH 026/112] Adjust contract payout multiplier for Gray Monday - Implemented a 0.25 multiplier adjustment if Gray Monday is active in the campaign. --- .../market/contractMarket/AtbMonthlyContractMarket.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/MekHQ/src/mekhq/campaign/market/contractMarket/AtbMonthlyContractMarket.java b/MekHQ/src/mekhq/campaign/market/contractMarket/AtbMonthlyContractMarket.java index 52aba7aaf9e..9828268fa25 100644 --- a/MekHQ/src/mekhq/campaign/market/contractMarket/AtbMonthlyContractMarket.java +++ b/MekHQ/src/mekhq/campaign/market/contractMarket/AtbMonthlyContractMarket.java @@ -522,6 +522,9 @@ public double calculatePaymentMultiplier(Campaign campaign, AtBContract contract } } + if (isIsGrayMonday(campaign)) { + multiplier *= 0.25; + } return multiplier; } From dd7175a25fb12117b264ca6d8a46c6a7a22939b8 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Mon, 27 Jan 2025 17:40:13 -0600 Subject: [PATCH 027/112] Add Gray Monday modifier to acquisition target rolls - Added a +4 modifier to acquisition target rolls when Gray Monday is active. --- MekHQ/src/mekhq/campaign/Campaign.java | 6 ++++++ .../market/contractMarket/AtbMonthlyContractMarket.java | 1 + MekHQ/src/mekhq/campaign/randomEvents/GrayMonday.java | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index 8da9e043f6d..25307efb36a 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -168,6 +168,7 @@ import static mekhq.campaign.personnel.turnoverAndRetention.RetirementDefectionTracker.Payout.isBreakingContract; import static mekhq.campaign.randomEvents.GrayMonday.EVENT_DATE_CLARION_NOTE; import static mekhq.campaign.randomEvents.GrayMonday.EVENT_DATE_GRAY_MONDAY; +import static mekhq.campaign.randomEvents.GrayMonday.isIsGrayMonday; import static mekhq.campaign.stratcon.SupportPointNegotiation.negotiateAdditionalSupportPoints; import static mekhq.campaign.unit.Unit.SITE_FACILITY_BASIC; import static mekhq.campaign.universe.Factions.getFactionLogo; @@ -7095,6 +7096,11 @@ public TargetRoll getTargetForAcquisition(final IAcquisitionWork acquisition, } TargetRoll target = new TargetRoll(skill.getFinalSkillValue(), skill.getSkillLevel().toString()); target.append(acquisition.getAllAcquisitionMods()); + + if (isIsGrayMonday(this)) { + target.addModifier(4, "Gray Monday"); + } + return target; } diff --git a/MekHQ/src/mekhq/campaign/market/contractMarket/AtbMonthlyContractMarket.java b/MekHQ/src/mekhq/campaign/market/contractMarket/AtbMonthlyContractMarket.java index 9828268fa25..fb08e29661b 100644 --- a/MekHQ/src/mekhq/campaign/market/contractMarket/AtbMonthlyContractMarket.java +++ b/MekHQ/src/mekhq/campaign/market/contractMarket/AtbMonthlyContractMarket.java @@ -522,6 +522,7 @@ public double calculatePaymentMultiplier(Campaign campaign, AtBContract contract } } + // This should always be the last modifier if (isIsGrayMonday(campaign)) { multiplier *= 0.25; } diff --git a/MekHQ/src/mekhq/campaign/randomEvents/GrayMonday.java b/MekHQ/src/mekhq/campaign/randomEvents/GrayMonday.java index 205879c4510..9a650cdfae2 100644 --- a/MekHQ/src/mekhq/campaign/randomEvents/GrayMonday.java +++ b/MekHQ/src/mekhq/campaign/randomEvents/GrayMonday.java @@ -132,7 +132,7 @@ public static boolean isIsGrayMonday(Campaign campaign) { boolean isGrayMonday = campaign.getCampaignOptions().isSimulateGrayMonday(); isGrayMonday = isGrayMonday && today.isAfter(EVENT_DATE_GRAY_MONDAY.minusDays(1)) - && EVENT_DATE_GRAY_MONDAY.isBefore(today.plusMonths(3)); + && EVENT_DATE_GRAY_MONDAY.isBefore(today.plusMonths(12)); return isGrayMonday; } } From af2353e74288bd987680df98b5cf6d4f76d5666d Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Mon, 27 Jan 2025 17:50:02 -0600 Subject: [PATCH 028/112] Adjust market item generation for "Gray Monday" event - Implemented a condition to account for "Gray Monday," reducing market unit rarity during the event. --- MekHQ/src/mekhq/campaign/Campaign.java | 4 +- .../AtbMonthlyContractMarket.java | 6 +- .../contractMarket/CamOpsContractMarket.java | 4 +- .../unitMarket/AtBMonthlyUnitMarket.java | 127 ++++++++++-------- .../campaign/randomEvents/GrayMonday.java | 2 +- 5 files changed, 77 insertions(+), 66 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index 25307efb36a..eb6a7f62ef4 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -168,7 +168,7 @@ import static mekhq.campaign.personnel.turnoverAndRetention.RetirementDefectionTracker.Payout.isBreakingContract; import static mekhq.campaign.randomEvents.GrayMonday.EVENT_DATE_CLARION_NOTE; import static mekhq.campaign.randomEvents.GrayMonday.EVENT_DATE_GRAY_MONDAY; -import static mekhq.campaign.randomEvents.GrayMonday.isIsGrayMonday; +import static mekhq.campaign.randomEvents.GrayMonday.isGrayMonday; import static mekhq.campaign.stratcon.SupportPointNegotiation.negotiateAdditionalSupportPoints; import static mekhq.campaign.unit.Unit.SITE_FACILITY_BASIC; import static mekhq.campaign.universe.Factions.getFactionLogo; @@ -7097,7 +7097,7 @@ public TargetRoll getTargetForAcquisition(final IAcquisitionWork acquisition, TargetRoll target = new TargetRoll(skill.getFinalSkillValue(), skill.getSkillLevel().toString()); target.append(acquisition.getAllAcquisitionMods()); - if (isIsGrayMonday(this)) { + if (isGrayMonday(this)) { target.addModifier(4, "Gray Monday"); } diff --git a/MekHQ/src/mekhq/campaign/market/contractMarket/AtbMonthlyContractMarket.java b/MekHQ/src/mekhq/campaign/market/contractMarket/AtbMonthlyContractMarket.java index fb08e29661b..e44e5d7cb85 100644 --- a/MekHQ/src/mekhq/campaign/market/contractMarket/AtbMonthlyContractMarket.java +++ b/MekHQ/src/mekhq/campaign/market/contractMarket/AtbMonthlyContractMarket.java @@ -43,7 +43,7 @@ import java.util.Set; import static megamek.common.Compute.d6; -import static mekhq.campaign.randomEvents.GrayMonday.isIsGrayMonday; +import static mekhq.campaign.randomEvents.GrayMonday.isGrayMonday; /** * Contract offers that are generated monthly under AtB rules. @@ -70,7 +70,7 @@ public AtBContract addAtBContract(Campaign campaign) { @Override public void generateContractOffers(Campaign campaign, boolean newCampaign) { - boolean isGrayMonday = isIsGrayMonday(campaign); + boolean isGrayMonday = isGrayMonday(campaign); if (((campaign.getLocalDate().getDayOfMonth() == 1)) || newCampaign) { // need to copy to prevent concurrent modification errors @@ -523,7 +523,7 @@ public double calculatePaymentMultiplier(Campaign campaign, AtBContract contract } // This should always be the last modifier - if (isIsGrayMonday(campaign)) { + if (isGrayMonday(campaign)) { multiplier *= 0.25; } diff --git a/MekHQ/src/mekhq/campaign/market/contractMarket/CamOpsContractMarket.java b/MekHQ/src/mekhq/campaign/market/contractMarket/CamOpsContractMarket.java index d56c023e777..020baebe98a 100644 --- a/MekHQ/src/mekhq/campaign/market/contractMarket/CamOpsContractMarket.java +++ b/MekHQ/src/mekhq/campaign/market/contractMarket/CamOpsContractMarket.java @@ -39,7 +39,7 @@ import java.util.*; import static megamek.common.Compute.d6; -import static mekhq.campaign.randomEvents.GrayMonday.isIsGrayMonday; +import static mekhq.campaign.randomEvents.GrayMonday.isGrayMonday; /** * Contract Market as described in Campaign Operations, 4th printing. @@ -68,7 +68,7 @@ public AtBContract addAtBContract(Campaign campaign) { @Override public void generateContractOffers(Campaign campaign, boolean newCampaign) { - boolean isGrayMonday = isIsGrayMonday(campaign); + boolean isGrayMonday = isGrayMonday(campaign); if (!(campaign.getLocalDate().getDayOfMonth() == 1) && !newCampaign) { return; diff --git a/MekHQ/src/mekhq/campaign/market/unitMarket/AtBMonthlyUnitMarket.java b/MekHQ/src/mekhq/campaign/market/unitMarket/AtBMonthlyUnitMarket.java index 327314c9e41..7aae6fc62dd 100644 --- a/MekHQ/src/mekhq/campaign/market/unitMarket/AtBMonthlyUnitMarket.java +++ b/MekHQ/src/mekhq/campaign/market/unitMarket/AtBMonthlyUnitMarket.java @@ -42,6 +42,7 @@ import static mekhq.campaign.market.enums.UnitMarketRarity.*; import static mekhq.campaign.market.enums.UnitMarketType.getPricePercentage; +import static mekhq.campaign.randomEvents.GrayMonday.isGrayMonday; public class AtBMonthlyUnitMarket extends AbstractUnitMarket { //region Constructors @@ -80,48 +81,49 @@ public void generateUnitOffers(final Campaign campaign) { Faction faction = campaign.getFaction(); int rarityModifier = campaign.getCampaignOptions().getUnitMarketRarityModifier(); - addOffers(campaign, getMarketItemCount(UNCOMMON, rarityModifier), UnitMarketType.OPEN, UnitType.MEK, - faction, IUnitRating.DRAGOON_F, 1); + addOffers(campaign, getMarketItemCount(campaign, UNCOMMON, rarityModifier), + UnitMarketType.OPEN, UnitType.MEK, faction, IUnitRating.DRAGOON_F, 1); - addOffers(campaign, getMarketItemCount(UNCOMMON, rarityModifier), UnitMarketType.OPEN, UnitType.AEROSPACEFIGHTER, - faction, IUnitRating.DRAGOON_F, 1); + addOffers(campaign, getMarketItemCount(campaign, UNCOMMON, rarityModifier), + UnitMarketType.OPEN, UnitType.AEROSPACEFIGHTER, faction, IUnitRating.DRAGOON_F, 1); - addOffers(campaign, getMarketItemCount(VERY_COMMON, rarityModifier), UnitMarketType.OPEN, UnitType.TANK, - faction, IUnitRating.DRAGOON_F, 1); + addOffers(campaign, getMarketItemCount(campaign, VERY_COMMON, rarityModifier), + UnitMarketType.OPEN, UnitType.TANK, faction, IUnitRating.DRAGOON_F, 1); - addOffers(campaign, getMarketItemCount(COMMON, rarityModifier), UnitMarketType.OPEN, UnitType.CONV_FIGHTER, - faction, IUnitRating.DRAGOON_F, 1); + addOffers(campaign, getMarketItemCount(campaign, COMMON, rarityModifier), + UnitMarketType.OPEN, UnitType.CONV_FIGHTER, faction, IUnitRating.DRAGOON_F, 1); - if ((contract != null) && (campaign.getLocalDate().isAfter(contract.getStartDate().minusDays(1)))) { + if ((contract != null) + && (campaign.getLocalDate().isAfter(contract.getStartDate().minusDays(1)))) { // Employer Market faction = contract.getEmployerFaction(); - addOffers(campaign, getMarketItemCount(RARE, rarityModifier), UnitMarketType.EMPLOYER, UnitType.MEK, - faction, IUnitRating.DRAGOON_D, -1); + addOffers(campaign, getMarketItemCount(campaign, RARE, rarityModifier), + UnitMarketType.EMPLOYER, UnitType.MEK, faction, IUnitRating.DRAGOON_D, -1); - addOffers(campaign, getMarketItemCount(RARE, rarityModifier), UnitMarketType.EMPLOYER, UnitType.AEROSPACEFIGHTER, - faction, IUnitRating.DRAGOON_D, -1); + addOffers(campaign, getMarketItemCount(campaign, RARE, rarityModifier), + UnitMarketType.EMPLOYER, UnitType.AEROSPACEFIGHTER, faction, IUnitRating.DRAGOON_D, -1); - addOffers(campaign, getMarketItemCount(COMMON, rarityModifier), UnitMarketType.EMPLOYER, UnitType.TANK, - faction, IUnitRating.DRAGOON_D, -1); + addOffers(campaign, getMarketItemCount(campaign, COMMON, rarityModifier), + UnitMarketType.EMPLOYER, UnitType.TANK, faction, IUnitRating.DRAGOON_D, -1); - addOffers(campaign, getMarketItemCount(UNCOMMON, rarityModifier), UnitMarketType.EMPLOYER, UnitType.CONV_FIGHTER, - faction, IUnitRating.DRAGOON_D, -1); + addOffers(campaign, getMarketItemCount(campaign, UNCOMMON, rarityModifier), + UnitMarketType.EMPLOYER, UnitType.CONV_FIGHTER, faction, IUnitRating.DRAGOON_D, -1); // Unwanted Salvage Market faction = contract.getEnemy(); - addOffers(campaign, getMarketItemCount(RARE, rarityModifier), UnitMarketType.EMPLOYER, UnitType.MEK, - faction, IUnitRating.DRAGOON_F, 2); + addOffers(campaign, getMarketItemCount(campaign, RARE, rarityModifier), + UnitMarketType.EMPLOYER, UnitType.MEK, faction, IUnitRating.DRAGOON_F, 2); - addOffers(campaign, getMarketItemCount(RARE, rarityModifier), UnitMarketType.EMPLOYER, UnitType.AEROSPACEFIGHTER, - faction, IUnitRating.DRAGOON_F, 2); + addOffers(campaign, getMarketItemCount(campaign, RARE, rarityModifier), + UnitMarketType.EMPLOYER, UnitType.AEROSPACEFIGHTER, faction, IUnitRating.DRAGOON_F, 2); - addOffers(campaign, getMarketItemCount(UNCOMMON, rarityModifier), UnitMarketType.EMPLOYER, UnitType.TANK, - faction, IUnitRating.DRAGOON_F, 2); + addOffers(campaign, getMarketItemCount(campaign, UNCOMMON, rarityModifier), + UnitMarketType.EMPLOYER, UnitType.TANK, faction, IUnitRating.DRAGOON_F, 2); - addOffers(campaign, getMarketItemCount(UNCOMMON, rarityModifier), UnitMarketType.EMPLOYER, UnitType.CONV_FIGHTER, - faction, IUnitRating.DRAGOON_F, 2); + addOffers(campaign, getMarketItemCount(campaign, UNCOMMON, rarityModifier), + UnitMarketType.EMPLOYER, UnitType.CONV_FIGHTER, faction, IUnitRating.DRAGOON_F, 2); } // Mercenary Market @@ -134,17 +136,17 @@ public void generateUnitOffers(final Campaign campaign) { modifier = -1; } - addOffers(campaign, getMarketItemCount(UNCOMMON, rarityModifier), UnitMarketType.MERCENARY, UnitType.MEK, - faction, IUnitRating.DRAGOON_C, modifier); + addOffers(campaign, getMarketItemCount(campaign, UNCOMMON, rarityModifier), + UnitMarketType.MERCENARY, UnitType.MEK, faction, IUnitRating.DRAGOON_C, modifier); - addOffers(campaign, getMarketItemCount(UNCOMMON, rarityModifier), UnitMarketType.MERCENARY, UnitType.AEROSPACEFIGHTER, - faction, IUnitRating.DRAGOON_C, modifier); + addOffers(campaign, getMarketItemCount(campaign, UNCOMMON, rarityModifier), + UnitMarketType.MERCENARY, UnitType.AEROSPACEFIGHTER, faction, IUnitRating.DRAGOON_C, modifier); - addOffers(campaign, getMarketItemCount(VERY_COMMON, rarityModifier), UnitMarketType.MERCENARY, UnitType.TANK, - faction, IUnitRating.DRAGOON_C, modifier); + addOffers(campaign, getMarketItemCount(campaign, VERY_COMMON, rarityModifier), + UnitMarketType.MERCENARY, UnitType.TANK, faction, IUnitRating.DRAGOON_C, modifier); - addOffers(campaign, getMarketItemCount(UNCOMMON, rarityModifier), UnitMarketType.MERCENARY, UnitType.CONV_FIGHTER, - faction, IUnitRating.DRAGOON_C, modifier); + addOffers(campaign, getMarketItemCount(campaign, UNCOMMON, rarityModifier), + UnitMarketType.MERCENARY, UnitType.CONV_FIGHTER, faction, IUnitRating.DRAGOON_C, modifier); } // Factory Market @@ -153,17 +155,17 @@ public void generateUnitOffers(final Campaign campaign) { .getFactionSet(campaign.getLocalDate())); if ((!campaign.getFaction().isClan()) && (faction != null) && (!faction.isClan())) { - addOffers(campaign, getMarketItemCount(RARE, rarityModifier), UnitMarketType.FACTORY, UnitType.MEK, - faction, IUnitRating.DRAGOON_A, 2); + addOffers(campaign, getMarketItemCount(campaign, RARE, rarityModifier), + UnitMarketType.FACTORY, UnitType.MEK, faction, IUnitRating.DRAGOON_A, 2); - addOffers(campaign, getMarketItemCount(RARE, rarityModifier), UnitMarketType.FACTORY, UnitType.AEROSPACEFIGHTER, - faction, IUnitRating.DRAGOON_A, 2); + addOffers(campaign, getMarketItemCount(campaign, RARE, rarityModifier), + UnitMarketType.FACTORY, UnitType.AEROSPACEFIGHTER, faction, IUnitRating.DRAGOON_A, 2); - addOffers(campaign, getMarketItemCount(COMMON, rarityModifier), UnitMarketType.FACTORY, UnitType.TANK, - faction, IUnitRating.DRAGOON_A, 2); + addOffers(campaign, getMarketItemCount(campaign, COMMON, rarityModifier), + UnitMarketType.FACTORY, UnitType.TANK, faction, IUnitRating.DRAGOON_A, 2); - addOffers(campaign, getMarketItemCount(UNCOMMON, rarityModifier), UnitMarketType.FACTORY, UnitType.CONV_FIGHTER, - faction, IUnitRating.DRAGOON_A, 2); + addOffers(campaign, getMarketItemCount(campaign, UNCOMMON, rarityModifier), + UnitMarketType.FACTORY, UnitType.CONV_FIGHTER, faction, IUnitRating.DRAGOON_A, 2); } } @@ -171,14 +173,14 @@ public void generateUnitOffers(final Campaign campaign) { // Clan Factory Market if ((faction.isClan()) && (campaign.getCurrentSystem().getFactionSet(campaign.getLocalDate()).contains(faction))) { - addOffers(campaign, getMarketItemCount(VERY_COMMON, rarityModifier), UnitMarketType.FACTORY, UnitType.MEK, - faction, IUnitRating.DRAGOON_A, -4); + addOffers(campaign, getMarketItemCount(campaign, VERY_COMMON, rarityModifier), + UnitMarketType.FACTORY, UnitType.MEK, faction, IUnitRating.DRAGOON_A, -4); - addOffers(campaign, getMarketItemCount(COMMON, rarityModifier), UnitMarketType.FACTORY, UnitType.AEROSPACEFIGHTER, - faction, IUnitRating.DRAGOON_A, -4); + addOffers(campaign, getMarketItemCount(campaign, COMMON, rarityModifier), + UnitMarketType.FACTORY, UnitType.AEROSPACEFIGHTER, faction, IUnitRating.DRAGOON_A, -4); - addOffers(campaign, getMarketItemCount(UNCOMMON, rarityModifier), UnitMarketType.FACTORY, UnitType.TANK, - faction, IUnitRating.DRAGOON_A, -4); + addOffers(campaign, getMarketItemCount(campaign, UNCOMMON, rarityModifier), + UnitMarketType.FACTORY, UnitType.TANK, faction, IUnitRating.DRAGOON_A, -4); } // Black Market @@ -186,17 +188,17 @@ public void generateUnitOffers(final Campaign campaign) { faction = ObjectUtility.getRandomItem(campaign.getCurrentSystem() .getFactionSet(campaign.getLocalDate())); - addOffers(campaign, getMarketItemCount(VERY_RARE, rarityModifier), UnitMarketType.BLACK_MARKET, UnitType.MEK, - faction, IUnitRating.DRAGOON_A, -8); + addOffers(campaign, getMarketItemCount(campaign, VERY_RARE, rarityModifier), + UnitMarketType.BLACK_MARKET, UnitType.MEK, faction, IUnitRating.DRAGOON_A, -8); - addOffers(campaign, getMarketItemCount(VERY_RARE, rarityModifier), UnitMarketType.BLACK_MARKET, UnitType.AEROSPACEFIGHTER, - faction, IUnitRating.DRAGOON_A, -8); + addOffers(campaign, getMarketItemCount(campaign, VERY_RARE, rarityModifier), + UnitMarketType.BLACK_MARKET, UnitType.AEROSPACEFIGHTER, faction, IUnitRating.DRAGOON_A, -8); - addOffers(campaign, getMarketItemCount(RARE, rarityModifier), UnitMarketType.BLACK_MARKET, UnitType.TANK, - faction, IUnitRating.DRAGOON_A, -8); + addOffers(campaign, getMarketItemCount(campaign, RARE, rarityModifier), + UnitMarketType.BLACK_MARKET, UnitType.TANK, faction, IUnitRating.DRAGOON_A, -8); - addOffers(campaign, getMarketItemCount(RARE, rarityModifier), UnitMarketType.BLACK_MARKET, UnitType.CONV_FIGHTER, - faction, IUnitRating.DRAGOON_A, -8); + addOffers(campaign, getMarketItemCount(campaign, RARE, rarityModifier), + UnitMarketType.BLACK_MARKET, UnitType.CONV_FIGHTER, faction, IUnitRating.DRAGOON_A, -8); } writeRefreshReport(campaign); @@ -209,10 +211,15 @@ public void generateUnitOffers(final Campaign campaign) { * @param rarityModifier the unit count modifier specified in campaign options * @return an integer representing the count of market items */ - private int getMarketItemCount(UnitMarketRarity rarity, int rarityModifier) { + private int getMarketItemCount(Campaign campaign, UnitMarketRarity rarity, int rarityModifier) { + int totalRarity = rarity.ordinal() + rarityModifier; + + if (isGrayMonday(campaign)) { + totalRarity -= 4; + } + return Compute.d6(1) - + rarity.ordinal() - + rarityModifier + + totalRarity - 3; } @@ -229,6 +236,10 @@ public void addOffers(final Campaign campaign, final int num, UnitMarketType mar market = UnitMarketType.OPEN; } + if (num <= 1) { + return; + } + for (int i = 0; i < num; i++) { final Collection movementModes = new ArrayList<>(); final Collection missionRoles = new ArrayList<>(); diff --git a/MekHQ/src/mekhq/campaign/randomEvents/GrayMonday.java b/MekHQ/src/mekhq/campaign/randomEvents/GrayMonday.java index 9a650cdfae2..596e18f3cc7 100644 --- a/MekHQ/src/mekhq/campaign/randomEvents/GrayMonday.java +++ b/MekHQ/src/mekhq/campaign/randomEvents/GrayMonday.java @@ -127,7 +127,7 @@ public GrayMonday(Campaign campaign, LocalDate today) { * @return {@code true} if the Gray Monday event should be active based on the campaign * configuration and current date, {@code false} otherwise */ - public static boolean isIsGrayMonday(Campaign campaign) { + public static boolean isGrayMonday(Campaign campaign) { LocalDate today = campaign.getLocalDate(); boolean isGrayMonday = campaign.getCampaignOptions().isSimulateGrayMonday(); isGrayMonday = isGrayMonday From 27089101bc829d0d0b456b73ee04def2183874bf Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Mon, 27 Jan 2025 17:51:42 -0600 Subject: [PATCH 029/112] Add 'simulateGrayMonday' setting to campaign presets - Added the 'simulateGrayMonday' parameter to multiple campaign preset configuration files with a default value of 'false,' except for 'TheCompleteExperience,' where it was set to 'true.' --- MekHQ/mmconf/campaignPresets/CampaignOperations.xml | 1 + MekHQ/mmconf/campaignPresets/CampaignOperationsStratCon.xml | 1 + MekHQ/mmconf/campaignPresets/NewPilotProgram.xml | 1 + MekHQ/mmconf/campaignPresets/TheCompleteExperience.xml | 1 + 4 files changed, 4 insertions(+) diff --git a/MekHQ/mmconf/campaignPresets/CampaignOperations.xml b/MekHQ/mmconf/campaignPresets/CampaignOperations.xml index c96b14c0dd8..e32a29fcd9b 100644 --- a/MekHQ/mmconf/campaignPresets/CampaignOperations.xml +++ b/MekHQ/mmconf/campaignPresets/CampaignOperations.xml @@ -491,6 +491,7 @@ false ANNUAL false + false 1.0 1.0 1.0 diff --git a/MekHQ/mmconf/campaignPresets/CampaignOperationsStratCon.xml b/MekHQ/mmconf/campaignPresets/CampaignOperationsStratCon.xml index 545c94b00eb..7ba6af65498 100644 --- a/MekHQ/mmconf/campaignPresets/CampaignOperationsStratCon.xml +++ b/MekHQ/mmconf/campaignPresets/CampaignOperationsStratCon.xml @@ -491,6 +491,7 @@ false ANNUAL false + false 1.0 1.0 1.0 diff --git a/MekHQ/mmconf/campaignPresets/NewPilotProgram.xml b/MekHQ/mmconf/campaignPresets/NewPilotProgram.xml index 06ea0ae60f9..61f1f063d6f 100644 --- a/MekHQ/mmconf/campaignPresets/NewPilotProgram.xml +++ b/MekHQ/mmconf/campaignPresets/NewPilotProgram.xml @@ -491,6 +491,7 @@ false ANNUAL false + false 1.0 1.0 1.0 diff --git a/MekHQ/mmconf/campaignPresets/TheCompleteExperience.xml b/MekHQ/mmconf/campaignPresets/TheCompleteExperience.xml index bc958215c21..1a2d0bf3e0e 100644 --- a/MekHQ/mmconf/campaignPresets/TheCompleteExperience.xml +++ b/MekHQ/mmconf/campaignPresets/TheCompleteExperience.xml @@ -492,6 +492,7 @@ false ANNUAL false + true 1.0 1.0 1.0 From e5446b8ac02369bfb54cf8dff0634ab7130bf6f9 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Mon, 27 Jan 2025 17:55:46 -0600 Subject: [PATCH 030/112] Adjusted date for Grey Monday in DateChooser - Updated the date for the Grey Monday turning point from August 7, 3132, to August 3, 3132. This ensures players picking that date will get the full Gray Monday experience. --- MekHQ/src/mekhq/gui/dialog/DateChooser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MekHQ/src/mekhq/gui/dialog/DateChooser.java b/MekHQ/src/mekhq/gui/dialog/DateChooser.java index 8db1e9eaa81..41d8a8fc311 100644 --- a/MekHQ/src/mekhq/gui/dialog/DateChooser.java +++ b/MekHQ/src/mekhq/gui/dialog/DateChooser.java @@ -810,7 +810,7 @@ private static TurningPoints getTurningPoints(int era) { } case 10 -> { turningPoints = List.of("GreyMonday"); - turningPointDates = List.of(LocalDate.of(3132, 8, 7)); + turningPointDates = List.of(LocalDate.of(3132, 8, 3)); eraLogo = new ImageIcon(LOGO_DIRECTORY + "era_darkage" + LOGO_FILE_TYPE); } case 11 -> { From 6c5619306946eb1c4a0f5e9706cb8db8af2b9943 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Mon, 27 Jan 2025 17:59:02 -0600 Subject: [PATCH 031/112] Rolled back unnecessary changes from another branch --- .../CampaignHasProblemOnLoad.properties | 6 +++--- .../VocationalExperienceAwardDialog.properties | 2 +- .../gui/dialog/CampaignHasProblemOnLoad.java | 18 +++++++++++------- .../VocationalExperienceAwardDialog.java | 17 +++++++++++------ 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/MekHQ/resources/mekhq/resources/CampaignHasProblemOnLoad.properties b/MekHQ/resources/mekhq/resources/CampaignHasProblemOnLoad.properties index c93826346e4..4d6e97f92ff 100644 --- a/MekHQ/resources/mekhq/resources/CampaignHasProblemOnLoad.properties +++ b/MekHQ/resources/mekhq/resources/CampaignHasProblemOnLoad.properties @@ -1,11 +1,11 @@ cancel.button=Cancel continue.button=Continue Regardless -CANT_LOAD_FROM_NEWER_VERSION.message={0}, we seem to be having a problem with our command and control\ +CANT_LOAD_FROM_NEWER_VERSION.message=%s, we seem to be having a problem with our command and control\ \ software. Checking the data, it looks like we might have a version mismatch. CANT_LOAD_FROM_NEWER_VERSION.ooc=A campaign can never be loaded into an older version. -CANT_LOAD_FROM_OLDER_VERSION.message={0}, we seem to be having a problem with our command and control\ +CANT_LOAD_FROM_OLDER_VERSION.message=%s, we seem to be having a problem with our command and control\ \ software. Checking the data, it looks like we still need to update our systems. CANT_LOAD_FROM_OLDER_VERSION.ooc=

To avoid file corruption and ensure a smooth experience, load\ \ and save your campaign in each Milestone released after the version your campaign was last\ @@ -23,7 +23,7 @@ CANT_LOAD_FROM_OLDER_VERSION.ooc=

To avoid file corruption and ensure a smooth
\

The MekHQ team will not offer assistance if you ignore this warning.

-ACTIVE_OR_FUTURE_CONTRACT.message=

{0}, our command and control software license doesn't support\ +ACTIVE_OR_FUTURE_CONTRACT.message=

%s, our command and control software license doesn't support\ \ this action.

\
\

We can continue, but it will void the warranty. diff --git a/MekHQ/resources/mekhq/resources/VocationalExperienceAwardDialog.properties b/MekHQ/resources/mekhq/resources/VocationalExperienceAwardDialog.properties index e09a38e8864..da93e3003a2 100644 --- a/MekHQ/resources/mekhq/resources/VocationalExperienceAwardDialog.properties +++ b/MekHQ/resources/mekhq/resources/VocationalExperienceAwardDialog.properties @@ -5,6 +5,6 @@ dialog.message=, our line-officers have flagged the following personnel as havin
It might be worth reviewing their records.\
\
-dialog.ooc=Each of the listed characters has gained {0} xp.\ +dialog.ooc=Each of the listed characters has gained %d xp.\
This represents improvements made naturally while performing the duties required by their\ \ assigned roles. \ No newline at end of file diff --git a/MekHQ/src/mekhq/gui/dialog/CampaignHasProblemOnLoad.java b/MekHQ/src/mekhq/gui/dialog/CampaignHasProblemOnLoad.java index 93f49c8f1bf..85a5b9939eb 100644 --- a/MekHQ/src/mekhq/gui/dialog/CampaignHasProblemOnLoad.java +++ b/MekHQ/src/mekhq/gui/dialog/CampaignHasProblemOnLoad.java @@ -19,16 +19,17 @@ package mekhq.gui.dialog; import megamek.common.annotations.Nullable; +import mekhq.MekHQ; import mekhq.campaign.Campaign; import mekhq.campaign.CampaignFactory.CampaignProblemType; import mekhq.campaign.personnel.Person; import mekhq.gui.baseComponents.MHQDialogImmersive; import java.util.List; +import java.util.ResourceBundle; import static mekhq.campaign.Campaign.AdministratorSpecialization.COMMAND; import static mekhq.campaign.CampaignFactory.CampaignProblemType.CANT_LOAD_FROM_NEWER_VERSION; -import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; /** * Dialog to inform and handle campaign-loading problems within MekHQ. @@ -42,7 +43,9 @@ * text based on the problem type and campaign information.

*/ public class CampaignHasProblemOnLoad extends MHQDialogImmersive { - private static final String RESOURCE_BUNDLE = "mekhq.resources.CampaignHasProblemOnLoad"; + private static final String BUNDLE_KEY = "mekhq.resources.CampaignHasProblemOnLoad"; + private static final ResourceBundle resources = ResourceBundle.getBundle( + BUNDLE_KEY, MekHQ.getMHQOptions().getLocale()); /** * Constructs the dialog to handle campaign load problems. @@ -57,7 +60,8 @@ public class CampaignHasProblemOnLoad extends MHQDialogImmersive { */ public CampaignHasProblemOnLoad(Campaign campaign, CampaignProblemType problemType) { super(campaign, getSpeaker(campaign), null, createInCharacterMessage(campaign, problemType), - createButtons(problemType), createOutOfCharacterMessage(problemType), null); + createButtons(problemType), createOutOfCharacterMessage(problemType), 0, + null, null, null); } /** @@ -79,10 +83,10 @@ public CampaignHasProblemOnLoad(Campaign campaign, CampaignProblemType problemTy */ private static List createButtons(CampaignProblemType problemType) { ButtonLabelTooltipPair btnCancel = new ButtonLabelTooltipPair( - getFormattedTextAt(RESOURCE_BUNDLE, "cancel.button"), null); + resources.getString("cancel.button"), null); ButtonLabelTooltipPair btnContinue = new ButtonLabelTooltipPair( - getFormattedTextAt(RESOURCE_BUNDLE, "continue.button"), null); + resources.getString("continue.button"), null); if (problemType == CANT_LOAD_FROM_NEWER_VERSION) { return List.of(btnCancel); @@ -119,7 +123,7 @@ private static String createInCharacterMessage(Campaign campaign, CampaignProble String typeKey = problemType.toString(); String commanderAddress = campaign.getCommanderAddress(false); - return getFormattedTextAt(RESOURCE_BUNDLE, typeKey + ".message", commanderAddress); + return String.format(resources.getString(typeKey + ".message"), commanderAddress); } /** @@ -133,6 +137,6 @@ private static String createInCharacterMessage(Campaign campaign, CampaignProble */ private static String createOutOfCharacterMessage(CampaignProblemType problemType) { String typeKey = problemType.toString(); - return getFormattedTextAt(RESOURCE_BUNDLE, typeKey + ".ooc"); + return resources.getString(typeKey + ".ooc"); } } diff --git a/MekHQ/src/mekhq/gui/dialog/VocationalExperienceAwardDialog.java b/MekHQ/src/mekhq/gui/dialog/VocationalExperienceAwardDialog.java index f3951185609..507a2fc298b 100644 --- a/MekHQ/src/mekhq/gui/dialog/VocationalExperienceAwardDialog.java +++ b/MekHQ/src/mekhq/gui/dialog/VocationalExperienceAwardDialog.java @@ -18,7 +18,9 @@ */ package mekhq.gui.dialog; +import megamek.client.ui.swing.util.UIUtil; import megamek.common.annotations.Nullable; +import mekhq.MekHQ; import mekhq.campaign.Campaign; import mekhq.campaign.CampaignOptions; import mekhq.campaign.mission.AtBContract; @@ -27,10 +29,10 @@ import mekhq.gui.baseComponents.MHQDialogImmersive; import java.util.List; +import java.util.ResourceBundle; import java.util.UUID; import static mekhq.campaign.Campaign.AdministratorSpecialization.HR; -import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; /** * A dialog that displays a notification to the commander about personnel @@ -42,7 +44,9 @@ * personnel records via hyperlinks.

*/ public class VocationalExperienceAwardDialog extends MHQDialogImmersive { - private static final String RESOURCE_BUNDLE = "mekhq.resources.VocationalExperienceAwardDialog"; + private static final String BUNDLE_KEY = "mekhq.resources.VocationalExperienceAwardDialog"; + private static final ResourceBundle resources = ResourceBundle.getBundle( + BUNDLE_KEY, MekHQ.getMHQOptions().getLocale()); /** * Constructs the {@link VocationalExperienceAwardDialog}. @@ -55,7 +59,8 @@ public class VocationalExperienceAwardDialog extends MHQDialogImmersive { */ public VocationalExperienceAwardDialog(Campaign campaign) { super(campaign, getSpeaker(campaign), null, createInCharacterMessage(campaign), - createButtons(), createOutOfCharacterMessage(campaign), null); + createButtons(), createOutOfCharacterMessage(campaign), 0, null, + UIUtil.scaleForGUI(800), null); setModal(false); setAlwaysOnTop(true); @@ -88,7 +93,7 @@ protected void handleHyperlinkClick(Campaign campaign, String hyperlinkReference */ private static List createButtons() { ButtonLabelTooltipPair btnConfirm = new ButtonLabelTooltipPair( - getFormattedTextAt(RESOURCE_BUNDLE, "confirm.button"), null); + resources.getString("confirm.button"), null); return List.of(btnConfirm); } @@ -123,7 +128,7 @@ private static String createInCharacterMessage(Campaign campaign) { StringBuilder message = new StringBuilder(); message.append(commanderAddress); - message.append(getFormattedTextAt(RESOURCE_BUNDLE, "dialog.message")); + message.append(resources.getString("dialog.message")); // Create a table to hold the personnel message.append("
"); @@ -188,6 +193,6 @@ private static String createOutOfCharacterMessage(Campaign campaign) { } } - return getFormattedTextAt(RESOURCE_BUNDLE, "dialog.ooc", advancement); + return String.format(resources.getString("dialog.ooc"), advancement); } } From caf10be39d810ccb9bc13548dbc0dc7efede0ca6 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Mon, 27 Jan 2025 19:11:32 -0600 Subject: [PATCH 032/112] Refactored dialog constructors to remove unused parameters Removed unnecessary null and scaling parameters from dialog constructors for cleaner and more maintainable code. This simplifies the method signatures without impacting functionality. --- MekHQ/src/mekhq/gui/dialog/CampaignHasProblemOnLoad.java | 3 +-- .../src/mekhq/gui/dialog/VocationalExperienceAwardDialog.java | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/MekHQ/src/mekhq/gui/dialog/CampaignHasProblemOnLoad.java b/MekHQ/src/mekhq/gui/dialog/CampaignHasProblemOnLoad.java index 85a5b9939eb..e470e5d5f3d 100644 --- a/MekHQ/src/mekhq/gui/dialog/CampaignHasProblemOnLoad.java +++ b/MekHQ/src/mekhq/gui/dialog/CampaignHasProblemOnLoad.java @@ -60,8 +60,7 @@ public class CampaignHasProblemOnLoad extends MHQDialogImmersive { */ public CampaignHasProblemOnLoad(Campaign campaign, CampaignProblemType problemType) { super(campaign, getSpeaker(campaign), null, createInCharacterMessage(campaign, problemType), - createButtons(problemType), createOutOfCharacterMessage(problemType), 0, - null, null, null); + createButtons(problemType), createOutOfCharacterMessage(problemType), 0); } /** diff --git a/MekHQ/src/mekhq/gui/dialog/VocationalExperienceAwardDialog.java b/MekHQ/src/mekhq/gui/dialog/VocationalExperienceAwardDialog.java index 507a2fc298b..be38ef8665c 100644 --- a/MekHQ/src/mekhq/gui/dialog/VocationalExperienceAwardDialog.java +++ b/MekHQ/src/mekhq/gui/dialog/VocationalExperienceAwardDialog.java @@ -18,7 +18,6 @@ */ package mekhq.gui.dialog; -import megamek.client.ui.swing.util.UIUtil; import megamek.common.annotations.Nullable; import mekhq.MekHQ; import mekhq.campaign.Campaign; @@ -59,8 +58,7 @@ public class VocationalExperienceAwardDialog extends MHQDialogImmersive { */ public VocationalExperienceAwardDialog(Campaign campaign) { super(campaign, getSpeaker(campaign), null, createInCharacterMessage(campaign), - createButtons(), createOutOfCharacterMessage(campaign), 0, null, - UIUtil.scaleForGUI(800), null); + createButtons(), createOutOfCharacterMessage(campaign), 0); setModal(false); setAlwaysOnTop(true); From 2d81f74e4e521503fcf3b5875775f8c98c0801e9 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Mon, 27 Jan 2025 19:17:46 -0600 Subject: [PATCH 033/112] Rolled back unnecessary changes from another branch --- .../CampaignHasProblemOnLoad.properties | 6 +++--- .../VocationalExperienceAwardDialog.properties | 2 +- .../gui/dialog/CampaignHasProblemOnLoad.java | 17 +++++++---------- .../dialog/VocationalExperienceAwardDialog.java | 15 ++++++--------- 4 files changed, 17 insertions(+), 23 deletions(-) diff --git a/MekHQ/resources/mekhq/resources/CampaignHasProblemOnLoad.properties b/MekHQ/resources/mekhq/resources/CampaignHasProblemOnLoad.properties index 4d6e97f92ff..c93826346e4 100644 --- a/MekHQ/resources/mekhq/resources/CampaignHasProblemOnLoad.properties +++ b/MekHQ/resources/mekhq/resources/CampaignHasProblemOnLoad.properties @@ -1,11 +1,11 @@ cancel.button=Cancel continue.button=Continue Regardless -CANT_LOAD_FROM_NEWER_VERSION.message=%s, we seem to be having a problem with our command and control\ +CANT_LOAD_FROM_NEWER_VERSION.message={0}, we seem to be having a problem with our command and control\ \ software. Checking the data, it looks like we might have a version mismatch. CANT_LOAD_FROM_NEWER_VERSION.ooc=A campaign can never be loaded into an older version. -CANT_LOAD_FROM_OLDER_VERSION.message=%s, we seem to be having a problem with our command and control\ +CANT_LOAD_FROM_OLDER_VERSION.message={0}, we seem to be having a problem with our command and control\ \ software. Checking the data, it looks like we still need to update our systems. CANT_LOAD_FROM_OLDER_VERSION.ooc=

To avoid file corruption and ensure a smooth experience, load\ \ and save your campaign in each Milestone released after the version your campaign was last\ @@ -23,7 +23,7 @@ CANT_LOAD_FROM_OLDER_VERSION.ooc=

To avoid file corruption and ensure a smooth
\

The MekHQ team will not offer assistance if you ignore this warning.

-ACTIVE_OR_FUTURE_CONTRACT.message=

%s, our command and control software license doesn't support\ +ACTIVE_OR_FUTURE_CONTRACT.message=

{0}, our command and control software license doesn't support\ \ this action.

\
\

We can continue, but it will void the warranty. diff --git a/MekHQ/resources/mekhq/resources/VocationalExperienceAwardDialog.properties b/MekHQ/resources/mekhq/resources/VocationalExperienceAwardDialog.properties index da93e3003a2..e09a38e8864 100644 --- a/MekHQ/resources/mekhq/resources/VocationalExperienceAwardDialog.properties +++ b/MekHQ/resources/mekhq/resources/VocationalExperienceAwardDialog.properties @@ -5,6 +5,6 @@ dialog.message=, our line-officers have flagged the following personnel as havin
It might be worth reviewing their records.\
\
-dialog.ooc=Each of the listed characters has gained %d xp.\ +dialog.ooc=Each of the listed characters has gained {0} xp.\
This represents improvements made naturally while performing the duties required by their\ \ assigned roles. \ No newline at end of file diff --git a/MekHQ/src/mekhq/gui/dialog/CampaignHasProblemOnLoad.java b/MekHQ/src/mekhq/gui/dialog/CampaignHasProblemOnLoad.java index e470e5d5f3d..93f49c8f1bf 100644 --- a/MekHQ/src/mekhq/gui/dialog/CampaignHasProblemOnLoad.java +++ b/MekHQ/src/mekhq/gui/dialog/CampaignHasProblemOnLoad.java @@ -19,17 +19,16 @@ package mekhq.gui.dialog; import megamek.common.annotations.Nullable; -import mekhq.MekHQ; import mekhq.campaign.Campaign; import mekhq.campaign.CampaignFactory.CampaignProblemType; import mekhq.campaign.personnel.Person; import mekhq.gui.baseComponents.MHQDialogImmersive; import java.util.List; -import java.util.ResourceBundle; import static mekhq.campaign.Campaign.AdministratorSpecialization.COMMAND; import static mekhq.campaign.CampaignFactory.CampaignProblemType.CANT_LOAD_FROM_NEWER_VERSION; +import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; /** * Dialog to inform and handle campaign-loading problems within MekHQ. @@ -43,9 +42,7 @@ * text based on the problem type and campaign information.

*/ public class CampaignHasProblemOnLoad extends MHQDialogImmersive { - private static final String BUNDLE_KEY = "mekhq.resources.CampaignHasProblemOnLoad"; - private static final ResourceBundle resources = ResourceBundle.getBundle( - BUNDLE_KEY, MekHQ.getMHQOptions().getLocale()); + private static final String RESOURCE_BUNDLE = "mekhq.resources.CampaignHasProblemOnLoad"; /** * Constructs the dialog to handle campaign load problems. @@ -60,7 +57,7 @@ public class CampaignHasProblemOnLoad extends MHQDialogImmersive { */ public CampaignHasProblemOnLoad(Campaign campaign, CampaignProblemType problemType) { super(campaign, getSpeaker(campaign), null, createInCharacterMessage(campaign, problemType), - createButtons(problemType), createOutOfCharacterMessage(problemType), 0); + createButtons(problemType), createOutOfCharacterMessage(problemType), null); } /** @@ -82,10 +79,10 @@ public CampaignHasProblemOnLoad(Campaign campaign, CampaignProblemType problemTy */ private static List createButtons(CampaignProblemType problemType) { ButtonLabelTooltipPair btnCancel = new ButtonLabelTooltipPair( - resources.getString("cancel.button"), null); + getFormattedTextAt(RESOURCE_BUNDLE, "cancel.button"), null); ButtonLabelTooltipPair btnContinue = new ButtonLabelTooltipPair( - resources.getString("continue.button"), null); + getFormattedTextAt(RESOURCE_BUNDLE, "continue.button"), null); if (problemType == CANT_LOAD_FROM_NEWER_VERSION) { return List.of(btnCancel); @@ -122,7 +119,7 @@ private static String createInCharacterMessage(Campaign campaign, CampaignProble String typeKey = problemType.toString(); String commanderAddress = campaign.getCommanderAddress(false); - return String.format(resources.getString(typeKey + ".message"), commanderAddress); + return getFormattedTextAt(RESOURCE_BUNDLE, typeKey + ".message", commanderAddress); } /** @@ -136,6 +133,6 @@ private static String createInCharacterMessage(Campaign campaign, CampaignProble */ private static String createOutOfCharacterMessage(CampaignProblemType problemType) { String typeKey = problemType.toString(); - return resources.getString(typeKey + ".ooc"); + return getFormattedTextAt(RESOURCE_BUNDLE, typeKey + ".ooc"); } } diff --git a/MekHQ/src/mekhq/gui/dialog/VocationalExperienceAwardDialog.java b/MekHQ/src/mekhq/gui/dialog/VocationalExperienceAwardDialog.java index be38ef8665c..f3951185609 100644 --- a/MekHQ/src/mekhq/gui/dialog/VocationalExperienceAwardDialog.java +++ b/MekHQ/src/mekhq/gui/dialog/VocationalExperienceAwardDialog.java @@ -19,7 +19,6 @@ package mekhq.gui.dialog; import megamek.common.annotations.Nullable; -import mekhq.MekHQ; import mekhq.campaign.Campaign; import mekhq.campaign.CampaignOptions; import mekhq.campaign.mission.AtBContract; @@ -28,10 +27,10 @@ import mekhq.gui.baseComponents.MHQDialogImmersive; import java.util.List; -import java.util.ResourceBundle; import java.util.UUID; import static mekhq.campaign.Campaign.AdministratorSpecialization.HR; +import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; /** * A dialog that displays a notification to the commander about personnel @@ -43,9 +42,7 @@ * personnel records via hyperlinks.

*/ public class VocationalExperienceAwardDialog extends MHQDialogImmersive { - private static final String BUNDLE_KEY = "mekhq.resources.VocationalExperienceAwardDialog"; - private static final ResourceBundle resources = ResourceBundle.getBundle( - BUNDLE_KEY, MekHQ.getMHQOptions().getLocale()); + private static final String RESOURCE_BUNDLE = "mekhq.resources.VocationalExperienceAwardDialog"; /** * Constructs the {@link VocationalExperienceAwardDialog}. @@ -58,7 +55,7 @@ public class VocationalExperienceAwardDialog extends MHQDialogImmersive { */ public VocationalExperienceAwardDialog(Campaign campaign) { super(campaign, getSpeaker(campaign), null, createInCharacterMessage(campaign), - createButtons(), createOutOfCharacterMessage(campaign), 0); + createButtons(), createOutOfCharacterMessage(campaign), null); setModal(false); setAlwaysOnTop(true); @@ -91,7 +88,7 @@ protected void handleHyperlinkClick(Campaign campaign, String hyperlinkReference */ private static List createButtons() { ButtonLabelTooltipPair btnConfirm = new ButtonLabelTooltipPair( - resources.getString("confirm.button"), null); + getFormattedTextAt(RESOURCE_BUNDLE, "confirm.button"), null); return List.of(btnConfirm); } @@ -126,7 +123,7 @@ private static String createInCharacterMessage(Campaign campaign) { StringBuilder message = new StringBuilder(); message.append(commanderAddress); - message.append(resources.getString("dialog.message")); + message.append(getFormattedTextAt(RESOURCE_BUNDLE, "dialog.message")); // Create a table to hold the personnel message.append("
"); @@ -191,6 +188,6 @@ private static String createOutOfCharacterMessage(Campaign campaign) { } } - return String.format(resources.getString("dialog.ooc"), advancement); + return getFormattedTextAt(RESOURCE_BUNDLE, "dialog.ooc", advancement); } } From 9ee61c6be363b2028d3a893a2375cb0aecd15fdd Mon Sep 17 00:00:00 2001 From: psikomonkie <189469115+psikomonkie@users.noreply.github.com> Date: Tue, 28 Jan 2025 17:02:31 -0500 Subject: [PATCH 034/112] Issue 1703: Allow bays to be added to aerospace fighters --- MekHQ/src/mekhq/gui/MekLabTab.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/MekHQ/src/mekhq/gui/MekLabTab.java b/MekHQ/src/mekhq/gui/MekLabTab.java index d850ea8fa38..0625632a7c8 100644 --- a/MekHQ/src/mekhq/gui/MekLabTab.java +++ b/MekHQ/src/mekhq/gui/MekLabTab.java @@ -554,6 +554,7 @@ private class AeroPanel extends EntityPanel { private ASStructureTab structureTab; private ASEquipmentTab equipmentTab; private ASBuildTab buildTab; + private TransportTab transportTab; private PreviewTab previewTab; public AeroPanel(Aero a) { @@ -575,14 +576,17 @@ public void reloadTabs() { equipmentTab = new ASEquipmentTab(this); buildTab = new ASBuildTab(this); FluffTab fluffTab = new FluffTab(this); + transportTab = new TransportTab(this); structureTab.addRefreshedListener(this); equipmentTab.addRefreshedListener(this); buildTab.addRefreshedListener(this); + transportTab.addRefreshedListener(this); fluffTab.setRefreshedListener(this); addTab("Structure/Armor", new JScrollPaneWithSpeed(structureTab)); addTab("Equipment", new JScrollPaneWithSpeed(equipmentTab)); addTab("Assign Criticals", new JScrollPaneWithSpeed(buildTab)); + addTab("Transport Bays", new JScrollPaneWithSpeed(transportTab)); addTab("Fluff", new JScrollPaneWithSpeed(fluffTab)); addTab("Preview", new JScrollPaneWithSpeed(previewTab)); this.repaint(); @@ -593,6 +597,7 @@ public void refreshAll() { structureTab.refresh(); equipmentTab.refresh(); buildTab.refresh(); + transportTab.refresh(); previewTab.refresh(); refreshSummary(); } @@ -616,7 +621,7 @@ public void refreshEquipment() { @Override public void refreshTransport() { - // not used for fighters + transportTab.refresh(); } @Override From 141d7ee1af7050258e666f646d772a86c8dba3d6 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sat, 1 Feb 2025 13:53:55 -0600 Subject: [PATCH 035/112] Fix dependent departure message to handle singular and plural forms Updated the dependent departure message logic to correctly use singular or plural form based on the number of dependents. Added new resource strings for localization and adjusted the message formatting accordingly. --- MekHQ/resources/mekhq/resources/Campaign.properties | 4 +++- MekHQ/src/mekhq/campaign/Campaign.java | 12 ++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/MekHQ/resources/mekhq/resources/Campaign.properties b/MekHQ/resources/mekhq/resources/Campaign.properties index 1884f237d2f..ac79026737a 100644 --- a/MekHQ/resources/mekhq/resources/Campaign.properties +++ b/MekHQ/resources/mekhq/resources/Campaign.properties @@ -82,7 +82,9 @@ turnoverPersonnelKilled.text=You have personnel who have left the unit or divorce.text=%s has divorced %s. #### Unsorted Campaign Resources -dependentLeavesForce.text=%s dependent%s have departed the force. +dependentLeavesForce.text=%s %s have departed the force. +dependentLeavesForce.dependent.singular=dependent +dependentLeavesForce.dependent.plural=dependents dependentJoinsForce.text=%s has started traveling with the force. relativeJoinsForce.text=%s has started traveling with the force. They are %s's %s. relativeJoinsForceSpouse.text=spouse diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index 65009b721e0..d2a46ecdcb2 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -5042,8 +5042,12 @@ int dependentsRollForRemoval(List dependents, int dependentCapacity) { } if (!dependentsToRemove.isEmpty()) { + String pluralizer = dependentsToRemove.size() == 1 + ? resources.getString("dependentLeavesForce.dependent.singular") + : resources.getString("dependentLeavesForce.dependent.plural"); + addReport(String.format(resources.getString("dependentLeavesForce.text"), - dependentsToRemove.size(), dependentsToRemove.size() == 1 ? "" : "s")); + dependentsToRemove.size(), pluralizer)); } for (Person dependent : dependentsToRemove) { @@ -9190,7 +9194,7 @@ public void writePartInUseMapToXML(final PrintWriter pw, int indent) { } } - /** + /** * Wipes the Parts in use map for the purpose of resetting all values to their default */ public void wipePartsInUseMap() { @@ -9216,13 +9220,13 @@ public ImageIcon getCampaignFactionIcon() { } return icon; } - + /** * Checks if another active scenario has this scenarioID as it's linkedScenarioID and returns true if it finds one. */ public boolean checkLinkedScenario(int scenarioID) { for (Scenario scenario : getScenarios()) { - if ((scenario.getLinkedScenario() == scenarioID) + if ((scenario.getLinkedScenario() == scenarioID) && (getScenario(scenario.getId()).getStatus().isCurrent())) { return true; } From 196836f4cbe72254b5cafda38bde7e5555e47e95 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sat, 1 Feb 2025 13:56:37 -0600 Subject: [PATCH 036/112] Use parameterized logging for totalTonnage logging Replaced string formatting with parameterized logging for consistency and to improve performance. This follows best practices for logging and ensures safer handling of log messages. --- .../campaign/mission/resupplyAndCaches/PerformResupply.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/PerformResupply.java b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/PerformResupply.java index 5fd477fc436..252507610a9 100644 --- a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/PerformResupply.java +++ b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/PerformResupply.java @@ -178,7 +178,7 @@ public static void performResupply(Resupply resupply, AtBContract contract, int } } - logger.info(String.format("totalTonnage: %s", totalTonnage)); + logger.info("totalTonnage: {}", totalTonnage); // This shouldn't occur, but we include it as insurance. From d09bfd662ab5892df480e19c88ac008d70170b02 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sat, 1 Feb 2025 14:27:13 -0600 Subject: [PATCH 037/112] Refactored ForceType handling for improved code clarity Replaced direct ForceType comparisons with a new `isForceType` method for consistency and readability. Enhanced ForceType functionality by adding inheritance and parent relationship standardization logic. Updated relevant GUI and logic components to utilize the refined ForceType API. --- .../mekhq/resources/ForceType.properties | 7 ++ .../src/mekhq/campaign/force/CombatTeam.java | 5 +- MekHQ/src/mekhq/campaign/force/Force.java | 25 +++++ MekHQ/src/mekhq/campaign/force/ForceType.java | 106 ++++++++++++++---- .../mekhq/campaign/mission/AtBContract.java | 3 +- .../mission/resupplyAndCaches/Resupply.java | 8 +- .../resupplyAndCaches/ResupplyUtilities.java | 5 +- MekHQ/src/mekhq/gui/ForceRenderer.java | 7 +- .../mekhq/gui/adapter/TOEMouseAdapter.java | 24 ++-- .../DialogContractStart.java | 5 +- .../src/mekhq/gui/utilities/StaticChecks.java | 4 +- MekHQ/src/mekhq/gui/view/ForceViewPanel.java | 4 +- 12 files changed, 155 insertions(+), 48 deletions(-) create mode 100644 MekHQ/resources/mekhq/resources/ForceType.properties diff --git a/MekHQ/resources/mekhq/resources/ForceType.properties b/MekHQ/resources/mekhq/resources/ForceType.properties new file mode 100644 index 00000000000..22c4a5360b3 --- /dev/null +++ b/MekHQ/resources/mekhq/resources/ForceType.properties @@ -0,0 +1,7 @@ +STANDARD.label=Standard +SUPPORT.label=Support +SUPPORT.symbol= \u2205 +CONVOY.label=Convoy +CONVOY.symbol= \u039E +SECURITY.label=Security +SECURITY.symbol= \u2727 \ No newline at end of file diff --git a/MekHQ/src/mekhq/campaign/force/CombatTeam.java b/MekHQ/src/mekhq/campaign/force/CombatTeam.java index 03bc2fdb055..8e0b3eab9f1 100644 --- a/MekHQ/src/mekhq/campaign/force/CombatTeam.java +++ b/MekHQ/src/mekhq/campaign/force/CombatTeam.java @@ -56,6 +56,7 @@ import static megamek.common.EntityWeightClass.WEIGHT_ULTRA_LIGHT; import static mekhq.campaign.force.Force.COMBAT_TEAM_OVERRIDE_NONE; import static mekhq.campaign.force.Force.COMBAT_TEAM_OVERRIDE_TRUE; +import static mekhq.campaign.force.ForceType.STANDARD; import static mekhq.campaign.force.FormationLevel.LANCE; /** @@ -308,7 +309,7 @@ public boolean isEligible(Campaign campaign) { return false; } - if (!force.getForceType().isStandard()) { + if (!force.isForceType(STANDARD)) { force.setCombatTeamStatus(false); return false; } @@ -370,7 +371,7 @@ size > getStandardForceSize(campaign.getFaction()) + 2) { return false; } - if (!parentForce.getForceType().isStandard()) { + if (!parentForce.isForceType(STANDARD)) { force.setCombatTeamStatus(false); return false; } diff --git a/MekHQ/src/mekhq/campaign/force/Force.java b/MekHQ/src/mekhq/campaign/force/Force.java index fd4bae1ddcc..e750a9fea77 100644 --- a/MekHQ/src/mekhq/campaign/force/Force.java +++ b/MekHQ/src/mekhq/campaign/force/Force.java @@ -158,10 +158,35 @@ public void setDescription(String d) { this.desc = d; } + /** + * @return The {@code ForceType} currently assigned to this instance. + */ public ForceType getForceType() { return forceType; } + /** + * This method compares the provided {@code forceType} with the current instance's + * {@code ForceType} to determine if they match. + * + * @param forceType The {@code ForceType} to compare against. + * @return {@code true} if the current instance matches the specified {@code forceType}; + * otherwise, {@code false}. + */ + public boolean isForceType(ForceType forceType) { + return this.forceType == forceType; + } + + /** + * Updates the {@code ForceType} for this instance and optionally propagates the change + * to all sub-forces. + * + *

If the {@code setForSubForces} flag is {@code true}, the method recursively sets the + * provided {@code forceType} for all sub-forces of this instance.

+ * + * @param forceType The new {@code ForceType} to assign to this instance. + * @param setForSubForces A flag indicating whether the change should also apply to sub-forces. + */ public void setForceType(ForceType forceType, boolean setForSubForces) { this.forceType = forceType; if (setForSubForces) { diff --git a/MekHQ/src/mekhq/campaign/force/ForceType.java b/MekHQ/src/mekhq/campaign/force/ForceType.java index f3508e99fe9..72a5d2d1fec 100644 --- a/MekHQ/src/mekhq/campaign/force/ForceType.java +++ b/MekHQ/src/mekhq/campaign/force/ForceType.java @@ -20,56 +20,118 @@ import megamek.logging.MMLogger; +import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; + /** * Represents the various types of forces available. - * - * It is used to classify and manipulate forces within the game. + *

+ * It is used to classify and manipulate forces within the game. + *

*/ public enum ForceType { // region Enum Declarations /** - * Standard force type, typically used for combat and general operations. + * Standard force type, typically used for combat. */ - STANDARD("Standard"), + STANDARD(true, false), /** - * Support force type, generally ignored by MekHQ. + * Support force type, used by forces that should be deployed in StratCon but not involved in + * combat. */ - SUPPORT("Support"), + SUPPORT(false, false), /** - * Convoy force type, typically used for transport and supply operations. + * Convoy force type, typically used by the Resupply module. */ - CONVOY("Convoy"), + CONVOY(true, true), /** - * Security force type, typically used for protection and guarding operations. + * Security force type, typically used by the Prisoner Events module. */ - SECURITY("Security"); + SECURITY(true, true); + // region Fields + private final boolean standardizeParents; + private final boolean childrenInherit; - // Fields - private final String name; + // region Constructor - // Constructor /** - * Constructs a {@code ForceType} with a specified name. + * Constructor for the {@code ForceType} enum. * - * @param name the name of the force type, used for displaying or referencing. + * @param standardizeParents Whether changing to this ForceType changes the ForceType in all + * parent forces to STANDARD + * @param childrenInherit Whether changing to this ForceType changes the ForceType in all + * child forces to this ForceType. */ - ForceType(String name) { - this.name = name; + ForceType(boolean standardizeParents, boolean childrenInherit) { + this.standardizeParents = standardizeParents; + this.childrenInherit = childrenInherit; } + // endregion Constructor // region Getters /** - * Returns the name of this force type. + * Retrieves the display name for the ForceType by fetching a localized label from the relevant + * resource bundle. + * + *

The method uses the {@code name} of the current instance to construct a resource + * key in the format {@code [name].label}. This key is used to look up a localized string + * from the {@code ForceType} resource bundle located in the {@code mekhq.resources} package. + * The formatted text at the specified key is returned as the display name.

+ * + * @return The localized display name for the current instance. + */ + public String getDisplayName() { + final String RESOURCE_BUNDLE = "mekhq.resources.ForceType"; + final String RESOURCE_KEY = name() + ".label"; + + return getFormattedTextAt(RESOURCE_BUNDLE, RESOURCE_KEY); + } + + /** + * Retrieves the symbol associated with this ForceType. + * + *

The method determines the symbol to display for the current instance by looking up + * a localization resource key in the {@code ForceType} resource bundle, with keys formatted + * as {@code [enumName].symbol}.

+ * + *

If the current instance is {@code STANDARD}, an empty string is returned as the symbol.

+ * + * @return The localized symbol associated with the current instance, or an empty string + * if the instance is {@code STANDARD}. + */ + public String getSymbol() { + if (this == STANDARD) { + return ""; + } + + final String RESOURCE_BUNDLE = "mekhq.resources.ForceType"; + final String RESOURCE_KEY = name() + ".symbol"; + + return getFormattedTextAt(RESOURCE_BUNDLE, RESOURCE_KEY); + } + + /** + * This flag indicates whether, when changing to this ForceType, whether all parent forces + * should be changed to STANDARD. + * + * @return {@code true} if parent relationships should be standardized; {@code false} otherwise. + */ + public boolean shouldStandardizeParents() { + return standardizeParents; + } + + /** + * This flag indicates whether, when changing to this ForceType, whether all child forces + * should be changed to the same ForceType. * - * @return a string representing the name of the force type. + * @return {@code true} if children should inherit from parents; {@code false} otherwise. */ - public String getName() { - return name; + public boolean shouldChildrenInherit() { + return childrenInherit; } /** @@ -124,7 +186,7 @@ public static ForceType fromOrdinal(int ordinal) { } MMLogger logger = MMLogger.create(ForceType.class); - logger.error(String.format("Unknown ForceType ordinal: %s - returning STANDARD.", ordinal)); + logger.error("Unknown ForceType ordinal: {} - returning STANDARD.", ordinal); return STANDARD; } diff --git a/MekHQ/src/mekhq/campaign/mission/AtBContract.java b/MekHQ/src/mekhq/campaign/mission/AtBContract.java index e73af54d5a3..fd8b25d2a50 100644 --- a/MekHQ/src/mekhq/campaign/mission/AtBContract.java +++ b/MekHQ/src/mekhq/campaign/mission/AtBContract.java @@ -90,6 +90,7 @@ import static megamek.common.enums.SkillLevel.parseFromInteger; import static megamek.common.enums.SkillLevel.parseFromString; import static mekhq.campaign.force.CombatTeam.getStandardForceSize; +import static mekhq.campaign.force.ForceType.STANDARD; import static mekhq.campaign.force.FormationLevel.BATTALION; import static mekhq.campaign.force.FormationLevel.COMPANY; import static mekhq.campaign.mission.AtBDynamicScenarioFactory.getEntity; @@ -2069,7 +2070,7 @@ private static double estimatePlayerPower(Campaign campaign) { int playerGBV = 0; int playerUnitCount = 0; for (Force force : campaign.getAllForces()) { - if (!force.getForceType().isStandard()) { + if (!force.isForceType(STANDARD)) { continue; } diff --git a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java index 4f6dc0aeaa9..f6734c1c143 100644 --- a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java +++ b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java @@ -46,6 +46,8 @@ import static megamek.common.MiscType.F_SPONSON_TURRET; import static megamek.common.enums.SkillLevel.NONE; import static mekhq.campaign.force.CombatTeam.getStandardForceSize; +import static mekhq.campaign.force.ForceType.CONVOY; +import static mekhq.campaign.force.ForceType.STANDARD; import static mekhq.campaign.market.procurement.Procurement.getFactionTechCode; import static mekhq.utilities.EntityUtilities.getEntityFromUnitId; @@ -390,7 +392,7 @@ static int calculateTargetCargoTonnage(Campaign campaign, AtBContract contract) continue; } - if (!force.getForceType().isStandard()) { + if (!force.isForceType(STANDARD)) { continue; } @@ -783,12 +785,12 @@ private void calculatePlayerConvoyValues() { totalPlayerCargoCapacity = 0; for (Force force : campaign.getAllForces()) { - if (!force.getForceType().isConvoy()) { + if (!force.isForceType(CONVOY)) { continue; } // This ensures each convoy is only counted once - if (force.getParentForce() != null && force.getParentForce().getForceType().isConvoy()) { + if (force.getParentForce() != null && force.getParentForce().isForceType(CONVOY)) { continue; } diff --git a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/ResupplyUtilities.java b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/ResupplyUtilities.java index a0d4aab829b..7a03d54a8b2 100644 --- a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/ResupplyUtilities.java +++ b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/ResupplyUtilities.java @@ -34,6 +34,7 @@ import static java.lang.Math.floor; import static java.lang.Math.max; +import static mekhq.campaign.force.ForceType.CONVOY; 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; @@ -83,11 +84,11 @@ public static void processAbandonedConvoy(Campaign campaign, AtBContract contrac for (Force force : campaign.getAllForces()) { Force parentForce = force.getParentForce(); - if (parentForce != null && (force.getParentForce().getForceType().isConvoy())) { + if (parentForce != null && (force.getParentForce().isForceType(CONVOY))) { continue; } - if (force.getForceType().isConvoy() && force.getScenarioId() == scenarioId) { + if (force.isForceType(CONVOY) && force.getScenarioId() == scenarioId) { new DialogAbandonedConvoy(campaign, contract, force); for (UUID unitID : force.getAllUnits(false)) { diff --git a/MekHQ/src/mekhq/gui/ForceRenderer.java b/MekHQ/src/mekhq/gui/ForceRenderer.java index 5e5121d0b36..574bc8fb2bb 100644 --- a/MekHQ/src/mekhq/gui/ForceRenderer.java +++ b/MekHQ/src/mekhq/gui/ForceRenderer.java @@ -157,12 +157,7 @@ public Component getTreeCellRendererComponent(JTree tree, Object value, boolean } ForceType forceType = force.getForceType(); - String typeKey = switch (forceType) { - case STANDARD -> ""; - case SUPPORT -> " \u2205"; - case CONVOY -> " \u039E"; - case SECURITY -> " \u2727"; - }; + String typeKey = forceType.getSymbol(); String formattedForceName = String.format("%s%s%s%s%s%s", force.isCombatTeam() ? "" : "", diff --git a/MekHQ/src/mekhq/gui/adapter/TOEMouseAdapter.java b/MekHQ/src/mekhq/gui/adapter/TOEMouseAdapter.java index 5d485899455..5b60998e22c 100644 --- a/MekHQ/src/mekhq/gui/adapter/TOEMouseAdapter.java +++ b/MekHQ/src/mekhq/gui/adapter/TOEMouseAdapter.java @@ -66,6 +66,9 @@ import static mekhq.campaign.force.Force.COMBAT_TEAM_OVERRIDE_FALSE; import static mekhq.campaign.force.Force.COMBAT_TEAM_OVERRIDE_NONE; import static mekhq.campaign.force.Force.COMBAT_TEAM_OVERRIDE_TRUE; +import static mekhq.campaign.force.ForceType.CONVOY; +import static mekhq.campaign.force.ForceType.SECURITY; +import static mekhq.campaign.force.ForceType.SUPPORT; public class TOEMouseAdapter extends JPopupMenuAdapter { private static final MMLogger logger = MMLogger.create(TOEMouseAdapter.class); @@ -429,20 +432,27 @@ public void actionPerformed(ActionEvent action) { } ForceType forceType = ForceType.STANDARD; - if (command.contains("SUPPORT")) { - forceType = ForceType.SUPPORT; + if (command.contains(SUPPORT.name())) { + forceType = SUPPORT; } - if (command.contains("CONVOY")) { - forceType = ForceType.CONVOY; + if (command.contains(CONVOY.name())) { + forceType = CONVOY; } - if (command.contains("SECURITY")) { - forceType = ForceType.SECURITY; + if (command.contains(SECURITY.name())) { + forceType = SECURITY; } for (final Force force : forces) { - force.setForceType(forceType, true); + force.setForceType(forceType, forceType.shouldChildrenInherit()); + + if (forceType.shouldStandardizeParents()) { + for (Force parentForce : force.getAllParents()) { + parentForce.setForceType(forceType, false); + } + } + MekHQ.triggerEvent(new OrganizationChangedEvent(force)); } diff --git a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogContractStart.java b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogContractStart.java index 231927f2d1f..c9f757a5369 100644 --- a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogContractStart.java +++ b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogContractStart.java @@ -32,6 +32,7 @@ import java.util.ResourceBundle; import java.util.UUID; +import static mekhq.campaign.force.ForceType.CONVOY; import static mekhq.campaign.mission.resupplyAndCaches.Resupply.isProhibitedUnitType; import static mekhq.campaign.mission.resupplyAndCaches.ResupplyUtilities.estimateCargoRequirements; import static mekhq.gui.baseComponents.MHQDialogImmersive.getSpeakerDescription; @@ -195,11 +196,11 @@ private static String generateContractStartMessage(Campaign campaign, AtBContrac double totalPlayerCargoCapacity = 0; for (Force force : campaign.getAllForces()) { - if (!force.getForceType().isConvoy()) { + if (!force.isForceType(CONVOY)) { continue; } - if (force.getParentForce() != null && force.getParentForce().getForceType().isConvoy()) { + if (force.getParentForce() != null && force.getParentForce().isForceType(CONVOY)) { continue; } diff --git a/MekHQ/src/mekhq/gui/utilities/StaticChecks.java b/MekHQ/src/mekhq/gui/utilities/StaticChecks.java index 2c7f57b61ab..89b69525c6c 100644 --- a/MekHQ/src/mekhq/gui/utilities/StaticChecks.java +++ b/MekHQ/src/mekhq/gui/utilities/StaticChecks.java @@ -28,6 +28,8 @@ import java.util.*; import java.util.stream.Stream; +import static mekhq.campaign.force.ForceType.STANDARD; + public class StaticChecks { public static boolean areAllForcesUndeployed(final Campaign campaign, final List forces) { @@ -37,7 +39,7 @@ public static boolean areAllForcesUndeployed(final Campaign campaign, final List } public static boolean areAllStandardForces(Vector forces) { - return forces.stream().allMatch(force -> force.getForceType().isStandard()); + return forces.stream().allMatch(force -> force.isForceType(STANDARD)); } public static boolean areAllUnitsAvailable(Vector units) { diff --git a/MekHQ/src/mekhq/gui/view/ForceViewPanel.java b/MekHQ/src/mekhq/gui/view/ForceViewPanel.java index 6f68de5278f..c2cbe190927 100644 --- a/MekHQ/src/mekhq/gui/view/ForceViewPanel.java +++ b/MekHQ/src/mekhq/gui/view/ForceViewPanel.java @@ -196,11 +196,11 @@ private void fillStats() { ForceType forceType = force.getForceType(); - String forceLabel = ""; + String forceLabel; if (forceType.isStandard()) { forceLabel = force.getFormationLevel().toString(); } else { - forceLabel = forceType.getName() + ' ' + force.getFormationLevel().toString(); + forceLabel = forceType.getDisplayName() + ' ' + force.getFormationLevel().toString(); } lblType.setText("" + forceLabel + ""); From ceb988aa5ac26d2d4c4b3084ff0ca153d714e87f Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sat, 1 Feb 2025 14:39:39 -0600 Subject: [PATCH 038/112] Set parent force type to STANDARD in TOEMouseAdapter Previously, parent forces were assigned the same type as their child. This change ensures that parent forces are consistently set to STANDARD when standardization is required. Also fixed spacing in ForceType property symbols for consistency. --- MekHQ/resources/mekhq/resources/ForceType.properties | 6 +++--- MekHQ/src/mekhq/gui/adapter/TOEMouseAdapter.java | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/MekHQ/resources/mekhq/resources/ForceType.properties b/MekHQ/resources/mekhq/resources/ForceType.properties index 22c4a5360b3..359aa488a59 100644 --- a/MekHQ/resources/mekhq/resources/ForceType.properties +++ b/MekHQ/resources/mekhq/resources/ForceType.properties @@ -1,7 +1,7 @@ STANDARD.label=Standard SUPPORT.label=Support -SUPPORT.symbol= \u2205 +SUPPORT.symbol=\ \u2205 CONVOY.label=Convoy -CONVOY.symbol= \u039E +CONVOY.symbol=\ \u039E SECURITY.label=Security -SECURITY.symbol= \u2727 \ No newline at end of file +SECURITY.symbol=\ \u2727 \ No newline at end of file diff --git a/MekHQ/src/mekhq/gui/adapter/TOEMouseAdapter.java b/MekHQ/src/mekhq/gui/adapter/TOEMouseAdapter.java index 5b60998e22c..a1696aa04a2 100644 --- a/MekHQ/src/mekhq/gui/adapter/TOEMouseAdapter.java +++ b/MekHQ/src/mekhq/gui/adapter/TOEMouseAdapter.java @@ -68,6 +68,7 @@ import static mekhq.campaign.force.Force.COMBAT_TEAM_OVERRIDE_TRUE; import static mekhq.campaign.force.ForceType.CONVOY; import static mekhq.campaign.force.ForceType.SECURITY; +import static mekhq.campaign.force.ForceType.STANDARD; import static mekhq.campaign.force.ForceType.SUPPORT; public class TOEMouseAdapter extends JPopupMenuAdapter { @@ -449,7 +450,7 @@ public void actionPerformed(ActionEvent action) { if (forceType.shouldStandardizeParents()) { for (Force parentForce : force.getAllParents()) { - parentForce.setForceType(forceType, false); + parentForce.setForceType(STANDARD, false); } } From 3319655f20dbf2d0bad22376576e2904699c87f4 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sat, 1 Feb 2025 14:59:31 -0600 Subject: [PATCH 039/112] Replace exponential random death with configurable random death Replaced the complex exponential random death system with a simpler configurable percentage-based random death mechanism. Updated related UI, campaign options, and methods to support customizable death probabilities, improving flexibility and customization for users. --- .../CampaignOptionsDialog.properties | 8 +- .../mekhq/resources/Personnel.properties | 6 +- MekHQ/src/mekhq/campaign/Campaign.java | 18 +-- MekHQ/src/mekhq/campaign/CampaignOptions.java | 16 ++- .../death/ExponentialRandomDeath.java | 86 ------------ .../campaign/personnel/death/RandomDeath.java | 52 +++++++ .../personnel/enums/RandomDeathMethod.java | 4 +- MekHQ/src/mekhq/gui/CampaignGUI.java | 5 + .../contents/BiographyTab.java | 15 ++ .../personnel/death/AbstractDeathTest.java | 42 ++++-- .../death/ExponentialRandomDeathTest.java | 131 ------------------ 11 files changed, 136 insertions(+), 247 deletions(-) delete mode 100644 MekHQ/src/mekhq/campaign/personnel/death/ExponentialRandomDeath.java create mode 100644 MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java delete mode 100644 MekHQ/unittests/mekhq/campaign/personnel/death/ExponentialRandomDeathTest.java diff --git a/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties b/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties index 40620f18833..58f630d1a5f 100644 --- a/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties +++ b/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties @@ -801,10 +801,14 @@ lblExtraRandomOrigin.tooltip=Random origin is randomized to the planetary level # createDeathTab lblDeathTab.text=Death Options \u270E lblRandomDeathMethod.text=Use Random Death -lblRandomDeathMethod.tooltip=Should characters randomly die? This system follows a realistic death\ - \ curve based on real world data. +lblRandomDeathMethod.tooltip=Should characters randomly die? lblUseRandomDeathSuicideCause.text=Enable Cause of Death: Suicide lblUseRandomDeathSuicideCause.tooltip=This includes suicide as a potential cause for a random death. +lblRandomDeathChance.text=Random Death Percentage \u26A0 +lblRandomDeathChance.tooltip=This is the percent chance per day that any member of your force will\ + \ randomly die.\ +
\ +
Requirement: This option is only relevant if Random Death is selected. # createDeathAgeGroupsPanel lblDeathAgeGroupsPanel.text=Death by Age Group diff --git a/MekHQ/resources/mekhq/resources/Personnel.properties b/MekHQ/resources/mekhq/resources/Personnel.properties index 0e049e46610..0bbc8a65cc5 100644 --- a/MekHQ/resources/mekhq/resources/Personnel.properties +++ b/MekHQ/resources/mekhq/resources/Personnel.properties @@ -504,11 +504,7 @@ Profession.CIVILIAN.toolTipText=The Civilian Profession contains Dependents and RandomDeathMethod.NONE.text=Disabled RandomDeathMethod.NONE.toolTipText=Random death is disabled RandomDeathMethod.RANDOM.text=Enabled -RandomDeathMethod.RANDOM.toolTipText=This uses an exponential equation in the format c *\ - \ 10^n * e^(k * age) to determine the probability of a person dying a random death on a given\ - \ day.
Infant mortality is significantly lower than would be expected when using this equation.\ - \
The default equation was derived from the death rate by age and sex in the United States of\ - \ America in 2018, as per Statista +RandomDeathMethod.RANDOM.toolTipText=Random death is enabled # RandomDependentMethod Enum RandomDependentMethod.NONE.text=Disabled diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index 0e57a30fd28..1b35f18c6e3 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -4810,7 +4810,7 @@ public boolean newDay() { personnelMarket.generatePersonnelForDay(this); // TODO : AbstractContractMarket : Uncomment - // getContractMarket().processNewWeek(this); + // getContractMarket().processNewDay(this); unitMarket.processNewDay(this); // Process New Day for AtB @@ -5563,11 +5563,11 @@ public void restore() { */ public void cleanUp() { // Cleans non-existing spouses - for (Person p : personnel.values()) { - if (p.getGenealogy().hasSpouse()) { - if (!personnel.containsKey(p.getGenealogy().getSpouse().getId())) { - p.getGenealogy().setSpouse(null); - p.setMaidenName(null); + for (Person person : personnel.values()) { + if (person.getGenealogy().hasSpouse()) { + if (!personnel.containsKey(person.getGenealogy().getSpouse().getId())) { + person.getGenealogy().setSpouse(null); + person.setMaidenName(null); } } } @@ -9154,7 +9154,7 @@ public void writePartInUseMapToXML(final PrintWriter pw, int indent) { } } - /** + /** * Wipes the Parts in use map for the purpose of resetting all values to their default */ public void wipePartsInUseMap() { @@ -9180,13 +9180,13 @@ public ImageIcon getCampaignFactionIcon() { } return icon; } - + /** * Checks if another active scenario has this scenarioID as it's linkedScenarioID and returns true if it finds one. */ public boolean checkLinkedScenario(int scenarioID) { for (Scenario scenario : getScenarios()) { - if ((scenario.getLinkedScenario() == scenarioID) + if ((scenario.getLinkedScenario() == scenarioID) && (getScenario(scenario.getId()).getStatus().isCurrent())) { return true; } diff --git a/MekHQ/src/mekhq/campaign/CampaignOptions.java b/MekHQ/src/mekhq/campaign/CampaignOptions.java index 6e47c37a1ce..f83dd649536 100644 --- a/MekHQ/src/mekhq/campaign/CampaignOptions.java +++ b/MekHQ/src/mekhq/campaign/CampaignOptions.java @@ -20,15 +20,12 @@ package mekhq.campaign; import megamek.Version; -import megamek.client.ui.swing.GUIPreferences; import megamek.codeUtilities.MathUtility; -import megamek.common.Configuration; import megamek.common.EquipmentType; import megamek.common.TechConstants; import megamek.common.enums.SkillLevel; import megamek.common.preference.ClientPreferences; import megamek.common.preference.PreferenceManager; -import megamek.common.util.fileUtils.MegaMekFile; import megamek.logging.MMLogger; import mekhq.MekHQ; import mekhq.Utilities; @@ -374,6 +371,7 @@ public static String getTechLevelName(final int techLevel) { private RandomDeathMethod randomDeathMethod; private Map enabledRandomDeathAgeGroups; private boolean useRandomDeathSuicideCause; + private double randomDeathChance; // endregion Life Paths Tab //region Turnover and Retention @@ -968,6 +966,7 @@ public CampaignOptions() { getEnabledRandomDeathAgeGroups().put(AgeGroup.TODDLER, false); getEnabledRandomDeathAgeGroups().put(AgeGroup.BABY, false); setUseRandomDeathSuicideCause(false); + setRandomDeathChance(0.00002); // endregion Life Paths Tab // region Turnover and Retention @@ -2981,6 +2980,14 @@ public boolean isUseRandomDeathSuicideCause() { public void setUseRandomDeathSuicideCause(final boolean useRandomDeathSuicideCause) { this.useRandomDeathSuicideCause = useRandomDeathSuicideCause; } + + public double getRandomDeathChance() { + return randomDeathChance; + } + + public void setRandomDeathChance(final double randomDeathChance) { + this.randomDeathChance = randomDeathChance; + } // endregion Death // region Awards @@ -5025,6 +5032,7 @@ public void writeToXml(final PrintWriter pw, int indent) { } MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, "enabledRandomDeathAgeGroups"); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "useRandomDeathSuicideCause", isUseRandomDeathSuicideCause()); + MHQXMLUtility.writeSimpleXMLTag(pw, indent, "randomDeathChance", getRandomDeathChance()); MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, "ageRangeRandomDeathMaleValues"); MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, "ageRangeRandomDeathMaleValues"); MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, "ageRangeRandomDeathFemaleValues"); @@ -5842,6 +5850,8 @@ public static CampaignOptions generateCampaignOptionsFromXml(Node wn, Version ve } } else if (wn2.getNodeName().equalsIgnoreCase("useRandomDeathSuicideCause")) { retVal.setUseRandomDeathSuicideCause(Boolean.parseBoolean(wn2.getTextContent().trim())); + } else if (wn2.getNodeName().equalsIgnoreCase("randomDeathChance")) { + retVal.setRandomDeathChance(Double.parseDouble(wn2.getTextContent().trim())); } else if (wn2.getNodeName().equalsIgnoreCase("useRandomRetirement")) { retVal.setUseRandomRetirement(Boolean.parseBoolean(wn2.getTextContent().trim())); } else if (wn2.getNodeName().equalsIgnoreCase("turnoverBaseTn")) { diff --git a/MekHQ/src/mekhq/campaign/personnel/death/ExponentialRandomDeath.java b/MekHQ/src/mekhq/campaign/personnel/death/ExponentialRandomDeath.java deleted file mode 100644 index 0c08c7f92d4..00000000000 --- a/MekHQ/src/mekhq/campaign/personnel/death/ExponentialRandomDeath.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (c) 2021-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 as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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. - * - * You should have received a copy of the GNU General Public License - * along with MekHQ. If not, see . - */ -package mekhq.campaign.personnel.death; - -import megamek.common.Compute; -import megamek.common.enums.Gender; -import mekhq.campaign.CampaignOptions; -import mekhq.campaign.personnel.enums.RandomDeathMethod; - -/** - * Implements a random death generator using gender-specific exponential equations. - * These equations estimate the probability of death based on the person's age, gender, - * and coefficients derived from real-world data (US death statistics in 2018). - * The death rates are calculated weekly, compared with a randomly generated value, - * and returned as a boolean indicating whether the person dies or not. - */ -public class ExponentialRandomDeath extends AbstractDeath { - //region Variable Declarations - /** - *

An array of constants representing the male-specific coefficients (c, n, k) - * used in the gender-dependent exponential death equation in the format:

- * - *
c * 10^n * e^(k * age)
- */ - private final double[] MALE_DEATH_RATE = new double[]{5.4757, -7.0, 0.0709}; - - /** - *

An array of constants representing the female-specific coefficients (c, n, k) - * used in the gender-dependent exponential death equation in the format:

- * - *
c * 10^n * e^(k * age)
- */ - private final double[] FEMALE_DEATH_RATE = new double[]{2.4641, -7.0, 0.0752}; - //endregion Variable Declarations - - //region Constructors - /** - * Constructor for ExponentialRandomDeath. - * Initializes the death method type, campaign options, and causes of death. - * - * @param options The campaign options object that contains relevant settings. - * @param initializeCauses Whether to initialize random causes of death. - */ - public ExponentialRandomDeath(final CampaignOptions options, final boolean initializeCauses) { - super(RandomDeathMethod.RANDOM, options, initializeCauses); - } - //endregion Constructors - - /** - * Determines if a person dies a random death based on gender-specific exponential equations. - * The calculation uses weekly probabilities derived from gender-specific coefficients and age, - * compared to a randomly generated value. - * - *

The exponential equation used is:

- *
c * 10^n * e^(k * age * 7)
- * - * @param age The person's age. - * @param gender The person's gender. - * @return {@code true} if the person is selected to randomly die, {@code false} otherwise. - */ - @Override - public boolean randomlyDies(final int age, final Gender gender) { - double[] deathRateArray = gender.isMale() ? MALE_DEATH_RATE : FEMALE_DEATH_RATE; - double chanceOfDeath = deathRateArray[0] * Math.pow(10, deathRateArray[1]) - * Math.exp(deathRateArray[2] * age * 7); // Multiply character age by 7 for weekly rate - - // Compare the random float with the calculated weekly death rate - return Compute.randomFloat() < chanceOfDeath; - } -} diff --git a/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java b/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java new file mode 100644 index 00000000000..8251f519d4e --- /dev/null +++ b/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2022 - 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 as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with MekHQ. If not, see . + */ +package mekhq.campaign.personnel.death; + +import megamek.common.Compute; +import megamek.common.enums.Gender; +import mekhq.campaign.CampaignOptions; +import mekhq.campaign.personnel.enums.RandomDeathMethod; + +public class RandomDeath extends AbstractDeath { + //region Variable Declarations + private double percentage; + //endregion Variable Declarations + + //region Constructors + public RandomDeath(final CampaignOptions options, final boolean initializeCauses) { + super(RandomDeathMethod.RANDOM, options, initializeCauses); + setPercentage(options.getRandomDeathChance()); + } + //endregion Constructors + + //region Getters/Setters + public double getPercentage() { + return percentage; + } + + public void setPercentage(final double percentage) { + this.percentage = percentage; + } + //endregion Getters/Setters + + @Override + public boolean randomlyDies(final int age, final Gender gender) { + return Compute.randomFloat() < getPercentage(); + } +} diff --git a/MekHQ/src/mekhq/campaign/personnel/enums/RandomDeathMethod.java b/MekHQ/src/mekhq/campaign/personnel/enums/RandomDeathMethod.java index 468525c5d42..e69c89665cf 100644 --- a/MekHQ/src/mekhq/campaign/personnel/enums/RandomDeathMethod.java +++ b/MekHQ/src/mekhq/campaign/personnel/enums/RandomDeathMethod.java @@ -22,7 +22,7 @@ import mekhq.campaign.CampaignOptions; import mekhq.campaign.personnel.death.AbstractDeath; import mekhq.campaign.personnel.death.DisabledRandomDeath; -import mekhq.campaign.personnel.death.ExponentialRandomDeath; +import mekhq.campaign.personnel.death.RandomDeath; import java.util.ResourceBundle; @@ -68,7 +68,7 @@ public AbstractDeath getMethod(final CampaignOptions options) { public AbstractDeath getMethod(final CampaignOptions options, final boolean initializeCauses) { return switch (this) { - case RANDOM -> new ExponentialRandomDeath(options, initializeCauses); + case RANDOM -> new RandomDeath(options, initializeCauses); case NONE -> new DisabledRandomDeath(options, initializeCauses); }; } diff --git a/MekHQ/src/mekhq/gui/CampaignGUI.java b/MekHQ/src/mekhq/gui/CampaignGUI.java index 19a7de103db..7c54a9d9c46 100644 --- a/MekHQ/src/mekhq/gui/CampaignGUI.java +++ b/MekHQ/src/mekhq/gui/CampaignGUI.java @@ -54,6 +54,8 @@ import mekhq.campaign.personnel.Person; import mekhq.campaign.personnel.SkillType; import mekhq.campaign.personnel.autoAwards.AutoAwardsController; +import mekhq.campaign.personnel.death.AbstractDeath; +import mekhq.campaign.personnel.death.RandomDeath; import mekhq.campaign.personnel.divorce.RandomDivorce; import mekhq.campaign.personnel.enums.*; import mekhq.campaign.personnel.marriage.RandomMarriage; @@ -1485,9 +1487,12 @@ private void menuOptionsActionPerformed(final ActionEvent evt) { } } + AbstractDeath death = getCampaign().getDeath(); if ((randomDeathMethod != newOptions.getRandomDeathMethod()) || (useRandomDeathSuicideCause != newOptions.isUseRandomDeathSuicideCause())) { getCampaign().setDeath(newOptions.getRandomDeathMethod().getMethod(newOptions)); + } else if (death instanceof RandomDeath) { + ((RandomDeath) death).setPercentage(newOptions.getRandomDeathChance()); } if (randomDivorceMethod != newOptions.getRandomDivorceMethod()) { diff --git a/MekHQ/src/mekhq/gui/campaignOptions/contents/BiographyTab.java b/MekHQ/src/mekhq/gui/campaignOptions/contents/BiographyTab.java index 607e86a430a..ff77dd6059f 100644 --- a/MekHQ/src/mekhq/gui/campaignOptions/contents/BiographyTab.java +++ b/MekHQ/src/mekhq/gui/campaignOptions/contents/BiographyTab.java @@ -111,6 +111,8 @@ public class BiographyTab { private JLabel lblRandomDeathMethod; private MMComboBox comboRandomDeathMethod; private JCheckBox chkUseRandomDeathSuicideCause; + private JLabel lblRandomDeathChance; + private JSpinner spnRandomDeathChance; private JPanel pnlDeathAgeGroup; private Map chkEnabledRandomDeathAgeGroups; @@ -278,6 +280,8 @@ private void initializeDeathTab() { lblRandomDeathMethod = new JLabel(); comboRandomDeathMethod = new MMComboBox<>("comboRandomDeathMethod", RandomDeathMethod.values()); chkUseRandomDeathSuicideCause = new JCheckBox(); + lblRandomDeathChance = new JLabel(); + spnRandomDeathChance = new JSpinner(); pnlDeathAgeGroup = new JPanel(); chkEnabledRandomDeathAgeGroups = new HashMap<>(); @@ -760,6 +764,10 @@ public Component getListCellRendererComponent(final JList list, final Object }); chkUseRandomDeathSuicideCause = new CampaignOptionsCheckBox("UseRandomDeathSuicideCause"); + lblRandomDeathChance = new CampaignOptionsLabel("RandomDeathChance"); + spnRandomDeathChance = new CampaignOptionsSpinner("RandomDeathChance", + 0, 0, 100, 0.000001); + pnlDeathAgeGroup = createDeathAgeGroupsPanel(); // Layout the Panel @@ -777,6 +785,11 @@ public Component getListCellRendererComponent(final JList list, final Object layoutLeft.gridy++; panelLeft.add(chkUseRandomDeathSuicideCause, layoutLeft); + layoutLeft.gridy++; + panelLeft.add(lblRandomDeathChance, layoutLeft); + layoutLeft.gridx++; + panelLeft.add(spnRandomDeathChance, layoutLeft); + final JPanel panelParent = new CampaignOptionsStandardPanel("DeathTab", true); final GridBagConstraints layoutParent = new CampaignOptionsGridBagConstraints(panelParent); @@ -1306,6 +1319,7 @@ public void loadValuesFromCampaignOptions(@Nullable CampaignOptions presetCampai // Death comboRandomDeathMethod.setSelectedItem(options.getRandomDeathMethod()); chkUseRandomDeathSuicideCause.setSelected(options.isUseRandomDeathSuicideCause()); + spnRandomDeathChance.setValue(options.getRandomDeathChance()); Map deathAgeGroups = options.getEnabledRandomDeathAgeGroups(); for (final AgeGroup ageGroup : AgeGroup.values()) { @@ -1395,6 +1409,7 @@ public void applyCampaignOptionsToCampaign(@Nullable CampaignOptions presetCampa // Death options.setRandomDeathMethod(comboRandomDeathMethod.getSelectedItem()); options.setUseRandomDeathSuicideCause(chkUseRandomDeathSuicideCause.isSelected()); + options.setRandomDeathChance((double) spnRandomDeathChance.getValue()); for (final AgeGroup ageGroup : AgeGroup.values()) { options.getEnabledRandomDeathAgeGroups().put(ageGroup, chkEnabledRandomDeathAgeGroups.get(ageGroup).isSelected()); diff --git a/MekHQ/unittests/mekhq/campaign/personnel/death/AbstractDeathTest.java b/MekHQ/unittests/mekhq/campaign/personnel/death/AbstractDeathTest.java index b4dc657c265..4a956af05ac 100644 --- a/MekHQ/unittests/mekhq/campaign/personnel/death/AbstractDeathTest.java +++ b/MekHQ/unittests/mekhq/campaign/personnel/death/AbstractDeathTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2025 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2022 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * @@ -23,6 +23,7 @@ import mekhq.campaign.personnel.Person; import mekhq.campaign.personnel.enums.AgeGroup; import mekhq.campaign.personnel.enums.PersonnelStatus; +import mekhq.campaign.personnel.enums.PrisonerStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -36,10 +37,8 @@ import java.util.HashMap; import java.util.Map; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -150,12 +149,37 @@ public void testCanDie() { // Age Group must be enabled when(mockPerson.isImmortal()).thenReturn(false); assertNotNull(mockDeath.canDie(mockPerson, AgeGroup.CHILD, true)); + + // Can't be Clan Personnel with Random Clan Death Disabled + when(mockPerson.isClanPersonnel()).thenReturn(true); + when(mockDeath.isUseRandomClanPersonnelDeath()).thenReturn(false); + when(mockDeath.isUseRandomPrisonerDeath()).thenReturn(true); + assertNotNull(mockDeath.canDie(mockPerson, AgeGroup.ADULT, true)); + + // Can be Non-Clan Personnel with Random Clan Death Disabled + when(mockPerson.isClanPersonnel()).thenReturn(false); + assertNull(mockDeath.canDie(mockPerson, AgeGroup.ADULT, true)); + + // Can be a Non-Prisoner with Random Prisoner Death Disabled + when(mockPerson.getPrisonerStatus()).thenReturn(PrisonerStatus.FREE); + when(mockDeath.isUseRandomPrisonerDeath()).thenReturn(false); + assertNull(mockDeath.canDie(mockPerson, AgeGroup.ADULT, true)); + + // Can't be a Prisoner with Random Prisoner Death Disabled + when(mockPerson.getPrisonerStatus()).thenReturn(PrisonerStatus.PRISONER); + assertNotNull(mockDeath.canDie(mockPerson, AgeGroup.ADULT, true)); + + // Can be a Clan Prisoner with Random Clan and Random Prisoner Death Enabled + lenient().when(mockPerson.isClanPersonnel()).thenReturn(true); + when(mockDeath.isUseRandomClanPersonnelDeath()).thenReturn(true); + when(mockDeath.isUseRandomPrisonerDeath()).thenReturn(true); + assertNull(mockDeath.canDie(mockPerson, AgeGroup.ADULT, true)); } //region New Day @Test - public void testProcessNewWeek() { - doCallRealMethod().when(mockDeath).processNewWeek(any(), any(), any()); + public void testProcessNewDay() { + doCallRealMethod().when(mockDeath).processNewDay(any(), any(), any()); when(mockDeath.getCause(any(), any(), anyInt())).thenReturn(PersonnelStatus.DISEASE); final Person mockPerson = mock(Person.class); @@ -166,21 +190,21 @@ public void testProcessNewWeek() { // Can't be dead when(mockDeath.canDie(any(), any(), anyBoolean())).thenReturn("Dead"); - assertFalse(mockDeath.processNewWeek(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson)); + assertFalse(mockDeath.processNewDay(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson)); // Randomly Dies - Change Status Works Properly when(mockDeath.canDie(any(), any(), anyBoolean())).thenReturn(null); when(mockDeath.randomlyDies(anyInt(), any())).thenReturn(true); when(mockPerson.getStatus()).thenReturn(PersonnelStatus.DISEASE); - assertTrue(mockDeath.processNewWeek(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson)); + assertTrue(mockDeath.processNewDay(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson)); // Randomly Dies - Issue Changing Status when(mockPerson.getStatus()).thenReturn(PersonnelStatus.ACTIVE); - assertFalse(mockDeath.processNewWeek(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson)); + assertFalse(mockDeath.processNewDay(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson)); // Doesn't Randomly Die when(mockDeath.randomlyDies(anyInt(), any())).thenReturn(false); - assertFalse(mockDeath.processNewWeek(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson)); + assertFalse(mockDeath.processNewDay(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson)); } } //endregion New Day diff --git a/MekHQ/unittests/mekhq/campaign/personnel/death/ExponentialRandomDeathTest.java b/MekHQ/unittests/mekhq/campaign/personnel/death/ExponentialRandomDeathTest.java deleted file mode 100644 index 81dcd335eda..00000000000 --- a/MekHQ/unittests/mekhq/campaign/personnel/death/ExponentialRandomDeathTest.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright (c) 2022-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 as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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. - * - * You should have received a copy of the GNU General Public License - * along with MekHQ. If not, see . - */ -package mekhq.campaign.personnel.death; - -import megamek.common.Compute; -import megamek.common.enums.Gender; -import mekhq.campaign.CampaignOptions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.MockedStatic; -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@ExtendWith(value = MockitoExtension.class) -public class ExponentialRandomDeathTest { - @Mock - private CampaignOptions mockOptions; - - private ExponentialRandomDeath exponentialRandomDeath; - - @BeforeEach - public void setUp() { - exponentialRandomDeath = new ExponentialRandomDeath(mockOptions, false); - } - - @Test - public void testRandomlyDiesForYoungMale() { - try (MockedStatic compute = Mockito.mockStatic(Compute.class)) { - // Mock random float to always return 0 (smallest possible value, ensuring "true") - compute.when(Compute::randomFloat).thenReturn(0f); - - // A male age 0 should "die" since the random value is always less than the death chance - assertTrue(exponentialRandomDeath.randomlyDies(0, Gender.MALE)); - } - } - - @Test - public void testRandomlyDiesForYoungFemale() { - try (MockedStatic compute = Mockito.mockStatic(Compute.class)) { - compute.when(Compute::randomFloat).thenReturn(0f); - - // A female age 0 should "die" since the random value is 0 - assertTrue(exponentialRandomDeath.randomlyDies(0, Gender.FEMALE)); - } - } - - @Test - public void testRandomlyDiesForHigherAgeMale() { - try (MockedStatic compute = Mockito.mockStatic(Compute.class)) { - // Mock random float to return a value slightly higher than expected for age 50 male - compute.when(Compute::randomFloat).thenReturn(0.00001f); - - // A male age 50 (relatively high chance) should still die based on the random value - assertTrue(exponentialRandomDeath.randomlyDies(50, Gender.MALE)); - } - } - - @Test - public void testRandomlyDiesForHigherAgeFemale() { - try (MockedStatic compute = Mockito.mockStatic(Compute.class)) { - // Mock random float to return a value slightly higher than expected for age 75 female - compute.when(Compute::randomFloat).thenReturn(0.0001f); - - // A female age 75 should die since the random value is lower than her chance of death - assertTrue(exponentialRandomDeath.randomlyDies(75, Gender.FEMALE)); - } - } - - @Test - public void testNoRandomDeathForLowChanceMale() { - try (MockedStatic compute = Mockito.mockStatic(Compute.class)) { - // Mock random float to return a large value, higher than any plausible death chance - compute.when(Compute::randomFloat).thenReturn(5.0f); - - // A male at any age will not die since the random value is very high - assertFalse(exponentialRandomDeath.randomlyDies(30, Gender.MALE)); - } - } - - @Test - public void testNoRandomDeathForLowChanceFemale() { - try (MockedStatic compute = Mockito.mockStatic(Compute.class)) { - compute.when(Compute::randomFloat).thenReturn(5.0f); - - // A female at any age will also not die since the random value is high - assertFalse(exponentialRandomDeath.randomlyDies(30, Gender.FEMALE)); - } - } - - @Test - public void testEdgeCaseForExtremelyOldMale() { - try (MockedStatic compute = Mockito.mockStatic(Compute.class)) { - // Mock random float to return a value close to 0, ensuring death for very old males - compute.when(Compute::randomFloat).thenReturn(0f); - - // A male age 200+ has an extremely high chance of death - assertTrue(exponentialRandomDeath.randomlyDies(200, Gender.MALE)); - } - } - - @Test - public void testEdgeCaseForExtremelyOldFemale() { - try (MockedStatic compute = Mockito.mockStatic(Compute.class)) { - compute.when(Compute::randomFloat).thenReturn(0f); - - // A female age 200+ has an extremely high chance of death - assertTrue(exponentialRandomDeath.randomlyDies(200, Gender.FEMALE)); - } - } -} From a79f40a818286195af6a4feb33223e8365ba6b93 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sat, 1 Feb 2025 15:42:34 -0600 Subject: [PATCH 040/112] Refactored random death logic and adjusted field types Updated `randomlyDies` method to include campaign context and refined logic for age-based death probabilities. Changed `randomDeathChance` from double to int for consistency, and improved UI text for clarity in Campaign Options dialog. --- .../CampaignOptionsDialog.properties | 2 +- MekHQ/src/mekhq/campaign/CampaignOptions.java | 10 +++---- .../personnel/death/AbstractDeath.java | 9 +++--- .../personnel/death/DisabledRandomDeath.java | 3 +- .../campaign/personnel/death/RandomDeath.java | 30 +++++++++++++++++-- .../contents/BiographyTab.java | 4 +-- 6 files changed, 42 insertions(+), 16 deletions(-) diff --git a/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties b/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties index 58f630d1a5f..1197ae46648 100644 --- a/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties +++ b/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties @@ -804,7 +804,7 @@ lblRandomDeathMethod.text=Use Random Death lblRandomDeathMethod.tooltip=Should characters randomly die? lblUseRandomDeathSuicideCause.text=Enable Cause of Death: Suicide lblUseRandomDeathSuicideCause.tooltip=This includes suicide as a potential cause for a random death. -lblRandomDeathChance.text=Random Death Percentage \u26A0 +lblRandomDeathChance.text=Random Death Chance: \u26A0 1 in lblRandomDeathChance.tooltip=This is the percent chance per day that any member of your force will\ \ randomly die.\
\ diff --git a/MekHQ/src/mekhq/campaign/CampaignOptions.java b/MekHQ/src/mekhq/campaign/CampaignOptions.java index f83dd649536..307c9baa447 100644 --- a/MekHQ/src/mekhq/campaign/CampaignOptions.java +++ b/MekHQ/src/mekhq/campaign/CampaignOptions.java @@ -371,7 +371,7 @@ public static String getTechLevelName(final int techLevel) { private RandomDeathMethod randomDeathMethod; private Map enabledRandomDeathAgeGroups; private boolean useRandomDeathSuicideCause; - private double randomDeathChance; + private int randomDeathChance; // endregion Life Paths Tab //region Turnover and Retention @@ -966,7 +966,7 @@ public CampaignOptions() { getEnabledRandomDeathAgeGroups().put(AgeGroup.TODDLER, false); getEnabledRandomDeathAgeGroups().put(AgeGroup.BABY, false); setUseRandomDeathSuicideCause(false); - setRandomDeathChance(0.00002); + setRandomDeathChance(6000); // endregion Life Paths Tab // region Turnover and Retention @@ -2981,11 +2981,11 @@ public void setUseRandomDeathSuicideCause(final boolean useRandomDeathSuicideCau this.useRandomDeathSuicideCause = useRandomDeathSuicideCause; } - public double getRandomDeathChance() { + public int getRandomDeathChance() { return randomDeathChance; } - public void setRandomDeathChance(final double randomDeathChance) { + public void setRandomDeathChance(final int randomDeathChance) { this.randomDeathChance = randomDeathChance; } // endregion Death @@ -5851,7 +5851,7 @@ public static CampaignOptions generateCampaignOptionsFromXml(Node wn, Version ve } else if (wn2.getNodeName().equalsIgnoreCase("useRandomDeathSuicideCause")) { retVal.setUseRandomDeathSuicideCause(Boolean.parseBoolean(wn2.getTextContent().trim())); } else if (wn2.getNodeName().equalsIgnoreCase("randomDeathChance")) { - retVal.setRandomDeathChance(Double.parseDouble(wn2.getTextContent().trim())); + retVal.setRandomDeathChance(Integer.parseInt(wn2.getTextContent().trim())); } else if (wn2.getNodeName().equalsIgnoreCase("useRandomRetirement")) { retVal.setUseRandomRetirement(Boolean.parseBoolean(wn2.getTextContent().trim())); } else if (wn2.getNodeName().equalsIgnoreCase("turnoverBaseTn")) { diff --git a/MekHQ/src/mekhq/campaign/personnel/death/AbstractDeath.java b/MekHQ/src/mekhq/campaign/personnel/death/AbstractDeath.java index 9bc5f1db53d..0fc7ac63d82 100644 --- a/MekHQ/src/mekhq/campaign/personnel/death/AbstractDeath.java +++ b/MekHQ/src/mekhq/campaign/personnel/death/AbstractDeath.java @@ -135,7 +135,7 @@ public boolean processNewWeek(final Campaign campaign, final LocalDate today, return false; } - if (randomlyDies(age, person.getGender())) { + if (randomlyDies(campaign, age, person.getGender())) { // We double-report here, to make sure the user definitely notices that a random death has occurred. // Prior to this change, it was exceptionally easy to miss these events. String color = MekHQ.getMHQOptions().getFontColorNegativeHexColor(); @@ -152,11 +152,12 @@ public boolean processNewWeek(final Campaign campaign, final LocalDate today, // region Random Death /** - * @param age the person's age - * @param gender the person's gender + * @param campaign the current campaign + * @param age the person's age + * @param gender the person's gender * @return true if the person is selected to randomly die, otherwise false */ - public abstract boolean randomlyDies(int age, Gender gender); + public abstract boolean randomlyDies(Campaign campaign, int age, Gender gender); // endregion Random Death // endregion New Day diff --git a/MekHQ/src/mekhq/campaign/personnel/death/DisabledRandomDeath.java b/MekHQ/src/mekhq/campaign/personnel/death/DisabledRandomDeath.java index d2013197987..aecedd9ead4 100644 --- a/MekHQ/src/mekhq/campaign/personnel/death/DisabledRandomDeath.java +++ b/MekHQ/src/mekhq/campaign/personnel/death/DisabledRandomDeath.java @@ -19,6 +19,7 @@ package mekhq.campaign.personnel.death; import megamek.common.enums.Gender; +import mekhq.campaign.Campaign; import mekhq.campaign.CampaignOptions; import mekhq.campaign.personnel.enums.RandomDeathMethod; @@ -30,7 +31,7 @@ public DisabledRandomDeath(final CampaignOptions options, final boolean initiali //endregion Constructors @Override - public boolean randomlyDies(final int age, final Gender gender) { + public boolean randomlyDies(Campaign campaign, final int age, final Gender gender) { return false; } } diff --git a/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java b/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java index 8251f519d4e..a6975847913 100644 --- a/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java +++ b/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java @@ -18,11 +18,13 @@ */ package mekhq.campaign.personnel.death; -import megamek.common.Compute; import megamek.common.enums.Gender; +import mekhq.campaign.Campaign; import mekhq.campaign.CampaignOptions; import mekhq.campaign.personnel.enums.RandomDeathMethod; +import static megamek.common.Compute.randomInt; + public class RandomDeath extends AbstractDeath { //region Variable Declarations private double percentage; @@ -45,8 +47,30 @@ public void setPercentage(final double percentage) { } //endregion Getters/Setters + /** + * Determines if a person randomly dies based on the campaign, age, and gender. + * + *

The probability of death increases as a person's age exceeds a specific + * threshold, with the chance of death growing exponentially for extra years lived.

+ * + * @param campaign The campaign that defines the base random death chance. + * @param age The individual's age. + * @param gender The individual's gender. Currently unused but supports future extensibility. + * @return {@code true} if the person randomly dies; {@code false} otherwise. + */ @Override - public boolean randomlyDies(final int age, final Gender gender) { - return Compute.randomFloat() < getPercentage(); + public boolean randomlyDies(Campaign campaign, final int age, final Gender gender) { + final int AGE_THRESHOLD = 90; + final double REDUCTION_MULTIPLIER = 0.90; + + int baseDieSize = campaign.getCampaignOptions().getRandomDeathChance(); + + // Calculate adjusted die size if the age exceeds the threshold + int adjustedDieSize = (age > AGE_THRESHOLD) + ? (int) Math.round(baseDieSize * Math.pow(REDUCTION_MULTIPLIER, (age - AGE_THRESHOLD))) + : baseDieSize; + + // Return random death outcome + return randomInt(adjustedDieSize) < getPercentage(); } } diff --git a/MekHQ/src/mekhq/gui/campaignOptions/contents/BiographyTab.java b/MekHQ/src/mekhq/gui/campaignOptions/contents/BiographyTab.java index ff77dd6059f..fc47045b218 100644 --- a/MekHQ/src/mekhq/gui/campaignOptions/contents/BiographyTab.java +++ b/MekHQ/src/mekhq/gui/campaignOptions/contents/BiographyTab.java @@ -766,7 +766,7 @@ public Component getListCellRendererComponent(final JList list, final Object lblRandomDeathChance = new CampaignOptionsLabel("RandomDeathChance"); spnRandomDeathChance = new CampaignOptionsSpinner("RandomDeathChance", - 0, 0, 100, 0.000001); + 6000, 1, 10000, 1); pnlDeathAgeGroup = createDeathAgeGroupsPanel(); @@ -1409,7 +1409,7 @@ public void applyCampaignOptionsToCampaign(@Nullable CampaignOptions presetCampa // Death options.setRandomDeathMethod(comboRandomDeathMethod.getSelectedItem()); options.setUseRandomDeathSuicideCause(chkUseRandomDeathSuicideCause.isSelected()); - options.setRandomDeathChance((double) spnRandomDeathChance.getValue()); + options.setRandomDeathChance((int) spnRandomDeathChance.getValue()); for (final AgeGroup ageGroup : AgeGroup.values()) { options.getEnabledRandomDeathAgeGroups().put(ageGroup, chkEnabledRandomDeathAgeGroups.get(ageGroup).isSelected()); From ff063544f24360f675c57c04115938662aea90e7 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sat, 1 Feb 2025 16:13:21 -0600 Subject: [PATCH 041/112] Remove RandomDeath functionality and related tests Eliminated the RandomDeath feature, including associated classes, enums, tests, and resources. Refactored the campaign options UI to exclude controls tied to RandomDeath functionality. --- .../mekhq/resources/CampaignGUI.properties | 2 - .../CampaignOptionsDialog.properties | 2 - .../mekhq/resources/Personnel.properties | 10 - .../mekhq/resources/RandomDeath.properties | 3 + MekHQ/src/mekhq/campaign/Campaign.java | 17 +- MekHQ/src/mekhq/campaign/CampaignOptions.java | 31 -- .../mekhq/campaign/io/CampaignXmlParser.java | 1 - .../personnel/death/AbstractDeath.java | 300 ---------------- .../personnel/death/DisabledRandomDeath.java | 37 -- .../campaign/personnel/death/RandomDeath.java | 330 ++++++++++++++++-- .../personnel/enums/RandomDeathMethod.java | 80 ----- MekHQ/src/mekhq/gui/CampaignGUI.java | 26 +- .../adapter/PersonnelTableMouseAdapter.java | 5 +- .../contents/BiographyTab.java | 31 +- .../personnel/death/AbstractDeathTest.java | 245 ------------- .../death/DisabledRandomDeathTest.java | 49 --- 16 files changed, 325 insertions(+), 844 deletions(-) create mode 100644 MekHQ/resources/mekhq/resources/RandomDeath.properties delete mode 100644 MekHQ/src/mekhq/campaign/personnel/death/AbstractDeath.java delete mode 100644 MekHQ/src/mekhq/campaign/personnel/death/DisabledRandomDeath.java delete mode 100644 MekHQ/src/mekhq/campaign/personnel/enums/RandomDeathMethod.java delete mode 100644 MekHQ/unittests/mekhq/campaign/personnel/death/AbstractDeathTest.java delete mode 100644 MekHQ/unittests/mekhq/campaign/personnel/death/DisabledRandomDeathTest.java diff --git a/MekHQ/resources/mekhq/resources/CampaignGUI.properties b/MekHQ/resources/mekhq/resources/CampaignGUI.properties index d0e434490cf..bf5c745994e 100644 --- a/MekHQ/resources/mekhq/resources/CampaignGUI.properties +++ b/MekHQ/resources/mekhq/resources/CampaignGUI.properties @@ -36,8 +36,6 @@ miRefreshForceIcons.text=Refresh Force Icon Directory miRefreshStoryIcons.text=Refresh Story Arc Icon Directory miRefreshAwards.text=Refresh Award Icon Directory miRefreshRanks.text=Refresh Rank Systems from File -miRefreshRandomDeathCauses.text=Refresh Random Death Causes from File -miRefreshRandomDeathCauses.toolTipText=This reloads the random death causes files, which is primarily useful when editing the userdata cause file and testing out the changes there. miRefreshFinancialInstitutions.text=Refresh Financial Institutions from File miRefreshFinancialInstitutions.toolTipText=This reloads the financial institutions files, which is primarily useful when editing the userdata institutions file and testing out the changes there. # Menu File Others diff --git a/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties b/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties index 1197ae46648..71aa7439d9d 100644 --- a/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties +++ b/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties @@ -800,8 +800,6 @@ lblExtraRandomOrigin.tooltip=Random origin is randomized to the planetary level # createDeathTab lblDeathTab.text=Death Options \u270E -lblRandomDeathMethod.text=Use Random Death -lblRandomDeathMethod.tooltip=Should characters randomly die? lblUseRandomDeathSuicideCause.text=Enable Cause of Death: Suicide lblUseRandomDeathSuicideCause.tooltip=This includes suicide as a potential cause for a random death. lblRandomDeathChance.text=Random Death Chance: \u26A0 1 in diff --git a/MekHQ/resources/mekhq/resources/Personnel.properties b/MekHQ/resources/mekhq/resources/Personnel.properties index 0bbc8a65cc5..8676ee20901 100644 --- a/MekHQ/resources/mekhq/resources/Personnel.properties +++ b/MekHQ/resources/mekhq/resources/Personnel.properties @@ -5,16 +5,6 @@ Freeborn.text=Freeborn -## Death -# AbstractDeath Class -cannotDie.Dead.text=They are already dead. -cannotDie.Immortal.text=They are immortal, and cannot die a random death. -cannotDie.AgeGroupDisabled.text=Their current age group is disabled. -cannotDie.RandomClanPersonnel.text=They are from clan origins, and random clan personnel death is disabled. -cannotDie.RandomPrisoner.text=They are a prisoner, and random prisoner death is disabled. - - - ## Divorce # AbstractDivorce Class cannotDivorce.NotMarried.text=They cannot divorce as they are not married. diff --git a/MekHQ/resources/mekhq/resources/RandomDeath.properties b/MekHQ/resources/mekhq/resources/RandomDeath.properties new file mode 100644 index 00000000000..40d0cff3ce5 --- /dev/null +++ b/MekHQ/resources/mekhq/resources/RandomDeath.properties @@ -0,0 +1,3 @@ +cannotDie.Dead.text=They are already dead. +cannotDie.Immortal.text=They are immortal and cannot die a random death. +cannotDie.AgeGroupDisabled.text=Their current age group is disabled. \ No newline at end of file diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index 1b35f18c6e3..4f42f8a7c18 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -85,8 +85,7 @@ import mekhq.campaign.parts.equipment.MissingEquipmentPart; import mekhq.campaign.personnel.*; import mekhq.campaign.personnel.autoAwards.AutoAwardsController; -import mekhq.campaign.personnel.death.AbstractDeath; -import mekhq.campaign.personnel.death.DisabledRandomDeath; +import mekhq.campaign.personnel.death.RandomDeath; import mekhq.campaign.personnel.divorce.AbstractDivorce; import mekhq.campaign.personnel.divorce.DisabledRandomDivorce; import mekhq.campaign.personnel.education.Academy; @@ -276,7 +275,6 @@ public class Campaign implements ITechManager { private AbstractContractMarket contractMarket; private AbstractUnitMarket unitMarket; - private transient AbstractDeath death; private transient AbstractDivorce divorce; private transient AbstractMarriage marriage; private transient AbstractProcreation procreation; @@ -378,7 +376,6 @@ public Campaign() { setPersonnelMarket(new PersonnelMarket()); setContractMarket(new AtbMonthlyContractMarket()); setUnitMarket(new DisabledUnitMarket()); - setDeath(new DisabledRandomDeath(getCampaignOptions(), false)); setDivorce(new DisabledRandomDivorce(getCampaignOptions())); setMarriage(new DisabledRandomMarriage(getCampaignOptions())); setProcreation(new DisabledRandomProcreation(getCampaignOptions())); @@ -627,14 +624,6 @@ public void setUnitMarket(final AbstractUnitMarket unitMarket) { // endregion Markets // region Personnel Modules - public AbstractDeath getDeath() { - return death; - } - - public void setDeath(final AbstractDeath death) { - this.death = death; - } - public AbstractDivorce getDivorce() { return divorce; } @@ -4355,6 +4344,8 @@ private void processResupply(AtBContract contract) { * @see #getPersonnelFilteringOutDeparted() Filters out departed personnel before daily processing */ public void processNewDayPersonnel() { + RandomDeath randomDeath = new RandomDeath(campaignOptions); + // This list ensures we don't hit a concurrent modification error List personnel = getPersonnelFilteringOutDeparted(); @@ -4389,7 +4380,7 @@ public void processNewDayPersonnel() { // Weekly events if (currentDay.getDayOfWeek() == DayOfWeek.MONDAY) { - if (!getDeath().processNewWeek(this, getLocalDate(), person)) { + if (!randomDeath.processNewWeek(this, getLocalDate(), person)) { // If the character has died, we don't need to process relationship events processWeeklyRelationshipEvents(person); } diff --git a/MekHQ/src/mekhq/campaign/CampaignOptions.java b/MekHQ/src/mekhq/campaign/CampaignOptions.java index 307c9baa447..f62b0b5febf 100644 --- a/MekHQ/src/mekhq/campaign/CampaignOptions.java +++ b/MekHQ/src/mekhq/campaign/CampaignOptions.java @@ -51,9 +51,6 @@ import java.util.*; import java.util.Map.Entry; -import static mekhq.campaign.personnel.enums.RandomDeathMethod.NONE; -import static mekhq.campaign.personnel.enums.RandomDeathMethod.RANDOM; - /** * @author natit */ @@ -368,7 +365,6 @@ public static String getTechLevelName(final int techLevel) { private Integer militaryAcademyAccidents; // Death - private RandomDeathMethod randomDeathMethod; private Map enabledRandomDeathAgeGroups; private boolean useRandomDeathSuicideCause; private int randomDeathChance; @@ -956,7 +952,6 @@ public CampaignOptions() { setMilitaryAcademyAccidents(10000); // Death - setRandomDeathMethod(NONE); setEnabledRandomDeathAgeGroups(new HashMap<>()); getEnabledRandomDeathAgeGroups().put(AgeGroup.ELDER, true); getEnabledRandomDeathAgeGroups().put(AgeGroup.ADULT, true); @@ -2951,20 +2946,6 @@ public void setMilitaryAcademyAccidents(Integer militaryAcademyAccidents) { this.militaryAcademyAccidents = militaryAcademyAccidents; } - /** - * @return the random death method to use - */ - public RandomDeathMethod getRandomDeathMethod() { - return randomDeathMethod; - } - - /** - * @param randomDeathMethod the random death method to use - */ - public void setRandomDeathMethod(final RandomDeathMethod randomDeathMethod) { - this.randomDeathMethod = randomDeathMethod; - } - public Map getEnabledRandomDeathAgeGroups() { return enabledRandomDeathAgeGroups; } @@ -5025,7 +5006,6 @@ public void writeToXml(final PrintWriter pw, int indent) { // endregion Education // region Death - MHQXMLUtility.writeSimpleXMLTag(pw, indent, "randomDeathMethod", getRandomDeathMethod().name()); MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, "enabledRandomDeathAgeGroups"); for (final Entry entry : getEnabledRandomDeathAgeGroups().entrySet()) { MHQXMLUtility.writeSimpleXMLTag(pw, indent, entry.getKey().name(), entry.getValue()); @@ -5822,17 +5802,6 @@ public static CampaignOptions generateCampaignOptionsFromXml(Node wn, Version ve } else if (wn2.getNodeName().equalsIgnoreCase("militaryAcademyAccidents")) { retVal.setMilitaryAcademyAccidents(Integer.parseInt(wn2.getTextContent().trim())); // endregion Education - - // region Death - } else if (wn2.getNodeName().equalsIgnoreCase("randomDeathMethod")) { - // <50.04 compatibility handler - if (wn2.getTextContent().trim().equalsIgnoreCase(NONE.name())) { - retVal.setRandomDeathMethod(NONE); - } else { - retVal.setRandomDeathMethod(RANDOM); - } - // Replace above with below when compatibility handler is removed. -// retVal.setRandomDeathMethod(RandomDeathMethod.valueOf(wn2.getTextContent().trim())); } else if (wn2.getNodeName().equalsIgnoreCase("enabledRandomDeathAgeGroups")) { if (!wn2.hasChildNodes()) { continue; diff --git a/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java b/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java index f50b2db86c1..7543fdfc0e4 100644 --- a/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java +++ b/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java @@ -335,7 +335,6 @@ public Campaign parse() throws CampaignXmlParseException, NullEntityException { cleanupGhostKills(retVal); // Update the Personnel Modules - retVal.setDeath(options.getRandomDeathMethod().getMethod(options)); retVal.setDivorce(options.getRandomDivorceMethod().getMethod(options)); retVal.setMarriage(options.getRandomMarriageMethod().getMethod(options)); retVal.setProcreation(options.getRandomProcreationMethod().getMethod(options)); diff --git a/MekHQ/src/mekhq/campaign/personnel/death/AbstractDeath.java b/MekHQ/src/mekhq/campaign/personnel/death/AbstractDeath.java deleted file mode 100644 index 0fc7ac63d82..00000000000 --- a/MekHQ/src/mekhq/campaign/personnel/death/AbstractDeath.java +++ /dev/null @@ -1,300 +0,0 @@ -/* - * Copyright (c) 2020-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 as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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. - * - * You should have received a copy of the GNU General Public License - * along with MekHQ. If not, see . - */ -package mekhq.campaign.personnel.death; - -import megamek.Version; -import megamek.common.annotations.Nullable; -import megamek.common.enums.Gender; -import megamek.common.util.weightedMaps.WeightedDoubleMap; -import megamek.logging.MMLogger; -import mekhq.MHQConstants; -import mekhq.MekHQ; -import mekhq.campaign.Campaign; -import mekhq.campaign.CampaignOptions; -import mekhq.campaign.personnel.Person; -import mekhq.campaign.personnel.enums.AgeGroup; -import mekhq.campaign.personnel.enums.PersonnelStatus; -import mekhq.campaign.personnel.enums.RandomDeathMethod; -import mekhq.campaign.personnel.enums.TenYearAgeRange; -import mekhq.utilities.MHQXMLUtility; -import mekhq.utilities.ReportingUtilities; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import java.io.File; -import java.io.FileInputStream; -import java.io.InputStream; -import java.time.LocalDate; -import java.util.HashMap; -import java.util.Map; -import java.util.ResourceBundle; - -import static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG; - -public abstract class AbstractDeath { - private static final MMLogger logger = MMLogger.create(AbstractDeath.class); - - // region Variable Declarations - private final RandomDeathMethod method; - private Map enabledAgeGroups; - private final boolean enableRandomDeathSuicideCause; - private final Map>> causes; - - private static final ResourceBundle resources = ResourceBundle.getBundle("mekhq.resources.Personnel", - MekHQ.getMHQOptions().getLocale()); - // endregion Variable Declarations - - // region Constructors - protected AbstractDeath(final RandomDeathMethod method, final CampaignOptions options, - final boolean initializeCauses) { - this.method = method; - setEnabledAgeGroups(options.getEnabledRandomDeathAgeGroups()); - this.enableRandomDeathSuicideCause = options.isUseRandomDeathSuicideCause(); - this.causes = new HashMap<>(); - if (initializeCauses && !method.isNone()) { - initializeCauses(); - } - } - // endregion Constructors - - // region Getters/Setters - public RandomDeathMethod getMethod() { - return method; - } - - public Map getEnabledAgeGroups() { - return enabledAgeGroups; - } - - public void setEnabledAgeGroups(final Map enabledAgeGroups) { - this.enabledAgeGroups = enabledAgeGroups; - } - - public boolean isEnableRandomDeathSuicideCause() { - return enableRandomDeathSuicideCause; - } - - public Map>> getCauses() { - return causes; - } - // endregion Getters/Setters - - /** - * This is used to determine if a person can die. - * - * @param person the person to determine for - * @param ageGroup the age group of the person in question - * @param randomDeath if this is for random death or manual death - * @return null if they can, otherwise the reason they cannot - */ - public @Nullable String canDie(final Person person, final AgeGroup ageGroup, - final boolean randomDeath) { - if (person.getStatus().isDead()) { - return resources.getString("cannotDie.Dead.text"); - } else if (randomDeath) { - if (person.isImmortal()) { - return resources.getString("cannotDie.Immortal.text"); - } else if (!getEnabledAgeGroups().get(ageGroup)) { - return resources.getString("cannotDie.AgeGroupDisabled.text"); - } - } - - return null; - } - - // region New Day - /** - * Processes new week random death for an individual. - * - * @param campaign the campaign to process - * @param today the current day - * @param person the person to process - */ - public boolean processNewWeek(final Campaign campaign, final LocalDate today, - final Person person) { - final int age = person.getAge(today); - final AgeGroup ageGroup = AgeGroup.determineAgeGroup(age); - if (canDie(person, ageGroup, true) != null) { - return false; - } - - if (randomlyDies(campaign, age, person.getGender())) { - // We double-report here, to make sure the user definitely notices that a random death has occurred. - // Prior to this change, it was exceptionally easy to miss these events. - String color = MekHQ.getMHQOptions().getFontColorNegativeHexColor(); - String formatOpener = ReportingUtilities.spanOpeningWithCustomColor(color); - campaign.addReport(String.format("%s has %sdied%s.", - person.getHyperlinkedFullTitle(), formatOpener, CLOSING_SPAN_TAG)); - - person.changeStatus(campaign, today, getCause(person, ageGroup, age)); - return person.getStatus().isDead(); - } else { - return false; - } - } - - // region Random Death - /** - * @param campaign the current campaign - * @param age the person's age - * @param gender the person's gender - * @return true if the person is selected to randomly die, otherwise false - */ - public abstract boolean randomlyDies(Campaign campaign, int age, Gender gender); - // endregion Random Death - // endregion New Day - - // region Cause - /** - * @param person the person who has died - * @param ageGroup the person's age group - * @param age the person's age - * @return the cause of the Person's random death - */ - public PersonnelStatus getCause(final Person person, final AgeGroup ageGroup, final int age) { - if (person.getStatus().isMIA()) { - return PersonnelStatus.KIA; - } else if (person.hasInjuries(false)) { - final PersonnelStatus status = determineIfInjuriesCausedTheDeath(person); - if (!status.isActive()) { - return status; - } - } - - if (person.isPregnant()) { - return PersonnelStatus.PREGNANCY_COMPLICATIONS; - } - - final Map> genderedCauses = getCauses() - .get(person.getGender()); - if (genderedCauses == null) { - return getDefaultCause(ageGroup); - } - - final WeightedDoubleMap ageRangeCauses = genderedCauses - .get(TenYearAgeRange.determineAgeRange(age)); - if (ageRangeCauses == null) { - return getDefaultCause(ageGroup); - } - - final PersonnelStatus cause = ageRangeCauses.randomItem(); - return (cause == null) ? getDefaultCause(ageGroup) : cause; - } - - /** - * @param ageGroup the age group to get the default random death cause for - * @return the default cause, which is old age for elders and natural causes for - * everyone else - */ - private PersonnelStatus getDefaultCause(final AgeGroup ageGroup) { - return ageGroup.isElder() ? PersonnelStatus.OLD_AGE : PersonnelStatus.NATURAL_CAUSES; - } - - /** - * @param person the person from whom may have died of injuries (which may be - * diseases, once - * that is implemented) - * @return the personnel status applicable to the form of injury that caused the - * death, or - * ACTIVE if it wasn't determined that injuries caused the death - */ - private PersonnelStatus determineIfInjuriesCausedTheDeath(final Person person) { - // We care about injuries that are major or deadly. We do not want any chronic - // conditions - // nor scratches - return person.getInjuries().stream().anyMatch(injury -> injury.getLevel().isMajorOrDeadly()) - ? PersonnelStatus.WOUNDS - : PersonnelStatus.ACTIVE; - } - // endregion Cause - - // region File I/O - public void initializeCauses() { - getCauses().clear(); - initializeCausesFromFile(new File(MHQConstants.RANDOM_DEATH_CAUSES_FILE_PATH)); - initializeCausesFromFile(new File(MHQConstants.USER_RANDOM_DEATH_CAUSES_FILE_PATH)); - } - - private void initializeCausesFromFile(final File file) { - if (!file.exists()) { - return; - } - - final Element element; - - // Open up the file - try (InputStream is = new FileInputStream(file)) { - element = MHQXMLUtility.newSafeDocumentBuilder().parse(is).getDocumentElement(); - } catch (Exception ex) { - logger.error("Failed to open file", ex); - return; - } - element.normalize(); - final Version version = new Version(element.getAttribute("version")); - final NodeList nl = element.getChildNodes(); - - logger.info("Parsing Random Death Causes from " + version + "-origin xml"); - for (int i = 0; i < nl.getLength(); i++) { - final Node wn = nl.item(i); - if (!wn.hasChildNodes()) { - continue; - } - - try { - final Gender gender = Gender.valueOf(wn.getNodeName()); - getCauses().putIfAbsent(gender, new HashMap<>()); - final NodeList nl2 = wn.getChildNodes(); - for (int j = 0; j < nl2.getLength(); j++) { - final Node wn2 = nl2.item(j); - if (!wn2.hasChildNodes()) { - continue; - } - - try { - final WeightedDoubleMap ageRangeCauses = new WeightedDoubleMap<>(); - getCauses().get(gender).put(TenYearAgeRange.valueOf(wn2.getNodeName()), ageRangeCauses); - final NodeList nl3 = wn2.getChildNodes(); - for (int k = 0; k < nl3.getLength(); k++) { - final Node wn3 = nl3.item(k); - if (wn3.getNodeType() != Node.ELEMENT_NODE) { - continue; - } - - try { - final PersonnelStatus status = PersonnelStatus.valueOf(wn3.getNodeName()); - if (status.isSuicide() && !isEnableRandomDeathSuicideCause()) { - continue; - } - ageRangeCauses.add(Double.parseDouble(wn3.getTextContent().trim()), status); - } catch (Exception ignored) { - - } - } - } catch (Exception ignored) { - - } - } - } catch (Exception ignored) { - - } - } - } - // endregion File I/O -} diff --git a/MekHQ/src/mekhq/campaign/personnel/death/DisabledRandomDeath.java b/MekHQ/src/mekhq/campaign/personnel/death/DisabledRandomDeath.java deleted file mode 100644 index aecedd9ead4..00000000000 --- a/MekHQ/src/mekhq/campaign/personnel/death/DisabledRandomDeath.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2020-2022 - 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 as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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. - * - * You should have received a copy of the GNU General Public License - * along with MekHQ. If not, see . - */ -package mekhq.campaign.personnel.death; - -import megamek.common.enums.Gender; -import mekhq.campaign.Campaign; -import mekhq.campaign.CampaignOptions; -import mekhq.campaign.personnel.enums.RandomDeathMethod; - -public class DisabledRandomDeath extends AbstractDeath { - //region Constructors - public DisabledRandomDeath(final CampaignOptions options, final boolean initializeCauses) { - super(RandomDeathMethod.NONE, options, initializeCauses); - } - //endregion Constructors - - @Override - public boolean randomlyDies(Campaign campaign, final int age, final Gender gender) { - return false; - } -} diff --git a/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java b/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java index a6975847913..70c8b7afa43 100644 --- a/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java +++ b/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java @@ -18,34 +18,203 @@ */ package mekhq.campaign.personnel.death; +import megamek.Version; +import megamek.common.annotations.Nullable; import megamek.common.enums.Gender; +import megamek.common.util.weightedMaps.WeightedDoubleMap; +import megamek.logging.MMLogger; +import mekhq.MHQConstants; +import mekhq.MekHQ; import mekhq.campaign.Campaign; import mekhq.campaign.CampaignOptions; -import mekhq.campaign.personnel.enums.RandomDeathMethod; +import mekhq.campaign.personnel.Person; +import mekhq.campaign.personnel.enums.AgeGroup; +import mekhq.campaign.personnel.enums.PersonnelStatus; +import mekhq.campaign.personnel.enums.TenYearAgeRange; +import mekhq.utilities.MHQInternationalization; +import mekhq.utilities.MHQXMLUtility; +import mekhq.utilities.ReportingUtilities; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.time.LocalDate; +import java.util.HashMap; +import java.util.Map; + +import static java.lang.Math.round; import static megamek.common.Compute.randomInt; +import static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG; + +public class RandomDeath { + private static final String RESOURCE_BUNDLE = "mekhq.resources.RandomDeath"; + private static final MMLogger logger = MMLogger.create(RandomDeath.class); + + private final Map enabledAgeGroups; + private final boolean enableRandomDeathSuicideCause; + private final Map>> causes; + private final int baseRandomDeathChance; + + public RandomDeath(final CampaignOptions campaignOptions) { + enabledAgeGroups = campaignOptions.getEnabledRandomDeathAgeGroups(); + enableRandomDeathSuicideCause = campaignOptions.isUseRandomDeathSuicideCause(); + baseRandomDeathChance = campaignOptions.getRandomDeathChance(); + causes = new HashMap<>(); + + initializeCauses(); + } + + public void initializeCauses() { + causes.clear(); + initializeCausesFromFile(new File(MHQConstants.RANDOM_DEATH_CAUSES_FILE_PATH)); + initializeCausesFromFile(new File(MHQConstants.USER_RANDOM_DEATH_CAUSES_FILE_PATH)); + } + + /** + * Initializes the random death causes by reading them from an XML file. + * + *

The XML file contains structured information about different causes of random death, + * organized by gender, age range, and personnel status. The method parses this file, processes + * the data, and populates the `causes` map accordingly.

+ * + * @param file The XML file containing the cause definitions. + */ + private void initializeCausesFromFile(final File file) { + if (!file.exists()) { + logger.warn("File does not exist: " + file.getPath()); + return; + } + + final Element rootElement = parseXmlFile(file); + if (rootElement == null) { + return; + } + + final Version version = new Version(rootElement.getAttribute("version")); + logger.info("Parsing Random Death Causes from " + version + "-origin XML"); + + final NodeList genderNodes = rootElement.getChildNodes(); + for (int i = 0; i < genderNodes.getLength(); i++) { + final Node genderNode = genderNodes.item(i); + if (isInvalidNode(genderNode)) { + continue; + } + + try { + parseGenderNode(genderNode); + } catch (Exception e) { + logger.error("Error parsing gender node: " + genderNode.getNodeName(), e); + } + } + } + + /** + * Parses the XML file into a DOM {@link Element}. + * + * @param file The input file. + * @return The root {@link Element} of the parsed XML document, or {@code null} if an error occurred. + */ + private Element parseXmlFile(final File file) { + try (InputStream is = new FileInputStream(file)) { + final Element element = MHQXMLUtility.newSafeDocumentBuilder() + .parse(is) + .getDocumentElement(); + element.normalize(); + return element; + } catch (Exception ex) { + logger.error("Failed to parse XML file: " + file.getPath(), ex); + return null; + } + } + + /** + * Processes a top-level gender node and parses its child nodes. + * + * @param genderNode The node corresponding to a gender. + */ + private void parseGenderNode(final Node genderNode) { + final Gender gender = Gender.valueOf(genderNode.getNodeName()); + causes.putIfAbsent(gender, new HashMap<>()); -public class RandomDeath extends AbstractDeath { - //region Variable Declarations - private double percentage; - //endregion Variable Declarations + final NodeList ageRangeNodes = genderNode.getChildNodes(); + for (int i = 0; i < ageRangeNodes.getLength(); i++) { + final Node ageRangeNode = ageRangeNodes.item(i); + if (isInvalidNode(ageRangeNode)) { + continue; + } - //region Constructors - public RandomDeath(final CampaignOptions options, final boolean initializeCauses) { - super(RandomDeathMethod.RANDOM, options, initializeCauses); - setPercentage(options.getRandomDeathChance()); + try { + parseAgeRangeNode(gender, ageRangeNode); + } catch (Exception e) { + logger.error("Error parsing age range node for gender: " + gender, e); + } + } } - //endregion Constructors - //region Getters/Setters - public double getPercentage() { - return percentage; + /** + * Processes an age range node and populates its causes. + * + * @param gender The gender associated with the node. + * @param ageRangeNode The node corresponding to an age range. + */ + private void parseAgeRangeNode(final Gender gender, final Node ageRangeNode) { + final TenYearAgeRange ageRange = TenYearAgeRange.valueOf(ageRangeNode.getNodeName()); + final WeightedDoubleMap ageRangeCauses = new WeightedDoubleMap<>(); + causes.get(gender).put(ageRange, ageRangeCauses); + + final NodeList statusNodes = ageRangeNode.getChildNodes(); + for (int i = 0; i < statusNodes.getLength(); i++) { + final Node statusNode = statusNodes.item(i); + if (!isElementNode(statusNode)) { + continue; + } + + try { + parseStatusNode(ageRangeCauses, statusNode); + } catch (Exception e) { + logger.error("Error parsing status node: " + statusNode.getNodeName(), e); + } + } } - public void setPercentage(final double percentage) { - this.percentage = percentage; + /** + * Processes a status node and updates the age range causes map. + * + * @param ageRangeCauses The map of causes for a particular age range. + * @param statusNode The node corresponding to a personnel status. + */ + private void parseStatusNode(final WeightedDoubleMap ageRangeCauses, final Node statusNode) { + final PersonnelStatus status = PersonnelStatus.valueOf(statusNode.getNodeName()); + if (status.isSuicide() && !enableRandomDeathSuicideCause) { + return; + } + + final double weight = Double.parseDouble(statusNode.getTextContent().trim()); + ageRangeCauses.add(weight, status); + } + + /** + * Validates an XML node to ensure it is meaningful (e.g., has child nodes). + * + * @param node The node to validate. + * @return {@code true} if the node is valid; {@code false} otherwise. + */ + private boolean isInvalidNode(final Node node) { + return node == null || !node.hasChildNodes(); + } + + /** + * Checks if a node is an XML element node. + * + * @param node The node to check. + * @return {@code true} if the node is an element node; {@code false} otherwise. + */ + private boolean isElementNode(final Node node) { + return node != null && node.getNodeType() == Node.ELEMENT_NODE; } - //endregion Getters/Setters /** * Determines if a person randomly dies based on the campaign, age, and gender. @@ -53,24 +222,141 @@ public void setPercentage(final double percentage) { *

The probability of death increases as a person's age exceeds a specific * threshold, with the chance of death growing exponentially for extra years lived.

* - * @param campaign The campaign that defines the base random death chance. * @param age The individual's age. * @param gender The individual's gender. Currently unused but supports future extensibility. * @return {@code true} if the person randomly dies; {@code false} otherwise. */ - @Override - public boolean randomlyDies(Campaign campaign, final int age, final Gender gender) { + public boolean randomlyDies(final int age, final Gender gender) { final int AGE_THRESHOLD = 90; final double REDUCTION_MULTIPLIER = 0.90; + final double FEMALE_MULTIPLIER = 1.1; + + int baseDieSize = baseRandomDeathChance; + + // If Random Death disabled? + if (baseDieSize == 0) { + return false; + } - int baseDieSize = campaign.getCampaignOptions().getRandomDeathChance(); + // Modifier for gender + if (gender == Gender.FEMALE) { + baseDieSize = (int) round(baseDieSize * FEMALE_MULTIPLIER); + } // Calculate adjusted die size if the age exceeds the threshold int adjustedDieSize = (age > AGE_THRESHOLD) - ? (int) Math.round(baseDieSize * Math.pow(REDUCTION_MULTIPLIER, (age - AGE_THRESHOLD))) + ? (int) round(baseDieSize * Math.pow(REDUCTION_MULTIPLIER, (age - AGE_THRESHOLD))) : baseDieSize; // Return random death outcome - return randomInt(adjustedDieSize) < getPercentage(); + return randomInt(adjustedDieSize) == 0; + } + + /** + * Determines if a person cannot die and returns a reason, if any. + * + *

Checks various conditions such as if the person is already dead, whether + * random death is enabled, if the person is immortal, or if the death for the + * provided age group is disabled. Returns {@code null} if none of these + * conditions prevent the person from dying.

+ * + * @param person The person to check. + * @param ageGroup The age group of the person. + * @param randomDeath Whether random death is enabled. + * @return A reason the person cannot die, or {@code null} if they can die. + */ + public @Nullable String canDie(final Person person, final AgeGroup ageGroup, final boolean randomDeath) { + if (person.getStatus().isDead()) { + return getCannotDieMessage("cannotDie.Dead.text"); + } + + if (randomDeath) { + if (person.isImmortal()) { + return getCannotDieMessage("cannotDie.Immortal.text"); + } + + if (!enabledAgeGroups.get(ageGroup)) { + return getCannotDieMessage("cannotDie.AgeGroupDisabled.text"); + } + } + + return null; + } + + /** + * Retrieves a localized message for why a person cannot die. + * + * @param messageKey The key for the message in the resource bundle. + * @return The localized reason message. + */ + private String getCannotDieMessage(final String messageKey) { + return MHQInternationalization.getFormattedTextAt(RESOURCE_BUNDLE, messageKey); + } + + + public boolean processNewWeek(final Campaign campaign, final LocalDate today, + final Person person) { + final int age = person.getAge(today); + final AgeGroup ageGroup = AgeGroup.determineAgeGroup(age); + + if (canDie(person, ageGroup, true) != null) { + return false; + } + + if (randomlyDies(age, person.getGender())) { + // We double-report here, to make sure the user definitely notices that a random death has occurred. + // Prior to this change, it was exceptionally easy to miss these events. + String color = MekHQ.getMHQOptions().getFontColorNegativeHexColor(); + String formatOpener = ReportingUtilities.spanOpeningWithCustomColor(color); + campaign.addReport(String.format("%s has %sdied%s.", + person.getHyperlinkedFullTitle(), formatOpener, CLOSING_SPAN_TAG)); + + person.changeStatus(campaign, today, getCause(person, ageGroup, age)); + + return true; + } else { + return false; + } + } + + public PersonnelStatus getCause(final Person person, final AgeGroup ageGroup, final int age) { + if (person.getStatus().isMIA()) { + return PersonnelStatus.KIA; + } else if (person.hasInjuries(false)) { + final PersonnelStatus status = determineIfInjuriesCausedTheDeath(person); + if (!status.isActive()) { + return status; + } + } + + if (person.isPregnant()) { + return PersonnelStatus.PREGNANCY_COMPLICATIONS; + } + + final Map> genderedCauses = causes + .get(person.getGender()); + if (genderedCauses == null) { + return getDefaultCause(ageGroup); + } + + final WeightedDoubleMap ageRangeCauses = genderedCauses + .get(TenYearAgeRange.determineAgeRange(age)); + if (ageRangeCauses == null) { + return getDefaultCause(ageGroup); + } + + final PersonnelStatus cause = ageRangeCauses.randomItem(); + return (cause == null) ? getDefaultCause(ageGroup) : cause; + } + + private PersonnelStatus determineIfInjuriesCausedTheDeath(final Person person) { + // We care about injuries that are major or deadly. We do not want any chronic conditions nor scratches + return person.getInjuries().stream().anyMatch(injury -> injury.getLevel().isMajorOrDeadly()) + ? PersonnelStatus.WOUNDS + : PersonnelStatus.ACTIVE; + } + + private PersonnelStatus getDefaultCause(final AgeGroup ageGroup) { + return ageGroup.isElder() ? PersonnelStatus.OLD_AGE : PersonnelStatus.NATURAL_CAUSES; } } diff --git a/MekHQ/src/mekhq/campaign/personnel/enums/RandomDeathMethod.java b/MekHQ/src/mekhq/campaign/personnel/enums/RandomDeathMethod.java deleted file mode 100644 index e69c89665cf..00000000000 --- a/MekHQ/src/mekhq/campaign/personnel/enums/RandomDeathMethod.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (c) 2020-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 as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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. - * - * You should have received a copy of the GNU General Public License - * along with MekHQ. If not, see . - */ -package mekhq.campaign.personnel.enums; - -import mekhq.MekHQ; -import mekhq.campaign.CampaignOptions; -import mekhq.campaign.personnel.death.AbstractDeath; -import mekhq.campaign.personnel.death.DisabledRandomDeath; -import mekhq.campaign.personnel.death.RandomDeath; - -import java.util.ResourceBundle; - -public enum RandomDeathMethod { - //region Enum Declarations - NONE("RandomDeathMethod.NONE.text", "RandomDeathMethod.NONE.toolTipText"), - RANDOM("RandomDeathMethod.RANDOM.text", "RandomDeathMethod.RANDOM.toolTipText"); - //endregion Enum Declarations - - //region Variable Declarations - private final String name; - private final String toolTipText; - //endregion Variable Declarations - - //region Constructors - RandomDeathMethod(final String name, final String toolTipText) { - final ResourceBundle resources = ResourceBundle.getBundle("mekhq.resources.Personnel", - MekHQ.getMHQOptions().getLocale()); - this.name = resources.getString(name); - this.toolTipText = resources.getString(toolTipText); - } - //endregion Constructors - - //region Getters - public String getToolTipText() { - return toolTipText; - } - //endregion Getters - - //region Boolean Comparison Methods - public boolean isNone() { - return this == NONE; - } - - public boolean isRandom() { - return this == RANDOM; - } - //endregion Boolean Comparison Methods - - public AbstractDeath getMethod(final CampaignOptions options) { - return getMethod(options, true); - } - - public AbstractDeath getMethod(final CampaignOptions options, final boolean initializeCauses) { - return switch (this) { - case RANDOM -> new RandomDeath(options, initializeCauses); - case NONE -> new DisabledRandomDeath(options, initializeCauses); - }; - } - - @Override - public String toString() { - return name; - } -} diff --git a/MekHQ/src/mekhq/gui/CampaignGUI.java b/MekHQ/src/mekhq/gui/CampaignGUI.java index 7c54a9d9c46..83047c31d53 100644 --- a/MekHQ/src/mekhq/gui/CampaignGUI.java +++ b/MekHQ/src/mekhq/gui/CampaignGUI.java @@ -54,10 +54,11 @@ import mekhq.campaign.personnel.Person; import mekhq.campaign.personnel.SkillType; import mekhq.campaign.personnel.autoAwards.AutoAwardsController; -import mekhq.campaign.personnel.death.AbstractDeath; -import mekhq.campaign.personnel.death.RandomDeath; import mekhq.campaign.personnel.divorce.RandomDivorce; -import mekhq.campaign.personnel.enums.*; +import mekhq.campaign.personnel.enums.PersonnelRole; +import mekhq.campaign.personnel.enums.RandomDivorceMethod; +import mekhq.campaign.personnel.enums.RandomMarriageMethod; +import mekhq.campaign.personnel.enums.RandomProcreationMethod; import mekhq.campaign.personnel.marriage.RandomMarriage; import mekhq.campaign.personnel.procreation.AbstractProcreation; import mekhq.campaign.personnel.procreation.RandomProcreation; @@ -583,16 +584,6 @@ private void initMenu() { miRefreshRanks.addActionListener(evt -> Ranks.reinitializeRankSystems(getCampaign())); menuRefresh.add(miRefreshRanks); - JMenuItem miRefreshRandomDeathCauses = new JMenuItem(resourceMap.getString("miRefreshRandomDeathCauses.text")); - miRefreshRandomDeathCauses.setToolTipText(resourceMap.getString("miRefreshRandomDeathCauses.toolTipText")); - miRefreshRandomDeathCauses.setName("miRefreshRandomDeathCauses"); - miRefreshRandomDeathCauses.setMnemonic(KeyEvent.VK_D); - miRefreshRandomDeathCauses.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_D, InputEvent.ALT_DOWN_MASK)); - miRefreshRandomDeathCauses.addActionListener(evt -> getCampaign().setDeath( - getCampaign().getCampaignOptions().getRandomDeathMethod() - .getMethod(getCampaign().getCampaignOptions()))); - menuRefresh.add(miRefreshRandomDeathCauses); - JMenuItem miRefreshFinancialInstitutions = new JMenuItem( resourceMap.getString("miRefreshFinancialInstitutions.text")); miRefreshFinancialInstitutions @@ -1456,7 +1447,6 @@ private void menuOptionsActionPerformed(final ActionEvent evt) { boolean rankIn = oldOptions.isUseTimeInRank(); boolean staticRATs = oldOptions.isUseStaticRATs(); boolean factionIntroDate = oldOptions.isFactionIntroDate(); - final RandomDeathMethod randomDeathMethod = oldOptions.getRandomDeathMethod(); final boolean useRandomDeathSuicideCause = oldOptions.isUseRandomDeathSuicideCause(); final RandomDivorceMethod randomDivorceMethod = oldOptions.getRandomDivorceMethod(); final RandomMarriageMethod randomMarriageMethod = oldOptions.getRandomMarriageMethod(); @@ -1487,14 +1477,6 @@ private void menuOptionsActionPerformed(final ActionEvent evt) { } } - AbstractDeath death = getCampaign().getDeath(); - if ((randomDeathMethod != newOptions.getRandomDeathMethod()) - || (useRandomDeathSuicideCause != newOptions.isUseRandomDeathSuicideCause())) { - getCampaign().setDeath(newOptions.getRandomDeathMethod().getMethod(newOptions)); - } else if (death instanceof RandomDeath) { - ((RandomDeath) death).setPercentage(newOptions.getRandomDeathChance()); - } - if (randomDivorceMethod != newOptions.getRandomDivorceMethod()) { getCampaign().setDivorce(newOptions.getRandomDivorceMethod().getMethod(newOptions)); } else { diff --git a/MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java b/MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java index cbe371ec561..11b810f72cb 100644 --- a/MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java +++ b/MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java @@ -2769,9 +2769,8 @@ protected Optional createPopupMenu() { menu.add(cbMenuItem); } - if (!gui.getCampaign().getCampaignOptions().getRandomDeathMethod().isNone() - && Stream.of(selected).noneMatch(p -> p.getStatus().isDead()) - && Stream.of(selected).allMatch(p -> p.isImmortal() == person.isImmortal())) { + if (Stream.of(selected).noneMatch(p -> p.getStatus().isDead()) + && Stream.of(selected).allMatch(p -> p.isImmortal() == person.isImmortal())) { cbMenuItem = new JCheckBoxMenuItem(resources.getString("miImmortal.text")); cbMenuItem.setToolTipText(resources.getString("miImmortal.toolTipText")); cbMenuItem.setName("miImmortal"); diff --git a/MekHQ/src/mekhq/gui/campaignOptions/contents/BiographyTab.java b/MekHQ/src/mekhq/gui/campaignOptions/contents/BiographyTab.java index fc47045b218..8d585d8fc7c 100644 --- a/MekHQ/src/mekhq/gui/campaignOptions/contents/BiographyTab.java +++ b/MekHQ/src/mekhq/gui/campaignOptions/contents/BiographyTab.java @@ -29,7 +29,6 @@ import mekhq.campaign.personnel.enums.AgeGroup; import mekhq.campaign.personnel.enums.FamilialRelationshipDisplayLevel; import mekhq.campaign.personnel.enums.PersonnelRole; -import mekhq.campaign.personnel.enums.RandomDeathMethod; import mekhq.campaign.personnel.ranks.RankSystem; import mekhq.campaign.universe.Faction; import mekhq.campaign.universe.Planet; @@ -108,8 +107,6 @@ public class BiographyTab { //end Backgrounds Tab //start Death Tab - private JLabel lblRandomDeathMethod; - private MMComboBox comboRandomDeathMethod; private JCheckBox chkUseRandomDeathSuicideCause; private JLabel lblRandomDeathChance; private JSpinner spnRandomDeathChance; @@ -277,8 +274,6 @@ private void initializeEducationTab() { * */ private void initializeDeathTab() { - lblRandomDeathMethod = new JLabel(); - comboRandomDeathMethod = new MMComboBox<>("comboRandomDeathMethod", RandomDeathMethod.values()); chkUseRandomDeathSuicideCause = new JCheckBox(); lblRandomDeathChance = new JLabel(); spnRandomDeathChance = new JSpinner(); @@ -749,25 +744,12 @@ public JPanel createDeathTab() { getImageDirectory() + "logo_clan_fire_mandrills.png"); // Contents - lblRandomDeathMethod = new CampaignOptionsLabel("RandomDeathMethod"); - comboRandomDeathMethod.setRenderer(new DefaultListCellRenderer() { - @Override - public Component getListCellRendererComponent(final JList list, final Object value, - final int index, final boolean isSelected, - final boolean cellHasFocus) { - super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - if (value instanceof RandomDeathMethod) { - list.setToolTipText(((RandomDeathMethod) value).getToolTipText()); - } - return this; - } - }); - chkUseRandomDeathSuicideCause = new CampaignOptionsCheckBox("UseRandomDeathSuicideCause"); - lblRandomDeathChance = new CampaignOptionsLabel("RandomDeathChance"); spnRandomDeathChance = new CampaignOptionsSpinner("RandomDeathChance", 6000, 1, 10000, 1); + chkUseRandomDeathSuicideCause = new CampaignOptionsCheckBox("UseRandomDeathSuicideCause"); + pnlDeathAgeGroup = createDeathAgeGroupsPanel(); // Layout the Panel @@ -777,18 +759,15 @@ public Component getListCellRendererComponent(final JList list, final Object layoutLeft.gridy = 0; layoutLeft.gridx = 0; layoutLeft.gridwidth = 1; - panelLeft.add(lblRandomDeathMethod, layoutLeft); + panelLeft.add(lblRandomDeathChance, layoutLeft); layoutLeft.gridx++; - panelLeft.add(comboRandomDeathMethod, layoutLeft); + panelLeft.add(spnRandomDeathChance, layoutLeft); layoutLeft.gridx = 0; layoutLeft.gridy++; panelLeft.add(chkUseRandomDeathSuicideCause, layoutLeft); layoutLeft.gridy++; - panelLeft.add(lblRandomDeathChance, layoutLeft); - layoutLeft.gridx++; - panelLeft.add(spnRandomDeathChance, layoutLeft); final JPanel panelParent = new CampaignOptionsStandardPanel("DeathTab", true); final GridBagConstraints layoutParent = new CampaignOptionsGridBagConstraints(panelParent); @@ -1317,7 +1296,6 @@ public void loadValuesFromCampaignOptions(@Nullable CampaignOptions presetCampai chkExtraRandomOrigin.setSelected(originOptions.isExtraRandomOrigin()); // Death - comboRandomDeathMethod.setSelectedItem(options.getRandomDeathMethod()); chkUseRandomDeathSuicideCause.setSelected(options.isUseRandomDeathSuicideCause()); spnRandomDeathChance.setValue(options.getRandomDeathChance()); @@ -1407,7 +1385,6 @@ public void applyCampaignOptionsToCampaign(@Nullable CampaignOptions presetCampa options.setRandomOriginOptions(originOptions); // Death - options.setRandomDeathMethod(comboRandomDeathMethod.getSelectedItem()); options.setUseRandomDeathSuicideCause(chkUseRandomDeathSuicideCause.isSelected()); options.setRandomDeathChance((int) spnRandomDeathChance.getValue()); for (final AgeGroup ageGroup : AgeGroup.values()) { diff --git a/MekHQ/unittests/mekhq/campaign/personnel/death/AbstractDeathTest.java b/MekHQ/unittests/mekhq/campaign/personnel/death/AbstractDeathTest.java deleted file mode 100644 index 4a956af05ac..00000000000 --- a/MekHQ/unittests/mekhq/campaign/personnel/death/AbstractDeathTest.java +++ /dev/null @@ -1,245 +0,0 @@ -/* - * Copyright (c) 2022 - 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 as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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. - * - * You should have received a copy of the GNU General Public License - * along with MekHQ. If not, see . - */ -package mekhq.campaign.personnel.death; - -import mekhq.campaign.Campaign; -import mekhq.campaign.CampaignOptions; -import mekhq.campaign.personnel.Person; -import mekhq.campaign.personnel.enums.AgeGroup; -import mekhq.campaign.personnel.enums.PersonnelStatus; -import mekhq.campaign.personnel.enums.PrisonerStatus; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.MockedStatic; -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.time.LocalDate; -import java.util.HashMap; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.*; - -/** - * Personnel Testing Tracker: - * 1) Death: - * a) AbstractDeath - * 2) Divorce: - * a) AbstractDivorce - * 3) Enums: - * a) Profession - * Unhandled: - * 1) Generator: All - * 2) Ranks: All - * 3) General: All - * - * Other Testing Tracker: - * 1) GUI Enums: Most are partially tested currently - * 2) Universe Enums: Most are unimplemented currently - */ -@ExtendWith(value = MockitoExtension.class) -public class AbstractDeathTest { -// Saved for Future Test Usage -/* - @Test - public void testIs() { - for (final FamilialRelationshipType familialRelationshipType : types) { - if (familialRelationshipType == FamilialRelationshipType.NONE) { - assertTrue(familialRelationshipType.isNone()); - } else { - assertFalse(familialRelationshipType.isNone()); - } - } - } - */ - - @Mock - private Campaign mockCampaign; - - @Mock - private CampaignOptions mockCampaignOptions; - - @Mock - private AbstractDeath mockDeath; - - @BeforeEach - public void beforeEach() { - lenient().when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions); - } - - //region Constructors - @Disabled // FIXME : Windchild : Test Missing - @Test - public void testConstructorInitializesCauses() { - - } - //endregion Constructors - - //region Getters/Setters - @Disabled // FIXME : Windchild : Test Missing - @Test - public void testGettersAndSetters() { -/* - when(mockCampaignOptions.isUseClannerDeath()).thenReturn(false); - when(mockCampaignOptions.isUsePrisonerDeath()).thenReturn(false); - when(mockCampaignOptions.isUseRandomSameSexDeath()).thenReturn(false); - when(mockCampaignOptions.isUseRandomClannerDeath()).thenReturn(false); - when(mockCampaignOptions.isUseRandomPrisonerDeath()).thenReturn(false); - - final AbstractDeath disabledDeath = new DisabledRandomDeath(mockCampaignOptions); - - assertEquals(RandomDeathMethod.NONE, disabledDeath.getMethod()); - assertFalse(disabledDeath.isUseClannerDeath()); - assertFalse(disabledDeath.isUsePrisonerDeath()); - assertFalse(disabledDeath.isUseRandomSameSexDeath()); - assertFalse(disabledDeath.isUseRandomClannerDeath()); - assertFalse(disabledDeath.isUseRandomPrisonerDeath()); -*/ - } - //endregion Getters/Setters - - @Test - public void testCanDie() { - doCallRealMethod().when(mockDeath).canDie(any(), any(), anyBoolean()); - - final Map enabledAgeGroups = new HashMap<>(); - enabledAgeGroups.put(AgeGroup.CHILD, false); - enabledAgeGroups.put(AgeGroup.ADULT, true); - when(mockDeath.getEnabledAgeGroups()).thenReturn(enabledAgeGroups); - - final Person mockPerson = mock(Person.class); - - // Can be retired - when(mockPerson.getStatus()).thenReturn(PersonnelStatus.RETIRED); - assertNull(mockDeath.canDie(mockPerson, AgeGroup.ADULT, false)); - - // Can't be dead - when(mockPerson.getStatus()).thenReturn(PersonnelStatus.KIA); - assertNotNull(mockDeath.canDie(mockPerson, AgeGroup.ADULT, false)); - - // Can't randomly die if immortal - when(mockPerson.getStatus()).thenReturn(PersonnelStatus.ACTIVE); - when(mockPerson.isImmortal()).thenReturn(true); - assertNotNull(mockDeath.canDie(mockPerson, AgeGroup.ADULT, true)); - - // Age Group must be enabled - when(mockPerson.isImmortal()).thenReturn(false); - assertNotNull(mockDeath.canDie(mockPerson, AgeGroup.CHILD, true)); - - // Can't be Clan Personnel with Random Clan Death Disabled - when(mockPerson.isClanPersonnel()).thenReturn(true); - when(mockDeath.isUseRandomClanPersonnelDeath()).thenReturn(false); - when(mockDeath.isUseRandomPrisonerDeath()).thenReturn(true); - assertNotNull(mockDeath.canDie(mockPerson, AgeGroup.ADULT, true)); - - // Can be Non-Clan Personnel with Random Clan Death Disabled - when(mockPerson.isClanPersonnel()).thenReturn(false); - assertNull(mockDeath.canDie(mockPerson, AgeGroup.ADULT, true)); - - // Can be a Non-Prisoner with Random Prisoner Death Disabled - when(mockPerson.getPrisonerStatus()).thenReturn(PrisonerStatus.FREE); - when(mockDeath.isUseRandomPrisonerDeath()).thenReturn(false); - assertNull(mockDeath.canDie(mockPerson, AgeGroup.ADULT, true)); - - // Can't be a Prisoner with Random Prisoner Death Disabled - when(mockPerson.getPrisonerStatus()).thenReturn(PrisonerStatus.PRISONER); - assertNotNull(mockDeath.canDie(mockPerson, AgeGroup.ADULT, true)); - - // Can be a Clan Prisoner with Random Clan and Random Prisoner Death Enabled - lenient().when(mockPerson.isClanPersonnel()).thenReturn(true); - when(mockDeath.isUseRandomClanPersonnelDeath()).thenReturn(true); - when(mockDeath.isUseRandomPrisonerDeath()).thenReturn(true); - assertNull(mockDeath.canDie(mockPerson, AgeGroup.ADULT, true)); - } - - //region New Day - @Test - public void testProcessNewDay() { - doCallRealMethod().when(mockDeath).processNewDay(any(), any(), any()); - when(mockDeath.getCause(any(), any(), anyInt())).thenReturn(PersonnelStatus.DISEASE); - - final Person mockPerson = mock(Person.class); - doNothing().when(mockPerson).changeStatus(any(), any(), any()); - - try (MockedStatic ageGroup = Mockito.mockStatic(AgeGroup.class)) { - ageGroup.when(() -> AgeGroup.determineAgeGroup(anyInt())).thenReturn(AgeGroup.ADULT); - - // Can't be dead - when(mockDeath.canDie(any(), any(), anyBoolean())).thenReturn("Dead"); - assertFalse(mockDeath.processNewDay(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson)); - - // Randomly Dies - Change Status Works Properly - when(mockDeath.canDie(any(), any(), anyBoolean())).thenReturn(null); - when(mockDeath.randomlyDies(anyInt(), any())).thenReturn(true); - when(mockPerson.getStatus()).thenReturn(PersonnelStatus.DISEASE); - assertTrue(mockDeath.processNewDay(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson)); - - // Randomly Dies - Issue Changing Status - when(mockPerson.getStatus()).thenReturn(PersonnelStatus.ACTIVE); - assertFalse(mockDeath.processNewDay(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson)); - - // Doesn't Randomly Die - when(mockDeath.randomlyDies(anyInt(), any())).thenReturn(false); - assertFalse(mockDeath.processNewDay(mockCampaign, LocalDate.ofYearDay(3025, 1), mockPerson)); - } - } - //endregion New Day - - //region Cause - @Disabled // FIXME : Windchild : Test Missing - @Test - public void testGetCause() { - - } - - @Disabled // FIXME : Windchild : Test Missing - @Test - public void testGetDefaultCause() { - - } - - @Disabled // FIXME : Windchild : Test Missing - @Test - public void testDetermineIfInjuriesCausedTheDeath() { - - } - //endregion Cause - - //region File I/O - @Disabled // FIXME : Windchild : Test Missing - @Test - public void testInitializeCauses() { - - } - - @Disabled // FIXME : Windchild : Test Missing - @Test - public void testInitializeCausesFromFile() { - - } - //endregion File I/O -} diff --git a/MekHQ/unittests/mekhq/campaign/personnel/death/DisabledRandomDeathTest.java b/MekHQ/unittests/mekhq/campaign/personnel/death/DisabledRandomDeathTest.java deleted file mode 100644 index 5409be458ca..00000000000 --- a/MekHQ/unittests/mekhq/campaign/personnel/death/DisabledRandomDeathTest.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2022-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 as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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. - * - * You should have received a copy of the GNU General Public License - * along with MekHQ. If not, see . - */ -package mekhq.campaign.personnel.death; - -import megamek.common.enums.Gender; -import mekhq.campaign.CampaignOptions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.HashMap; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.mockito.Mockito.when; - -@ExtendWith(value = MockitoExtension.class) -public class DisabledRandomDeathTest { - @Mock - private CampaignOptions mockOptions; - - @BeforeEach - public void beforeEach() { - when(mockOptions.getEnabledRandomDeathAgeGroups()).thenReturn(new HashMap<>()); - when(mockOptions.isUseRandomDeathSuicideCause()).thenReturn(false); - } - - @Test - public void testRandomlyDies() { - assertFalse(new DisabledRandomDeath(mockOptions, false).randomlyDies(0, Gender.MALE)); - } -} From 971c9e2b3d51dd1f32e11521b1a02ffa65c70df0 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sat, 1 Feb 2025 16:14:49 -0600 Subject: [PATCH 042/112] Remove RandomDeathMethodTest class Deleted the RandomDeathMethodTest class as it is no longer needed. The functionality tested by this class is either redundant or already covered in other areas of the codebase. This simplifies the code and reduces maintenance overhead. --- .../enums/RandomDeathMethodTest.java | 102 ------------------ 1 file changed, 102 deletions(-) delete mode 100644 MekHQ/unittests/mekhq/campaign/personnel/enums/RandomDeathMethodTest.java diff --git a/MekHQ/unittests/mekhq/campaign/personnel/enums/RandomDeathMethodTest.java b/MekHQ/unittests/mekhq/campaign/personnel/enums/RandomDeathMethodTest.java deleted file mode 100644 index 093e10ef032..00000000000 --- a/MekHQ/unittests/mekhq/campaign/personnel/enums/RandomDeathMethodTest.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (c) 2022-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 as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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. - * - * You should have received a copy of the GNU General Public License - * along with MekHQ. If not, see . - */ -package mekhq.campaign.personnel.enums; - -import mekhq.MekHQ; -import mekhq.campaign.CampaignOptions; -import mekhq.campaign.personnel.death.DisabledRandomDeath; -import mekhq.campaign.personnel.death.ExponentialRandomDeath; -import org.junit.jupiter.api.Test; - -import java.util.HashMap; -import java.util.Map; -import java.util.ResourceBundle; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class RandomDeathMethodTest { - //region Variable Declarations - private static final RandomDeathMethod[] methods = RandomDeathMethod.values(); - - private final transient ResourceBundle resources = ResourceBundle.getBundle("mekhq.resources.Personnel", - MekHQ.getMHQOptions().getLocale()); - //endregion Variable Declarations - - //region Getters - @Test - public void testGetToolTipText() { - assertEquals(resources.getString("RandomDeathMethod.NONE.toolTipText"), - RandomDeathMethod.NONE.getToolTipText()); - assertEquals(resources.getString("RandomDeathMethod.RANDOM.toolTipText"), - RandomDeathMethod.RANDOM.getToolTipText()); - } - //endregion Getters - - //region Boolean Comparison Methods - @Test - public void testIsNone() { - for (final RandomDeathMethod randomDeathMethod : methods) { - if (randomDeathMethod == RandomDeathMethod.NONE) { - assertTrue(randomDeathMethod.isNone()); - } else { - assertFalse(randomDeathMethod.isNone()); - } - } - } - - @Test - public void testIsDiceRoll() { - for (final RandomDeathMethod randomDeathMethod : methods) { - if (randomDeathMethod == RandomDeathMethod.RANDOM) { - assertTrue(randomDeathMethod.isRandom()); - } else { - assertFalse(randomDeathMethod.isRandom()); - } - } - } - //endregion Boolean Comparison Methods - - @Test - public void testGetMethod() { - final CampaignOptions mockOptions = mock(CampaignOptions.class); - when(mockOptions.getEnabledRandomDeathAgeGroups()).thenReturn(new HashMap<>()); - when(mockOptions.isUseRandomDeathSuicideCause()).thenReturn(false); - - final Map ageRangeMap = new HashMap<>(); - for (final TenYearAgeRange range : TenYearAgeRange.values()) { - ageRangeMap.put(range, 1d); - } - - assertInstanceOf(DisabledRandomDeath.class, RandomDeathMethod.NONE.getMethod(mockOptions)); - assertInstanceOf(ExponentialRandomDeath.class, RandomDeathMethod.RANDOM.getMethod(mockOptions, false)); - } - - @Test - public void testToStringOverride() { - assertEquals(resources.getString("RandomDeathMethod.NONE.text"), - RandomDeathMethod.NONE.toString()); - assertEquals(resources.getString("RandomDeathMethod.RANDOM.text"), - RandomDeathMethod.RANDOM.toString()); - } -} From e715e451c7db8f9c4a2db564a75e37dc73d84a65 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sat, 1 Feb 2025 16:24:24 -0600 Subject: [PATCH 043/112] Refactor RandomDeath initialization in Campaign Moved RandomDeath initialization from processNewDayPersonnel to the Campaign constructor to streamline object management. Enhanced RandomDeath documentation, clarified method responsibilities, and aligned logging with modern format standards. --- MekHQ/src/mekhq/campaign/Campaign.java | 4 +- .../campaign/personnel/death/RandomDeath.java | 157 ++++++++++++++---- 2 files changed, 125 insertions(+), 36 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index 4f42f8a7c18..e0ca1059b4f 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -275,6 +275,7 @@ public class Campaign implements ITechManager { private AbstractContractMarket contractMarket; private AbstractUnitMarket unitMarket; + private RandomDeath randomDeath; private transient AbstractDivorce divorce; private transient AbstractMarriage marriage; private transient AbstractProcreation procreation; @@ -376,6 +377,7 @@ public Campaign() { setPersonnelMarket(new PersonnelMarket()); setContractMarket(new AtbMonthlyContractMarket()); setUnitMarket(new DisabledUnitMarket()); + randomDeath = new RandomDeath(campaignOptions); setDivorce(new DisabledRandomDivorce(getCampaignOptions())); setMarriage(new DisabledRandomMarriage(getCampaignOptions())); setProcreation(new DisabledRandomProcreation(getCampaignOptions())); @@ -4344,8 +4346,6 @@ private void processResupply(AtBContract contract) { * @see #getPersonnelFilteringOutDeparted() Filters out departed personnel before daily processing */ public void processNewDayPersonnel() { - RandomDeath randomDeath = new RandomDeath(campaignOptions); - // This list ensures we don't hit a concurrent modification error List personnel = getPersonnelFilteringOutDeparted(); diff --git a/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java b/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java index 70c8b7afa43..c68e27a4a6f 100644 --- a/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java +++ b/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2022-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * @@ -49,6 +49,23 @@ import static megamek.common.Compute.randomInt; import static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG; +/** + * Handles logic for simulating random deaths in a campaign. + * + *

The {@code RandomDeath} class is responsible for determining whether a person dies randomly + * based on various factors such as age, gender, campaign settings, and defined death causes. + * It provides functionality to configure and process random deaths, manage XML-based cause sources, + * and track different categories of death causes.

+ * + *

Core Features:

+ *
    + *
  • Supports enabling/disabling random death categories by age group.
  • + *
  • Allows separate configuration for suicide-related deaths.
  • + *
  • Adjusts random death chances based on age, gender, and campaign-wide configurations.
  • + *
  • Parses random death causes from XML files, organized by gender and age range.
  • + *
  • Provides detailed reasons and causes of death using weighted probability.
  • + *
+ */ public class RandomDeath { private static final String RESOURCE_BUNDLE = "mekhq.resources.RandomDeath"; private static final MMLogger logger = MMLogger.create(RandomDeath.class); @@ -58,6 +75,15 @@ public class RandomDeath { private final Map>> causes; private final int baseRandomDeathChance; + /** + * Constructs a {@code RandomDeath} object using campaign-specific options. + * + *

Initializes configurable options such as enabling specific age groups for random deaths, + * enabling or disabling suicide causes, and retrieving the base random death chances. + * The death causes map is also initialized by reading relevant files.

+ * + * @param campaignOptions The campaign options containing random death configurations. + */ public RandomDeath(final CampaignOptions campaignOptions) { enabledAgeGroups = campaignOptions.getEnabledRandomDeathAgeGroups(); enableRandomDeathSuicideCause = campaignOptions.isUseRandomDeathSuicideCause(); @@ -67,6 +93,12 @@ public RandomDeath(final CampaignOptions campaignOptions) { initializeCauses(); } + /** + * Clears and reloads the random death causes from default and user-defined XML files. + * + *

Both the default XML file and the user-defined XML file are read and processed to populate + * the {@code causes} map.

+ */ public void initializeCauses() { causes.clear(); initializeCausesFromFile(new File(MHQConstants.RANDOM_DEATH_CAUSES_FILE_PATH)); @@ -76,15 +108,15 @@ public void initializeCauses() { /** * Initializes the random death causes by reading them from an XML file. * - *

The XML file contains structured information about different causes of random death, - * organized by gender, age range, and personnel status. The method parses this file, processes - * the data, and populates the `causes` map accordingly.

+ *

The XML file contains structured data about causes of random deaths, organized by + * gender, age range, and personnel statuses. The method parses the file and populates + * the {@code causes} map.

* * @param file The XML file containing the cause definitions. */ private void initializeCausesFromFile(final File file) { if (!file.exists()) { - logger.warn("File does not exist: " + file.getPath()); + logger.warn("File does not exist: {}", file.getPath()); return; } @@ -94,7 +126,7 @@ private void initializeCausesFromFile(final File file) { } final Version version = new Version(rootElement.getAttribute("version")); - logger.info("Parsing Random Death Causes from " + version + "-origin XML"); + logger.info("Parsing Random Death Causes from {}-origin XML", version); final NodeList genderNodes = rootElement.getChildNodes(); for (int i = 0; i < genderNodes.getLength(); i++) { @@ -106,13 +138,13 @@ private void initializeCausesFromFile(final File file) { try { parseGenderNode(genderNode); } catch (Exception e) { - logger.error("Error parsing gender node: " + genderNode.getNodeName(), e); + logger.error("Error parsing gender node: {} - {}", genderNode.getNodeName(), e); } } } /** - * Parses the XML file into a DOM {@link Element}. + * Parses the specified XML file into a DOM {@link Element}. * * @param file The input file. * @return The root {@link Element} of the parsed XML document, or {@code null} if an error occurred. @@ -125,15 +157,15 @@ private Element parseXmlFile(final File file) { element.normalize(); return element; } catch (Exception ex) { - logger.error("Failed to parse XML file: " + file.getPath(), ex); + logger.error("Failed to parse XML file: {} - {}", file.getPath(), ex); return null; } } /** - * Processes a top-level gender node and parses its child nodes. + * Processes a top-level gender node from the XML and parses its child nodes. * - * @param genderNode The node corresponding to a gender. + * @param genderNode The node representing a gender and its associated death causes. */ private void parseGenderNode(final Node genderNode) { final Gender gender = Gender.valueOf(genderNode.getNodeName()); @@ -155,10 +187,10 @@ private void parseGenderNode(final Node genderNode) { } /** - * Processes an age range node and populates its causes. + * Processes an age range node and populates its associated causes. * - * @param gender The gender associated with the node. - * @param ageRangeNode The node corresponding to an age range. + * @param gender The gender associated with the age range node. + * @param ageRangeNode The node representing an age range and its associated causes. */ private void parseAgeRangeNode(final Gender gender, final Node ageRangeNode) { final TenYearAgeRange ageRange = TenYearAgeRange.valueOf(ageRangeNode.getNodeName()); @@ -181,10 +213,14 @@ private void parseAgeRangeNode(final Gender gender, final Node ageRangeNode) { } /** - * Processes a status node and updates the age range causes map. + * Processes a status node and updates the details in the age range's causes map. + * + *

This method handles parsing of the text content (probability weight) and + * links it to the specified {@code PersonnelStatus}. Factors such as whether + * suicide causes are enabled are also considered.

* * @param ageRangeCauses The map of causes for a particular age range. - * @param statusNode The node corresponding to a personnel status. + * @param statusNode The node representing a specific personnel status. */ private void parseStatusNode(final WeightedDoubleMap ageRangeCauses, final Node statusNode) { final PersonnelStatus status = PersonnelStatus.valueOf(statusNode.getNodeName()); @@ -197,10 +233,11 @@ private void parseStatusNode(final WeightedDoubleMap ageRangeCa } /** - * Validates an XML node to ensure it is meaningful (e.g., has child nodes). + * Determines if an XML node is invalid for processing. * * @param node The node to validate. - * @return {@code true} if the node is valid; {@code false} otherwise. + * @return {@code true} if the node is invalid (e.g., null or without child nodes), + * {@code false} otherwise. */ private boolean isInvalidNode(final Node node) { return node == null || !node.hasChildNodes(); @@ -217,14 +254,18 @@ private boolean isElementNode(final Node node) { } /** - * Determines if a person randomly dies based on the campaign, age, and gender. + * Determines whether an individual dies randomly based on age, gender, and campaign configuration. * - *

The probability of death increases as a person's age exceeds a specific - * threshold, with the chance of death growing exponentially for extra years lived.

+ *

The chance of random death is influenced by:

+ *
    + *
  • Age: The risk increases exponentially after a certain threshold.
  • + *
  • Gender: Gender-based multipliers affect the base death chance.
  • + *
  • Campaign settings: The base random death chance is configured globally.
  • + *
* - * @param age The individual's age. - * @param gender The individual's gender. Currently unused but supports future extensibility. - * @return {@code true} if the person randomly dies; {@code false} otherwise. + * @param age The age of the individual. + * @param gender The gender of the individual. + * @return {@code true} if the individual dies randomly; {@code false} otherwise. */ public boolean randomlyDies(final int age, final Gender gender) { final int AGE_THRESHOLD = 90; @@ -253,17 +294,20 @@ public boolean randomlyDies(final int age, final Gender gender) { } /** - * Determines if a person cannot die and returns a reason, if any. + * Checks whether a person is exempt from random death and provides a reason if applicable. * - *

Checks various conditions such as if the person is already dead, whether - * random death is enabled, if the person is immortal, or if the death for the - * provided age group is disabled. Returns {@code null} if none of these - * conditions prevent the person from dying.

+ *

The following conditions are evaluated:

+ *
    + *
  • If the person is dead: Returns a reason indicating they are already dead.
  • + *
  • If random death is enabled and the person is immortal: Returns the immortality reason.
  • + *
  • If the person's age group is disabled for random deaths: Returns the reason for the + * age group being excluded.
  • + *
* - * @param person The person to check. - * @param ageGroup The age group of the person. - * @param randomDeath Whether random death is enabled. - * @return A reason the person cannot die, or {@code null} if they can die. + * @param person The individual to evaluate. + * @param ageGroup The person's age group. + * @param randomDeath Whether random deaths are enabled in the campaign. + * @return A string describing why the individual cannot die, or {@code null} if no restrictions apply. */ public @Nullable String canDie(final Person person, final AgeGroup ageGroup, final boolean randomDeath) { if (person.getStatus().isDead()) { @@ -293,7 +337,18 @@ private String getCannotDieMessage(final String messageKey) { return MHQInternationalization.getFormattedTextAt(RESOURCE_BUNDLE, messageKey); } - + /** + * Processes random death checks for the given individual in a weekly tick. + * + *

If the person dies, this method updates the campaign and individual status accordingly, + * and generates a detailed death report. Random death reasons and causes are evaluated as per + * the configuration and individual factors.

+ * + * @param campaign The active campaign to update. + * @param today The current date. + * @param person The person being evaluated. + * @return {@code true} if the person dies during this week; otherwise, {@code false}. + */ public boolean processNewWeek(final Campaign campaign, final LocalDate today, final Person person) { final int age = person.getAge(today); @@ -319,6 +374,18 @@ public boolean processNewWeek(final Campaign campaign, final LocalDate today, } } + /** + * Determines the reason or cause of death for a person. + * + *

Factors including age, gender, injuries, and other conditions like pregnancy + * are considered in determining the death cause. If no specific cause is found, + * a default cause is selected.

+ * + * @param person The person who has died. + * @param ageGroup The age group of the person. + * @param age The person's age. + * @return The {@code PersonnelStatus} representing the cause of death. + */ public PersonnelStatus getCause(final Person person, final AgeGroup ageGroup, final int age) { if (person.getStatus().isMIA()) { return PersonnelStatus.KIA; @@ -349,6 +416,17 @@ public PersonnelStatus getCause(final Person person, final AgeGroup ageGroup, fi return (cause == null) ? getDefaultCause(ageGroup) : cause; } + /** + * Determines whether a person's death was caused by major or deadly injuries. + * + *

This method evaluates the person's injuries and checks if any of them are classified + * as "major or deadly." Only significant injuries are considered for this determination, + * while minor or chronic conditions are ignored.

+ * + * @param person The person whose injuries are being evaluated. + * @return {@link PersonnelStatus#WOUNDS} if major or deadly injuries caused the death; + * otherwise, {@link PersonnelStatus#ACTIVE} if no significant injuries are found. + */ private PersonnelStatus determineIfInjuriesCausedTheDeath(final Person person) { // We care about injuries that are major or deadly. We do not want any chronic conditions nor scratches return person.getInjuries().stream().anyMatch(injury -> injury.getLevel().isMajorOrDeadly()) @@ -356,6 +434,17 @@ private PersonnelStatus determineIfInjuriesCausedTheDeath(final Person person) { : PersonnelStatus.ACTIVE; } + /** + * Determines the default cause of death based on the age group of the person. + * + *

The method assigns a default cause of death based on whether the person is considered + * elderly. Elderly persons are assigned {@link PersonnelStatus#OLD_AGE} as the cause, + * while younger individuals are assigned {@link PersonnelStatus#NATURAL_CAUSES}.

+ * + * @param ageGroup The age group of the person. + * @return {@link PersonnelStatus#OLD_AGE} if the person is in the elder age group; + * otherwise, {@link PersonnelStatus#NATURAL_CAUSES}. + */ private PersonnelStatus getDefaultCause(final AgeGroup ageGroup) { return ageGroup.isElder() ? PersonnelStatus.OLD_AGE : PersonnelStatus.NATURAL_CAUSES; } From 85e75ba6ddb4dbd22b81cc1484f52e7b9e16905a Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sat, 1 Feb 2025 16:37:24 -0600 Subject: [PATCH 044/112] Refactored RandomDeath and added unit tests. Replaced direct method call with a static import for consistency and improved readability. Added comprehensive unit tests for the RandomDeath class to validate death-related logic, including various scenarios such as age, gender, and campaign settings. --- .../campaign/personnel/death/RandomDeath.java | 4 +- .../personnel/death/RandomDeathTest.java | 205 ++++++++++++++++++ 2 files changed, 207 insertions(+), 2 deletions(-) create mode 100644 MekHQ/unittests/mekhq/campaign/personnel/death/RandomDeathTest.java diff --git a/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java b/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java index c68e27a4a6f..71134a158bb 100644 --- a/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java +++ b/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java @@ -31,7 +31,6 @@ import mekhq.campaign.personnel.enums.AgeGroup; import mekhq.campaign.personnel.enums.PersonnelStatus; import mekhq.campaign.personnel.enums.TenYearAgeRange; -import mekhq.utilities.MHQInternationalization; import mekhq.utilities.MHQXMLUtility; import mekhq.utilities.ReportingUtilities; import org.w3c.dom.Element; @@ -47,6 +46,7 @@ import static java.lang.Math.round; import static megamek.common.Compute.randomInt; +import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; import static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG; /** @@ -334,7 +334,7 @@ public boolean randomlyDies(final int age, final Gender gender) { * @return The localized reason message. */ private String getCannotDieMessage(final String messageKey) { - return MHQInternationalization.getFormattedTextAt(RESOURCE_BUNDLE, messageKey); + return getFormattedTextAt(RESOURCE_BUNDLE, messageKey); } /** diff --git a/MekHQ/unittests/mekhq/campaign/personnel/death/RandomDeathTest.java b/MekHQ/unittests/mekhq/campaign/personnel/death/RandomDeathTest.java new file mode 100644 index 00000000000..59a87c9e111 --- /dev/null +++ b/MekHQ/unittests/mekhq/campaign/personnel/death/RandomDeathTest.java @@ -0,0 +1,205 @@ +package mekhq.campaign.personnel.death; + +import megamek.common.enums.Gender; +import mekhq.campaign.Campaign; +import mekhq.campaign.CampaignOptions; +import mekhq.campaign.personnel.Person; +import mekhq.campaign.personnel.enums.AgeGroup; +import mekhq.campaign.personnel.enums.PersonnelStatus; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.util.Map; + +import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; +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.*; + +/** + * Unit tests for the {@link RandomDeath} class. + * + *

This class contains a suite of tests that validate the functionality of various methods + * in the {@code RandomDeath} class, including determining if a person can die, simulating + * random deaths, processing weekly death events, and evaluating campaign-specific configurations.

+ * + *

These tests use mocked dependencies, such as {@link Person}, {@link Campaign}, and + * {@link CampaignOptions}, to isolate the functionality of individual methods and ensure + * accurate testing without requiring an entire campaign environment.

+ * + *

Key Testing Scenarios:

+ *
    + *
  • Validating random death behavior based on age, gender, and configurations.
  • + *
  • Ensuring the correct reasons are returned when a person cannot die.
  • + *
  • Simulating weekly death processing and ensuring outcomes are consistent.
  • + *
  • Validating edge cases, such as no deaths when chances are zero or disabled configurations.
  • + *
+ */ +public class RandomDeathTest { + private static final String RESOURCE_BUNDLE = "mekhq.resources.RandomDeath"; + + @Test + public void testCanDie_PersonAlreadyDead() { + Person mockedPerson = mock(Person.class); + when(mockedPerson.getStatus()).thenReturn(PersonnelStatus.KIA); + + CampaignOptions mockedOptions = mock(CampaignOptions.class); + RandomDeath randomDeath = new RandomDeath(mockedOptions); + + String result = randomDeath.canDie(mockedPerson, AgeGroup.ELDER, true); + + assertEquals(getFormattedTextAt(RESOURCE_BUNDLE, "cannotDie.Dead.text"), result); + } + + @Test + public void testCanDie_PersonImmortal() { + Person mockedPerson = mock(Person.class); + when(mockedPerson.getStatus()).thenReturn(PersonnelStatus.ACTIVE); + when(mockedPerson.isImmortal()).thenReturn(true); + + CampaignOptions mockedOptions = mock(CampaignOptions.class); + RandomDeath randomDeath = new RandomDeath(mockedOptions); + + String result = randomDeath.canDie(mockedPerson, AgeGroup.ADULT, true); + + assertEquals(getFormattedTextAt(RESOURCE_BUNDLE, "cannotDie.Immortal.text"), result); + } + + @Test + public void testCanDie_AgeGroupDisabled() { + Person mockedPerson = mock(Person.class); + when(mockedPerson.getStatus()).thenReturn(PersonnelStatus.ACTIVE); + when(mockedPerson.isImmortal()).thenReturn(false); + + CampaignOptions mockedOptions = mock(CampaignOptions.class); + Map ageGroupMap = Map.of(AgeGroup.ADULT, false); + when(mockedOptions.getEnabledRandomDeathAgeGroups()).thenReturn(ageGroupMap); + + RandomDeath randomDeath = new RandomDeath(mockedOptions); + + String result = randomDeath.canDie(mockedPerson, AgeGroup.ADULT, true); + + assertEquals(getFormattedTextAt(RESOURCE_BUNDLE, "cannotDie.AgeGroupDisabled.text"), result); + } + + @Test + public void testCanDie_RandomDeathFalse() { + Person mockedPerson = mock(Person.class); + when(mockedPerson.getStatus()).thenReturn(PersonnelStatus.ACTIVE); + + CampaignOptions mockedOptions = mock(CampaignOptions.class); + RandomDeath randomDeath = new RandomDeath(mockedOptions); + + String result = randomDeath.canDie(mockedPerson, AgeGroup.ADULT, false); + + assertNull(result); + } + + @Test + public void testCanDie_CanDieNullMessage() { + Person mockedPerson = mock(Person.class); + when(mockedPerson.getStatus()).thenReturn(PersonnelStatus.ACTIVE); + when(mockedPerson.isImmortal()).thenReturn(false); + + CampaignOptions mockedOptions = mock(CampaignOptions.class); + Map ageGroupMap = Map.of(AgeGroup.ELDER, true); + when(mockedOptions.getEnabledRandomDeathAgeGroups()).thenReturn(ageGroupMap); + + RandomDeath randomDeath = new RandomDeath(mockedOptions); + + String result = randomDeath.canDie(mockedPerson, AgeGroup.ELDER, true); + + assertNull(result); + } + + @Test + public void testRandomlyDies_BaseChanceZero() { + CampaignOptions mockedOptions = mock(CampaignOptions.class); + when(mockedOptions.getRandomDeathChance()).thenReturn(0); + + RandomDeath randomDeath = new RandomDeath(mockedOptions); + + assertFalse(randomDeath.randomlyDies(30, Gender.MALE)); + } + + @Test + public void testRandomlyDies_AgeThresholdAbove() { + CampaignOptions mockedOptions = mock(CampaignOptions.class); + when(mockedOptions.getRandomDeathChance()).thenReturn(10); + + RandomDeath randomDeath = new RandomDeath(mockedOptions); + + assertFalse(randomDeath.randomlyDies(95, Gender.MALE)); + } + + @Test + public void testRandomlyDies_GenderFemaleMultiplier() { + CampaignOptions mockedOptions = mock(CampaignOptions.class); + when(mockedOptions.getRandomDeathChance()).thenReturn(10); + + RandomDeath randomDeath = new RandomDeath(mockedOptions); + + assertFalse(randomDeath.randomlyDies(30, Gender.FEMALE)); + } + + @Test + public void testProcessNewWeek_PersonCannotDie() { + // Mock setup + Person mockedPerson = mock(Person.class); + Campaign mockedCampaign = mock(Campaign.class); + LocalDate today = LocalDate.now(); + when(mockedPerson.getAge(today)).thenReturn(30); + when(mockedPerson.getGender()).thenReturn(Gender.MALE); + RandomDeath mockedRandomDeath = spy(new RandomDeath(mock(CampaignOptions.class))); + doReturn("Cannot die message").when(mockedRandomDeath).canDie(eq(mockedPerson), any(AgeGroup.class), eq(true)); + + // Call processNewWeek + boolean result = mockedRandomDeath.processNewWeek(mockedCampaign, today, mockedPerson); + + // Assertions + assertFalse(result); + } + + @Test + public void testProcessNewWeek_RandomlyDies() { + // Mock setup + Person mockedPerson = mock(Person.class); + Campaign mockedCampaign = mock(Campaign.class); + LocalDate today = LocalDate.now(); + when(mockedPerson.getAge(today)).thenReturn(70); + when(mockedPerson.getGender()).thenReturn(Gender.MALE); + + RandomDeath mockedRandomDeath = spy(new RandomDeath(mock(CampaignOptions.class))); + doReturn(null).when(mockedRandomDeath).canDie(eq(mockedPerson), any(AgeGroup.class), eq(true)); + doReturn(true).when(mockedRandomDeath).randomlyDies(eq(70), eq(Gender.MALE)); + doReturn(PersonnelStatus.NATURAL_CAUSES).when(mockedRandomDeath).getCause(eq(mockedPerson), any(AgeGroup.class), eq(70)); + + // Call processNewWeek + boolean result = mockedRandomDeath.processNewWeek(mockedCampaign, today, mockedPerson); + + // Assertions + assertTrue(result); + } + + @Test + public void testProcessNewWeek_RandomlySurvives() { + // Mock setup + Person mockedPerson = mock(Person.class); + Campaign mockedCampaign = mock(Campaign.class); + LocalDate today = LocalDate.now(); + when(mockedPerson.getAge(today)).thenReturn(50); + when(mockedPerson.getGender()).thenReturn(Gender.FEMALE); + + RandomDeath mockedRandomDeath = spy(new RandomDeath(mock(CampaignOptions.class))); + doReturn(null).when(mockedRandomDeath).canDie(eq(mockedPerson), any(AgeGroup.class), eq(true)); + doReturn(false).when(mockedRandomDeath).randomlyDies(eq(50), eq(Gender.FEMALE)); + + // Call processNewWeek + boolean result = mockedRandomDeath.processNewWeek(mockedCampaign, today, mockedPerson); + + // Assertions + assertFalse(result); + } +} From 7d91861ee1e61426e45a876bef2c3873f71669a9 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sat, 1 Feb 2025 16:45:33 -0600 Subject: [PATCH 045/112] Adjust random death options and simplify related XML settings Revised random death chance handling across multiple campaign presets, replacing detailed values with a simplified configuration. Updated tooltips and relevant texts for better clarity. Added updated copyright information where applicable. --- .../campaignPresets/CampaignOperations.xml | 34 +------------------ .../CampaignOperationsStratCon.xml | 34 +------------------ .../campaignPresets/NewPilotProgram.xml | 34 +------------------ .../campaignPresets/TheCompleteExperience.xml | 34 +------------------ .../CampaignOptionsDialog.properties | 2 +- MekHQ/src/mekhq/campaign/CampaignOptions.java | 2 +- .../adapter/PersonnelTableMouseAdapter.java | 2 +- .../personnel/death/RandomDeathTest.java | 18 ++++++++++ 8 files changed, 25 insertions(+), 135 deletions(-) diff --git a/MekHQ/mmconf/campaignPresets/CampaignOperations.xml b/MekHQ/mmconf/campaignPresets/CampaignOperations.xml index c96b14c0dd8..8c1bd4775ca 100644 --- a/MekHQ/mmconf/campaignPresets/CampaignOperations.xml +++ b/MekHQ/mmconf/campaignPresets/CampaignOperations.xml @@ -430,8 +430,6 @@ 10000 false 10000 - true - NONE false false @@ -441,38 +439,8 @@ false true - true - true false - 2.0E-5 - 5.4757,-7.0,0.0709 - 2.4641,-7.0,0.0752 - - 613.1 - 27.5 - 5155.0 - 1119.0 - 14.7 - 249.5 - 2196.5 - 176.1 - 14504.0 - 100.1 - 491.8 - - - 500.0 - 20.4 - 3788.0 - 670.0 - 11.8 - 140.2 - 1421.0 - 80.0 - 12870.0 - 38.8 - 302.5 - + 0 true true true diff --git a/MekHQ/mmconf/campaignPresets/CampaignOperationsStratCon.xml b/MekHQ/mmconf/campaignPresets/CampaignOperationsStratCon.xml index 545c94b00eb..8bf37522940 100644 --- a/MekHQ/mmconf/campaignPresets/CampaignOperationsStratCon.xml +++ b/MekHQ/mmconf/campaignPresets/CampaignOperationsStratCon.xml @@ -430,8 +430,6 @@ 10000 false 10000 - true - NONE false true @@ -441,38 +439,8 @@ false false - true - true false - 2.0E-5 - 5.4757,-7.0,0.0709 - 2.4641,-7.0,0.0752 - - 27.5 - 14.7 - 491.8 - 14504.0 - 2196.5 - 100.1 - 249.5 - 5155.0 - 1119.0 - 613.1 - 176.1 - - - 20.4 - 11.8 - 302.5 - 12870.0 - 1421.0 - 38.8 - 140.2 - 3788.0 - 670.0 - 500.0 - 80.0 - + 0 true true true diff --git a/MekHQ/mmconf/campaignPresets/NewPilotProgram.xml b/MekHQ/mmconf/campaignPresets/NewPilotProgram.xml index 06ea0ae60f9..9d6eaaa648f 100644 --- a/MekHQ/mmconf/campaignPresets/NewPilotProgram.xml +++ b/MekHQ/mmconf/campaignPresets/NewPilotProgram.xml @@ -430,8 +430,6 @@ 10000 false 10000 - true - EXPONENTIAL true true @@ -441,38 +439,8 @@ false false - true - true false - 2.0E-5 - 5.4757,-7.0,0.0709 - 2.4641,-7.0,0.0752 - - 176.1 - 14.7 - 27.5 - 249.5 - 14504.0 - 613.1 - 1119.0 - 2196.5 - 100.1 - 5155.0 - 491.8 - - - 80.0 - 11.8 - 20.4 - 140.2 - 12870.0 - 500.0 - 670.0 - 1421.0 - 38.8 - 3788.0 - 302.5 - + 6000 true true true diff --git a/MekHQ/mmconf/campaignPresets/TheCompleteExperience.xml b/MekHQ/mmconf/campaignPresets/TheCompleteExperience.xml index bc958215c21..5da1861299c 100644 --- a/MekHQ/mmconf/campaignPresets/TheCompleteExperience.xml +++ b/MekHQ/mmconf/campaignPresets/TheCompleteExperience.xml @@ -431,8 +431,6 @@ 10000 false 10000 - true - EXPONENTIAL false true @@ -442,38 +440,8 @@ false false - true - true false - 2.0E-5 - 5.4757,-7.0,0.0709 - 2.4641,-7.0,0.0752 - - 27.5 - 14.7 - 491.8 - 14504.0 - 2196.5 - 100.1 - 249.5 - 5155.0 - 1119.0 - 613.1 - 176.1 - - - 20.4 - 11.8 - 302.5 - 12870.0 - 1421.0 - 38.8 - 140.2 - 3788.0 - 670.0 - 500.0 - 80.0 - + 6000 true true true diff --git a/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties b/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties index 71aa7439d9d..ced8389275a 100644 --- a/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties +++ b/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties @@ -806,7 +806,7 @@ lblRandomDeathChance.text=Random Death Chance: \u26A0 1 in lblRandomDeathChance.tooltip=This is the percent chance per day that any member of your force will\ \ randomly die.\
\ -
Requirement: This option is only relevant if Random Death is selected. +
Warning: Setting this to 0 disabled Random Death # createDeathAgeGroupsPanel lblDeathAgeGroupsPanel.text=Death by Age Group diff --git a/MekHQ/src/mekhq/campaign/CampaignOptions.java b/MekHQ/src/mekhq/campaign/CampaignOptions.java index f62b0b5febf..03dafe329ad 100644 --- a/MekHQ/src/mekhq/campaign/CampaignOptions.java +++ b/MekHQ/src/mekhq/campaign/CampaignOptions.java @@ -961,7 +961,7 @@ public CampaignOptions() { getEnabledRandomDeathAgeGroups().put(AgeGroup.TODDLER, false); getEnabledRandomDeathAgeGroups().put(AgeGroup.BABY, false); setUseRandomDeathSuicideCause(false); - setRandomDeathChance(6000); + setRandomDeathChance(0); // endregion Life Paths Tab // region Turnover and Retention diff --git a/MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java b/MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java index 11b810f72cb..77e122a65ff 100644 --- a/MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java +++ b/MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2024 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2019-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * diff --git a/MekHQ/unittests/mekhq/campaign/personnel/death/RandomDeathTest.java b/MekHQ/unittests/mekhq/campaign/personnel/death/RandomDeathTest.java index 59a87c9e111..d86c87f075b 100644 --- a/MekHQ/unittests/mekhq/campaign/personnel/death/RandomDeathTest.java +++ b/MekHQ/unittests/mekhq/campaign/personnel/death/RandomDeathTest.java @@ -1,3 +1,21 @@ +/* + * 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 as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with MekHQ. If not, see . + */ package mekhq.campaign.personnel.death; import megamek.common.enums.Gender; From e0f14eddf6ebba369330201bdb3dcf024c1af6c6 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sat, 1 Feb 2025 16:48:07 -0600 Subject: [PATCH 046/112] Update random death tooltip to clarify mechanics and defaults Revised the tooltip for random death chance to explain the weekly die-roll mechanic, aging factor, and implications of the default value. Improved clarity and provided additional context for better user understanding. --- .../mekhq/resources/CampaignOptionsDialog.properties | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties b/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties index ced8389275a..659efb3137e 100644 --- a/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties +++ b/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties @@ -803,10 +803,12 @@ lblDeathTab.text=Death Options \u270E lblUseRandomDeathSuicideCause.text=Enable Cause of Death: Suicide lblUseRandomDeathSuicideCause.tooltip=This includes suicide as a potential cause for a random death. lblRandomDeathChance.text=Random Death Chance: \u26A0 1 in -lblRandomDeathChance.tooltip=This is the percent chance per day that any member of your force will\ - \ randomly die.\ +lblRandomDeathChance.tooltip=This is the number of sides on the die rolled each week to determine\ + \ whether a character randomly dies. Death occurs on a roll of 1. Once a character reaches 100\ + \ years old, this roll becomes exponentially harder each year.\
\ -
Warning: Setting this to 0 disabled Random Death +
Warning: Setting this to 0 disabled Random Death. The default value of 6000 gives an\ + \ average life expectancy of 90. # createDeathAgeGroupsPanel lblDeathAgeGroupsPanel.text=Death by Age Group From b1facf676e8cbd67bede53c039a0b9e6de240730 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sat, 1 Feb 2025 17:02:46 -0600 Subject: [PATCH 047/112] Improve error handling and optimize age range determination Added a try-catch block to handle parsing errors in status node processing, preventing potential crashes. Simplified the age range determination by reusing an existing static import. --- .../mekhq/campaign/personnel/death/RandomDeath.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java b/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java index 71134a158bb..6ca0a734567 100644 --- a/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java +++ b/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java @@ -46,6 +46,7 @@ import static java.lang.Math.round; import static megamek.common.Compute.randomInt; +import static mekhq.campaign.personnel.enums.TenYearAgeRange.determineAgeRange; import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; import static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG; @@ -228,8 +229,12 @@ private void parseStatusNode(final WeightedDoubleMap ageRangeCa return; } - final double weight = Double.parseDouble(statusNode.getTextContent().trim()); - ageRangeCauses.add(weight, status); + try { + final double weight = Double.parseDouble(statusNode.getTextContent().trim()); + ageRangeCauses.add(weight, status); + } catch (NumberFormatException e) { + logger.info("Unable to parse status node {}: {}", statusNode.getNodeName(), e.getMessage()); + } } /** @@ -406,8 +411,7 @@ public PersonnelStatus getCause(final Person person, final AgeGroup ageGroup, fi return getDefaultCause(ageGroup); } - final WeightedDoubleMap ageRangeCauses = genderedCauses - .get(TenYearAgeRange.determineAgeRange(age)); + final WeightedDoubleMap ageRangeCauses = genderedCauses.get(determineAgeRange(age)); if (ageRangeCauses == null) { return getDefaultCause(ageGroup); } From 8bbaac60e2e013314583eef33fc7570945bef0e3 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sat, 1 Feb 2025 17:07:12 -0600 Subject: [PATCH 048/112] Prevent unnecessary dice rolls for guaranteed deaths Added a check to bypass random dice rolling when death is guaranteed by ensuring the baseDieSize is 1 or less. This optimizes the code and avoids redundant calculations in such scenarios. --- MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java b/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java index 6ca0a734567..dece413c54a 100644 --- a/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java +++ b/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java @@ -294,6 +294,11 @@ public boolean randomlyDies(final int age, final Gender gender) { ? (int) round(baseDieSize * Math.pow(REDUCTION_MULTIPLIER, (age - AGE_THRESHOLD))) : baseDieSize; + // At this point death is guaranteed, so no need to roll. + if (baseDieSize <= 1) { + return true; + } + // Return random death outcome return randomInt(adjustedDieSize) == 0; } From 9964f826483eb4db7029ad4020bf74fd6729cd59 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sat, 1 Feb 2025 17:10:51 -0600 Subject: [PATCH 049/112] Disable random death for teenagers in all campaign presets. Random death for the TEENAGER age group was set to false across all campaign preset configuration files. This change ensures consistency and aligns with adjusted gameplay design decisions. --- MekHQ/mmconf/campaignPresets/CampaignOperations.xml | 2 +- MekHQ/mmconf/campaignPresets/CampaignOperationsStratCon.xml | 2 +- MekHQ/mmconf/campaignPresets/NewPilotProgram.xml | 2 +- MekHQ/mmconf/campaignPresets/TheCompleteExperience.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/MekHQ/mmconf/campaignPresets/CampaignOperations.xml b/MekHQ/mmconf/campaignPresets/CampaignOperations.xml index 8c1bd4775ca..5d651300884 100644 --- a/MekHQ/mmconf/campaignPresets/CampaignOperations.xml +++ b/MekHQ/mmconf/campaignPresets/CampaignOperations.xml @@ -437,7 +437,7 @@ true false false - true + false false 0 diff --git a/MekHQ/mmconf/campaignPresets/CampaignOperationsStratCon.xml b/MekHQ/mmconf/campaignPresets/CampaignOperationsStratCon.xml index 8bf37522940..07c45e7c981 100644 --- a/MekHQ/mmconf/campaignPresets/CampaignOperationsStratCon.xml +++ b/MekHQ/mmconf/campaignPresets/CampaignOperationsStratCon.xml @@ -432,7 +432,7 @@ 10000 false - true + false true true false diff --git a/MekHQ/mmconf/campaignPresets/NewPilotProgram.xml b/MekHQ/mmconf/campaignPresets/NewPilotProgram.xml index 9d6eaaa648f..2e87e7f8d41 100644 --- a/MekHQ/mmconf/campaignPresets/NewPilotProgram.xml +++ b/MekHQ/mmconf/campaignPresets/NewPilotProgram.xml @@ -434,7 +434,7 @@ true true false - true + false false false false diff --git a/MekHQ/mmconf/campaignPresets/TheCompleteExperience.xml b/MekHQ/mmconf/campaignPresets/TheCompleteExperience.xml index 5da1861299c..f0be11d3c82 100644 --- a/MekHQ/mmconf/campaignPresets/TheCompleteExperience.xml +++ b/MekHQ/mmconf/campaignPresets/TheCompleteExperience.xml @@ -433,7 +433,7 @@ 10000 false - true + false true true false From 9780a28d6e383799c1da9f6cf33d1a31183092a5 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sat, 1 Feb 2025 17:23:52 -0600 Subject: [PATCH 050/112] Refactored RandomDeath for testability and fixed logic errors Added an overridable randomInt method in RandomDeath to facilitate testing and replaced direct calls to Compute.randomInt. Corrected logic to use adjustedDieSize for guaranteed death checks, ensuring accuracy in edge cases. Expanded unit tests to cover various random and threshold scenarios. --- .../campaign/personnel/death/RandomDeath.java | 18 ++++++- .../personnel/death/RandomDeathTest.java | 54 +++++++++++++++++-- 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java b/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java index dece413c54a..8141399ab61 100644 --- a/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java +++ b/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java @@ -19,6 +19,7 @@ package mekhq.campaign.personnel.death; import megamek.Version; +import megamek.common.Compute; import megamek.common.annotations.Nullable; import megamek.common.enums.Gender; import megamek.common.util.weightedMaps.WeightedDoubleMap; @@ -45,7 +46,6 @@ import java.util.Map; import static java.lang.Math.round; -import static megamek.common.Compute.randomInt; import static mekhq.campaign.personnel.enums.TenYearAgeRange.determineAgeRange; import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; import static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG; @@ -295,7 +295,7 @@ public boolean randomlyDies(final int age, final Gender gender) { : baseDieSize; // At this point death is guaranteed, so no need to roll. - if (baseDieSize <= 1) { + if (adjustedDieSize <= 1) { return true; } @@ -457,4 +457,18 @@ private PersonnelStatus determineIfInjuriesCausedTheDeath(final Person person) { private PersonnelStatus getDefaultCause(final AgeGroup ageGroup) { return ageGroup.isElder() ? PersonnelStatus.OLD_AGE : PersonnelStatus.NATURAL_CAUSES; } + + /** + * Generates a random integer up to the given bound (exclusive) + * + *

We use this custom method to make it easier to test the random components of the + * `randomlyDies` method.

+ * + * @param bound The upper bound for the random number. + * @return A random integer between 0 (inclusive) and {@code bound} (exclusive). + */ + protected int randomInt(int bound) { + return Compute.randomInt(bound); + } + } diff --git a/MekHQ/unittests/mekhq/campaign/personnel/death/RandomDeathTest.java b/MekHQ/unittests/mekhq/campaign/personnel/death/RandomDeathTest.java index d86c87f075b..465b053ce32 100644 --- a/MekHQ/unittests/mekhq/campaign/personnel/death/RandomDeathTest.java +++ b/MekHQ/unittests/mekhq/campaign/personnel/death/RandomDeathTest.java @@ -137,7 +137,12 @@ public void testRandomlyDies_BaseChanceZero() { CampaignOptions mockedOptions = mock(CampaignOptions.class); when(mockedOptions.getRandomDeathChance()).thenReturn(0); - RandomDeath randomDeath = new RandomDeath(mockedOptions); + RandomDeath randomDeath = new RandomDeath(mockedOptions) { + @Override + protected int randomInt(int bound) { + return 0; // Doesn't matter because chance is zero + } + }; assertFalse(randomDeath.randomlyDies(30, Gender.MALE)); } @@ -147,7 +152,12 @@ public void testRandomlyDies_AgeThresholdAbove() { CampaignOptions mockedOptions = mock(CampaignOptions.class); when(mockedOptions.getRandomDeathChance()).thenReturn(10); - RandomDeath randomDeath = new RandomDeath(mockedOptions); + RandomDeath randomDeath = new RandomDeath(mockedOptions) { + @Override + protected int randomInt(int bound) { + return 1; // Simulate NOT rolling a 0 + } + }; assertFalse(randomDeath.randomlyDies(95, Gender.MALE)); } @@ -157,9 +167,45 @@ public void testRandomlyDies_GenderFemaleMultiplier() { CampaignOptions mockedOptions = mock(CampaignOptions.class); when(mockedOptions.getRandomDeathChance()).thenReturn(10); - RandomDeath randomDeath = new RandomDeath(mockedOptions); + RandomDeath randomDeath = new RandomDeath(mockedOptions) { + @Override + protected int randomInt(int bound) { + return 0; // Simulate rolling a 0 (death) + } + }; + + assertTrue(randomDeath.randomlyDies(30, Gender.FEMALE)); + } + + @Test + public void testRandomlyDies_AdjustedDieSizeAboveOne() { + CampaignOptions mockedOptions = mock(CampaignOptions.class); + when(mockedOptions.getRandomDeathChance()).thenReturn(20); // Base chance + + RandomDeath randomDeath = new RandomDeath(mockedOptions) { + @Override + protected int randomInt(int bound) { + return 5; // Simulate NOT rolling a 0 + } + }; + + assertFalse(randomDeath.randomlyDies(80, Gender.MALE)); + } + + @Test + public void testRandomlyDies_AgeThresholdGuaranteesDeath() { + CampaignOptions mockedOptions = mock(CampaignOptions.class); + when(mockedOptions.getRandomDeathChance()).thenReturn(10); + + RandomDeath randomDeath = new RandomDeath(mockedOptions) { + @Override + protected int randomInt(int bound) { + return 0; // Doesn't matter since death is guaranteed when adjustedDieSize <= 1 + } + }; - assertFalse(randomDeath.randomlyDies(30, Gender.FEMALE)); + // If adjustedDieSize = 1, death is guaranteed without a random roll + assertTrue(randomDeath.randomlyDies(150, Gender.MALE)); } @Test From 1c17f68759764171713989fbde7ae104a7ead584 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sat, 1 Feb 2025 21:40:00 -0600 Subject: [PATCH 051/112] Refactored RandomDeath for testability and fixed logic errors Added an overridable randomInt method in RandomDeath to facilitate testing and replaced direct calls to Compute.randomInt. Corrected logic to use adjustedDieSize for guaranteed death checks, ensuring accuracy in edge cases. Expanded unit tests to cover various random and threshold scenarios. --- MekHQ/src/mekhq/campaign/Campaign.java | 2 +- MekHQ/src/mekhq/campaign/CampaignOptions.java | 18 +- .../campaign/personnel/death/RandomDeath.java | 374 ++++++++++++++++-- .../contents/BiographyTab.java | 4 +- .../personnel/death/RandomDeathTest.java | 259 ++++++------ 5 files changed, 471 insertions(+), 186 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index e0ca1059b4f..dc402ed4a40 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -377,7 +377,7 @@ public Campaign() { setPersonnelMarket(new PersonnelMarket()); setContractMarket(new AtbMonthlyContractMarket()); setUnitMarket(new DisabledUnitMarket()); - randomDeath = new RandomDeath(campaignOptions); + randomDeath = new RandomDeath(this); setDivorce(new DisabledRandomDivorce(getCampaignOptions())); setMarriage(new DisabledRandomMarriage(getCampaignOptions())); setProcreation(new DisabledRandomProcreation(getCampaignOptions())); diff --git a/MekHQ/src/mekhq/campaign/CampaignOptions.java b/MekHQ/src/mekhq/campaign/CampaignOptions.java index 03dafe329ad..138b013f03c 100644 --- a/MekHQ/src/mekhq/campaign/CampaignOptions.java +++ b/MekHQ/src/mekhq/campaign/CampaignOptions.java @@ -367,7 +367,7 @@ public static String getTechLevelName(final int techLevel) { // Death private Map enabledRandomDeathAgeGroups; private boolean useRandomDeathSuicideCause; - private int randomDeathChance; + private double randomDeathMultiplier; // endregion Life Paths Tab //region Turnover and Retention @@ -961,7 +961,7 @@ public CampaignOptions() { getEnabledRandomDeathAgeGroups().put(AgeGroup.TODDLER, false); getEnabledRandomDeathAgeGroups().put(AgeGroup.BABY, false); setUseRandomDeathSuicideCause(false); - setRandomDeathChance(0); + setRandomDeathMultiplier(0); // endregion Life Paths Tab // region Turnover and Retention @@ -2962,12 +2962,12 @@ public void setUseRandomDeathSuicideCause(final boolean useRandomDeathSuicideCau this.useRandomDeathSuicideCause = useRandomDeathSuicideCause; } - public int getRandomDeathChance() { - return randomDeathChance; + public double getRandomDeathMultiplier() { + return randomDeathMultiplier; } - public void setRandomDeathChance(final int randomDeathChance) { - this.randomDeathChance = randomDeathChance; + public void setRandomDeathMultiplier(final int randomDeathMultiplier) { + this.randomDeathMultiplier = randomDeathMultiplier; } // endregion Death @@ -5012,7 +5012,7 @@ public void writeToXml(final PrintWriter pw, int indent) { } MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, "enabledRandomDeathAgeGroups"); MHQXMLUtility.writeSimpleXMLTag(pw, indent, "useRandomDeathSuicideCause", isUseRandomDeathSuicideCause()); - MHQXMLUtility.writeSimpleXMLTag(pw, indent, "randomDeathChance", getRandomDeathChance()); + MHQXMLUtility.writeSimpleXMLTag(pw, indent, "randomDeathMultiplier", getRandomDeathMultiplier()); MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, "ageRangeRandomDeathMaleValues"); MHQXMLUtility.writeSimpleXMLCloseTag(pw, --indent, "ageRangeRandomDeathMaleValues"); MHQXMLUtility.writeSimpleXMLOpenTag(pw, indent++, "ageRangeRandomDeathFemaleValues"); @@ -5819,8 +5819,8 @@ public static CampaignOptions generateCampaignOptionsFromXml(Node wn, Version ve } } else if (wn2.getNodeName().equalsIgnoreCase("useRandomDeathSuicideCause")) { retVal.setUseRandomDeathSuicideCause(Boolean.parseBoolean(wn2.getTextContent().trim())); - } else if (wn2.getNodeName().equalsIgnoreCase("randomDeathChance")) { - retVal.setRandomDeathChance(Integer.parseInt(wn2.getTextContent().trim())); + } else if (wn2.getNodeName().equalsIgnoreCase("randomDeathMultiplier")) { + retVal.setRandomDeathMultiplier(Integer.parseInt(wn2.getTextContent().trim())); } else if (wn2.getNodeName().equalsIgnoreCase("useRandomRetirement")) { retVal.setUseRandomRetirement(Boolean.parseBoolean(wn2.getTextContent().trim())); } else if (wn2.getNodeName().equalsIgnoreCase("turnoverBaseTn")) { diff --git a/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java b/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java index 8141399ab61..7281f6fe3ae 100644 --- a/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java +++ b/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java @@ -20,6 +20,7 @@ import megamek.Version; import megamek.common.Compute; +import megamek.common.EquipmentType; import megamek.common.annotations.Nullable; import megamek.common.enums.Gender; import megamek.common.util.weightedMaps.WeightedDoubleMap; @@ -28,10 +29,14 @@ import mekhq.MekHQ; import mekhq.campaign.Campaign; import mekhq.campaign.CampaignOptions; +import mekhq.campaign.personnel.Injury; import mekhq.campaign.personnel.Person; import mekhq.campaign.personnel.enums.AgeGroup; import mekhq.campaign.personnel.enums.PersonnelStatus; import mekhq.campaign.personnel.enums.TenYearAgeRange; +import mekhq.campaign.universe.Faction; +import mekhq.campaign.universe.enums.EraFlag; +import mekhq.campaign.universe.eras.Era; import mekhq.utilities.MHQXMLUtility; import mekhq.utilities.ReportingUtilities; import org.w3c.dom.Element; @@ -43,10 +48,12 @@ import java.io.InputStream; import java.time.LocalDate; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Set; -import static java.lang.Math.round; import static mekhq.campaign.personnel.enums.TenYearAgeRange.determineAgeRange; +import static mekhq.campaign.universe.enums.EraFlag.*; import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; import static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG; @@ -71,10 +78,50 @@ public class RandomDeath { private static final String RESOURCE_BUNDLE = "mekhq.resources.RandomDeath"; private static final MMLogger logger = MMLogger.create(RandomDeath.class); + private final Campaign campaign; + private final CampaignOptions campaignOptions; private final Map enabledAgeGroups; private final boolean enableRandomDeathSuicideCause; - private final Map>> causes; - private final int baseRandomDeathChance; + private Map>> causes; + private final double randomDeathMultiplier; + + // Base Chances + private final List deathChances = List.of( + new RandomDeathChance(9, 15, 12), + new RandomDeathChance(19, 16, 14), + new RandomDeathChance(29, 17, 8), + new RandomDeathChance(39, 20, 10), + new RandomDeathChance(49, 30, 18), + new RandomDeathChance(59, 70, 40), + new RandomDeathChance(69, 149, 90), + new RandomDeathChance(79, 385, 233), + new RandomDeathChance(89, 1000, 714), + new RandomDeathChance(99, 2500, 2000), + new RandomDeathChance(Integer.MAX_VALUE, 50, 3333) + ); + + // Multipliers + private final double ERA_MULTIPLIER_AGE_OF_WAR = 1.2; + private final double ERA_MULTIPLIER_STAR_LEAGUE = 0.9; + private final double ERA_MULTIPLIER_SUCCESSION_WARS = 1.05; + private final double ERA_MULTIPLIER_CLAN_INVASION = 0.95; + private final double ERA_MULTIPLIER_CIVIL_WAR = 0.93; + private final double ERA_MULTIPLIER_JIHAD = 1.0; + private final double ERA_MULTIPLIER_REPUBLIC = 0.92; + private final double ERA_MULTIPLIER_DARK_AGE = 0.95; + private final double ERA_MULTIPLIER_ILCLAN = 0.85; + + private final double FACTION_MULTIPLIER_CLANS = 0.85; + private final double FACTION_MULTIPLIER_IS_MAJOR = 0.9; + private final double FACTION_MULTIPLIER_IS_MINOR = 0.95; + private final double FACTION_MULTIPLIER_PERIPHERY = 1.00; + private final double FACTION_MULTIPLIER_PERIPHERY_DEEP = 1.30; + private final double FACTION_MULTIPLIER_PIRATE = 1.35; + private final double FACTION_MULTIPLIER_MERCENARY = 1.00; + + private final double MEDICAL_MULTIPLIER_INJURY_TRANSIENT = 0.1; // per injury + private final double MEDICAL_MULTIPLIER_INJURY_PERMANENT = 0.25; // once no matter how many + private final double MEDICAL_MULTIPLIER_HPG_ACCESS = -0.05; /** * Constructs a {@code RandomDeath} object using campaign-specific options. @@ -83,17 +130,23 @@ public class RandomDeath { * enabling or disabling suicide causes, and retrieving the base random death chances. * The death causes map is also initialized by reading relevant files.

* - * @param campaignOptions The campaign options containing random death configurations. + * @param campaign The current campaign. */ - public RandomDeath(final CampaignOptions campaignOptions) { + public RandomDeath(final Campaign campaign) { + this.campaign = campaign; + this.campaignOptions = campaign.getCampaignOptions(); + enabledAgeGroups = campaignOptions.getEnabledRandomDeathAgeGroups(); enableRandomDeathSuicideCause = campaignOptions.isUseRandomDeathSuicideCause(); - baseRandomDeathChance = campaignOptions.getRandomDeathChance(); - causes = new HashMap<>(); + randomDeathMultiplier = campaignOptions.getRandomDeathMultiplier(); initializeCauses(); } + List getDeathChances() { + return deathChances; + } + /** * Clears and reloads the random death causes from default and user-defined XML files. * @@ -101,7 +154,7 @@ public RandomDeath(final CampaignOptions campaignOptions) { * the {@code causes} map.

*/ public void initializeCauses() { - causes.clear(); + causes = new HashMap<>(); initializeCausesFromFile(new File(MHQConstants.RANDOM_DEATH_CAUSES_FILE_PATH)); initializeCausesFromFile(new File(MHQConstants.USER_RANDOM_DEATH_CAUSES_FILE_PATH)); } @@ -259,48 +312,253 @@ private boolean isElementNode(final Node node) { } /** - * Determines whether an individual dies randomly based on age, gender, and campaign configuration. + * Determines if a person randomly dies based on various multipliers and random chance. * - *

The chance of random death is influenced by:

- *
    - *
  • Age: The risk increases exponentially after a certain threshold.
  • - *
  • Gender: Gender-based multipliers affect the base death chance.
  • - *
  • Campaign settings: The base random death chance is configured globally.
  • - *
+ *

This method calculates the probability of a person dying based on era, faction, health, + * and other modifiers, then performs a random roll to decide if the person dies.

* - * @param age The age of the individual. - * @param gender The gender of the individual. - * @return {@code true} if the individual dies randomly; {@code false} otherwise. + * @param person the person to evaluate for random death. + * @return {@code true} if the person randomly dies, {@code false} otherwise. */ - public boolean randomlyDies(final int age, final Gender gender) { - final int AGE_THRESHOLD = 90; - final double REDUCTION_MULTIPLIER = 0.90; - final double FEMALE_MULTIPLIER = 1.1; + public boolean randomlyDies(Person person) { + if (canDie(person, true) != null) { + return false; + } - int baseDieSize = baseRandomDeathChance; + Era era = campaign.getEra(); + Faction faction = campaign.getFaction(); - // If Random Death disabled? - if (baseDieSize == 0) { + // Determine base chance + double randomDeathChance = getBaseDeathChance(person); + + // If randomDeathChance is 0, we're never going to have a result other than zero, so just + // early exit. + if (randomDeathChance == 0) { return false; } - // Modifier for gender - if (gender == Gender.FEMALE) { - baseDieSize = (int) round(baseDieSize * FEMALE_MULTIPLIER); + // Apply Era Multiplier + randomDeathChance = randomDeathChance * getEraMultiplier(era); + + // Apply Faction Multiplier + randomDeathChance = randomDeathChance * getFactionMultiplier(faction); + + // Apply Health Multiplier + randomDeathChance = randomDeathChance * getHealthModifier(person); + + // Apply Campaign Options Multiplier + randomDeathChance = randomDeathChance * randomDeathMultiplier; + + // Round to the nearest int. We need an int for the final roll. + int actualDeathChance = (int) Math.round(randomDeathChance); + + if (actualDeathChance == 0) { + return false; } - // Calculate adjusted die size if the age exceeds the threshold - int adjustedDieSize = (age > AGE_THRESHOLD) - ? (int) round(baseDieSize * Math.pow(REDUCTION_MULTIPLIER, (age - AGE_THRESHOLD))) - : baseDieSize; + return randomInt(10000) < actualDeathChance; + } - // At this point death is guaranteed, so no need to roll. - if (adjustedDieSize <= 1) { - return true; + /** + * Retrieves the base death chance for a person based on their age and gender. + * + *

This method iterates over the list of predefined {@link RandomDeathChance} configurations + * and finds the matching rule based on the age of the person. Gender-based multipliers + * are applied accordingly.

+ * + * @param person the person whose death chance is being calculated. + * @return the base death chance as a double, based on the matching {@link RandomDeathChance}. + */ + double getBaseDeathChance(Person person) { + int age = person.getAge(campaign.getLocalDate()); + + for (RandomDeathChance deathChance : deathChances) { + // Check if the age falls within the range of this death chance + if (age <= deathChance.maximumAge) { + if (person.getGender().isFemale()) { + return deathChance.female; // Use female death chance + } else { + return deathChance.male; // Use male death chance + } + } } - // Return random death outcome - return randomInt(adjustedDieSize) == 0; + // If no matching entry is found for the provided age, default to 0 + return 0.0; + } + + /** + * Retrieves the era-based multiplier for determining the death chance. + * + *

The multiplier is obtained based on the characteristics of the current era (via + * {@link EraFlag}).

+ * + * @param era the current era being analyzed. + * @return the death chance multiplier specific to the provided era. + */ + double getEraMultiplier(Era era) { + Set flags = era.getFlags(); + + if (flags.contains(PRE_SPACEFLIGHT) || flags.contains(EARLY_SPACEFLIGHT) + || flags.contains(AGE_OF_WAR)) { + return ERA_MULTIPLIER_AGE_OF_WAR; + } + + if (flags.contains(STAR_LEAGUE)) { + return ERA_MULTIPLIER_STAR_LEAGUE; + } + + if (flags.contains(EARLY_SUCCESSION_WARS) || flags.contains(LATE_SUCCESSION_WARS_LOSTECH) + || flags.contains(LATE_SUCCESSION_WARS_RENAISSANCE)) { + return ERA_MULTIPLIER_SUCCESSION_WARS; + } + + if (flags.contains(CLAN_INVASION)) { + return ERA_MULTIPLIER_CLAN_INVASION; + } + + if (flags.contains(CIVIL_WAR)) { + return ERA_MULTIPLIER_CIVIL_WAR; + } + + if (flags.contains(JIHAD)) { + return ERA_MULTIPLIER_JIHAD; + } + + if (flags.contains(EARLY_REPUBLIC) || flags.contains(LATE_REPUBLIC)) { + return ERA_MULTIPLIER_REPUBLIC; + } + + if (flags.contains(DARK_AGES)) { + return ERA_MULTIPLIER_DARK_AGE; + } + + // this is the current era, so if we've not hit any of the others, that means we're in ilClan + return ERA_MULTIPLIER_ILCLAN; + } + + /** + * Retrieves the faction-based multiplier for determining the death chance. + * + *

The multiplier is determined based on the type of faction the campaign belongs to, + * such as Clan, Periphery, Major Power, or Mercenary. Each faction type has a predefined + * multiplier applied to the death chance.

+ * + * @param faction the faction to calculate the multiplier for. + * @return the death chance multiplier specific to the provided faction. + */ + double getFactionMultiplier(Faction faction) { + if (faction.isClan()) { + return FACTION_MULTIPLIER_CLANS; + } + + if (faction.isDeepPeriphery()) { + return FACTION_MULTIPLIER_PERIPHERY_DEEP; + } + + if (faction.isPeriphery()) { + return FACTION_MULTIPLIER_PERIPHERY; + } + + if (faction.isMinorPower()) { + return FACTION_MULTIPLIER_IS_MINOR; + } + + if (faction.isMajorPower()) { + return FACTION_MULTIPLIER_IS_MAJOR; + } + + if (faction.isPirate()) { + return FACTION_MULTIPLIER_PIRATE; + } + + // We also use the Mercenary modifier as a fallback + return FACTION_MULTIPLIER_MERCENARY; + } + + /** + * Calculates the health-based multiplier for determining the death chance. + * + *

The health multiplier accounts for HPG access, injuries, and other health modifiers. + * When cumulative injuries (transient or permanent) are present, and depending on the use of + * advanced medical care, additional multipliers are applied to represent the overall health.

+ * + * @param person the person to evaluate for health-related modifiers. + * @return the health multiplier as a double. + */ + double getHealthModifier(Person person) { + double healthMultiplier = 1; + + // Apply HPG access modifier if applicable + healthMultiplier += getHpgAccessMultiplier(); + + // Apply injury-related modifiers + if (person.needsFixing()) { + healthMultiplier += getInjuryModifier(person); + } + + return healthMultiplier; + } + + /** + * Calculates the multiplier based on HPG access if applicable. + * + * @return the HPG access multiplier, or 0 if no modifier is required. + */ + private double getHpgAccessMultiplier() { + Integer hpgRating = campaign.getLocation().getPlanet().getHPG(campaign.getLocalDate()); + if (hpgRating != null && hpgRating <= EquipmentType.RATING_B) { + return MEDICAL_MULTIPLIER_HPG_ACCESS; + } + return 0; + } + + /** + * Calculates the modifier based on the injuries of the person. + * + *

If advanced medical care is enabled in the campaign options, individual injuries are + * evaluated for either transient or permanent injuries. Otherwise, a simpler calculation is + * applied based on the total number of injuries.

+ * + * @param person the person whose injuries are evaluated. + * @return the injury-related health multiplier. + */ + private double getInjuryModifier(Person person) { + if (!campaignOptions.isUseAdvancedMedical()) { + // Simplified injury multiplier without advanced medical care + return MEDICAL_MULTIPLIER_INJURY_TRANSIENT * person.getHits(); + } + + // Advanced medical care: calculate based on individual injuries + return calculateAdvancedInjuryModifier(person.getInjuries()); + } + + /** + * Calculates the injury modifier when advanced medical care is used. + * + *

Counts both transient and permanent injuries, and applies appropriate modifiers.

+ * + * @param injuries the list of injuries to evaluate. + * @return the calculated health multiplier for advanced injuries. + */ + private double calculateAdvancedInjuryModifier(List injuries) { + boolean hasPermanentInjuries = false; + double injuryMultiplier = 0; + + for (Injury injury : injuries) { + if (injury.isPermanent()) { + hasPermanentInjuries = true; + } else { + injuryMultiplier += MEDICAL_MULTIPLIER_INJURY_TRANSIENT; + } + } + + // Apply permanent injury penalty if applicable + if (hasPermanentInjuries) { + injuryMultiplier += MEDICAL_MULTIPLIER_INJURY_PERMANENT; + } + + return injuryMultiplier; } /** @@ -315,11 +573,14 @@ public boolean randomlyDies(final int age, final Gender gender) { * * * @param person The individual to evaluate. - * @param ageGroup The person's age group. * @param randomDeath Whether random deaths are enabled in the campaign. * @return A string describing why the individual cannot die, or {@code null} if no restrictions apply. */ - public @Nullable String canDie(final Person person, final AgeGroup ageGroup, final boolean randomDeath) { + public @Nullable String canDie(final Person person, final boolean randomDeath) { + LocalDate today = campaign.getLocalDate(); + int age = person.getAge(today); + AgeGroup ageGroup = AgeGroup.determineAgeGroup(age); + if (person.getStatus().isDead()) { return getCannotDieMessage("cannotDie.Dead.text"); } @@ -364,11 +625,11 @@ public boolean processNewWeek(final Campaign campaign, final LocalDate today, final int age = person.getAge(today); final AgeGroup ageGroup = AgeGroup.determineAgeGroup(age); - if (canDie(person, ageGroup, true) != null) { + if (canDie(person, true) != null) { return false; } - if (randomlyDies(age, person.getGender())) { + if (randomlyDies(person)) { // We double-report here, to make sure the user definitely notices that a random death has occurred. // Prior to this change, it was exceptionally easy to miss these events. String color = MekHQ.getMHQOptions().getFontColorNegativeHexColor(); @@ -471,4 +732,35 @@ protected int randomInt(int bound) { return Compute.randomInt(bound); } + /** + * A record representing the random death chance information based on gender and maximum age. + * + *

This record stores the following information:

+ *
    + *
  • {@code maximumAge}: The maximum age to which the death chance applies.
  • + *
  • {@code male}: The death chance multiplier for male individuals.
  • + *
  • {@code female}: The death chance multiplier for female individuals.
  • + *
+ */ + public record RandomDeathChance(int maximumAge, double male, double female) { + /** + * Constructs a new {@code RandomDeathChance} record, which ensures the values are valid. + * + * @param maximumAge The maximum age limit for the death chance. + * @param male The death chance multiplier for males. + * @param female The death chance multiplier for females. + * @throws IllegalArgumentException if any values are invalid: + * - {@code maximumAge} must be greater than 0. + * - {@code male} and {@code female} must be non-negative. + */ + public RandomDeathChance { + if (maximumAge < 0) { + throw new IllegalArgumentException("maximumAge must be 0 or greater: " + maximumAge); + } + if (male < 0 || female < 0) { + throw new IllegalArgumentException("male and female multipliers must be non-negative: male=" + + male + ", female=" + female); + } + } + } } diff --git a/MekHQ/src/mekhq/gui/campaignOptions/contents/BiographyTab.java b/MekHQ/src/mekhq/gui/campaignOptions/contents/BiographyTab.java index 8d585d8fc7c..fb2db1537f9 100644 --- a/MekHQ/src/mekhq/gui/campaignOptions/contents/BiographyTab.java +++ b/MekHQ/src/mekhq/gui/campaignOptions/contents/BiographyTab.java @@ -1297,7 +1297,7 @@ public void loadValuesFromCampaignOptions(@Nullable CampaignOptions presetCampai // Death chkUseRandomDeathSuicideCause.setSelected(options.isUseRandomDeathSuicideCause()); - spnRandomDeathChance.setValue(options.getRandomDeathChance()); + spnRandomDeathChance.setValue(options.getRandomDeathMultiplier()); Map deathAgeGroups = options.getEnabledRandomDeathAgeGroups(); for (final AgeGroup ageGroup : AgeGroup.values()) { @@ -1386,7 +1386,7 @@ public void applyCampaignOptionsToCampaign(@Nullable CampaignOptions presetCampa // Death options.setUseRandomDeathSuicideCause(chkUseRandomDeathSuicideCause.isSelected()); - options.setRandomDeathChance((int) spnRandomDeathChance.getValue()); + options.setRandomDeathMultiplier((int) spnRandomDeathChance.getValue()); for (final AgeGroup ageGroup : AgeGroup.values()) { options.getEnabledRandomDeathAgeGroups().put(ageGroup, chkEnabledRandomDeathAgeGroups.get(ageGroup).isSelected()); diff --git a/MekHQ/unittests/mekhq/campaign/personnel/death/RandomDeathTest.java b/MekHQ/unittests/mekhq/campaign/personnel/death/RandomDeathTest.java index 465b053ce32..294f41ef1bb 100644 --- a/MekHQ/unittests/mekhq/campaign/personnel/death/RandomDeathTest.java +++ b/MekHQ/unittests/mekhq/campaign/personnel/death/RandomDeathTest.java @@ -24,11 +24,17 @@ import mekhq.campaign.personnel.Person; import mekhq.campaign.personnel.enums.AgeGroup; import mekhq.campaign.personnel.enums.PersonnelStatus; +import mekhq.campaign.universe.Faction; +import mekhq.campaign.universe.eras.Era; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import java.time.LocalDate; import java.util.Map; +import java.util.Set; +import static mekhq.campaign.personnel.enums.AgeGroup.*; +import static mekhq.campaign.universe.enums.EraFlag.STAR_LEAGUE; import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -58,212 +64,199 @@ public class RandomDeathTest { private static final String RESOURCE_BUNDLE = "mekhq.resources.RandomDeath"; + private static Campaign mockedCampaign; + private static CampaignOptions mockedCampaignOptions; + private static LocalDate mockedToday; + private static Person mockedPerson; + private static RandomDeath randomDeath; + + private static Map ageGroups; + + @BeforeAll + public static void beforeAll() { + // Prep Age Groups + ageGroups = Map.of( + ELDER, true, + ADULT, false, + TEENAGER, true, + PRETEEN, true, + CHILD, true, + TODDLER, true, + BABY, true + ); + + // Mock Campaign and CampaignOptions + mockedCampaign = mock(Campaign.class); + mockedCampaignOptions = mock(CampaignOptions.class); + mockedToday = LocalDate.of(3025,1,1); + mockedPerson = mock(Person.class); + + when(mockedCampaign.getCampaignOptions()).thenReturn(mockedCampaignOptions); + + when(mockedCampaignOptions.getEnabledRandomDeathAgeGroups()).thenReturn(ageGroups); + when(mockedCampaignOptions.isUseRandomDeathSuicideCause()).thenReturn(false); + when(mockedCampaignOptions.getRandomDeathMultiplier()).thenReturn(1.0); + when(mockedCampaign.getLocalDate()).thenReturn(mockedToday); + + randomDeath = new RandomDeath(mockedCampaign); + } + @Test public void testCanDie_PersonAlreadyDead() { - Person mockedPerson = mock(Person.class); + when(mockedPerson.getAge(any(LocalDate.class))).thenReturn(1); when(mockedPerson.getStatus()).thenReturn(PersonnelStatus.KIA); - CampaignOptions mockedOptions = mock(CampaignOptions.class); - RandomDeath randomDeath = new RandomDeath(mockedOptions); - - String result = randomDeath.canDie(mockedPerson, AgeGroup.ELDER, true); + String result = randomDeath.canDie(mockedPerson, true); assertEquals(getFormattedTextAt(RESOURCE_BUNDLE, "cannotDie.Dead.text"), result); } @Test public void testCanDie_PersonImmortal() { - Person mockedPerson = mock(Person.class); when(mockedPerson.getStatus()).thenReturn(PersonnelStatus.ACTIVE); when(mockedPerson.isImmortal()).thenReturn(true); - CampaignOptions mockedOptions = mock(CampaignOptions.class); - RandomDeath randomDeath = new RandomDeath(mockedOptions); - - String result = randomDeath.canDie(mockedPerson, AgeGroup.ADULT, true); + String result = randomDeath.canDie(mockedPerson, true); assertEquals(getFormattedTextAt(RESOURCE_BUNDLE, "cannotDie.Immortal.text"), result); } @Test public void testCanDie_AgeGroupDisabled() { - Person mockedPerson = mock(Person.class); when(mockedPerson.getStatus()).thenReturn(PersonnelStatus.ACTIVE); when(mockedPerson.isImmortal()).thenReturn(false); + when(mockedPerson.getAge(mockedToday)).thenReturn(21); - CampaignOptions mockedOptions = mock(CampaignOptions.class); - Map ageGroupMap = Map.of(AgeGroup.ADULT, false); - when(mockedOptions.getEnabledRandomDeathAgeGroups()).thenReturn(ageGroupMap); - - RandomDeath randomDeath = new RandomDeath(mockedOptions); - - String result = randomDeath.canDie(mockedPerson, AgeGroup.ADULT, true); + String result = randomDeath.canDie(mockedPerson, true); assertEquals(getFormattedTextAt(RESOURCE_BUNDLE, "cannotDie.AgeGroupDisabled.text"), result); } @Test public void testCanDie_RandomDeathFalse() { - Person mockedPerson = mock(Person.class); when(mockedPerson.getStatus()).thenReturn(PersonnelStatus.ACTIVE); - CampaignOptions mockedOptions = mock(CampaignOptions.class); - RandomDeath randomDeath = new RandomDeath(mockedOptions); - - String result = randomDeath.canDie(mockedPerson, AgeGroup.ADULT, false); + String result = randomDeath.canDie(mockedPerson, false); assertNull(result); } @Test - public void testCanDie_CanDieNullMessage() { - Person mockedPerson = mock(Person.class); + public void testCanDie_RandomDeathTrue() { when(mockedPerson.getStatus()).thenReturn(PersonnelStatus.ACTIVE); when(mockedPerson.isImmortal()).thenReturn(false); + when(mockedPerson.getAge(mockedToday)).thenReturn(106); - CampaignOptions mockedOptions = mock(CampaignOptions.class); - Map ageGroupMap = Map.of(AgeGroup.ELDER, true); - when(mockedOptions.getEnabledRandomDeathAgeGroups()).thenReturn(ageGroupMap); - - RandomDeath randomDeath = new RandomDeath(mockedOptions); - - String result = randomDeath.canDie(mockedPerson, AgeGroup.ELDER, true); + String result = randomDeath.canDie(mockedPerson, true); assertNull(result); } @Test - public void testRandomlyDies_BaseChanceZero() { - CampaignOptions mockedOptions = mock(CampaignOptions.class); - when(mockedOptions.getRandomDeathChance()).thenReturn(0); + void testRandomlyDies_DeathChanceZero() { + when(mockedPerson.getAge(any())).thenReturn(25); + when(mockedPerson.getGender()).thenReturn(Gender.MALE); - RandomDeath randomDeath = new RandomDeath(mockedOptions) { - @Override - protected int randomInt(int bound) { - return 0; // Doesn't matter because chance is zero - } - }; + randomDeath = spy(randomDeath); + doReturn(0.0).when(randomDeath).getBaseDeathChance(mockedPerson); + doReturn(null).when(randomDeath).canDie(mockedPerson, true); + + boolean result = randomDeath.randomlyDies(mockedPerson); - assertFalse(randomDeath.randomlyDies(30, Gender.MALE)); + assertFalse(result); } @Test - public void testRandomlyDies_AgeThresholdAbove() { - CampaignOptions mockedOptions = mock(CampaignOptions.class); - when(mockedOptions.getRandomDeathChance()).thenReturn(10); + void testRandomlyDies_NotDeath() { + // Mocking the Person object + when(mockedPerson.getAge(any())).thenReturn(30); + when(mockedPerson.getGender()).thenReturn(Gender.MALE); - RandomDeath randomDeath = new RandomDeath(mockedOptions) { - @Override - protected int randomInt(int bound) { - return 1; // Simulate NOT rolling a 0 - } - }; + // Mocking the Era object + Era mockedEra = mock(Era.class); + when(mockedEra.getFlags()).thenReturn(Set.of(STAR_LEAGUE)); - assertFalse(randomDeath.randomlyDies(95, Gender.MALE)); - } + // Mocking the Faction object + Faction mockedFaction = mock(Faction.class); + when(mockedFaction.isClan()).thenReturn(false); - @Test - public void testRandomlyDies_GenderFemaleMultiplier() { - CampaignOptions mockedOptions = mock(CampaignOptions.class); - when(mockedOptions.getRandomDeathChance()).thenReturn(10); + // Mocking the Campaign object + when(mockedCampaign.getEra()).thenReturn(mockedEra); + when(mockedCampaign.getFaction()).thenReturn(mockedFaction); + when(mockedCampaign.getLocalDate()).thenReturn(LocalDate.now()); - RandomDeath randomDeath = new RandomDeath(mockedOptions) { + // Create the RandomDeath object normally, then spy on it + RandomDeath realRandomDeath = new RandomDeath(mockedCampaign) { @Override protected int randomInt(int bound) { - return 0; // Simulate rolling a 0 (death) + return 1000; // Simulate rolling a 1000 } }; - assertTrue(randomDeath.randomlyDies(30, Gender.FEMALE)); - } + RandomDeath randomDeath = spy(realRandomDeath); - @Test - public void testRandomlyDies_AdjustedDieSizeAboveOne() { - CampaignOptions mockedOptions = mock(CampaignOptions.class); - when(mockedOptions.getRandomDeathChance()).thenReturn(20); // Base chance + // Ensure mocked methods return valid results + doReturn(1000.0).when(randomDeath).getBaseDeathChance(mockedPerson); + doReturn(1.0).when(randomDeath).getEraMultiplier(mockedEra); + doReturn(1.0).when(randomDeath).getFactionMultiplier(mockedFaction); + doReturn(1.0).when(randomDeath).getHealthModifier(mockedPerson); + doReturn(null).when(randomDeath).canDie(mockedPerson, true); - RandomDeath randomDeath = new RandomDeath(mockedOptions) { - @Override - protected int randomInt(int bound) { - return 5; // Simulate NOT rolling a 0 - } - }; + // Use mocked CampaignOptions + when(mockedCampaignOptions.getRandomDeathMultiplier()).thenReturn(1.0); + when(mockedCampaign.getCampaignOptions()).thenReturn(mockedCampaignOptions); + + // Act + boolean result = randomDeath.randomlyDies(mockedPerson); - assertFalse(randomDeath.randomlyDies(80, Gender.MALE)); + // Assert + assertFalse(result); } @Test - public void testRandomlyDies_AgeThresholdGuaranteesDeath() { - CampaignOptions mockedOptions = mock(CampaignOptions.class); - when(mockedOptions.getRandomDeathChance()).thenReturn(10); + void testRandomlyDies_Dies() { + // Mocking the Person object + when(mockedPerson.getAge(any())).thenReturn(30); + when(mockedPerson.getGender()).thenReturn(Gender.MALE); + + // Mocking the Era object + Era mockedEra = mock(Era.class); + when(mockedEra.getFlags()).thenReturn(Set.of(STAR_LEAGUE)); + + // Mocking the Faction object + Faction mockedFaction = mock(Faction.class); + when(mockedFaction.isClan()).thenReturn(false); - RandomDeath randomDeath = new RandomDeath(mockedOptions) { + // Mocking the Campaign object + when(mockedCampaign.getEra()).thenReturn(mockedEra); + when(mockedCampaign.getFaction()).thenReturn(mockedFaction); + when(mockedCampaign.getLocalDate()).thenReturn(LocalDate.now()); + + // Create the RandomDeath object normally, then spy on it + RandomDeath realRandomDeath = new RandomDeath(mockedCampaign) { @Override protected int randomInt(int bound) { - return 0; // Doesn't matter since death is guaranteed when adjustedDieSize <= 1 + return 1; // Simulate rolling a 1 } }; - // If adjustedDieSize = 1, death is guaranteed without a random roll - assertTrue(randomDeath.randomlyDies(150, Gender.MALE)); - } - - @Test - public void testProcessNewWeek_PersonCannotDie() { - // Mock setup - Person mockedPerson = mock(Person.class); - Campaign mockedCampaign = mock(Campaign.class); - LocalDate today = LocalDate.now(); - when(mockedPerson.getAge(today)).thenReturn(30); - when(mockedPerson.getGender()).thenReturn(Gender.MALE); - RandomDeath mockedRandomDeath = spy(new RandomDeath(mock(CampaignOptions.class))); - doReturn("Cannot die message").when(mockedRandomDeath).canDie(eq(mockedPerson), any(AgeGroup.class), eq(true)); - - // Call processNewWeek - boolean result = mockedRandomDeath.processNewWeek(mockedCampaign, today, mockedPerson); - - // Assertions - assertFalse(result); - } + RandomDeath randomDeath = spy(realRandomDeath); // Spy on the real object - @Test - public void testProcessNewWeek_RandomlyDies() { - // Mock setup - Person mockedPerson = mock(Person.class); - Campaign mockedCampaign = mock(Campaign.class); - LocalDate today = LocalDate.now(); - when(mockedPerson.getAge(today)).thenReturn(70); - when(mockedPerson.getGender()).thenReturn(Gender.MALE); + // Ensure mocked methods return valid results + doReturn(1000.0).when(randomDeath).getBaseDeathChance(mockedPerson); + doReturn(1.0).when(randomDeath).getEraMultiplier(mockedEra); + doReturn(1.0).when(randomDeath).getFactionMultiplier(mockedFaction); + doReturn(1.0).when(randomDeath).getHealthModifier(mockedPerson); + doReturn(null).when(randomDeath).canDie(mockedPerson, true); - RandomDeath mockedRandomDeath = spy(new RandomDeath(mock(CampaignOptions.class))); - doReturn(null).when(mockedRandomDeath).canDie(eq(mockedPerson), any(AgeGroup.class), eq(true)); - doReturn(true).when(mockedRandomDeath).randomlyDies(eq(70), eq(Gender.MALE)); - doReturn(PersonnelStatus.NATURAL_CAUSES).when(mockedRandomDeath).getCause(eq(mockedPerson), any(AgeGroup.class), eq(70)); + // Use mocked CampaignOptions + when(mockedCampaignOptions.getRandomDeathMultiplier()).thenReturn(1.0); + when(mockedCampaign.getCampaignOptions()).thenReturn(mockedCampaignOptions); - // Call processNewWeek - boolean result = mockedRandomDeath.processNewWeek(mockedCampaign, today, mockedPerson); + // Act + boolean result = randomDeath.randomlyDies(mockedPerson); - // Assertions + // Assert assertTrue(result); } - - @Test - public void testProcessNewWeek_RandomlySurvives() { - // Mock setup - Person mockedPerson = mock(Person.class); - Campaign mockedCampaign = mock(Campaign.class); - LocalDate today = LocalDate.now(); - when(mockedPerson.getAge(today)).thenReturn(50); - when(mockedPerson.getGender()).thenReturn(Gender.FEMALE); - - RandomDeath mockedRandomDeath = spy(new RandomDeath(mock(CampaignOptions.class))); - doReturn(null).when(mockedRandomDeath).canDie(eq(mockedPerson), any(AgeGroup.class), eq(true)); - doReturn(false).when(mockedRandomDeath).randomlyDies(eq(50), eq(Gender.FEMALE)); - - // Call processNewWeek - boolean result = mockedRandomDeath.processNewWeek(mockedCampaign, today, mockedPerson); - - // Assertions - assertFalse(result); - } } From d8878e56af4fd43ba9b0f013d6f4ec7f9c1bca1c Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sat, 1 Feb 2025 21:47:30 -0600 Subject: [PATCH 052/112] Refactored tests to use @BeforeEach instead of @BeforeAll Replaced the static @BeforeAll setup method with an instance-based @BeforeEach method for better test isolation and improved clarity. Adjusted initialization to ensure proper mocking and compatibility with the change. --- .../campaign/personnel/death/RandomDeathTest.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/MekHQ/unittests/mekhq/campaign/personnel/death/RandomDeathTest.java b/MekHQ/unittests/mekhq/campaign/personnel/death/RandomDeathTest.java index 294f41ef1bb..4b7278991fe 100644 --- a/MekHQ/unittests/mekhq/campaign/personnel/death/RandomDeathTest.java +++ b/MekHQ/unittests/mekhq/campaign/personnel/death/RandomDeathTest.java @@ -26,7 +26,7 @@ import mekhq.campaign.personnel.enums.PersonnelStatus; import mekhq.campaign.universe.Faction; import mekhq.campaign.universe.eras.Era; -import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.time.LocalDate; @@ -72,8 +72,8 @@ public class RandomDeathTest { private static Map ageGroups; - @BeforeAll - public static void beforeAll() { + @BeforeEach + public void beforeAll() { // Prep Age Groups ageGroups = Map.of( ELDER, true, @@ -85,14 +85,12 @@ public static void beforeAll() { BABY, true ); - // Mock Campaign and CampaignOptions mockedCampaign = mock(Campaign.class); mockedCampaignOptions = mock(CampaignOptions.class); - mockedToday = LocalDate.of(3025,1,1); + mockedToday = LocalDate.of(3025, 1, 1); mockedPerson = mock(Person.class); when(mockedCampaign.getCampaignOptions()).thenReturn(mockedCampaignOptions); - when(mockedCampaignOptions.getEnabledRandomDeathAgeGroups()).thenReturn(ageGroups); when(mockedCampaignOptions.isUseRandomDeathSuicideCause()).thenReturn(false); when(mockedCampaignOptions.getRandomDeathMultiplier()).thenReturn(1.0); From 1040848170a94ae1c3e082a038273aff5393566b Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sat, 1 Feb 2025 23:22:58 -0600 Subject: [PATCH 053/112] Renamed and updated Random Death Chance to Multiplier Replaced "Random Death Chance" with "Random Death Multiplier" for better clarity on its functionality. Updated associated labels, tooltips, and logic to reflect the change, and adjusted death determination calculations to use a consistent die size. Modified default values and presets to align with the new multiplier-based system. --- .../Random Death in MekHQ.pdf | Bin 0 -> 87301 bytes .../campaignPresets/CampaignOperations.xml | 2 +- .../CampaignOperationsStratCon.xml | 2 +- .../campaignPresets/NewPilotProgram.xml | 2 +- .../campaignPresets/TheCompleteExperience.xml | 2 +- .../CampaignOptionsDialog.properties | 11 ++++----- .../resources/mekhq/resources/GUI.properties | 2 +- .../campaign/personnel/death/RandomDeath.java | 16 ++++++------- .../contents/BiographyTab.java | 22 +++++++++--------- 9 files changed, 28 insertions(+), 31 deletions(-) create mode 100644 MekHQ/docs/Personnel Modules/Random Death in MekHQ.pdf diff --git a/MekHQ/docs/Personnel Modules/Random Death in MekHQ.pdf b/MekHQ/docs/Personnel Modules/Random Death in MekHQ.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4705c694c6819ac3acb705516d6ce6f557870a31 GIT binary patch literal 87301 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�q}iF=2u8!8f9aP=U61^3$3G1Y5}(_#XUY01cm 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 literal 0 HcmV?d00001 diff --git a/MekHQ/mmconf/campaignPresets/CampaignOperations.xml b/MekHQ/mmconf/campaignPresets/CampaignOperations.xml index 5d651300884..6b7c8d9cac9 100644 --- a/MekHQ/mmconf/campaignPresets/CampaignOperations.xml +++ b/MekHQ/mmconf/campaignPresets/CampaignOperations.xml @@ -440,7 +440,7 @@ false
false - 0 + 0.0 true true true diff --git a/MekHQ/mmconf/campaignPresets/CampaignOperationsStratCon.xml b/MekHQ/mmconf/campaignPresets/CampaignOperationsStratCon.xml index 07c45e7c981..6cd1e5d6a9e 100644 --- a/MekHQ/mmconf/campaignPresets/CampaignOperationsStratCon.xml +++ b/MekHQ/mmconf/campaignPresets/CampaignOperationsStratCon.xml @@ -440,7 +440,7 @@ false
false - 0 + 0.0 true true true diff --git a/MekHQ/mmconf/campaignPresets/NewPilotProgram.xml b/MekHQ/mmconf/campaignPresets/NewPilotProgram.xml index 2e87e7f8d41..72cee0522b1 100644 --- a/MekHQ/mmconf/campaignPresets/NewPilotProgram.xml +++ b/MekHQ/mmconf/campaignPresets/NewPilotProgram.xml @@ -440,7 +440,7 @@ false false - 6000 + 1.0 true true true diff --git a/MekHQ/mmconf/campaignPresets/TheCompleteExperience.xml b/MekHQ/mmconf/campaignPresets/TheCompleteExperience.xml index f0be11d3c82..6893a7ce618 100644 --- a/MekHQ/mmconf/campaignPresets/TheCompleteExperience.xml +++ b/MekHQ/mmconf/campaignPresets/TheCompleteExperience.xml @@ -441,7 +441,7 @@ false false - 6000 + 1.0 true true true diff --git a/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties b/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties index 659efb3137e..00d0f1a29c9 100644 --- a/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties +++ b/MekHQ/resources/mekhq/resources/CampaignOptionsDialog.properties @@ -802,13 +802,12 @@ lblExtraRandomOrigin.tooltip=Random origin is randomized to the planetary level lblDeathTab.text=Death Options \u270E lblUseRandomDeathSuicideCause.text=Enable Cause of Death: Suicide lblUseRandomDeathSuicideCause.tooltip=This includes suicide as a potential cause for a random death. -lblRandomDeathChance.text=Random Death Chance: \u26A0 1 in -lblRandomDeathChance.tooltip=This is the number of sides on the die rolled each week to determine\ - \ whether a character randomly dies. Death occurs on a roll of 1. Once a character reaches 100\ - \ years old, this roll becomes exponentially harder each year.\ +lblRandomDeathMultiplier.text=Random Death Multiplier \u2318 \u26A0 +lblRandomDeathMultiplier.tooltip=This multiplier is applied directly to a character's chance to\ + \ randomly die. The higher this value, the more likely characters will die from random causes.\
\ -
Warning: Setting this to 0 disabled Random Death. The default value of 6000 gives an\ - \ average life expectancy of 90. +
Warning: Setting this to 0 disables Random Death. The default value of 1.0 gives an\ + \ average life expectancy of 84 for men and 89 for women. # createDeathAgeGroupsPanel lblDeathAgeGroupsPanel.text=Death by Age Group diff --git a/MekHQ/resources/mekhq/resources/GUI.properties b/MekHQ/resources/mekhq/resources/GUI.properties index b9400436f62..0dc32f99a18 100644 --- a/MekHQ/resources/mekhq/resources/GUI.properties +++ b/MekHQ/resources/mekhq/resources/GUI.properties @@ -1058,7 +1058,7 @@ PersonnelTableModelColumn.CLAN_PERSONNEL.text=Clan Personnel PersonnelTableModelColumn.MARRIAGEABLE.text=Interested in Marriage PersonnelTableModelColumn.DIVORCEABLE.text=Divorceable PersonnelTableModelColumn.TRYING_TO_CONCEIVE.text=Interested in Children -PersonnelTableModelColumn.IMMORTAL.text=Immortal +PersonnelTableModelColumn.IMMORTAL.text=Excluded from Random Death PersonnelTableModelColumn.TOUGHNESS.text=Toughness PersonnelTableModelColumn.FATIGUE.text=Fatigue PersonnelTableModelColumn.EDGE.text=Edge diff --git a/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java b/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java index 7281f6fe3ae..49e6232acab 100644 --- a/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java +++ b/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java @@ -87,8 +87,8 @@ public class RandomDeath { // Base Chances private final List deathChances = List.of( - new RandomDeathChance(9, 15, 12), - new RandomDeathChance(19, 16, 14), + new RandomDeathChance(9, 4, 2), + new RandomDeathChance(19, 9, 4), new RandomDeathChance(29, 17, 8), new RandomDeathChance(39, 20, 10), new RandomDeathChance(49, 30, 18), @@ -97,7 +97,7 @@ public class RandomDeath { new RandomDeathChance(79, 385, 233), new RandomDeathChance(89, 1000, 714), new RandomDeathChance(99, 2500, 2000), - new RandomDeathChance(Integer.MAX_VALUE, 50, 3333) + new RandomDeathChance(Integer.MAX_VALUE, 5000, 3333) ); // Multipliers @@ -123,6 +123,8 @@ public class RandomDeath { private final double MEDICAL_MULTIPLIER_INJURY_PERMANENT = 0.25; // once no matter how many private final double MEDICAL_MULTIPLIER_HPG_ACCESS = -0.05; + // Die Size + private final int DIE_SIZE = 1000000; /** * Constructs a {@code RandomDeath} object using campaign-specific options. * @@ -143,10 +145,6 @@ public RandomDeath(final Campaign campaign) { initializeCauses(); } - List getDeathChances() { - return deathChances; - } - /** * Clears and reloads the random death causes from default and user-defined XML files. * @@ -356,7 +354,7 @@ public boolean randomlyDies(Person person) { return false; } - return randomInt(10000) < actualDeathChance; + return randomInt(DIE_SIZE) < actualDeathChance; } /** @@ -526,7 +524,7 @@ private double getHpgAccessMultiplier() { private double getInjuryModifier(Person person) { if (!campaignOptions.isUseAdvancedMedical()) { // Simplified injury multiplier without advanced medical care - return MEDICAL_MULTIPLIER_INJURY_TRANSIENT * person.getHits(); + return 1 + (MEDICAL_MULTIPLIER_INJURY_TRANSIENT * person.getHits()); } // Advanced medical care: calculate based on individual injuries diff --git a/MekHQ/src/mekhq/gui/campaignOptions/contents/BiographyTab.java b/MekHQ/src/mekhq/gui/campaignOptions/contents/BiographyTab.java index fb2db1537f9..345b5c7eef0 100644 --- a/MekHQ/src/mekhq/gui/campaignOptions/contents/BiographyTab.java +++ b/MekHQ/src/mekhq/gui/campaignOptions/contents/BiographyTab.java @@ -108,8 +108,8 @@ public class BiographyTab { //start Death Tab private JCheckBox chkUseRandomDeathSuicideCause; - private JLabel lblRandomDeathChance; - private JSpinner spnRandomDeathChance; + private JLabel lblRandomDeathMultiplier; + private JSpinner spnRandomDeathMultiplier; private JPanel pnlDeathAgeGroup; private Map chkEnabledRandomDeathAgeGroups; @@ -275,8 +275,8 @@ private void initializeEducationTab() { */ private void initializeDeathTab() { chkUseRandomDeathSuicideCause = new JCheckBox(); - lblRandomDeathChance = new JLabel(); - spnRandomDeathChance = new JSpinner(); + lblRandomDeathMultiplier = new JLabel(); + spnRandomDeathMultiplier = new JSpinner(); pnlDeathAgeGroup = new JPanel(); chkEnabledRandomDeathAgeGroups = new HashMap<>(); @@ -744,9 +744,9 @@ public JPanel createDeathTab() { getImageDirectory() + "logo_clan_fire_mandrills.png"); // Contents - lblRandomDeathChance = new CampaignOptionsLabel("RandomDeathChance"); - spnRandomDeathChance = new CampaignOptionsSpinner("RandomDeathChance", - 6000, 1, 10000, 1); + lblRandomDeathMultiplier = new CampaignOptionsLabel("RandomDeathMultiplier"); + spnRandomDeathMultiplier = new CampaignOptionsSpinner("RandomDeathMultiplier", + 1.0, 0, 100., 0.01); chkUseRandomDeathSuicideCause = new CampaignOptionsCheckBox("UseRandomDeathSuicideCause"); @@ -759,9 +759,9 @@ public JPanel createDeathTab() { layoutLeft.gridy = 0; layoutLeft.gridx = 0; layoutLeft.gridwidth = 1; - panelLeft.add(lblRandomDeathChance, layoutLeft); + panelLeft.add(lblRandomDeathMultiplier, layoutLeft); layoutLeft.gridx++; - panelLeft.add(spnRandomDeathChance, layoutLeft); + panelLeft.add(spnRandomDeathMultiplier, layoutLeft); layoutLeft.gridx = 0; layoutLeft.gridy++; @@ -1297,7 +1297,7 @@ public void loadValuesFromCampaignOptions(@Nullable CampaignOptions presetCampai // Death chkUseRandomDeathSuicideCause.setSelected(options.isUseRandomDeathSuicideCause()); - spnRandomDeathChance.setValue(options.getRandomDeathMultiplier()); + spnRandomDeathMultiplier.setValue(options.getRandomDeathMultiplier()); Map deathAgeGroups = options.getEnabledRandomDeathAgeGroups(); for (final AgeGroup ageGroup : AgeGroup.values()) { @@ -1386,7 +1386,7 @@ public void applyCampaignOptionsToCampaign(@Nullable CampaignOptions presetCampa // Death options.setUseRandomDeathSuicideCause(chkUseRandomDeathSuicideCause.isSelected()); - options.setRandomDeathMultiplier((int) spnRandomDeathChance.getValue()); + options.setRandomDeathMultiplier((int) spnRandomDeathMultiplier.getValue()); for (final AgeGroup ageGroup : AgeGroup.values()) { options.getEnabledRandomDeathAgeGroups().put(ageGroup, chkEnabledRandomDeathAgeGroups.get(ageGroup).isSelected()); From 4e9bddc7686e4066b7816df2f73376661bb3e750 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sun, 2 Feb 2025 12:06:15 -0600 Subject: [PATCH 054/112] Add Canopus-specific death chance multiplier Introduced a new faction multiplier for the Magistracy of Canopus with a unique value of 0.85. Updated the `getFactionMultiplier` method to apply this multiplier using string comparison for faction identification. Also refactored imports to use wildcard for improved readability. --- .../Random Death in MekHQ.pdf | Bin 87301 -> 87416 bytes .../campaign/personnel/death/RandomDeath.java | 14 ++++++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/MekHQ/docs/Personnel Modules/Random Death in MekHQ.pdf b/MekHQ/docs/Personnel Modules/Random Death in MekHQ.pdf index 4705c694c6819ac3acb705516d6ce6f557870a31..bb327f7f552893e6afd44d2f22d20a0cb5f06ae3 100644 GIT binary patch delta 28439 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 U(x8ngOL&tiAt$Gdnk?bp0GpK<>Hq)$ delta 28263 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 F{vWyIrP2TZ diff --git a/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java b/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java index 49e6232acab..d7dca29d92b 100644 --- a/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java +++ b/MekHQ/src/mekhq/campaign/personnel/death/RandomDeath.java @@ -35,6 +35,7 @@ import mekhq.campaign.personnel.enums.PersonnelStatus; import mekhq.campaign.personnel.enums.TenYearAgeRange; import mekhq.campaign.universe.Faction; +import mekhq.campaign.universe.Factions; import mekhq.campaign.universe.enums.EraFlag; import mekhq.campaign.universe.eras.Era; import mekhq.utilities.MHQXMLUtility; @@ -47,10 +48,7 @@ import java.io.FileInputStream; import java.io.InputStream; import java.time.LocalDate; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import static mekhq.campaign.personnel.enums.TenYearAgeRange.determineAgeRange; import static mekhq.campaign.universe.enums.EraFlag.*; @@ -118,6 +116,7 @@ public class RandomDeath { private final double FACTION_MULTIPLIER_PERIPHERY_DEEP = 1.30; private final double FACTION_MULTIPLIER_PIRATE = 1.35; private final double FACTION_MULTIPLIER_MERCENARY = 1.00; + private final double FACTION_MULTIPLIER_CANOPUS = 0.85; private final double MEDICAL_MULTIPLIER_INJURY_TRANSIENT = 0.1; // per injury private final double MEDICAL_MULTIPLIER_INJURY_PERMANENT = 0.25; // once no matter how many @@ -446,6 +445,13 @@ public boolean randomlyDies(Person person) { * @return the death chance multiplier specific to the provided faction. */ double getFactionMultiplier(Faction faction) { + // We have to use String Comparison here due to how + Faction canopus = Factions.getInstance().getFaction("MOC"); + + if (Objects.equals(faction, canopus)) { + return FACTION_MULTIPLIER_CANOPUS; + } + if (faction.isClan()) { return FACTION_MULTIPLIER_CLANS; } From cd323b0c6aca44a7cceaff1b9994b3769c4edb7e Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sun, 2 Feb 2025 16:30:56 -0600 Subject: [PATCH 055/112] Update rankSystems version to 0.50.04-SNAPSHOT Updated the version attribute in ranks.xml to reflect the new version, 0.50.04-SNAPSHOT. This ensures compatibility with the latest changes and versioning consistency across the project. --- MekHQ/userdata/data/universe/ranks.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MekHQ/userdata/data/universe/ranks.xml b/MekHQ/userdata/data/universe/ranks.xml index f8d8b8591d3..7ae3bcbce24 100644 --- a/MekHQ/userdata/data/universe/ranks.xml +++ b/MekHQ/userdata/data/universe/ranks.xml @@ -1,3 +1,3 @@ - + From 8cff324dfa11650c6cff8edd1305bb9b613fb0df Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sun, 2 Feb 2025 19:00:59 -0600 Subject: [PATCH 056/112] Converted Honor Rating into an Enum - Replaced hardcoded honor rating logic with an `HonorRating` enum for better readability and maintainability. - Moved honor rating computation to the `Faction` class, simplifying related code in `AtBDynamicScenarioFactory`. --- .../mission/AtBDynamicScenarioFactory.java | 68 ++----------------- .../src/mekhq/campaign/universe/Faction.java | 64 ++++++++++++++--- .../campaign/universe/enums/HonorRating.java | 44 ++++++++++++ 3 files changed, 106 insertions(+), 70 deletions(-) create mode 100644 MekHQ/src/mekhq/campaign/universe/enums/HonorRating.java diff --git a/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java b/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java index 62de083eb35..afaf71c706a 100644 --- a/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java +++ b/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java @@ -58,6 +58,7 @@ import mekhq.campaign.universe.*; import mekhq.campaign.universe.Faction.Tag; import mekhq.campaign.universe.enums.EraFlag; +import mekhq.campaign.universe.enums.HonorRating; import mekhq.campaign.universe.fameAndInfamy.BatchallFactions; import java.io.File; @@ -97,10 +98,6 @@ public class AtBDynamicScenarioFactory { // indexed by dragoons rating private static final int[] infantryToBAUpgradeTNs = { 12, 10, 8, 6, 4, 2 }; - private static final double STRICT = 0.75; - private static final double OPPORTUNISTIC = 1.0; - private static final double LIBERAL = 1.25; - private static final int REINFORCEMENT_ARRIVAL_SCALE = 15; private static final ResourceBundle resources = ResourceBundle.getBundle( @@ -985,7 +982,7 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac logger.info(String.format("Base bidding budget is %s BV2. This is seed force" + " multiplied by scenario force multiplier", forceBVBudget)); - forceBVBudget = (int) round(forceBVBudget * getHonorRating(campaign, factionCode)); + forceBVBudget = (int) round(forceBVBudget * faction.getHonorRating(campaign).getBvMultiplier()); logger.info(String.format("Honor Rating changed it to %s BV2", forceBVBudget)); @@ -1106,7 +1103,7 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac && BatchallFactions.usesBatchalls(factionCode) && contract.isBatchallAccepted()) { reportResultsOfBidding(campaign, bidAwayForces, generatedForce, supplementedForces, - factionCode); + faction); } } @@ -1205,47 +1202,6 @@ private static int getBattleValue(Campaign campaign, Entity entity) { return battleValue; } - /** - * Calculates the honor rating for a given Clan. - * - * @param campaign the ongoing campaign - * @param factionCode the faction code for which to calculate honor rating - * @return the honor rating as a double value - */ - private static double getHonorRating(Campaign campaign, String factionCode) { - // Our research showed the post-Invasion shift in Clan doctrine to occur between 3053 and 3055 - boolean isPostInvasion = campaign.getLocalDate().getYear() >= 3053 + randomInt(2); - - // This is based on the table found on page 274 of Total Warfare - // Any Clan not mentioned on that table is assumed to be Strict → Opportunistic - return switch (factionCode) { - case "CCC", "CHH", "CIH", "CNC", "CSR" -> OPPORTUNISTIC; - case "CCO", "CGS", "CSV" -> STRICT; - case "CGB", "CWIE" -> { - if (isPostInvasion) { - yield LIBERAL; - } else { - yield STRICT; - } - } - case "CDS" -> LIBERAL; - case "CW" -> { - if (isPostInvasion) { - yield LIBERAL; - } else { - yield OPPORTUNISTIC; - } - } - default -> { - if (isPostInvasion) { - yield OPPORTUNISTIC; - } else { - yield STRICT; - } - } - }; - } - /** * Reports the results of Clan bidding for a scenario. * @@ -1256,21 +1212,11 @@ private static double getHonorRating(Campaign campaign, String factionCode) { */ private static void reportResultsOfBidding(Campaign campaign, List bidAwayForces, BotForce generatedForce, int supplementedForces, - String factionCode) { - double honor = getHonorRating(campaign, factionCode); - String honorLevel; - - if (honor == STRICT) { - honorLevel = "STRICT"; - } else if (honor == OPPORTUNISTIC) { - honorLevel = "OPPORTUNISTIC"; - } else { - honorLevel = "LIBERAL"; - } + Faction faction) { + HonorRating honorRating = faction.getHonorRating(campaign); - logger.info(String.format("The honor of %s is rated as %s", - Factions.getInstance().getFaction(factionCode).getFullName(campaign.getGameYear()), - honorLevel)); + logger.info("The honor of {} is rated as {}", faction.getFullName(campaign.getGameYear()), + honorRating); boolean useVerboseBidding = campaign.getCampaignOptions().isUseVerboseBidding(); StringBuilder report = new StringBuilder(); diff --git a/MekHQ/src/mekhq/campaign/universe/Faction.java b/MekHQ/src/mekhq/campaign/universe/Faction.java index 2bc0891a126..4335bbdcb9d 100644 --- a/MekHQ/src/mekhq/campaign/universe/Faction.java +++ b/MekHQ/src/mekhq/campaign/universe/Faction.java @@ -21,20 +21,26 @@ */ package mekhq.campaign.universe; -import java.awt.Color; -import java.time.LocalDate; -import java.util.*; -import java.util.Map.Entry; - -import org.w3c.dom.DOMException; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - import megamek.common.annotations.Nullable; import megamek.logging.MMLogger; import mekhq.Utilities; import mekhq.campaign.Campaign; +import mekhq.campaign.universe.enums.HonorRating; import mekhq.utilities.MHQXMLUtility; +import org.w3c.dom.DOMException; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.awt.*; +import java.time.LocalDate; +import java.util.List; +import java.util.*; +import java.util.Map.Entry; + +import static megamek.common.Compute.randomInt; +import static mekhq.campaign.universe.enums.HonorRating.LIBERAL; +import static mekhq.campaign.universe.enums.HonorRating.OPPORTUNISTIC; +import static mekhq.campaign.universe.enums.HonorRating.STRICT; /** * @author Jay Lawson (jaylawson39 at yahoo.com) @@ -508,4 +514,44 @@ public enum Tag { /** Faction is lenient with mercenary command rights (Camops p. 42) */ LENIENT } + + /** + * Calculates the honor rating for a given Clan. + * + * @param campaign the ongoing campaign + * @return the honor rating as an {@link HonorRating} enum + */ + public HonorRating getHonorRating(Campaign campaign) { + // Our research showed the post-Invasion shift in Clan doctrine to occur between 3053 and 3055 + boolean isPostInvasion = campaign.getLocalDate().getYear() >= 3053 + randomInt(2); + + // This is based on the table found on page 274 of Total Warfare + // Any Clan not mentioned on that table is assumed to be Strict → Opportunistic + return switch (shortName) { + case "CCC", "CHH", "CIH", "CNC", "CSR" -> OPPORTUNISTIC; + case "CCO", "CGS", "CSV" -> STRICT; + case "CGB", "CWIE" -> { + if (isPostInvasion) { + yield LIBERAL; + } else { + yield STRICT; + } + } + case "CDS" -> LIBERAL; + case "CW" -> { + if (isPostInvasion) { + yield LIBERAL; + } else { + yield OPPORTUNISTIC; + } + } + default -> { + if (isPostInvasion) { + yield OPPORTUNISTIC; + } else { + yield STRICT; + } + } + }; + } } diff --git a/MekHQ/src/mekhq/campaign/universe/enums/HonorRating.java b/MekHQ/src/mekhq/campaign/universe/enums/HonorRating.java new file mode 100644 index 00000000000..38f037b50c2 --- /dev/null +++ b/MekHQ/src/mekhq/campaign/universe/enums/HonorRating.java @@ -0,0 +1,44 @@ +package mekhq.campaign.universe.enums; + +/** + * Represents the honor of a Clan + */ +public enum HonorRating { + NONE(0.0, Integer.MAX_VALUE), + LIBERAL(1.25, Integer.MAX_VALUE), + OPPORTUNISTIC(1.0, 5), + STRICT(0.75, 0); + + private final double bvMultiplier; + private final int bondsmanTargetNumber; + + /** + * Constructor for HonorRating enum to initialize its properties. + * + * @param bvMultiplier Battle Value multiplier associated with the honor level - used by + * Clan Bidding + * @param bondsmanTargetNumber Target number for determining bondsmen with this style + */ + HonorRating(double bvMultiplier, int bondsmanTargetNumber) { + this.bvMultiplier = bvMultiplier; + this.bondsmanTargetNumber = bondsmanTargetNumber; + } + + /** + * Gets the Battle Value multiplier associated with this capture style. + * + * @return the bvMultiplier + */ + public double getBvMultiplier() { + return bvMultiplier; + } + + /** + * Gets the target number for becoming a bondsman. + * + * @return the bondsmanTargetNumber + */ + public int getBondsmanTargetNumber() { + return bondsmanTargetNumber; + } +} From d67c293e3b3d484e43fc3f43254899cefdba8d02 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sun, 2 Feb 2025 19:04:52 -0600 Subject: [PATCH 057/112] Add updated copyright notices for MekHQ files Updated copyright headers in HonorRating.java and Faction.java to reflect the current years of 2025. Ensured consistency with licensing and copyright information across the files. --- MekHQ/src/mekhq/campaign/universe/Faction.java | 2 +- .../campaign/universe/enums/HonorRating.java | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/MekHQ/src/mekhq/campaign/universe/Faction.java b/MekHQ/src/mekhq/campaign/universe/Faction.java index 4335bbdcb9d..48ac4b26b15 100644 --- a/MekHQ/src/mekhq/campaign/universe/Faction.java +++ b/MekHQ/src/mekhq/campaign/universe/Faction.java @@ -2,7 +2,7 @@ * Faction.java * * Copyright (c) 2009 - Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved. - * Copyright (c) 2009-2021 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2009-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * diff --git a/MekHQ/src/mekhq/campaign/universe/enums/HonorRating.java b/MekHQ/src/mekhq/campaign/universe/enums/HonorRating.java index 38f037b50c2..52121f2cd95 100644 --- a/MekHQ/src/mekhq/campaign/universe/enums/HonorRating.java +++ b/MekHQ/src/mekhq/campaign/universe/enums/HonorRating.java @@ -1,3 +1,21 @@ +/* + * 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 as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with MekHQ. If not, see . + */ package mekhq.campaign.universe.enums; /** From 7a387082656a30356f6416924f94bdb06a5b8476 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sun, 2 Feb 2025 19:33:03 -0600 Subject: [PATCH 058/112] Refactored `PrisonerStatus` Enum and Reorganized Related Code - Moved `PrisonerStatus` enum to a new package for `randomEvents` as this is where the prisoner code will be living once the upcoming prisoner rework is merged. - Adjusted all references accordingly - Added new test cases and updating resource bundles. --- .../mekhq/resources/PrisonerStatus.properties | 10 ++ MekHQ/src/mekhq/campaign/Campaign.java | 7 +- MekHQ/src/mekhq/campaign/CampaignOptions.java | 4 +- .../campaign/ResolveScenarioTracker.java | 4 +- .../src/mekhq/campaign/personnel/Person.java | 1 + .../personnel/enums/PrisonerStatus.java | 134 ------------------ .../personnel/marriage/AbstractMarriage.java | 2 +- .../procreation/AbstractProcreation.java | 6 +- .../prisoners/enums/PrisonerStatus.java | 102 +++++++++++++ .../adapter/PersonnelTableMouseAdapter.java | 1 + .../contents/PersonnelTab.java | 6 +- .../gui/sorter/PrisonerStatusSorter.java | 2 +- .../mekhq/campaign/personnel/PersonTest.java | 27 ++-- .../personnel/death/AbstractDeathTest.java | 9 +- .../divorce/AbstractDivorceTest.java | 2 +- .../personnel/enums/PrisonerStatusTest.java | 125 ---------------- .../marriage/AbstractMarriageTest.java | 2 +- .../procreation/AbstractProcreationTest.java | 2 +- .../prisoners/enums/PrisonerStatusTest.java | 88 ++++++++++++ .../UntreatedPersonnelNagLogicTest.java | 2 +- 20 files changed, 244 insertions(+), 292 deletions(-) create mode 100644 MekHQ/resources/mekhq/resources/PrisonerStatus.properties delete mode 100644 MekHQ/src/mekhq/campaign/personnel/enums/PrisonerStatus.java create mode 100644 MekHQ/src/mekhq/campaign/randomEvents/prisoners/enums/PrisonerStatus.java delete mode 100644 MekHQ/unittests/mekhq/campaign/personnel/enums/PrisonerStatusTest.java create mode 100644 MekHQ/unittests/mekhq/campaign/randomEvents/prisoners/enums/PrisonerStatusTest.java diff --git a/MekHQ/resources/mekhq/resources/PrisonerStatus.properties b/MekHQ/resources/mekhq/resources/PrisonerStatus.properties new file mode 100644 index 00000000000..86191885a45 --- /dev/null +++ b/MekHQ/resources/mekhq/resources/PrisonerStatus.properties @@ -0,0 +1,10 @@ +FREE.label=Free +FREE.titleExtension= +PRISONER.label=Prisoner +PRISONER.titleExtension=Prisoner +PRISONER_DEFECTOR.label=Prisoner (willing to defect) +PRISONER_DEFECTOR.titleExtension=Prisoner* +BECOMING_BONDSMAN.label=Becoming Bondsman +BECOMING_BONDSMAN.titleExtension=Becoming Bondsman +BONDSMAN.label=Bondsman +BONDSMAN.titleExtension=Bondsman \ No newline at end of file diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index d9af7a7c8dc..17b95212340 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -102,6 +102,7 @@ import mekhq.campaign.personnel.ranks.Ranks; import mekhq.campaign.personnel.turnoverAndRetention.Fatigue; import mekhq.campaign.personnel.turnoverAndRetention.RetirementDefectionTracker; +import mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus; import mekhq.campaign.rating.CamOpsReputation.ReputationController; import mekhq.campaign.rating.FieldManualMercRevDragoonsRating; import mekhq.campaign.rating.IUnitRating; @@ -9160,7 +9161,7 @@ public void writePartInUseMapToXML(final PrintWriter pw, int indent) { } } - /** + /** * Wipes the Parts in use map for the purpose of resetting all values to their default */ public void wipePartsInUseMap() { @@ -9186,13 +9187,13 @@ public ImageIcon getCampaignFactionIcon() { } return icon; } - + /** * Checks if another active scenario has this scenarioID as it's linkedScenarioID and returns true if it finds one. */ public boolean checkLinkedScenario(int scenarioID) { for (Scenario scenario : getScenarios()) { - if ((scenario.getLinkedScenario() == scenarioID) + if ((scenario.getLinkedScenario() == scenarioID) && (getScenario(scenario.getId()).getStatus().isCurrent())) { return true; } diff --git a/MekHQ/src/mekhq/campaign/CampaignOptions.java b/MekHQ/src/mekhq/campaign/CampaignOptions.java index 987c9d329fa..3c6d8e4e622 100644 --- a/MekHQ/src/mekhq/campaign/CampaignOptions.java +++ b/MekHQ/src/mekhq/campaign/CampaignOptions.java @@ -20,15 +20,12 @@ package mekhq.campaign; import megamek.Version; -import megamek.client.ui.swing.GUIPreferences; import megamek.codeUtilities.MathUtility; -import megamek.common.Configuration; import megamek.common.EquipmentType; import megamek.common.TechConstants; import megamek.common.enums.SkillLevel; import megamek.common.preference.ClientPreferences; import megamek.common.preference.PreferenceManager; -import megamek.common.util.fileUtils.MegaMekFile; import megamek.logging.MMLogger; import mekhq.MekHQ; import mekhq.Utilities; @@ -43,6 +40,7 @@ import mekhq.campaign.parts.enums.PartRepairType; import mekhq.campaign.personnel.Skills; import mekhq.campaign.personnel.enums.*; +import mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus; import mekhq.campaign.rating.UnitRatingMethod; import mekhq.service.mrms.MRMSOption; import mekhq.utilities.MHQXMLUtility; diff --git a/MekHQ/src/mekhq/campaign/ResolveScenarioTracker.java b/MekHQ/src/mekhq/campaign/ResolveScenarioTracker.java index f0c13dae0fb..e5ad9036f8e 100644 --- a/MekHQ/src/mekhq/campaign/ResolveScenarioTracker.java +++ b/MekHQ/src/mekhq/campaign/ResolveScenarioTracker.java @@ -24,13 +24,13 @@ import megamek.client.IClient; import megamek.common.*; import megamek.common.annotations.Nullable; +import megamek.common.autoresolve.acar.SimulatedClient; import megamek.common.event.PostGameResolution; import megamek.common.loaders.EntityLoadingException; import megamek.common.options.OptionsConstants; import megamek.logging.MMLogger; import mekhq.MekHQ; import mekhq.Utilities; -import megamek.common.autoresolve.acar.SimulatedClient; import mekhq.campaign.event.PersonBattleFinishedEvent; import mekhq.campaign.finances.Money; import mekhq.campaign.finances.enums.TransactionType; @@ -41,8 +41,8 @@ import mekhq.campaign.parts.Part; import mekhq.campaign.personnel.Person; import mekhq.campaign.personnel.enums.PersonnelStatus; -import mekhq.campaign.personnel.enums.PrisonerStatus; import mekhq.campaign.personnel.turnoverAndRetention.Fatigue; +import mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus; import mekhq.campaign.unit.TestUnit; import mekhq.campaign.unit.Unit; import mekhq.campaign.unit.actions.AdjustLargeCraftAmmoAction; diff --git a/MekHQ/src/mekhq/campaign/personnel/Person.java b/MekHQ/src/mekhq/campaign/personnel/Person.java index 168def2bc5e..80cfed17453 100644 --- a/MekHQ/src/mekhq/campaign/personnel/Person.java +++ b/MekHQ/src/mekhq/campaign/personnel/Person.java @@ -56,6 +56,7 @@ import mekhq.campaign.personnel.ranks.RankSystem; import mekhq.campaign.personnel.ranks.RankValidator; import mekhq.campaign.personnel.ranks.Ranks; +import mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus; import mekhq.campaign.unit.Unit; import mekhq.campaign.universe.Faction; import mekhq.campaign.universe.Factions; diff --git a/MekHQ/src/mekhq/campaign/personnel/enums/PrisonerStatus.java b/MekHQ/src/mekhq/campaign/personnel/enums/PrisonerStatus.java deleted file mode 100644 index 036e1ff0221..00000000000 --- a/MekHQ/src/mekhq/campaign/personnel/enums/PrisonerStatus.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright (c) 2020-2022 - 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 as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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. - * - * You should have received a copy of the GNU General Public License - * along with MekHQ. If not, see . - */ -package mekhq.campaign.personnel.enums; - -import java.util.ResourceBundle; - -import megamek.logging.MMLogger; -import mekhq.MekHQ; - -public enum PrisonerStatus { - // region Enum Declarations - /** - * This is used for personnel who are not (currently) prisoners - */ - FREE("PrisonerStatus.FREE.text", "PrisonerStatus.FREE.titleExtension"), - /** - * This is used to track standard personnel who are prisoners and not willing to - * defect - */ - PRISONER("PrisonerStatus.PRISONER.text", "PrisonerStatus.PRISONER.titleExtension"), - /** - * This is used to track standard personnel who are prisoners and are willing to - * defect - */ - PRISONER_DEFECTOR("PrisonerStatus.PRISONER_DEFECTOR.text", "PrisonerStatus.PRISONER_DEFECTOR.titleExtension"), - /** - * This is used to track clan personnel who become Bondsmen when captured - */ - BONDSMAN("PrisonerStatus.BONDSMAN.text", "PrisonerStatus.BONDSMAN.titleExtension"); - // endregion Enum Declarations - - // region Variable Declarations - private final String name; - private final String titleExtension; - // endregion Variable Declarations - - // region Constructors - PrisonerStatus(final String name, final String titleExtension) { - final ResourceBundle resources = ResourceBundle.getBundle("mekhq.resources.Personnel", - MekHQ.getMHQOptions().getLocale()); - this.name = resources.getString(name); - this.titleExtension = resources.getString(titleExtension); - } - // endregion Constructors - - // region Getters - public String getTitleExtension() { - return titleExtension; - } - // endregion Getters - - // region Boolean Comparison Methods - public boolean isFree() { - return this == FREE; - } - - public boolean isFreeOrBondsman() { - return isFree() || isBondsman(); - } - - public boolean isPrisoner() { - return this == PRISONER; - } - - public boolean isPrisonerDefector() { - return this == PRISONER_DEFECTOR; - } - - public boolean isBondsman() { - return this == BONDSMAN; - } - - public boolean isCurrentPrisoner() { - return isPrisoner() || isPrisonerDefector(); - } - // endregion Boolean Comparison Methods - - // region File I/O - /** - * @param text The saved value to parse, either the older magic number save - * format or the - * PrisonerStatus.name() value - * @return the Prisoner Status in question - */ - public static PrisonerStatus parseFromString(final String text) { - try { - return valueOf(text); - } catch (Exception ignored) { - - } - - // Magic Number Save Format - try { - switch (Integer.parseInt(text)) { - case 0: - return FREE; - case 1: - return PRISONER; - case 2: - return BONDSMAN; - default: - break; - } - } catch (Exception ignored) { - - } - - MMLogger.create(PrisonerStatus.class) - .error("Unable to parse " + text + " into a PrisonerStatus. Returning FREE."); - return FREE; - } - // endregion File I/O - - @Override - public String toString() { - return name; - } -} diff --git a/MekHQ/src/mekhq/campaign/personnel/marriage/AbstractMarriage.java b/MekHQ/src/mekhq/campaign/personnel/marriage/AbstractMarriage.java index ff7be2c4977..79ec938a1ce 100644 --- a/MekHQ/src/mekhq/campaign/personnel/marriage/AbstractMarriage.java +++ b/MekHQ/src/mekhq/campaign/personnel/marriage/AbstractMarriage.java @@ -29,8 +29,8 @@ import mekhq.campaign.log.PersonalLogger; import mekhq.campaign.personnel.Person; import mekhq.campaign.personnel.enums.MergingSurnameStyle; -import mekhq.campaign.personnel.enums.PrisonerStatus; import mekhq.campaign.personnel.enums.RandomMarriageMethod; +import mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus; import java.time.LocalDate; import java.util.ArrayList; diff --git a/MekHQ/src/mekhq/campaign/personnel/procreation/AbstractProcreation.java b/MekHQ/src/mekhq/campaign/personnel/procreation/AbstractProcreation.java index 4891a960b2a..9d90e4c2485 100644 --- a/MekHQ/src/mekhq/campaign/personnel/procreation/AbstractProcreation.java +++ b/MekHQ/src/mekhq/campaign/personnel/procreation/AbstractProcreation.java @@ -33,8 +33,12 @@ import mekhq.campaign.log.PersonalLogger; import mekhq.campaign.personnel.Person; import mekhq.campaign.personnel.PersonnelOptions; -import mekhq.campaign.personnel.enums.*; +import mekhq.campaign.personnel.enums.FamilialRelationshipType; +import mekhq.campaign.personnel.enums.GenderDescriptors; +import mekhq.campaign.personnel.enums.PersonnelStatus; +import mekhq.campaign.personnel.enums.RandomProcreationMethod; import mekhq.campaign.personnel.enums.education.EducationLevel; +import mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus; import mekhq.campaign.universe.Faction; import mekhq.campaign.universe.Planet; diff --git a/MekHQ/src/mekhq/campaign/randomEvents/prisoners/enums/PrisonerStatus.java b/MekHQ/src/mekhq/campaign/randomEvents/prisoners/enums/PrisonerStatus.java new file mode 100644 index 00000000000..1b4205c2b2d --- /dev/null +++ b/MekHQ/src/mekhq/campaign/randomEvents/prisoners/enums/PrisonerStatus.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2020-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 as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with MekHQ. If not, see . + */ +package mekhq.campaign.randomEvents.prisoners.enums; + +import megamek.logging.MMLogger; + +import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; + +public enum PrisonerStatus { + // region Enum Declarations + FREE, + PRISONER, + PRISONER_DEFECTOR, + BONDSMAN, + BECOMING_BONDSMAN; + // endregion Enum Declarations + + final private String RESOURCE_BUNDLE = "mekhq.resources.PrisonerStatus"; + + // region Getters + public String getLabel() { + final String RESOURCE_KEY = name() + ".label"; + + return getFormattedTextAt(RESOURCE_BUNDLE, RESOURCE_KEY); + } + + public String getTitleExtension() { + final String RESOURCE_KEY = name() + ".titleExtension"; + + return getFormattedTextAt(RESOURCE_BUNDLE, RESOURCE_KEY); + } + // endregion Getters + + // region Boolean Comparison Methods + public boolean isFree() { + return this == FREE; + } + + public boolean isFreeOrBondsman() { + return isFree() || isBondsman(); + } + + public boolean isPrisoner() { + return this == PRISONER; + } + + public boolean isPrisonerDefector() { + return this == PRISONER_DEFECTOR; + } + + public boolean isBecomingBondsman() { + return this == BECOMING_BONDSMAN; + } + + public boolean isBondsman() { + return this == BONDSMAN; + } + + public boolean isCurrentPrisoner() { + return isPrisoner() || isPrisonerDefector(); + } + // endregion Boolean Comparison Methods + + // region File I/O + /** + * @param text The saved value to parse, either the older magic number save + * format or the + * PrisonerStatus.name() value + * @return the Prisoner Status in question + */ + public static PrisonerStatus parseFromString(final String text) { + try { + return valueOf(text); + } catch (Exception ignored) { + MMLogger.create(PrisonerStatus.class) + .error("Unable to parse {} into a PrisonerStatus. Returning {}.", text, FREE); + return FREE; + } + } + // endregion File I/O + + @Override + public String toString() { + return getLabel(); + } +} diff --git a/MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java b/MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java index cbe371ec561..473e02b2fb6 100644 --- a/MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java +++ b/MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java @@ -55,6 +55,7 @@ import mekhq.campaign.personnel.ranks.RankSystem; import mekhq.campaign.personnel.ranks.RankValidator; import mekhq.campaign.personnel.ranks.Ranks; +import mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus; import mekhq.campaign.unit.Unit; import mekhq.campaign.universe.Faction; import mekhq.campaign.universe.Planet; diff --git a/MekHQ/src/mekhq/gui/campaignOptions/contents/PersonnelTab.java b/MekHQ/src/mekhq/gui/campaignOptions/contents/PersonnelTab.java index 33cef6e378b..92d8468a7c2 100644 --- a/MekHQ/src/mekhq/gui/campaignOptions/contents/PersonnelTab.java +++ b/MekHQ/src/mekhq/gui/campaignOptions/contents/PersonnelTab.java @@ -24,7 +24,11 @@ import megamek.common.enums.SkillLevel; import mekhq.campaign.CampaignOptions; import mekhq.campaign.personnel.Skills; -import mekhq.campaign.personnel.enums.*; +import mekhq.campaign.personnel.enums.AwardBonus; +import mekhq.campaign.personnel.enums.PersonnelRole; +import mekhq.campaign.personnel.enums.PrisonerCaptureStyle; +import mekhq.campaign.personnel.enums.TimeInDisplayFormat; +import mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus; import mekhq.gui.campaignOptions.components.*; import javax.swing.*; diff --git a/MekHQ/src/mekhq/gui/sorter/PrisonerStatusSorter.java b/MekHQ/src/mekhq/gui/sorter/PrisonerStatusSorter.java index c4938946f60..a462c73a0a0 100644 --- a/MekHQ/src/mekhq/gui/sorter/PrisonerStatusSorter.java +++ b/MekHQ/src/mekhq/gui/sorter/PrisonerStatusSorter.java @@ -18,7 +18,7 @@ */ package mekhq.gui.sorter; -import mekhq.campaign.personnel.enums.PrisonerStatus; +import mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus; import java.util.Comparator; diff --git a/MekHQ/unittests/mekhq/campaign/personnel/PersonTest.java b/MekHQ/unittests/mekhq/campaign/personnel/PersonTest.java index 1def0b6c4c4..d60c3442201 100644 --- a/MekHQ/unittests/mekhq/campaign/personnel/PersonTest.java +++ b/MekHQ/unittests/mekhq/campaign/personnel/PersonTest.java @@ -18,27 +18,26 @@ */ package mekhq.campaign.personnel; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; - -import java.time.LocalDate; -import java.util.UUID; - -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - import megamek.common.Entity; import megamek.common.EntityWeightClass; import megamek.common.TechConstants; import mekhq.campaign.Campaign; import mekhq.campaign.CampaignOptions; import mekhq.campaign.personnel.enums.AwardBonus; -import mekhq.campaign.personnel.enums.PrisonerStatus; +import mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus; import mekhq.campaign.unit.Unit; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.time.LocalDate; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; public class PersonTest { private Person mockPerson; diff --git a/MekHQ/unittests/mekhq/campaign/personnel/death/AbstractDeathTest.java b/MekHQ/unittests/mekhq/campaign/personnel/death/AbstractDeathTest.java index 5fecc7eca60..280fa3217e2 100644 --- a/MekHQ/unittests/mekhq/campaign/personnel/death/AbstractDeathTest.java +++ b/MekHQ/unittests/mekhq/campaign/personnel/death/AbstractDeathTest.java @@ -23,7 +23,7 @@ import mekhq.campaign.personnel.Person; import mekhq.campaign.personnel.enums.AgeGroup; import mekhq.campaign.personnel.enums.PersonnelStatus; -import mekhq.campaign.personnel.enums.PrisonerStatus; +import mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -37,7 +37,10 @@ import java.util.HashMap; import java.util.Map; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -144,7 +147,7 @@ public void testCanDie() { when(mockPerson.getStatus()).thenReturn(PersonnelStatus.ACTIVE); when(mockPerson.isImmortal()).thenReturn(true); assertNotNull(mockDeath.canDie(mockPerson, AgeGroup.ADULT, true)); - + // Age Group must be enabled when(mockPerson.isImmortal()).thenReturn(false); assertNotNull(mockDeath.canDie(mockPerson, AgeGroup.CHILD, true)); diff --git a/MekHQ/unittests/mekhq/campaign/personnel/divorce/AbstractDivorceTest.java b/MekHQ/unittests/mekhq/campaign/personnel/divorce/AbstractDivorceTest.java index 773a4198c63..e67479257d8 100644 --- a/MekHQ/unittests/mekhq/campaign/personnel/divorce/AbstractDivorceTest.java +++ b/MekHQ/unittests/mekhq/campaign/personnel/divorce/AbstractDivorceTest.java @@ -21,7 +21,7 @@ import mekhq.campaign.Campaign; import mekhq.campaign.CampaignOptions; import mekhq.campaign.personnel.Person; -import mekhq.campaign.personnel.enums.PrisonerStatus; +import mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; diff --git a/MekHQ/unittests/mekhq/campaign/personnel/enums/PrisonerStatusTest.java b/MekHQ/unittests/mekhq/campaign/personnel/enums/PrisonerStatusTest.java deleted file mode 100644 index 98ed511e306..00000000000 --- a/MekHQ/unittests/mekhq/campaign/personnel/enums/PrisonerStatusTest.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (c) 2022 - 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 as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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. - * - * You should have received a copy of the GNU General Public License - * along with MekHQ. If not, see . - */ -package mekhq.campaign.personnel.enums; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.ResourceBundle; - -import org.junit.jupiter.api.Test; - -import mekhq.MekHQ; - -class PrisonerStatusTest { - // region Variable Declarations - private static final PrisonerStatus[] statuses = PrisonerStatus.values(); - - private final transient ResourceBundle resources = ResourceBundle.getBundle("mekhq.resources.Personnel", - MekHQ.getMHQOptions().getLocale()); - // endregion Variable Declarations - - // region Getters - @Test - void testGetTitleExtension() { - assertEquals(resources.getString("PrisonerStatus.FREE.titleExtension"), - PrisonerStatus.FREE.getTitleExtension()); - assertEquals(resources.getString("PrisonerStatus.PRISONER_DEFECTOR.titleExtension"), - PrisonerStatus.PRISONER_DEFECTOR.getTitleExtension()); - } - // endregion Getters - - // region Boolean Comparison Methods - @Test - void testIsFree() { - for (final PrisonerStatus prisonerStatus : statuses) { - if (prisonerStatus == PrisonerStatus.FREE) { - assertTrue(prisonerStatus.isFree()); - } else { - assertFalse(prisonerStatus.isFree()); - } - } - } - - @Test - void testIsPrisoner() { - for (final PrisonerStatus prisonerStatus : statuses) { - if (prisonerStatus == PrisonerStatus.PRISONER) { - assertTrue(prisonerStatus.isPrisoner()); - } else { - assertFalse(prisonerStatus.isPrisoner()); - } - } - } - - @Test - void testIsPrisonerDefector() { - for (final PrisonerStatus prisonerStatus : statuses) { - if (prisonerStatus == PrisonerStatus.PRISONER_DEFECTOR) { - assertTrue(prisonerStatus.isPrisonerDefector()); - } else { - assertFalse(prisonerStatus.isPrisonerDefector()); - } - } - } - - @Test - void testIsBondsman() { - for (final PrisonerStatus prisonerStatus : statuses) { - if (prisonerStatus == PrisonerStatus.BONDSMAN) { - assertTrue(prisonerStatus.isBondsman()); - } else { - assertFalse(prisonerStatus.isBondsman()); - } - } - } - - @Test - void testIsCurrentPrisoner() { - for (final PrisonerStatus prisonerStatus : statuses) { - if ((prisonerStatus == PrisonerStatus.PRISONER) - || (prisonerStatus == PrisonerStatus.PRISONER_DEFECTOR)) { - assertTrue(prisonerStatus.isCurrentPrisoner()); - } else { - assertFalse(prisonerStatus.isCurrentPrisoner()); - } - } - } - // endregion Boolean Comparison Methods - - // region File I/O - @Test - void testParseFromString() { - // Normal Parsing - assertEquals(PrisonerStatus.FREE, PrisonerStatus.parseFromString("FREE")); - assertEquals(PrisonerStatus.BONDSMAN, PrisonerStatus.parseFromString("BONDSMAN")); - - // Error Case - assertEquals(PrisonerStatus.FREE, PrisonerStatus.parseFromString("3")); - assertEquals(PrisonerStatus.FREE, PrisonerStatus.parseFromString("blah")); - } - // endregion File I/O - - @Test - void testToStringOverride() { - assertEquals(resources.getString("PrisonerStatus.FREE.text"), PrisonerStatus.FREE.toString()); - assertEquals(resources.getString("PrisonerStatus.BONDSMAN.text"), PrisonerStatus.BONDSMAN.toString()); - } -} diff --git a/MekHQ/unittests/mekhq/campaign/personnel/marriage/AbstractMarriageTest.java b/MekHQ/unittests/mekhq/campaign/personnel/marriage/AbstractMarriageTest.java index 04d5c3eb659..d0340ebb831 100644 --- a/MekHQ/unittests/mekhq/campaign/personnel/marriage/AbstractMarriageTest.java +++ b/MekHQ/unittests/mekhq/campaign/personnel/marriage/AbstractMarriageTest.java @@ -24,10 +24,10 @@ import mekhq.campaign.personnel.Person; import mekhq.campaign.personnel.enums.MergingSurnameStyle; import mekhq.campaign.personnel.enums.PersonnelStatus; -import mekhq.campaign.personnel.enums.PrisonerStatus; import mekhq.campaign.personnel.enums.RandomMarriageMethod; import mekhq.campaign.personnel.familyTree.Genealogy; import mekhq.campaign.personnel.ranks.RankSystem; +import mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; diff --git a/MekHQ/unittests/mekhq/campaign/personnel/procreation/AbstractProcreationTest.java b/MekHQ/unittests/mekhq/campaign/personnel/procreation/AbstractProcreationTest.java index 44c744f12b7..b5d25c4664f 100644 --- a/MekHQ/unittests/mekhq/campaign/personnel/procreation/AbstractProcreationTest.java +++ b/MekHQ/unittests/mekhq/campaign/personnel/procreation/AbstractProcreationTest.java @@ -24,9 +24,9 @@ import mekhq.campaign.CampaignOptions; import mekhq.campaign.personnel.Person; import mekhq.campaign.personnel.enums.PersonnelStatus; -import mekhq.campaign.personnel.enums.PrisonerStatus; import mekhq.campaign.personnel.enums.RandomProcreationMethod; import mekhq.campaign.personnel.familyTree.Genealogy; +import mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; diff --git a/MekHQ/unittests/mekhq/campaign/randomEvents/prisoners/enums/PrisonerStatusTest.java b/MekHQ/unittests/mekhq/campaign/randomEvents/prisoners/enums/PrisonerStatusTest.java new file mode 100644 index 00000000000..480ca1cb2f6 --- /dev/null +++ b/MekHQ/unittests/mekhq/campaign/randomEvents/prisoners/enums/PrisonerStatusTest.java @@ -0,0 +1,88 @@ +/* + * 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 as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with MekHQ. If not, see . + */ +package mekhq.campaign.randomEvents.prisoners.enums; + +import org.junit.jupiter.api.Test; + +import static mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus.FREE; +import static mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus.PRISONER; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class PrisonerStatusTest { + @Test + public void testParseFromString_ValidStatus() { + PrisonerStatus status = PrisonerStatus.parseFromString("PRISONER"); + assertEquals(PRISONER, status); + } + + @Test + public void testParseFromString_InvalidStatus() { + PrisonerStatus status = PrisonerStatus.parseFromString("INVALID_STATUS"); + + assertEquals(FREE, status); + } + + @Test + public void testParseFromString_NullStatus() { + PrisonerStatus status = PrisonerStatus.parseFromString(null); + + assertEquals(FREE, status); + } + + @Test + public void testParseFromString_EmptyString() { + PrisonerStatus status = PrisonerStatus.parseFromString(""); + + assertEquals(FREE, status); + } + + @Test + public void testGetLabel_notInvalid() { + for (PrisonerStatus status : PrisonerStatus.values()) { + String label = status.getLabel(); + assertTrue(isTitleExtensionValid(label)); + } + } + + @Test + public void testGetTitleExtension_notInvalid() { + for (PrisonerStatus status : PrisonerStatus.values()) { + String titleExtension = status.getTitleExtension(); + assertTrue(isTitleExtensionValid(titleExtension)); + } + } + + /** + * Checks if the given text is a valid title extension. A valid title extension + * does not start or end with an exclamation mark ('!'). + * + *

If {@link mekhq.utilities.MHQInternationalization} fails to fetch a valid return it + * returns the key between two {@code !}. So by checking the returned string doesn't begin and + * end with that punctuation, we can easily verify that all statuses have been provided results + * for the keys we're using.

+ * + * @param text The text to validate as a title extension. + * @return true if the text is valid (does not start or end with an '!'); + * false otherwise. + */ + public static boolean isTitleExtensionValid(String text) { + return !text.startsWith("!") && !text.endsWith("!"); + } +} diff --git a/MekHQ/unittests/mekhq/gui/dialog/nagDialogs/nagLogic/UntreatedPersonnelNagLogicTest.java b/MekHQ/unittests/mekhq/gui/dialog/nagDialogs/nagLogic/UntreatedPersonnelNagLogicTest.java index 3dd99a20477..d8f6174c592 100644 --- a/MekHQ/unittests/mekhq/gui/dialog/nagDialogs/nagLogic/UntreatedPersonnelNagLogicTest.java +++ b/MekHQ/unittests/mekhq/gui/dialog/nagDialogs/nagLogic/UntreatedPersonnelNagLogicTest.java @@ -25,8 +25,8 @@ import mekhq.campaign.personnel.SkillType; import mekhq.campaign.personnel.enums.PersonnelRole; import mekhq.campaign.personnel.enums.PersonnelStatus; -import mekhq.campaign.personnel.enums.PrisonerStatus; import mekhq.campaign.personnel.ranks.Ranks; +import mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus; import mekhq.campaign.universe.Systems; import mekhq.gui.dialog.nagDialogs.UntreatedPersonnelNagDialog; import org.junit.jupiter.api.BeforeAll; From 852c6113114f3d315ead3ec9e12ac8d1a86ec89c Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sun, 2 Feb 2025 19:35:02 -0600 Subject: [PATCH 059/112] Update copyright years to 2025 Updated all relevant files to extend copyright years to 2025 for the MegaMek Team. This ensures compliance with the project's legal and licensing requirements. --- MekHQ/src/mekhq/campaign/ResolveScenarioTracker.java | 2 +- MekHQ/src/mekhq/campaign/personnel/Person.java | 2 +- .../src/mekhq/campaign/personnel/marriage/AbstractMarriage.java | 2 +- .../campaign/personnel/procreation/AbstractProcreation.java | 2 +- MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java | 2 +- MekHQ/src/mekhq/gui/sorter/PrisonerStatusSorter.java | 2 +- MekHQ/unittests/mekhq/campaign/personnel/PersonTest.java | 2 +- .../mekhq/campaign/personnel/death/AbstractDeathTest.java | 2 +- .../mekhq/campaign/personnel/divorce/AbstractDivorceTest.java | 2 +- .../mekhq/campaign/personnel/marriage/AbstractMarriageTest.java | 2 +- .../campaign/personnel/procreation/AbstractProcreationTest.java | 2 +- .../nagDialogs/nagLogic/UntreatedPersonnelNagLogicTest.java | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/ResolveScenarioTracker.java b/MekHQ/src/mekhq/campaign/ResolveScenarioTracker.java index e5ad9036f8e..364609e6901 100644 --- a/MekHQ/src/mekhq/campaign/ResolveScenarioTracker.java +++ b/MekHQ/src/mekhq/campaign/ResolveScenarioTracker.java @@ -2,7 +2,7 @@ * ResolveScenarioTracker.java * * Copyright (c) 2009 Jay Lawson (jaylawson39 at yahoo.com). All rights reserved. - * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * diff --git a/MekHQ/src/mekhq/campaign/personnel/Person.java b/MekHQ/src/mekhq/campaign/personnel/Person.java index 80cfed17453..06e9c7ebeaa 100644 --- a/MekHQ/src/mekhq/campaign/personnel/Person.java +++ b/MekHQ/src/mekhq/campaign/personnel/Person.java @@ -1,6 +1,6 @@ /* * Copyright (c) 2009 - Jay Lawson (jaylawson39 at yahoo.com). All Rights Reserved. - * Copyright (c) 2020-2024 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2020-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * diff --git a/MekHQ/src/mekhq/campaign/personnel/marriage/AbstractMarriage.java b/MekHQ/src/mekhq/campaign/personnel/marriage/AbstractMarriage.java index 79ec938a1ce..c04a8467aac 100644 --- a/MekHQ/src/mekhq/campaign/personnel/marriage/AbstractMarriage.java +++ b/MekHQ/src/mekhq/campaign/personnel/marriage/AbstractMarriage.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2024 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2021-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * diff --git a/MekHQ/src/mekhq/campaign/personnel/procreation/AbstractProcreation.java b/MekHQ/src/mekhq/campaign/personnel/procreation/AbstractProcreation.java index 9d90e4c2485..c55d8f85d60 100644 --- a/MekHQ/src/mekhq/campaign/personnel/procreation/AbstractProcreation.java +++ b/MekHQ/src/mekhq/campaign/personnel/procreation/AbstractProcreation.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2024 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2021-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * diff --git a/MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java b/MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java index 473e02b2fb6..a2b013ade44 100644 --- a/MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java +++ b/MekHQ/src/mekhq/gui/adapter/PersonnelTableMouseAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2024 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2019-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * diff --git a/MekHQ/src/mekhq/gui/sorter/PrisonerStatusSorter.java b/MekHQ/src/mekhq/gui/sorter/PrisonerStatusSorter.java index a462c73a0a0..f475e48bd60 100644 --- a/MekHQ/src/mekhq/gui/sorter/PrisonerStatusSorter.java +++ b/MekHQ/src/mekhq/gui/sorter/PrisonerStatusSorter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2020-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * diff --git a/MekHQ/unittests/mekhq/campaign/personnel/PersonTest.java b/MekHQ/unittests/mekhq/campaign/personnel/PersonTest.java index d60c3442201..df4846cd79f 100644 --- a/MekHQ/unittests/mekhq/campaign/personnel/PersonTest.java +++ b/MekHQ/unittests/mekhq/campaign/personnel/PersonTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2024-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * diff --git a/MekHQ/unittests/mekhq/campaign/personnel/death/AbstractDeathTest.java b/MekHQ/unittests/mekhq/campaign/personnel/death/AbstractDeathTest.java index 280fa3217e2..b4a705f1cc7 100644 --- a/MekHQ/unittests/mekhq/campaign/personnel/death/AbstractDeathTest.java +++ b/MekHQ/unittests/mekhq/campaign/personnel/death/AbstractDeathTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2022-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * diff --git a/MekHQ/unittests/mekhq/campaign/personnel/divorce/AbstractDivorceTest.java b/MekHQ/unittests/mekhq/campaign/personnel/divorce/AbstractDivorceTest.java index e67479257d8..b593d851f21 100644 --- a/MekHQ/unittests/mekhq/campaign/personnel/divorce/AbstractDivorceTest.java +++ b/MekHQ/unittests/mekhq/campaign/personnel/divorce/AbstractDivorceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2024 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2022-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * diff --git a/MekHQ/unittests/mekhq/campaign/personnel/marriage/AbstractMarriageTest.java b/MekHQ/unittests/mekhq/campaign/personnel/marriage/AbstractMarriageTest.java index d0340ebb831..8179afeea92 100644 --- a/MekHQ/unittests/mekhq/campaign/personnel/marriage/AbstractMarriageTest.java +++ b/MekHQ/unittests/mekhq/campaign/personnel/marriage/AbstractMarriageTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2024 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2022-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * diff --git a/MekHQ/unittests/mekhq/campaign/personnel/procreation/AbstractProcreationTest.java b/MekHQ/unittests/mekhq/campaign/personnel/procreation/AbstractProcreationTest.java index b5d25c4664f..d5f1f2dbf3d 100644 --- a/MekHQ/unittests/mekhq/campaign/personnel/procreation/AbstractProcreationTest.java +++ b/MekHQ/unittests/mekhq/campaign/personnel/procreation/AbstractProcreationTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2024 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2022-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * diff --git a/MekHQ/unittests/mekhq/gui/dialog/nagDialogs/nagLogic/UntreatedPersonnelNagLogicTest.java b/MekHQ/unittests/mekhq/gui/dialog/nagDialogs/nagLogic/UntreatedPersonnelNagLogicTest.java index d8f6174c592..760a747a885 100644 --- a/MekHQ/unittests/mekhq/gui/dialog/nagDialogs/nagLogic/UntreatedPersonnelNagLogicTest.java +++ b/MekHQ/unittests/mekhq/gui/dialog/nagDialogs/nagLogic/UntreatedPersonnelNagLogicTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2024-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * From af91811d1650d979333a2dc099ec8cf7720fd667 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sun, 2 Feb 2025 19:59:53 -0600 Subject: [PATCH 060/112] Use `ModifiedConstantSkillGenerator` for Skill Generation Replaced `TaharqaSkillGenerator` with `ModifiedConstantSkillGenerator` in `AtBScenarioModifierApplicator` and `BotForceRandomizer`. This ensures consistency with updated skill generation logic used elsewhere in MekHQ. I updated our generation methods back in 2024, but apparently missed these two instances. --- MekHQ/src/mekhq/campaign/mission/BotForceRandomizer.java | 6 +++--- .../campaign/mission/atb/AtBScenarioModifierApplicator.java | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/mission/BotForceRandomizer.java b/MekHQ/src/mekhq/campaign/mission/BotForceRandomizer.java index 8a922275ba8..e8276d69560 100644 --- a/MekHQ/src/mekhq/campaign/mission/BotForceRandomizer.java +++ b/MekHQ/src/mekhq/campaign/mission/BotForceRandomizer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2024 - The Megamek Team. All Rights Reserved. + * Copyright (c) 2021-2025 - The Megamek Team. All Rights Reserved. * * This file is part of MekHQ. * @@ -23,7 +23,7 @@ import megamek.client.generator.RandomNameGenerator; import megamek.client.generator.enums.SkillGeneratorType; import megamek.client.generator.skillGenerators.AbstractSkillGenerator; -import megamek.client.generator.skillGenerators.TaharqaSkillGenerator; +import megamek.client.generator.skillGenerators.ModifiedConstantSkillGenerator; import megamek.codeUtilities.StringUtility; import megamek.common.*; import megamek.common.annotations.Nullable; @@ -491,7 +491,7 @@ public Entity getEntity(int uType, int weightClass, Campaign campaign) { innerMap.put(Crew.MAP_GIVEN_NAME, crewNameArray[0]); innerMap.put(Crew.MAP_SURNAME, crewNameArray[1]); - final AbstractSkillGenerator skillGenerator = new TaharqaSkillGenerator(); + final AbstractSkillGenerator skillGenerator = new ModifiedConstantSkillGenerator(); skillGenerator.setLevel(skill); if (faction.isClan()) { skillGenerator.setType(SkillGeneratorType.CLAN); diff --git a/MekHQ/src/mekhq/campaign/mission/atb/AtBScenarioModifierApplicator.java b/MekHQ/src/mekhq/campaign/mission/atb/AtBScenarioModifierApplicator.java index b4764936c28..ba7e34028b5 100644 --- a/MekHQ/src/mekhq/campaign/mission/atb/AtBScenarioModifierApplicator.java +++ b/MekHQ/src/mekhq/campaign/mission/atb/AtBScenarioModifierApplicator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2019-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * @@ -20,7 +20,7 @@ import megamek.client.generator.enums.SkillGeneratorType; import megamek.client.generator.skillGenerators.AbstractSkillGenerator; -import megamek.client.generator.skillGenerators.TaharqaSkillGenerator; +import megamek.client.generator.skillGenerators.ModifiedConstantSkillGenerator; import megamek.codeUtilities.MathUtility; import megamek.common.*; import megamek.common.enums.SkillLevel; @@ -214,7 +214,7 @@ public static void adjustSkill(AtBDynamicScenario scenario, Campaign campaign, scenario.getEffectiveOpforSkill().ordinal() + skillAdjustment, SkillLevel.ULTRA_GREEN.ordinal(), SkillLevel.LEGENDARY.ordinal())]; // fire up a skill generator set to the appropriate skill model - final AbstractSkillGenerator abstractSkillGenerator = new TaharqaSkillGenerator(); + final AbstractSkillGenerator abstractSkillGenerator = new ModifiedConstantSkillGenerator(); abstractSkillGenerator.setLevel(adjustedSkill); if (Factions.getInstance().getFaction(scenario.getContract(campaign).getEnemyCode()).isClan()) { From 629173b6413117c93dde403b407aa51a2d915c36 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sun, 2 Feb 2025 20:07:12 -0600 Subject: [PATCH 061/112] Added "Intercept the Escapees" Scenario Template - Introduced a new scenario template XML defining the "Intercept the Escapees" scenario. This scenario will be used by the upcoming prisoner rework. --- .../Intercept the Escapees.xml | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 MekHQ/data/scenariotemplates/Intercept the Escapees.xml diff --git a/MekHQ/data/scenariotemplates/Intercept the Escapees.xml b/MekHQ/data/scenariotemplates/Intercept the Escapees.xml new file mode 100644 index 00000000000..e29337e9382 --- /dev/null +++ b/MekHQ/data/scenariotemplates/Intercept the Escapees.xml @@ -0,0 +1,172 @@ + + + Intercept the Escapees + SPECIAL_JAIL_BREAK + Prevent the escapees from linking up with enemy forces. + Allied Command has located the escapees. Ensure they do not fall into enemy hands by wiping out the escapees, or by destroying 50% of enemy forces. Failure will grant the enemy valuable intel and cost us 1 CVP. + false + false + + + false + 25 + 25 + 5 + AllGroundTerrain + true + 5 + + + + Player + + -1 + false + -2 + -3 + true + true + true + true + false + + 3 + + 4 + 0 + 0 + 1.0 + Player + 0 + 1 + 4 + 0 + + 0 + 0 + false + + + + OpFor + + -1 + false + -2 + -3 + true + false + true + false + false + + 5 + 0 + 2 + 1.0 + OpFor + 1 + 5 + 4 + 0 + 50 + 0 + OppositeEdge + Player + false + + RECON + RAIDER + CAVALRY + + + + + Escapees + + -1 + false + -2 + 0 + true + false + true + false + false + + 10 + + 7 + 0 + 2 + 1.0 + Escapees + 6 + 5 + 4 + 1 + + 0 + 0 + None + false + + + + + + + OpFor + + + + + ScenarioVictory + Fixed + 2 + + + + + ScenarioDefeat + Fixed + 1 + + + + Destroy or rout 50% of the following force(s) and unit(s): + NONE + ForceWithdraw + 95 + true + None + + + + Escapees + + + + + ScenarioVictory + Fixed + 2 + + + + + ScenarioDefeat + Fixed + 1 + + + + Destroy 100% of the following force(s) and unit(s): + NONE + Destroy + 100 + true + None + + + From 5045a55ba320290d94b45e2c972ba18f134fb0b7 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sun, 2 Feb 2025 20:11:54 -0600 Subject: [PATCH 062/112] Added "SPECIAL_JAIL_BREAK" scenario type to ScenarioType enum Updated the ScenarioType enum to include a new type, "SPECIAL_JAIL_BREAK", and added a corresponding helper method, isJailBreak(), to identify this scenario. Adjusted the copyright notice to cover 2025. --- .../mekhq/campaign/mission/enums/ScenarioType.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/mission/enums/ScenarioType.java b/MekHQ/src/mekhq/campaign/mission/enums/ScenarioType.java index 9b0f8ae4b63..906366611cb 100644 --- a/MekHQ/src/mekhq/campaign/mission/enums/ScenarioType.java +++ b/MekHQ/src/mekhq/campaign/mission/enums/ScenarioType.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2024-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * @@ -40,7 +40,8 @@ public enum ScenarioType { NONE, SPECIAL_LOSTECH, - SPECIAL_RESUPPLY; + SPECIAL_RESUPPLY, + SPECIAL_JAIL_BREAK; /** * @return {@code true} if the scenario is considered a LosTech scenario, {@code false} otherwise. @@ -56,6 +57,13 @@ public boolean isResupply() { return this == SPECIAL_RESUPPLY; } + /** + * @return {@code true} if the scenario is considered a Jail Break scenario, {@code false} otherwise. + */ + public boolean isJailBreak() { + return this == SPECIAL_JAIL_BREAK; + } + /** * Parses a {@code ScenarioType} from a string input. * From ce3652bc8c5bf0b7762e8c32048f018b44d4dbea Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sun, 2 Feb 2025 20:21:58 -0600 Subject: [PATCH 063/112] Added 'None' Generation Method to `ScenarioForceTemplate` - Implemented a 'None' option in `ForceGenerationMethod` to allow bot forces where no units are generated, enabling more flexible scenario templates. - Adjusted `AtBDynamicScenarioFactory` to account for this method in force generation logic. This resolves a situation where we need to have all objective-significant Bot Forces generated when the scenario is created. However, if we want the Bot Force to have specific units and not random ones, we then need to remove the already generated ones. By using this method, we're able to generate an empty force during scenario creation which can then be populated later. This will be used by the upcoming prisoner rework. --- .../mission/AtBDynamicScenarioFactory.java | 5 ++++ .../mission/ScenarioForceTemplate.java | 26 +++++++++---------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java b/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java index 62de083eb35..2de56ea0297 100644 --- a/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java +++ b/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java @@ -624,6 +624,10 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac // Generate a tactical formation (lance/star/etc.) until the BV or unit count // limits are exceeded while (!stopGenerating) { + if (forceTemplate.getGenerationMethod() == ForceGenerationMethod.None.ordinal()) { + break; + } + List generatedLance; // Generate a tactical formations for this force based on the desired weight class. @@ -965,6 +969,7 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac } if (generatedForce.getTeam() != 1 + && forceTemplate.getGenerationMethod() != ForceGenerationMethod.None.ordinal() && campaign.getCampaignOptions().isUseGenericBattleValue() && BatchallFactions.usesBatchalls(factionCode) && contract.isBatchallAccepted()) { diff --git a/MekHQ/src/mekhq/campaign/mission/ScenarioForceTemplate.java b/MekHQ/src/mekhq/campaign/mission/ScenarioForceTemplate.java index b323ebfcdbc..086c68bda90 100644 --- a/MekHQ/src/mekhq/campaign/mission/ScenarioForceTemplate.java +++ b/MekHQ/src/mekhq/campaign/mission/ScenarioForceTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2024 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2019-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * @@ -18,17 +18,6 @@ */ package mekhq.campaign.mission; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; - -import org.w3c.dom.Node; - import jakarta.xml.bind.JAXBContext; import jakarta.xml.bind.JAXBElement; import jakarta.xml.bind.Unmarshaller; @@ -41,6 +30,10 @@ import megamek.common.UnitType; import megamek.common.annotations.Nullable; import megamek.logging.MMLogger; +import org.w3c.dom.Node; + +import java.util.*; +import java.util.stream.Collectors; public class ScenarioForceTemplate implements Comparable { private static final MMLogger logger = MMLogger.create(ScenarioForceTemplate.class); @@ -156,7 +149,7 @@ public enum ForceGenerationMethod { */ BVScaled, - /* + /** * Scale on the unit count, based on number of already generated units flagged * as contributing towards unit count */ @@ -175,7 +168,12 @@ public enum ForceGenerationMethod { /** * Using one or more fixed MULs */ - FixedMUL + FixedMUL, + + /** + * Don't generate units. For use when you want to add units separately + */ + None } /** From 8790f0c9299fde9b373dc491d1ab939f6826cd02 Mon Sep 17 00:00:00 2001 From: psikomonkie <189469115+psikomonkie@users.noreply.github.com> Date: Tue, 4 Feb 2025 14:38:28 -0500 Subject: [PATCH 064/112] Issue 5845: In Stratcon scenario wizard, leadership units consider transport assignments --- .../AssignForceToTransport.properties | 4 + .../mekhq/resources/AtBStratCon.properties | 2 + .../utilities/CampaignTransportUtilities.java | 15 +++ .../gui/stratcon/StratconScenarioWizard.java | 94 ++++++++++++++++++- 4 files changed, 112 insertions(+), 3 deletions(-) diff --git a/MekHQ/resources/mekhq/resources/AssignForceToTransport.properties b/MekHQ/resources/mekhq/resources/AssignForceToTransport.properties index 0c48da9b289..c242e6c02b7 100644 --- a/MekHQ/resources/mekhq/resources/AssignForceToTransport.properties +++ b/MekHQ/resources/mekhq/resources/AssignForceToTransport.properties @@ -45,3 +45,7 @@ AtBGameThread.loadTransportDialog.LOAD_FTR_DIALOG_TITLE.title =Load Fighters ont AtBGameThread.loadTransportDialog.LOAD_GND_DIALOG_TEXT.text =Would you like the ground unit(s) assigned to {0} to deploy loaded into its bays? AtBGameThread.loadTransportDialog.LOAD_GND_DIALOG_TITLE.title =Load Ground Units onto Transport? +CampaignTransportUtilities.selectTransport.null.text=None +CampaignTransportUtilities.selectTransport.TACTICAL_TRANSPORT.text=Tactical +CampaignTransportUtilities.selectTransport.SHIP_TRANSPORT.text=Ship + diff --git a/MekHQ/resources/mekhq/resources/AtBStratCon.properties b/MekHQ/resources/mekhq/resources/AtBStratCon.properties index 9e39565a6cc..bd7f21eaabb 100644 --- a/MekHQ/resources/mekhq/resources/AtBStratCon.properties +++ b/MekHQ/resources/mekhq/resources/AtBStratCon.properties @@ -12,6 +12,8 @@ lblLeadershipInstructions.Text=The force commander's leadership allows the
\
Available BV: %sTransport Type: + selectForceForTemplate.Text=Select a force from the list below.\
\
If multiple forces are selected, only the first will be deployed. diff --git a/MekHQ/src/mekhq/campaign/utilities/CampaignTransportUtilities.java b/MekHQ/src/mekhq/campaign/utilities/CampaignTransportUtilities.java index 4eed44c1ac3..d410f7a79b9 100644 --- a/MekHQ/src/mekhq/campaign/utilities/CampaignTransportUtilities.java +++ b/MekHQ/src/mekhq/campaign/utilities/CampaignTransportUtilities.java @@ -22,6 +22,8 @@ import megamek.common.*; import mekhq.campaign.enums.CampaignTransportType; import mekhq.campaign.unit.enums.TransporterType; +import mekhq.utilities.MHQInternationalization; +import org.apache.commons.math3.util.Pair; import java.util.*; @@ -254,5 +256,18 @@ public EnumSet getTransporterTypes(Infantry entity, CampaignTra private static Optional getTransportTypeClassifier(Entity entity) { return visitors.stream().filter(v -> v.isInterestedIn(entity)).findFirst(); } + + /** + * Return "None" in the first position + * @return vector of transport options, with none first + */ + public static Vector> getLeadershipDropdownVectorPair() { + Vector> retVal = new Vector<>(); + retVal.add(new Pair<>(MHQInternationalization.getTextAt("mekhq.resources.AssignForceToTransport", "CampaignTransportUtilities.selectTransport.null.text"), null)); + retVal.add(new Pair<>(MHQInternationalization.getTextAt("mekhq.resources.AssignForceToTransport", "CampaignTransportUtilities.selectTransport.TACTICAL_TRANSPORT.text"), CampaignTransportType.TACTICAL_TRANSPORT)); + retVal.add(new Pair<>(MHQInternationalization.getTextAt("mekhq.resources.AssignForceToTransport", "CampaignTransportUtilities.selectTransport.SHIP_TRANSPORT.text"), CampaignTransportType.SHIP_TRANSPORT)); + + return retVal; + } // endregion Static Helpers } diff --git a/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java b/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java index 8382498e0f2..2429074d4f1 100644 --- a/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java +++ b/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java @@ -27,6 +27,7 @@ import mekhq.MekHQ; import mekhq.campaign.Campaign; import mekhq.campaign.Campaign.AdministratorSpecialization; +import mekhq.campaign.enums.CampaignTransportType; import mekhq.campaign.force.Force; import mekhq.campaign.mission.ScenarioForceTemplate; import mekhq.campaign.personnel.Person; @@ -36,13 +37,20 @@ import mekhq.campaign.stratcon.StratconTrackState; import mekhq.campaign.unit.Unit; import mekhq.gui.utilities.JScrollPaneWithSpeed; +import mekhq.utilities.MHQInternationalization; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.math3.util.Pair; import javax.swing.*; import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; import java.awt.*; import java.awt.event.ActionEvent; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; import java.util.List; import java.util.*; +import java.util.stream.Collectors; import static mekhq.campaign.mission.AtBDynamicScenarioFactory.scaleObjectiveTimeLimits; import static mekhq.campaign.mission.AtBDynamicScenarioFactory.translateTemplateObjectives; @@ -52,6 +60,7 @@ import static mekhq.campaign.stratcon.StratconRulesManager.ReinforcementResultsType.FAILED; import static mekhq.campaign.stratcon.StratconScenario.ScenarioState.PRIMARY_FORCES_COMMITTED; import static mekhq.campaign.stratcon.StratconScenario.ScenarioState.REINFORCEMENTS_COMMITTED; +import static mekhq.campaign.utilities.CampaignTransportUtilities.getLeadershipDropdownVectorPair; import static mekhq.gui.baseComponents.MHQDialogImmersive.getSpeakerDescription; import static mekhq.gui.baseComponents.MHQDialogImmersive.getSpeakerIcon; import static mekhq.utilities.ImageUtilities.scaleImageIconToWidth; @@ -64,14 +73,20 @@ public class StratconScenarioWizard extends JDialog { private final Campaign campaign; private StratconTrackState currentTrackState; private StratconCampaignState currentCampaignState; - private final transient ResourceBundle resources = ResourceBundle.getBundle("mekhq.resources.AtBStratCon", + private final String resourcePath = "mekhq.resources.AtBStratCon"; + private final transient ResourceBundle resources = ResourceBundle.getBundle(resourcePath, MekHQ.getMHQOptions().getLocale()); private final Map> availableForceLists = new HashMap<>(); private final Map> availableUnitLists = new HashMap<>(); + private List eligibleLeadershipUnits; private JList availableInfantryUnits = new JList<>(); private JList availableLeadershipUnits = new JList<>(); + private Map> availableTransportedLeadershipUnits = new HashMap<>(); + private CampaignTransportType selectedCampaignTransportType = null; + + private JComboBox cboTransportType = new JComboBox<>(); private JPanel contentPanel; private JButton btnCommit; @@ -81,6 +96,12 @@ public class StratconScenarioWizard extends JDialog { public StratconScenarioWizard(Campaign campaign) { this.campaign = campaign; this.setModalityType(ModalityType.APPLICATION_MODAL); + + for (CampaignTransportType campaignTransportType : getLeadershipDropdownVectorPair().stream().map(Pair::getValue).collect(Collectors.toSet())) { + if (campaignTransportType != null) { + availableTransportedLeadershipUnits.put(campaignTransportType, new JList<>()); + } + } } /** @@ -153,7 +174,7 @@ private void setUI(boolean isPrimaryForce) { getContentPane().removeAll(); // Create a new panel to hold all components - contentPanel = new JPanel(); + contentPanel = new JPanel(new CardLayout()); contentPanel.setLayout(new GridBagLayout()); GridBagConstraints gbc = new GridBagConstraints(); @@ -174,7 +195,7 @@ private void setUI(boolean isPrimaryForce) { if (isPrimaryForce) { gbc.gridy++; int leadershipSkill = currentScenario.getBackingScenario().getLanceCommanderSkill(S_LEADER, campaign); - List eligibleLeadershipUnits = getEligibleLeadershipUnits( + eligibleLeadershipUnits = getEligibleLeadershipUnits( campaign, currentScenario.getPrimaryForceIDs(), leadershipSkill); eligibleLeadershipUnits.sort(Comparator.comparing(this::getForceNameReversed)); @@ -408,10 +429,30 @@ private void setLeadershipUI(GridBagConstraints gbc, List eligibleUnits, i String.format(resources.getString("lblLeadershipInstructions.Text"), maxSelectionSize)); contentPanel.add(lblLeadershipInstructions, gbc); + // Transport Type + gbc.gridy++; + JLabel lblTransportInstructions = new JLabel(MHQInternationalization.getTextAt(resourcePath, "lblLeadershipTransportInstructions.text")); + contentPanel.add(lblTransportInstructions, gbc); + + gbc.gridy++; + + cboTransportType = new JComboBox<>(new Vector<> + (getLeadershipDropdownVectorPair().stream().map(Pair::getKey).collect(Collectors.toSet()))); + cboTransportType.setSelectedItem(getLeadershipDropdownVectorPair().firstElement().getKey()); + + contentPanel.add(cboTransportType, gbc); + + gbc.gridy++; + CardLayout leadershipTransportCard = new CardLayout(); + JPanel leadershipUnitJPanel = new JPanel(leadershipTransportCard); availableLeadershipUnits = addIndividualUnitSelector(eligibleUnits, gbc, maxSelectionSize, true); + + ItemListener dropdownChangeListener = evt -> campaignTransportTypeChangeHandler(evt, eligibleUnits, leadershipUnitJPanel); + cboTransportType.addItemListener(dropdownChangeListener); + contentPanel.add(leadershipUnitJPanel); } /** @@ -497,6 +538,19 @@ private JList addIndividualUnitSelector(List units, GridBagConstrain return availableUnits; } + private void campaignTransportTypeChangeHandler(ItemEvent event, List allUnits, JPanel leadershipUnitJPanel) { + if (!(event.getSource() instanceof JComboBox) || (event.getStateChange() != ItemEvent.SELECTED )) { + return; + } + + for (Pair pair : getLeadershipDropdownVectorPair()) { + if (pair.getKey().equals(cboTransportType.getSelectedItem())) { + selectedCampaignTransportType = pair.getValue(); + break; + } + } + } + /** * Worker function that builds an "html-enabled" string indicating the brief * status of a force @@ -985,6 +1039,8 @@ private void availableUnitSelectorChanged(ListSelectionEvent event, JLabel selec } JList changedList = (JList) event.getSource(); + ListSelectionListener[] listeners = (((JList) event.getSource()).getListSelectionListeners()); + ((JList) event.getSource()).removeListSelectionListener(listeners[0]); int selectedItems; if (usesBV) { @@ -993,6 +1049,8 @@ private void availableUnitSelectorChanged(ListSelectionEvent event, JLabel selec selectedItems += unit.getEntity().calculateBattleValue(true, true); selectionCountLabel.setText(String.format("%d %s", selectedItems, resources.getString("unitsSelectedLabel.bv"))); + selectTransportedUnitsAndTransport(selectedCampaignTransportType, unit,changedList); + } } else { selectedItems = changedList.getSelectedIndices().length; @@ -1037,6 +1095,36 @@ private void availableUnitSelectorChanged(ListSelectionEvent event, JLabel selec unitStatusLabel.setText(sb.toString()); pack(); + + ((JList) event.getSource()).addListSelectionListener(listeners[0]); + } + + private void selectTransportedUnitsAndTransport(CampaignTransportType campaignTransportType, Unit unit, JList changedList) { + if (campaignTransportType != null) { + if (unit.hasTransportedUnits(campaignTransportType)) { + Set potentialTransportedUnits = unit.getTransportedUnits(campaignTransportType); + for (Unit transportedUnit : potentialTransportedUnits) { + // if this unit isn't selected but is an eligible leadership unit + if (!changedList.getSelectedValuesList().contains(transportedUnit) + && (eligibleLeadershipUnits.contains(transportedUnit))) { + + int index = eligibleLeadershipUnits.indexOf(transportedUnit); + changedList.setSelectedIndices(ArrayUtils.add(changedList.getSelectedIndices(), index)); + } + } + } + + if (unit.hasTransportAssignment(campaignTransportType)) { + Unit transport = unit.getTransportAssignment(campaignTransportType).getTransport(); + // if this unit isn't selected but is an eligible leadership unit + if (!changedList.getSelectedValuesList().contains(transport) + && (eligibleLeadershipUnits.contains(transport))) { + + int index = eligibleLeadershipUnits.indexOf(transport); + changedList.setSelectedIndices(ArrayUtils.add(changedList.getSelectedIndices(), index)); + } + } + } } /** From ded44e27ba7805badf0acc1f690bd85be98e9828 Mon Sep 17 00:00:00 2001 From: psikomonkie <189469115+psikomonkie@users.noreply.github.com> Date: Tue, 4 Feb 2025 14:48:38 -0500 Subject: [PATCH 065/112] Issue 5845: Removed unused parameters --- MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java b/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java index 2429074d4f1..f4bafeccd6d 100644 --- a/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java +++ b/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java @@ -450,7 +450,7 @@ private void setLeadershipUI(GridBagConstraints gbc, List eligibleUnits, i availableLeadershipUnits = addIndividualUnitSelector(eligibleUnits, gbc, maxSelectionSize, true); - ItemListener dropdownChangeListener = evt -> campaignTransportTypeChangeHandler(evt, eligibleUnits, leadershipUnitJPanel); + ItemListener dropdownChangeListener = this::campaignTransportTypeChangeHandler; cboTransportType.addItemListener(dropdownChangeListener); contentPanel.add(leadershipUnitJPanel); } @@ -538,7 +538,7 @@ private JList addIndividualUnitSelector(List units, GridBagConstrain return availableUnits; } - private void campaignTransportTypeChangeHandler(ItemEvent event, List allUnits, JPanel leadershipUnitJPanel) { + private void campaignTransportTypeChangeHandler(ItemEvent event) { if (!(event.getSource() instanceof JComboBox) || (event.getStateChange() != ItemEvent.SELECTED )) { return; } From c83e6be36363eb719e3e81bc65f7305528e932e4 Mon Sep 17 00:00:00 2001 From: Pavel Braginskiy Date: Tue, 4 Feb 2025 13:03:06 -0800 Subject: [PATCH 066/112] Cap splashscreen button width --- MekHQ/src/mekhq/gui/panels/StartupScreenPanel.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/MekHQ/src/mekhq/gui/panels/StartupScreenPanel.java b/MekHQ/src/mekhq/gui/panels/StartupScreenPanel.java index 4b9930881e1..a09a4e95e73 100644 --- a/MekHQ/src/mekhq/gui/panels/StartupScreenPanel.java +++ b/MekHQ/src/mekhq/gui/panels/StartupScreenPanel.java @@ -182,6 +182,11 @@ protected void initialize() { // the button width "look" reasonable. int maximumWidth = (int) (0.9 * scaledMonitorSize.width) - splash.getPreferredSize().width; + //no more than 50% of image width + if (maximumWidth > (int) (0.5 * splash.getPreferredSize().width)) { + maximumWidth = (int) (0.5 * splash.getPreferredSize().width); + } + Dimension minButtonDim = new Dimension((int) (maximumWidth / 1.618), 25); if (textDim.getWidth() > minButtonDim.getWidth()) { minButtonDim = textDim; From 95354de055e55f853560760d577a2614c9b615c6 Mon Sep 17 00:00:00 2001 From: psikomonkie <189469115+psikomonkie@users.noreply.github.com> Date: Tue, 4 Feb 2025 16:58:31 -0500 Subject: [PATCH 067/112] Issue 5980: Improved commander updating logic --- MekHQ/src/mekhq/campaign/force/Force.java | 25 +++++++++++-------- .../mekhq/campaign/io/CampaignXmlParser.java | 10 ++++---- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/force/Force.java b/MekHQ/src/mekhq/campaign/force/Force.java index 955d8402dc7..1246157860a 100644 --- a/MekHQ/src/mekhq/campaign/force/Force.java +++ b/MekHQ/src/mekhq/campaign/force/Force.java @@ -617,22 +617,25 @@ public void updateCommander(Campaign campaign) { overrideForceCommanderID = null; } } + //If our force commander is null or not eligible (not in the force/subforces), let's assign a random person + if ((forceCommanderID == null) || (!eligibleCommanders.contains(forceCommanderID))) { - Collections.shuffle(eligibleCommanders); - Person highestRankedPerson = campaign.getPerson(eligibleCommanders.get(0)); + Collections.shuffle(eligibleCommanders); + Person highestRankedPerson = campaign.getPerson(eligibleCommanders.get(0)); - for (UUID eligibleCommanderId : eligibleCommanders) { - Person eligibleCommander = campaign.getPerson(eligibleCommanderId); - if (eligibleCommander == null) { - continue; - } + for (UUID eligibleCommanderId : eligibleCommanders) { + Person eligibleCommander = campaign.getPerson(eligibleCommanderId); + if (eligibleCommander == null) { + continue; + } - if (eligibleCommander.outRanksUsingSkillTiebreaker(campaign, highestRankedPerson)) { - highestRankedPerson = eligibleCommander; + if (eligibleCommander.outRanksUsingSkillTiebreaker(campaign, highestRankedPerson)) { + highestRankedPerson = eligibleCommander; + } } - } - forceCommanderID = highestRankedPerson.getId(); + forceCommanderID = highestRankedPerson.getId(); + } updateCombatTeamCommanderIfCombatTeam(campaign); if (getParentForce() != null) { diff --git a/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java b/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java index f50b2db86c1..135437aaf71 100644 --- a/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java +++ b/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java @@ -469,6 +469,11 @@ public Campaign parse() throws CampaignXmlParseException, NullEntityException { System.currentTimeMillis() - timestamp)); timestamp = System.currentTimeMillis(); + // This removes the risk of having forces with invalid leadership getting locked in + for (Force force : retVal.getAllForces()) { + force.updateCommander(retVal); + } + // ok, once we are sure that campaign has been set for all units, we can // now go through and initializeParts and run diagnostics List removeUnits = new ArrayList<>(); @@ -874,11 +879,6 @@ private static void processForces(Campaign retVal, Node wn, Version version) { } } - // This removes the risk of having forces with invalid leadership getting locked in - for (Force force : retVal.getAllForces()) { - force.updateCommander(retVal); - } - recalculateCombatTeams(retVal); logger.info("Load of Force Organization complete!"); } From ed79a65b3172181091d62f42375545580ea47ee2 Mon Sep 17 00:00:00 2001 From: psikomonkie <189469115+psikomonkie@users.noreply.github.com> Date: Tue, 4 Feb 2025 18:49:00 -0500 Subject: [PATCH 068/112] Issue 5980: Reverted changes to automatic force commander selection --- MekHQ/src/mekhq/campaign/force/Force.java | 25 ++++++++++------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/force/Force.java b/MekHQ/src/mekhq/campaign/force/Force.java index 1246157860a..955d8402dc7 100644 --- a/MekHQ/src/mekhq/campaign/force/Force.java +++ b/MekHQ/src/mekhq/campaign/force/Force.java @@ -617,25 +617,22 @@ public void updateCommander(Campaign campaign) { overrideForceCommanderID = null; } } - //If our force commander is null or not eligible (not in the force/subforces), let's assign a random person - if ((forceCommanderID == null) || (!eligibleCommanders.contains(forceCommanderID))) { - Collections.shuffle(eligibleCommanders); - Person highestRankedPerson = campaign.getPerson(eligibleCommanders.get(0)); + Collections.shuffle(eligibleCommanders); + Person highestRankedPerson = campaign.getPerson(eligibleCommanders.get(0)); - for (UUID eligibleCommanderId : eligibleCommanders) { - Person eligibleCommander = campaign.getPerson(eligibleCommanderId); - if (eligibleCommander == null) { - continue; - } - - if (eligibleCommander.outRanksUsingSkillTiebreaker(campaign, highestRankedPerson)) { - highestRankedPerson = eligibleCommander; - } + for (UUID eligibleCommanderId : eligibleCommanders) { + Person eligibleCommander = campaign.getPerson(eligibleCommanderId); + if (eligibleCommander == null) { + continue; } - forceCommanderID = highestRankedPerson.getId(); + if (eligibleCommander.outRanksUsingSkillTiebreaker(campaign, highestRankedPerson)) { + highestRankedPerson = eligibleCommander; + } } + + forceCommanderID = highestRankedPerson.getId(); updateCombatTeamCommanderIfCombatTeam(campaign); if (getParentForce() != null) { From b244113a68d60c55652dcf6d94b36b9fbdee4dce Mon Sep 17 00:00:00 2001 From: psikomonkie <189469115+psikomonkie@users.noreply.github.com> Date: Tue, 4 Feb 2025 19:34:58 -0500 Subject: [PATCH 069/112] Fixed backwards "last compatible version" check --- MekHQ/src/mekhq/CampaignPreset.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MekHQ/src/mekhq/CampaignPreset.java b/MekHQ/src/mekhq/CampaignPreset.java index 800469eddc1..f4171b25f47 100644 --- a/MekHQ/src/mekhq/CampaignPreset.java +++ b/MekHQ/src/mekhq/CampaignPreset.java @@ -442,7 +442,7 @@ public static List loadCampaignPresetsFromDirectory(final @Nulla return null; } - if (LAST_COMPATIBLE_VERSION.isLowerThan(version)) { + if (version.isLowerThan(LAST_COMPATIBLE_VERSION)) { String message = String.format( "Cannot parse Campaign Preset from %s in newer version %s.", version.toString(), MHQConstants.VERSION); From 1da16c5795ce94358dcfae0e6460bb257eee2aef Mon Sep 17 00:00:00 2001 From: psikomonkie <189469115+psikomonkie@users.noreply.github.com> Date: Tue, 4 Feb 2025 21:26:43 -0500 Subject: [PATCH 070/112] Updated last Compatible Version --- MekHQ/src/mekhq/CampaignPreset.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MekHQ/src/mekhq/CampaignPreset.java b/MekHQ/src/mekhq/CampaignPreset.java index f4171b25f47..849c4190663 100644 --- a/MekHQ/src/mekhq/CampaignPreset.java +++ b/MekHQ/src/mekhq/CampaignPreset.java @@ -65,7 +65,7 @@ * @author Justin "Windchild" Bowen */ public class CampaignPreset { - static final private Version LAST_COMPATIBLE_VERSION = new Version("0.50.03-SNAPSHOT"); + static final private Version LAST_COMPATIBLE_VERSION = new Version("0.50.02"); private static final MMLogger logger = MMLogger.create(CampaignPreset.class); From 142ec3269d2d2de089b564eba065dc538e0b3ddc Mon Sep 17 00:00:00 2001 From: jschmetzer <60976087+jschmetzer@users.noreply.github.com> Date: Wed, 5 Feb 2025 14:11:20 -0500 Subject: [PATCH 071/112] Update Prestigious Academies.xml Added destruction year for Outreach Military Training Command academies (3067) --- MekHQ/data/universe/academies/Prestigious Academies.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/MekHQ/data/universe/academies/Prestigious Academies.xml b/MekHQ/data/universe/academies/Prestigious Academies.xml index bdce2b6f5ba..423fefb7ec7 100644 --- a/MekHQ/data/universe/academies/Prestigious Academies.xml +++ b/MekHQ/data/universe/academies/Prestigious Academies.xml @@ -4841,6 +4841,7 @@ Operated by Wolf's Dragoons, the Outreach Mercenary Training Command provides top-tier military training outside traditional state institutions. Its programs span from fundamental academic subjects to advanced military courses. The academy is open to anyone able to afford its fees. Outreach 3056 + 3067 10500 300 5 @@ -4859,6 +4860,7 @@ Operated by Wolf's Dragoons, the Outreach Mercenary Training Command provides top-tier military training outside traditional state institutions. Its programs span from fundamental academic subjects to advanced military courses. The academy is open to anyone able to afford its fees. Outreach 3056 + 3067 10500 300 5 @@ -4877,6 +4879,7 @@ Operated by Wolf's Dragoons, the Outreach Mercenary Training Command provides top-tier military training outside traditional state institutions. Its programs span from fundamental academic subjects to advanced military courses. The academy is open to anyone able to afford its fees. Outreach 3056 + 3067 11667 900 5 @@ -4946,6 +4949,7 @@ Operated by Wolf's Dragoons, the Outreach Mercenary Training Command provides top-tier military training outside traditional state institutions. Its programs span from fundamental academic subjects to advanced military courses. The academy is open to anyone able to afford its fees. Outreach 3056 + 3067 22458 900 5 From 7817c62375c884c2bc71a09179458b9d6bf5832b Mon Sep 17 00:00:00 2001 From: Scoppio Date: Wed, 5 Feb 2025 19:13:35 -0300 Subject: [PATCH 072/112] feat: refactor flags to use EquipmentFlag instead --- .../campaign/mission/resupplyAndCaches/Resupply.java | 10 +++------- MekHQ/src/mekhq/campaign/unit/Unit.java | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java index 9a35b0a3536..c2cd9595901 100644 --- a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java +++ b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java @@ -1,6 +1,7 @@ package mekhq.campaign.mission.resupplyAndCaches; import megamek.common.Entity; +import megamek.common.EquipmentFlag; import megamek.common.Mek; import megamek.logging.MMLogger; import mekhq.campaign.Campaign; @@ -548,13 +549,8 @@ private boolean isIneligiblePart(Part part, Unit unit) { * @return {@code true} if the part is in the exclusion list, {@code false} otherwise. */ private boolean checkExclusionList(Part part) { - if (part instanceof EquipmentPart) { - List excludedTypes = List.of(F_SPONSON_TURRET); - for (BigInteger excludedType : excludedTypes) { - if (((EquipmentPart) part).getType().hasFlag(excludedType)) { - return true; - } - } + if (part instanceof EquipmentPart equipmentPart) { + return equipmentPart.getType().hasFlag(F_SPONSON_TURRET); } return false; } diff --git a/MekHQ/src/mekhq/campaign/unit/Unit.java b/MekHQ/src/mekhq/campaign/unit/Unit.java index fd75dd2381e..d0e2c69d1ee 100644 --- a/MekHQ/src/mekhq/campaign/unit/Unit.java +++ b/MekHQ/src/mekhq/campaign/unit/Unit.java @@ -1342,7 +1342,7 @@ public boolean isDamaged() { } public String getHeatSinkTypeString(int year) { - BigInteger heatSinkType = MiscType.F_HEAT_SINK; + EquipmentFlag heatSinkType = MiscType.F_HEAT_SINK; boolean heatSinkIsClanTechBase = false; for (Mounted mounted : getEntity().getEquipment()) { From aaff8604405c4cff88d7558485aef685808068e4 Mon Sep 17 00:00:00 2001 From: Scoppio Date: Wed, 5 Feb 2025 19:49:43 -0300 Subject: [PATCH 073/112] fix: test fixes --- .../parts/equipment/EquipmentPartTest.java | 29 +++++++++---------- .../equipment/MissingEquipmentPartTest.java | 29 +++++++++---------- 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/MekHQ/unittests/mekhq/campaign/parts/equipment/EquipmentPartTest.java b/MekHQ/unittests/mekhq/campaign/parts/equipment/EquipmentPartTest.java index 3338f871680..9a89f5812b0 100644 --- a/MekHQ/unittests/mekhq/campaign/parts/equipment/EquipmentPartTest.java +++ b/MekHQ/unittests/mekhq/campaign/parts/equipment/EquipmentPartTest.java @@ -41,7 +41,6 @@ import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; -import java.math.BigInteger; import java.util.ArrayList; import java.util.List; import java.util.Vector; @@ -221,23 +220,23 @@ public void isOmniPoddableTest() { // ... we need to be Mek Equipment ... doAnswer(inv -> { - BigInteger flag = inv.getArgument(0); + EquipmentFlag flag = inv.getArgument(0); return MiscType.F_MEK_EQUIPMENT.equals(flag); - }).when(miscType).hasFlag(any()); + }).when(miscType).hasFlag(any(EquipmentFlag.class)); assertTrue(equipmentPart.isOmniPoddable()); // ... or Tank Equipment ... doAnswer(inv -> { - BigInteger flag = inv.getArgument(0); + EquipmentFlag flag = inv.getArgument(0); return MiscType.F_TANK_EQUIPMENT.equals(flag); - }).when(miscType).hasFlag(any()); + }).when(miscType).hasFlag(any(EquipmentFlag.class)); assertTrue(equipmentPart.isOmniPoddable()); // ... or Aero Equipment ... doAnswer(inv -> { - BigInteger flag = inv.getArgument(0); + EquipmentFlag flag = inv.getArgument(0); return MiscType.F_FIGHTER_EQUIPMENT.equals(flag); - }).when(miscType).hasFlag(any()); + }).when(miscType).hasFlag(any(EquipmentFlag.class)); assertTrue(equipmentPart.isOmniPoddable()); // WeaponType @@ -250,30 +249,30 @@ public void isOmniPoddableTest() { // ... we need to be Mek Equipment ... doAnswer(inv -> { - BigInteger flag = inv.getArgument(0); + EquipmentFlag flag = inv.getArgument(0); return WeaponType.F_MEK_WEAPON.equals(flag); - }).when(weaponType).hasFlag(any()); + }).when(weaponType).hasFlag(any(EquipmentFlag.class)); assertTrue(equipmentPart.isOmniPoddable()); // ... or Tank Equipment ... doAnswer(inv -> { - BigInteger flag = inv.getArgument(0); + EquipmentFlag flag = inv.getArgument(0); return WeaponType.F_TANK_WEAPON.equals(flag); - }).when(weaponType).hasFlag(any()); + }).when(weaponType).hasFlag(any(EquipmentFlag.class)); assertTrue(equipmentPart.isOmniPoddable()); // ... or Fighter Equipment ... doAnswer(inv -> { - BigInteger flag = inv.getArgument(0); + EquipmentFlag flag = inv.getArgument(0); return WeaponType.F_AERO_WEAPON.equals(flag); - }).when(weaponType).hasFlag(any()); + }).when(weaponType).hasFlag(any(EquipmentFlag.class)); assertTrue(equipmentPart.isOmniPoddable()); // ... but not Capital scale. doAnswer(inv -> { - BigInteger flag = inv.getArgument(0); + EquipmentFlag flag = inv.getArgument(0); return WeaponType.F_AERO_WEAPON.equals(flag); - }).when(weaponType).hasFlag(any()); + }).when(weaponType).hasFlag(any(EquipmentFlag.class)); when(weaponType.isCapital()).thenReturn(true); assertFalse(equipmentPart.isOmniPoddable()); } diff --git a/MekHQ/unittests/mekhq/campaign/parts/equipment/MissingEquipmentPartTest.java b/MekHQ/unittests/mekhq/campaign/parts/equipment/MissingEquipmentPartTest.java index 91fc0639e80..82d3c5f15ab 100644 --- a/MekHQ/unittests/mekhq/campaign/parts/equipment/MissingEquipmentPartTest.java +++ b/MekHQ/unittests/mekhq/campaign/parts/equipment/MissingEquipmentPartTest.java @@ -39,7 +39,6 @@ import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; -import java.math.BigInteger; import static mekhq.campaign.parts.equipment.EquipmentUtilities.getEquipmentType; import static org.junit.jupiter.api.Assertions.*; @@ -214,23 +213,23 @@ public void isOmniPoddableTest() { // ... we need to be Mek Equipment ... doAnswer(inv -> { - BigInteger flag = inv.getArgument(0); + EquipmentFlag flag = inv.getArgument(0); return MiscType.F_MEK_EQUIPMENT.equals(flag); - }).when(miscType).hasFlag(any()); + }).when(miscType).hasFlag(any(EquipmentFlag.class)); assertTrue(missingPart.isOmniPoddable()); // ... or Tank Equipment ... doAnswer(inv -> { - BigInteger flag = inv.getArgument(0); + EquipmentFlag flag = inv.getArgument(0); return MiscType.F_TANK_EQUIPMENT.equals(flag); - }).when(miscType).hasFlag(any()); + }).when(miscType).hasFlag(any(EquipmentFlag.class)); assertTrue(missingPart.isOmniPoddable()); // ... or Aero Equipment ... doAnswer(inv -> { - BigInteger flag = inv.getArgument(0); + EquipmentFlag flag = inv.getArgument(0); return MiscType.F_FIGHTER_EQUIPMENT.equals(flag); - }).when(miscType).hasFlag(any()); + }).when(miscType).hasFlag(any(EquipmentFlag.class)); assertTrue(missingPart.isOmniPoddable()); // WeaponType @@ -243,30 +242,30 @@ public void isOmniPoddableTest() { // ... we need to be Mek Equipment ... doAnswer(inv -> { - BigInteger flag = inv.getArgument(0); + EquipmentFlag flag = inv.getArgument(0); return WeaponType.F_MEK_WEAPON.equals(flag); - }).when(weaponType).hasFlag(any()); + }).when(weaponType).hasFlag(any(EquipmentFlag.class)); assertTrue(missingPart.isOmniPoddable()); // ... or Tank Equipment ... doAnswer(inv -> { - BigInteger flag = inv.getArgument(0); + EquipmentFlag flag = inv.getArgument(0); return WeaponType.F_TANK_WEAPON.equals(flag); - }).when(weaponType).hasFlag(any()); + }).when(weaponType).hasFlag(any(EquipmentFlag.class)); assertTrue(missingPart.isOmniPoddable()); // ... or Fighter Equipment ... doAnswer(inv -> { - BigInteger flag = inv.getArgument(0); + EquipmentFlag flag = inv.getArgument(0); return WeaponType.F_AERO_WEAPON.equals(flag); - }).when(weaponType).hasFlag(any()); + }).when(weaponType).hasFlag(any(EquipmentFlag.class)); assertTrue(missingPart.isOmniPoddable()); // ... but not Capital scale. doAnswer(inv -> { - BigInteger flag = inv.getArgument(0); + EquipmentFlag flag = inv.getArgument(0); return WeaponType.F_AERO_WEAPON.equals(flag); - }).when(weaponType).hasFlag(any()); + }).when(weaponType).hasFlag(any(EquipmentFlag.class)); when(weaponType.isCapital()).thenReturn(true); assertFalse(missingPart.isOmniPoddable()); } From e84e393d1a16381e60f871e2f22de9180b183d09 Mon Sep 17 00:00:00 2001 From: Scoppio Date: Wed, 5 Feb 2025 21:06:32 -0300 Subject: [PATCH 074/112] fix: local bots property empty on non-atb games --- MekHQ/src/mekhq/GameThread.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/MekHQ/src/mekhq/GameThread.java b/MekHQ/src/mekhq/GameThread.java index 8ea76557a28..1c08caa2e51 100644 --- a/MekHQ/src/mekhq/GameThread.java +++ b/MekHQ/src/mekhq/GameThread.java @@ -136,6 +136,9 @@ public Client getClient() { } protected Map getLocalBots() { + if (localBots == null) { + return Collections.emptyMap(); + } return localBots.getLocalBots(); } @@ -153,6 +156,7 @@ public void run() { createController(); swingGui = new ClientGUI(client, controller); controller.clientgui = swingGui; + localBots = (ClientGUI) swingGui; swingGui.initialize(); try { From 36e4ae4d3b4f89727a451901c2c41e4dfeabadfc Mon Sep 17 00:00:00 2001 From: HammerGS Date: Wed, 5 Feb 2025 19:30:35 -0700 Subject: [PATCH 075/112] Updating History.txt --- MekHQ/docs/history.txt | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/MekHQ/docs/history.txt b/MekHQ/docs/history.txt index db4b098fbf4..3b7a312bbb3 100644 --- a/MekHQ/docs/history.txt +++ b/MekHQ/docs/history.txt @@ -1,6 +1,29 @@ MEKHQ VERSION HISTORY: --------------- 0.50.04-SNAPSHOT ++ PR #5852: RAW CamOps Delivery Times ++ PR #5853: Remove Legacy AtB's Campaign Parts Availability System ++ PR #5868: Updated Several Scenario Effects to use SupplyCache over SupportPointUpdate ++ Fix #5695: Refactored Personnel Cleanup and Random Dependent Removal ++ PR #5873: Added Force Type Enumeration ++ PR #5892: Refactored Resupply Messaging to use Internalization ++ PR #5905: Refactored Currency to No Longer Always Display as C-Bills ++ PR #5907: Implemented Clarion Note & Gray Monday ++ PR #5920: MegaMekLab Issue 1703: Allow bays to be added to aerospace fighters ++ PR #5955: Implemented Death Rework ++ PR #5959: Updated rankSystems version to 0.50.04-SNAPSHOT ++ PR #5962: Converted Honor Rating into an Enum ++ PR #5963: Refactored PrisonerStatus Enum and Reorganized Related Code ++ PR #5964: Use ModifiedConstantSkillGenerator for Skill Generation ++ PR #5965: Added "Intercept the Escapees" Scenario Template ++ PR #5966: Added 'None' Generation Method to ScenarioForceTemplate ++ Fix #5845: In Stratcon scenario wizard, leadership units consider transport assignments ++ PR #5982: Cap splashscreen button width ++ Fix #5980: Improved commander updating logic ++ PR #5985: Fixed backwards "last compatible version" check ++ PR #5988: Feat: refactor flags to use EquipmentFlag instead ++ Fix #5987: local bots property empty on non-atb games #5989 + 0.50.03 (2025-02-02 2030 UTC) + PR #5568: Spare parts rules reference adjustment From a1238174229eda98c1483a5af175a40b05542c55 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Wed, 5 Feb 2025 20:36:32 -0600 Subject: [PATCH 076/112] Corrected `randomDeathMultiplier` to use Double --- MekHQ/src/mekhq/campaign/CampaignOptions.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/CampaignOptions.java b/MekHQ/src/mekhq/campaign/CampaignOptions.java index f679bb37b61..a5de2c1ed63 100644 --- a/MekHQ/src/mekhq/campaign/CampaignOptions.java +++ b/MekHQ/src/mekhq/campaign/CampaignOptions.java @@ -2957,7 +2957,7 @@ public double getRandomDeathMultiplier() { return randomDeathMultiplier; } - public void setRandomDeathMultiplier(final int randomDeathMultiplier) { + public void setRandomDeathMultiplier(final double randomDeathMultiplier) { this.randomDeathMultiplier = randomDeathMultiplier; } // endregion Death @@ -5754,7 +5754,7 @@ public static CampaignOptions generateCampaignOptionsFromXml(Node wn, Version ve } else if (wn2.getNodeName().equalsIgnoreCase("useRandomDeathSuicideCause")) { retVal.setUseRandomDeathSuicideCause(Boolean.parseBoolean(wn2.getTextContent().trim())); } else if (wn2.getNodeName().equalsIgnoreCase("randomDeathMultiplier")) { - retVal.setRandomDeathMultiplier(Integer.parseInt(wn2.getTextContent().trim())); + retVal.setRandomDeathMultiplier(Double.parseDouble(wn2.getTextContent().trim())); } else if (wn2.getNodeName().equalsIgnoreCase("useRandomRetirement")) { retVal.setUseRandomRetirement(Boolean.parseBoolean(wn2.getTextContent().trim())); } else if (wn2.getNodeName().equalsIgnoreCase("turnoverBaseTn")) { From 9951f824e4e2eb831031b65c817a1cf522ce584a Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Wed, 5 Feb 2025 20:40:37 -0600 Subject: [PATCH 077/112] Fix random death multiplier type mismatch Corrected RandomDeathMultiplier to use double instead of int. This ensures consistency with the spinner value type and prevents potential data loss or runtime errors. --- .../src/mekhq/gui/campaignOptions/contents/BiographyTab.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MekHQ/src/mekhq/gui/campaignOptions/contents/BiographyTab.java b/MekHQ/src/mekhq/gui/campaignOptions/contents/BiographyTab.java index 345b5c7eef0..a388758972d 100644 --- a/MekHQ/src/mekhq/gui/campaignOptions/contents/BiographyTab.java +++ b/MekHQ/src/mekhq/gui/campaignOptions/contents/BiographyTab.java @@ -746,7 +746,7 @@ public JPanel createDeathTab() { // Contents lblRandomDeathMultiplier = new CampaignOptionsLabel("RandomDeathMultiplier"); spnRandomDeathMultiplier = new CampaignOptionsSpinner("RandomDeathMultiplier", - 1.0, 0, 100., 0.01); + 1.0, 0, 100.0, 0.01); chkUseRandomDeathSuicideCause = new CampaignOptionsCheckBox("UseRandomDeathSuicideCause"); @@ -1386,7 +1386,7 @@ public void applyCampaignOptionsToCampaign(@Nullable CampaignOptions presetCampa // Death options.setUseRandomDeathSuicideCause(chkUseRandomDeathSuicideCause.isSelected()); - options.setRandomDeathMultiplier((int) spnRandomDeathMultiplier.getValue()); + options.setRandomDeathMultiplier((double) spnRandomDeathMultiplier.getValue()); for (final AgeGroup ageGroup : AgeGroup.values()) { options.getEnabledRandomDeathAgeGroups().put(ageGroup, chkEnabledRandomDeathAgeGroups.get(ageGroup).isSelected()); From ad86f4447e34a67e90ef8dd60a0cb8499d19d9eb Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Wed, 5 Feb 2025 20:58:28 -0600 Subject: [PATCH 078/112] Rolled Back Finance Changes --- MekHQ/src/mekhq/campaign/Campaign.java | 20 ++-- .../campaign/finances/CurrencyManager.java | 94 ++++++------------- .../src/mekhq/campaign/finances/Finances.java | 45 ++------- .../gui/dialog/PersonnelMarketDialog.java | 3 +- 4 files changed, 41 insertions(+), 121 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index 3646ce283df..1a62047b06e 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -47,7 +47,6 @@ import mekhq.campaign.againstTheBot.AtBConfiguration; import mekhq.campaign.enums.CampaignTransportType; import mekhq.campaign.event.*; -import mekhq.campaign.finances.Currency; import mekhq.campaign.finances.*; import mekhq.campaign.finances.enums.TransactionType; import mekhq.campaign.force.CombatTeam; @@ -86,7 +85,10 @@ import mekhq.campaign.personnel.divorce.DisabledRandomDivorce; import mekhq.campaign.personnel.education.Academy; import mekhq.campaign.personnel.education.EducationController; -import mekhq.campaign.personnel.enums.*; +import mekhq.campaign.personnel.enums.PersonnelRole; +import mekhq.campaign.personnel.enums.PersonnelStatus; +import mekhq.campaign.personnel.enums.Phenotype; +import mekhq.campaign.personnel.enums.SplittingSurnameStyle; import mekhq.campaign.personnel.familyTree.Genealogy; import mekhq.campaign.personnel.generator.*; import mekhq.campaign.personnel.marriage.AbstractMarriage; @@ -98,8 +100,8 @@ import mekhq.campaign.personnel.ranks.Ranks; import mekhq.campaign.personnel.turnoverAndRetention.Fatigue; import mekhq.campaign.personnel.turnoverAndRetention.RetirementDefectionTracker; -import mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus; import mekhq.campaign.randomEvents.GrayMonday; +import mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus; import mekhq.campaign.rating.CamOpsReputation.ReputationController; import mekhq.campaign.rating.FieldManualMercRevDragoonsRating; import mekhq.campaign.rating.IUnitRating; @@ -4770,7 +4772,6 @@ public boolean newDay() { // Advance the day by one final LocalDate yesterday = currentDay; - final Currency oldCurrency = CurrencyManager.getInstance().getDefaultCurrency(); currentDay = currentDay.plusDays(1); // Determine if we have an active contract or not, as this can get used @@ -4841,7 +4842,7 @@ public boolean newDay() { setShoppingList(goShopping(getShoppingList())); // check for anything in finances - finances.newDay(this, yesterday, getLocalDate(), oldCurrency); + finances.newDay(this, yesterday, getLocalDate()); // process removal of old personnel data on the first day of each month if ((campaignOptions.isUsePersonnelRemoval()) && (currentDay.getDayOfMonth() == 1)) { @@ -9122,13 +9123,4 @@ public boolean checkLinkedScenario(int scenarioID) { } return false; } - - /** - * Retrieves the symbol of the default currency from the {@link CurrencyManager}. - * - * @return the symbol of the default currency as a {@link String}. - */ - public String getCurrencyString() { - return CurrencyManager.getInstance().getDefaultCurrency().getSymbol(); - } } diff --git a/MekHQ/src/mekhq/campaign/finances/CurrencyManager.java b/MekHQ/src/mekhq/campaign/finances/CurrencyManager.java index 5ca99c25e1c..c1b93e4c3bc 100644 --- a/MekHQ/src/mekhq/campaign/finances/CurrencyManager.java +++ b/MekHQ/src/mekhq/campaign/finances/CurrencyManager.java @@ -2,7 +2,6 @@ * CurrencyManager.java * * Copyright (c) 2019 Vicente Cartas Espinel (vicente.cartas at outlook.com). All rights reserved. - * Copyright (c) 2025 The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * @@ -23,6 +22,8 @@ import megamek.logging.MMLogger; import mekhq.campaign.Campaign; +import mekhq.campaign.mission.AtBContract; +import mekhq.campaign.mission.Contract; import mekhq.campaign.universe.Faction; import mekhq.campaign.universe.Factions; import mekhq.campaign.universe.PlanetarySystem; @@ -128,27 +129,7 @@ MoneyFormatter getUiAmountAndNamePrinter() { return this.uiAmountAndNamePrinter; } - /** - * Retrieves the default currency for the current campaign, based on the campaign's - * date, planetary system, and faction details. - *

- * This method ensures the default currency is updated if the campaign's date or - * planetary system has changed since the last check. It uses various conditions - * to determine the appropriate default currency, including: - *

- *
    - *
  • The year range validity for each currency
  • - *
  • Special cases for Clan factions (e.g., "KSK" or "SFC" currencies)
  • - *
  • The "default" status of available currencies
  • - *
- * - * If no campaign is active, the backup currency is returned. Certain decisions - * regarding currency selection (e.g., based on contracts or planetary factions) - * are currently disabled. - * - * @return the default {@link Currency} for the campaign. - */ - public synchronized Currency getDefaultCurrency() { + synchronized Currency getDefaultCurrency() { if (this.campaign == null) { return this.backupCurrency; } @@ -165,65 +146,46 @@ public synchronized Currency getDefaultCurrency() { this.lastSystem = currentSystem; this.defaultCurrency = this.backupCurrency; -// Map possibleCurrencies = new HashMap<>(); + Map possibleCurrencies = new HashMap<>(); - // Use the default currency in this time period if it exists + // Use the default currency in this time period, if it exists int year = date.getYear(); for (Currency currency : this.currencies) { - boolean isWithinYearRange = (year >= currency.getStartYear()) && (year <= currency.getEndYear()); - boolean isKSKorSFC = campaign.getFaction().isClan() && - ("KSK".equals(String.valueOf(currency)) || "SFC".equals(String.valueOf(currency))); - - if (isWithinYearRange) { - // Special case for Clan factions - if (isKSKorSFC) { - return defaultCurrency = currency; - } + if ((year >= currency.getStartYear()) && (year <= currency.getEndYear())) { - // Check if the current currency is default if (currency.getIsDefault()) { return defaultCurrency = currency; } - // Add currency to possible options - // This is where we'd construct a list for the commented code to use. -// possibleCurrencies.put(currency.getCode(), currency); + possibleCurrencies.put(currency.getCode(), currency); } } - - - // The next two options have been disabled until we have a way to easily communicate to - // the user why their funds are being changed. This is especially true for H-Bills, as - // they are undesirable compared to the C-Bill so players should be given a choice to - // change. The code is good, though, so shouldn't be deleted as it could prove useful - // later. - // Use the currency of the Faction in any of our contracts, if it exists -// for (Contract contract : this.campaign.getActiveContracts()) { -// if (contract instanceof AtBContract) { -// Currency currency = possibleCurrencies.getOrDefault( -// Factions.getInstance().getFaction(((AtBContract) contract).getEmployerCode()) -// .getCurrencyCode(), -// null); -// -// if (currency != null) { -// return defaultCurrency = currency; -// } -// } -// } + for (Contract contract : this.campaign.getActiveContracts()) { + if (contract instanceof AtBContract) { + Currency currency = possibleCurrencies.getOrDefault( + Factions.getInstance().getFaction(((AtBContract) contract).getEmployerCode()) + .getCurrencyCode(), + null); + + if (currency != null) { + return defaultCurrency = currency; + } + } + } // Use the currency of one of the factions in the planet where the unit is // deployed, if it exists -// if (currentSystem != null) { -// Set factions = currentSystem.getFactionSet(date); -// for (Faction faction : factions) { -// Currency currency = possibleCurrencies.getOrDefault(faction.getCurrencyCode(), null); -// if (currency != null) { -// return defaultCurrency = currency; -// } -// } -// } + if (currentSystem != null) { + Set factions = currentSystem.getFactionSet(date); + for (Faction faction : factions) { + Currency currency = possibleCurrencies.getOrDefault(faction.getCurrencyCode(), null); + if (currency != null) { + return defaultCurrency = currency; + } + } + } } return defaultCurrency; diff --git a/MekHQ/src/mekhq/campaign/finances/Finances.java b/MekHQ/src/mekhq/campaign/finances/Finances.java index c9488ac9e62..cc28f4f5980 100644 --- a/MekHQ/src/mekhq/campaign/finances/Finances.java +++ b/MekHQ/src/mekhq/campaign/finances/Finances.java @@ -35,20 +35,21 @@ import mekhq.utilities.ReportingUtilities; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVPrinter; -import org.joda.money.CurrencyMismatchException; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import java.io.BufferedWriter; import java.io.File; import java.io.PrintWriter; -import java.math.BigDecimal; import java.nio.file.Files; import java.nio.file.Paths; import java.time.LocalDate; import java.time.Period; import java.time.temporal.ChronoUnit; -import java.util.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -127,21 +128,7 @@ public void setWentIntoDebt(final @Nullable LocalDate wentIntoDebt) { public Money getBalance() { Money balance = Money.zero(); - - Currency currency = CurrencyManager.getInstance().getDefaultCurrency(); - - for (Transaction transaction : transactions) { - try { - balance.plus(transaction.getAmount()); - } catch (CurrencyMismatchException e) { - // This means the finances were logged in the wrong currency. - // This can be caused by data mismatch or by legacy campaigns. - // The fix is easy: convert the transaction to the right currency - convertFinances(transaction, currency); - } - } - - return balance; + return balance.plus(transactions.stream().map(Transaction::getAmount).collect(Collectors.toList())); } public Money getLoanBalance() { @@ -271,15 +258,7 @@ public void addLoan(Loan loan) { loans.add(loan); } - public void newDay(final Campaign campaign, final LocalDate yesterday, final LocalDate today, Currency oldCurrency) { - // check for currency change - Currency newCurrency = CurrencyManager.getInstance().getDefaultCurrency(); - if (!Objects.equals(newCurrency, oldCurrency)) { - for (Transaction transaction : transactions) { - convertFinances(transaction, newCurrency); - } - } - + public void newDay(final Campaign campaign, final LocalDate yesterday, final LocalDate today) { // check for a new fiscal year if (campaign.getCampaignOptions().getFinancialYearDuration().isEndOfFinancialYear(campaign.getLocalDate())) { // calculate profits @@ -467,18 +446,6 @@ public void newDay(final Campaign campaign, final LocalDate yesterday, final Loc loans = newLoans; } - private static void convertFinances(Transaction transaction, Currency newCurrency) { - Money currentMoney = transaction.getAmount(); - - // Perform the currency conversion (amount * conversionRate) - // TODO currency specific conversion rates. Replace hardcoded '1' - double newAmount = currentMoney.getAmount().multiply(BigDecimal.valueOf(1)).doubleValue(); - - Money convertedMoney = Money.of(newAmount, newCurrency); - - transaction.setAmount(convertedMoney); - } - /** * Calculates the profits made by the campaign based on the transactions * recorded. diff --git a/MekHQ/src/mekhq/gui/dialog/PersonnelMarketDialog.java b/MekHQ/src/mekhq/gui/dialog/PersonnelMarketDialog.java index 5ce1bba2bed..a47fa64adf4 100644 --- a/MekHQ/src/mekhq/gui/dialog/PersonnelMarketDialog.java +++ b/MekHQ/src/mekhq/gui/dialog/PersonnelMarketDialog.java @@ -178,8 +178,7 @@ public void windowClosing(WindowEvent e) { gridBagConstraints.anchor = GridBagConstraints.WEST; panelFilterBtns.add(radioNormalRoll, gridBagConstraints); - radioPaidRecruitment.setText(String.format("Make paid recruitment roll next week (%s)", - campaign.getCurrencyString())); + radioPaidRecruitment.setText("Make paid recruitment roll next week (100,000 C-bills)"); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 2; gridBagConstraints.gridwidth = 2; From a428dfdca986deb7a6e7eaa4a4343be13121706e Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Wed, 5 Feb 2025 21:13:53 -0600 Subject: [PATCH 079/112] Rolled Back Currency Changes II --- MekHQ/data/universe/currencies.xml | 39 +++++------------------------- 1 file changed, 6 insertions(+), 33 deletions(-) diff --git a/MekHQ/data/universe/currencies.xml b/MekHQ/data/universe/currencies.xml index 8d4c0665db2..19dc68280a4 100644 --- a/MekHQ/data/universe/currencies.xml +++ b/MekHQ/data/universe/currencies.xml @@ -25,47 +25,20 @@ defaultEnd - Indicates which year the currency ended being the default currency - Kerensky - KSK - 0 - K - 2807 - 3152 - - - Fox Credit - SFC - 0 - Fox - 3133 - 999999 - true - - - Hegemony Dollar - HGD - 0 - HG$ - 1 - 2570 - true - - - Star Dollar + Star League Dollar SLD - 0 - S$ + 2 + SL$ 2571 2785 - true - ComStar Bill + ComStar bill CSB 0 C-Bill - 2572 - 3132 + 1 + 999999 true true From bc960350957c21ab6a77130a2683d707fb867b1c Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Wed, 5 Feb 2025 21:27:23 -0600 Subject: [PATCH 080/112] Adjusted Gray Monday Employer Dialog to Trigger on the Correct Day Changed event logic to activate on day 3 after Gray Monday instead of day 2. This ensures consistency in timing for dialog display and related contract interactions. --- MekHQ/src/mekhq/campaign/randomEvents/GrayMonday.java | 4 ++-- MekHQ/src/mekhq/gui/dialog/randomEvents/GrayMondayDialog.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/randomEvents/GrayMonday.java b/MekHQ/src/mekhq/campaign/randomEvents/GrayMonday.java index 596e18f3cc7..6d78ffdff7d 100644 --- a/MekHQ/src/mekhq/campaign/randomEvents/GrayMonday.java +++ b/MekHQ/src/mekhq/campaign/randomEvents/GrayMonday.java @@ -52,9 +52,9 @@ public GrayMonday(Campaign campaign, LocalDate today) { int daysAfterGrayMonday = (int) DAYS.between(EVENT_DATE_GRAY_MONDAY, today); if (daysAfterGrayMonday > 0 && daysAfterGrayMonday <= 4) { - boolean shouldShowDialog = daysAfterGrayMonday != 2; + boolean shouldShowDialog = daysAfterGrayMonday != 3; - if (daysAfterGrayMonday == 2) { + if (daysAfterGrayMonday == 3) { for (AtBContract contract : campaign.getAtBContracts()) { LocalDate startDate = contract.getStartDate(); if (!startDate.isBefore(today)) { diff --git a/MekHQ/src/mekhq/gui/dialog/randomEvents/GrayMondayDialog.java b/MekHQ/src/mekhq/gui/dialog/randomEvents/GrayMondayDialog.java index e507bce6937..00ebf23218d 100644 --- a/MekHQ/src/mekhq/gui/dialog/randomEvents/GrayMondayDialog.java +++ b/MekHQ/src/mekhq/gui/dialog/randomEvents/GrayMondayDialog.java @@ -103,7 +103,7 @@ protected JPanel buildSpeakerPanel(@Nullable Person speaker, Campaign campaign) // Get speaker details String speakerName = speaker.getFullTitle(); - if (campaign.getLocalDate().equals(EVENT_DATE_GRAY_MONDAY.plusDays(2))) { + if (campaign.getLocalDate().equals(EVENT_DATE_GRAY_MONDAY.plusDays(3))) { if (chosenContract != null) { speakerName = chosenContract.getEmployerName(campaign.getGameYear()); } @@ -112,7 +112,7 @@ protected JPanel buildSpeakerPanel(@Nullable Person speaker, Campaign campaign) // Add speaker image (icon) ImageIcon speakerIcon = getSpeakerIcon(campaign, speaker); - if (campaign.getLocalDate().equals(EVENT_DATE_GRAY_MONDAY.plusDays(2))) { + if (campaign.getLocalDate().equals(EVENT_DATE_GRAY_MONDAY.plusDays(3))) { if (chosenContract != null) { String employerCode = chosenContract.getEmployerCode(); speakerIcon = Factions.getFactionLogo(campaign, employerCode, true); From 6b4676c8ed6d3cbbed69b28cc9e42f4c2d1834c6 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Wed, 5 Feb 2025 21:29:34 -0600 Subject: [PATCH 081/112] Fixed incorrect trigger for Gray Monday event adjustment Corrected the condition to trigger the financial adjustment from 1 day after Gray Monday to 2 days after. This aligns the logic with the intended event timeline and ensures proper functionality. --- MekHQ/src/mekhq/campaign/randomEvents/GrayMonday.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MekHQ/src/mekhq/campaign/randomEvents/GrayMonday.java b/MekHQ/src/mekhq/campaign/randomEvents/GrayMonday.java index 6d78ffdff7d..93a24b761aa 100644 --- a/MekHQ/src/mekhq/campaign/randomEvents/GrayMonday.java +++ b/MekHQ/src/mekhq/campaign/randomEvents/GrayMonday.java @@ -69,7 +69,7 @@ public GrayMonday(Campaign campaign, LocalDate today) { } } - if (daysAfterGrayMonday == 1) { + if (daysAfterGrayMonday == 2) { Finances finances = campaign.getFinances(); Money balance = finances.getBalance(); Money adjustedBalance = balance.multipliedBy(0.01); From 3228b5213cb6280965658214cada52120e5abf0b Mon Sep 17 00:00:00 2001 From: firefly2442 Date: Wed, 5 Feb 2025 21:49:00 -0600 Subject: [PATCH 082/112] fix: addresses the off by one issue presenting the wrong filtering results for skill level for mass training, fixes issue #5922 --- MekHQ/src/mekhq/gui/dialog/BatchXPDialog.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MekHQ/src/mekhq/gui/dialog/BatchXPDialog.java b/MekHQ/src/mekhq/gui/dialog/BatchXPDialog.java index 00476751b70..f9cb62a2117 100644 --- a/MekHQ/src/mekhq/gui/dialog/BatchXPDialog.java +++ b/MekHQ/src/mekhq/gui/dialog/BatchXPDialog.java @@ -170,8 +170,8 @@ private JComponent getButtonPanel() { choiceExp.setMaximumSize(new Dimension(Short.MAX_VALUE, (int) choiceType.getPreferredSize().getHeight())); DefaultComboBoxModel personExpModel = new DefaultComboBoxModel<>(); personExpModel.addElement(new PersonTypeItem(resourceMap.getString("experience.choice.text"), null)); - for (int i = SkillLevel.ULTRA_GREEN.ordinal(); i < SkillLevel.ELITE.ordinal(); i++) { - personExpModel.addElement(new PersonTypeItem(Skills.SKILL_LEVELS[i].toString(), i)); + for (int i = SkillLevel.ULTRA_GREEN.ordinal(); i <= SkillLevel.ELITE.ordinal(); i++) { + personExpModel.addElement(new PersonTypeItem(SkillLevel.parseFromInteger(i).toString(), SkillLevel.parseFromInteger(i).getAdjustedValue())); } choiceExp.setModel(personExpModel); choiceExp.setSelectedIndex(0); From 759e5afb90d7f0095bff8913b8c5dab6342c16a5 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Thu, 6 Feb 2025 12:27:49 -0600 Subject: [PATCH 083/112] Refactored getAllUnits to simplify logic and added tests Refactored the `getAllUnits` method by restructuring conditional logic and initializing `allUnits` at the declaration point, improving code clarity. Added comprehensive unit tests to validate the method's behavior across various scenarios, ensuring correctness for both standard and non-standard force cases. --- MekHQ/src/mekhq/campaign/force/Force.java | 8 +- .../mekhq/campaign/force/ForceTest.java | 158 ++++++++++++++++++ 2 files changed, 161 insertions(+), 5 deletions(-) create mode 100644 MekHQ/unittests/mekhq/campaign/force/ForceTest.java diff --git a/MekHQ/src/mekhq/campaign/force/Force.java b/MekHQ/src/mekhq/campaign/force/Force.java index e750a9fea77..0b67dc36aa0 100644 --- a/MekHQ/src/mekhq/campaign/force/Force.java +++ b/MekHQ/src/mekhq/campaign/force/Force.java @@ -413,12 +413,10 @@ public Vector getUnits() { * @return all the unit ids in this force and all of its subforces */ public Vector getAllUnits(boolean standardForcesOnly) { - Vector allUnits; + Vector allUnits = new Vector<>(); - if (standardForcesOnly && forceType.isStandard()) { - allUnits = new Vector<>(); - } else { - allUnits = new Vector<>(units); + if (!standardForcesOnly || forceType.isStandard()) { + allUnits.addAll(units); } for (Force force : subForces) { diff --git a/MekHQ/unittests/mekhq/campaign/force/ForceTest.java b/MekHQ/unittests/mekhq/campaign/force/ForceTest.java new file mode 100644 index 00000000000..0e4da237653 --- /dev/null +++ b/MekHQ/unittests/mekhq/campaign/force/ForceTest.java @@ -0,0 +1,158 @@ +package mekhq.campaign.force; + +import org.junit.jupiter.api.Test; + +import java.util.UUID; +import java.util.Vector; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ForceTest { + @Test + void testGetAllUnits_ParentForceStandard_NoChildForces() { + // Arrange + Force force = new Force("Test Force"); + UUID unit1 = UUID.randomUUID(); + UUID unit2 = UUID.randomUUID(); + force.addUnit(unit1); + force.addUnit(unit2); + + // Act + Vector allUnits = force.getAllUnits(true); + + // Assert + assertEquals(2, allUnits.size()); + } + + @Test + void testGetAllUnits_ParentForceStandard_ChildForcesAlsoStandard() { + // Arrange + Force force = new Force("Parent Force"); + force.setForceType(ForceType.STANDARD, true); + UUID unit = UUID.randomUUID(); + force.addUnit(unit); + + Force childForce = new Force("Child Force"); + unit = UUID.randomUUID(); + childForce.addUnit(unit); + force.addSubForce(childForce, true); + + Force childForce2 = new Force("Child Force (layer 2)"); + unit = UUID.randomUUID(); + childForce2.addUnit(unit); + childForce.addSubForce(childForce2, true); + + // Act + Vector allUnits = force.getAllUnits(true); + + // Assert + assertEquals(3, allUnits.size()); + } + + @Test + void testGetAllUnits_ParentForceStandard_ChildForcesNotStandard() { + // Arrange + Force force = new Force("Parent Force"); + force.setForceType(ForceType.STANDARD, true); + UUID unit = UUID.randomUUID(); + force.addUnit(unit); + + Force childForce = new Force("Child Force"); + unit = UUID.randomUUID(); + childForce.addUnit(unit); + force.addSubForce(childForce, true); + + Force childForce2 = new Force("Child Force (layer 2)"); + unit = UUID.randomUUID(); + childForce2.addUnit(unit); + childForce.addSubForce(childForce2, true); + + childForce.setForceType(ForceType.CONVOY, true); + + // Act + Vector allUnits = force.getAllUnits(true); + + // Assert + assertEquals(1, allUnits.size()); + } + + @Test + void testGetAllUnits_AllForcesNotStandard() { + // Arrange + Force force = new Force("Parent Force"); + UUID unit = UUID.randomUUID(); + force.addUnit(unit); + + Force childForce = new Force("Child Force"); + unit = UUID.randomUUID(); + childForce.addUnit(unit); + force.addSubForce(childForce, true); + + Force childForce2 = new Force("Child Force (layer 2)"); + unit = UUID.randomUUID(); + childForce2.addUnit(unit); + childForce.addSubForce(childForce2, true); + + force.setForceType(ForceType.SECURITY, true); + + // Act + Vector allUnits = force.getAllUnits(true); + + // Assert + assertEquals(0, allUnits.size()); + } + + @Test + void testGetAllUnits_AllForcesNotStandard_NoStandardFilter() { + // Arrange + Force force = new Force("Parent Force"); + UUID unit = UUID.randomUUID(); + force.addUnit(unit); + + Force childForce = new Force("Child Force"); + unit = UUID.randomUUID(); + childForce.addUnit(unit); + force.addSubForce(childForce, true); + + Force childForce2 = new Force("Child Force (layer 2)"); + unit = UUID.randomUUID(); + childForce2.addUnit(unit); + childForce.addSubForce(childForce2, true); + + force.setForceType(ForceType.SECURITY, true); + + // Act + Vector allUnits = force.getAllUnits(false); + + // Assert + assertEquals(3, allUnits.size()); + } + + @Test + void testGetAllUnits_AllForcesStandard_SecondLayerEmpty() { + // Arrange + Force force = new Force("Parent Force"); + force.setForceType(ForceType.STANDARD, true); + UUID unit = UUID.randomUUID(); + force.addUnit(unit); + + Force childForce = new Force("Child Force"); + unit = UUID.randomUUID(); + childForce.addUnit(unit); + force.addSubForce(childForce, true); + + Force childForce2 = new Force("Child Force (layer 2)"); + childForce.addSubForce(childForce2, true); + + Force childForce3 = new Force("Child Force (layer 3)"); + unit = UUID.randomUUID(); + childForce3.addUnit(unit); + childForce.addSubForce(childForce3, true); + + // Act + Vector allUnits = force.getAllUnits(true); + + // Assert + assertEquals(3, allUnits.size()); + } +} From 2cf22e2c50e731b1de0409d71cd30e60de1e358f Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Thu, 6 Feb 2025 22:26:59 -0600 Subject: [PATCH 084/112] Fixed Clan Ghost Bear Greeting Keys - Corrected `greetingCGBVersion1*` keys to `greetingCGB*`. No greeting text content was changed. --- .../mekhq/resources/FameAndInfamy.properties | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/MekHQ/resources/mekhq/resources/FameAndInfamy.properties b/MekHQ/resources/mekhq/resources/FameAndInfamy.properties index 2257a94cd27..2714ad0d3b3 100644 --- a/MekHQ/resources/mekhq/resources/FameAndInfamy.properties +++ b/MekHQ/resources/mekhq/resources/FameAndInfamy.properties @@ -122,21 +122,21 @@ greetingCFMLevel4Type1.text=Your defenses will fuel our fire, leaving nothing bu greetingCFMLevel4Type2.text=We burn through all. What pathetic resistance will you offer? -greetingCGBVersion1Level0Type0.text=We are impressed by your resolve. Identify your forces, so that we may share this Trial. -greetingCGBVersion1Level0Type1.text=You have shown the heart of a warrior. Let us test one another in this honored battle. -greetingCGBVersion1Level0Type2.text=Your courage is an inspiration. Together, let us make this a battle to be remembered. -greetingCGBVersion1Level1Type0.text=We respect your strength. Declare your forces, and we will meet you in battle. -greetingCGBVersion1Level1Type1.text=You have shown bravery. Let this be a Trial worthy of our ancestors. -greetingCGBVersion1Level1Type2.text=We acknowledge your courage. Stand before us, and let us see who is truly strong. -greetingCGBVersion1Level2Type0.text=Identify your forces or be forgotten. -greetingCGBVersion1Level2Type1.text=We claim what we desire. Your stand is of little concern. -greetingCGBVersion1Level2Type2.text=We care not for your defense. We will proceed. -greetingCGBVersion1Level3Type0.text=We see through your weakness. Declare your forces, or be removed. -greetingCGBVersion1Level3Type1.text=You amount to little more than a fleeting challenge. Prepare to fall. -greetingCGBVersion1Level3Type2.text=Your efforts are inconsequential. We shall take what is ours. -greetingCGBVersion1Level4Type0.text=We dismiss your efforts. Reveal your forces, or be swept aside. -greetingCGBVersion1Level4Type1.text=You are nothing but prey, and we are the hunters. Identify yourselves. -greetingCGBVersion1Level4Type2.text=Your resistance is beneath notice. Prepare to be obliterated. +greetingCGBLevel0Type0.text=We are impressed by your resolve. Identify your forces, so that we may share this Trial. +greetingCGBLevel0Type1.text=You have shown the heart of a warrior. Let us test one another in this honored battle. +greetingCGBLevel0Type2.text=Your courage is an inspiration. Together, let us make this a battle to be remembered. +greetingCGBLevel1Type0.text=We respect your strength. Declare your forces, and we will meet you in battle. +greetingCGBLevel1Type1.text=You have shown bravery. Let this be a Trial worthy of our ancestors. +greetingCGBLevel1Type2.text=We acknowledge your courage. Stand before us, and let us see who is truly strong. +greetingCGBLevel2Type0.text=Identify your forces or be forgotten. +greetingCGBLevel2Type1.text=We claim what we desire. Your stand is of little concern. +greetingCGBLevel2Type2.text=We care not for your defense. We will proceed. +greetingCGBLevel3Type0.text=We see through your weakness. Declare your forces, or be removed. +greetingCGBLevel3Type1.text=You amount to little more than a fleeting challenge. Prepare to fall. +greetingCGBLevel3Type2.text=Your efforts are inconsequential. We shall take what is ours. +greetingCGBLevel4Type0.text=We dismiss your efforts. Reveal your forces, or be swept aside. +greetingCGBLevel4Type1.text=You are nothing but prey, and we are the hunters. Identify yourselves. +greetingCGBLevel4Type2.text=Your resistance is beneath notice. Prepare to be obliterated. greetingCGSLevel0Type0.text=You seek it as if it were your own, and we admire that strength. Together, let us see who is worthy of this prize. From fff3c87c9bf389b13d125aba0e360b8ecbe1eef5 Mon Sep 17 00:00:00 2001 From: firefly2442 Date: Thu, 6 Feb 2025 22:34:06 -0600 Subject: [PATCH 085/112] fix: pull out skill level into variable to prevent duplicate calls --- MekHQ/src/mekhq/gui/dialog/BatchXPDialog.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MekHQ/src/mekhq/gui/dialog/BatchXPDialog.java b/MekHQ/src/mekhq/gui/dialog/BatchXPDialog.java index f9cb62a2117..49db1a01d0a 100644 --- a/MekHQ/src/mekhq/gui/dialog/BatchXPDialog.java +++ b/MekHQ/src/mekhq/gui/dialog/BatchXPDialog.java @@ -171,7 +171,8 @@ private JComponent getButtonPanel() { DefaultComboBoxModel personExpModel = new DefaultComboBoxModel<>(); personExpModel.addElement(new PersonTypeItem(resourceMap.getString("experience.choice.text"), null)); for (int i = SkillLevel.ULTRA_GREEN.ordinal(); i <= SkillLevel.ELITE.ordinal(); i++) { - personExpModel.addElement(new PersonTypeItem(SkillLevel.parseFromInteger(i).toString(), SkillLevel.parseFromInteger(i).getAdjustedValue())); + final SkillLevel skillLevel = SkillLevel.parseFromInteger(i); + personExpModel.addElement(new PersonTypeItem(skillLevel.toString(), skillLevel.getAdjustedValue())); } choiceExp.setModel(personExpModel); choiceExp.setSelectedIndex(0); From 564d4363c476a3ac5df04fefcd6a1abbac4c8e05 Mon Sep 17 00:00:00 2001 From: psikomonkie <189469115+psikomonkie@users.noreply.github.com> Date: Fri, 7 Feb 2025 16:14:36 -0500 Subject: [PATCH 086/112] Issue 5979: Force players to use commit when deploying forces --- MekHQ/src/mekhq/gui/StratconPanel.java | 33 ++++++++++++++++++- .../gui/stratcon/StratconScenarioWizard.java | 13 ++++++-- .../gui/stratcon/TrackForceAssignmentUI.java | 6 ++-- 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/MekHQ/src/mekhq/gui/StratconPanel.java b/MekHQ/src/mekhq/gui/StratconPanel.java index df0bf5c16b5..91b76758eb5 100644 --- a/MekHQ/src/mekhq/gui/StratconPanel.java +++ b/MekHQ/src/mekhq/gui/StratconPanel.java @@ -37,8 +37,10 @@ import java.awt.geom.Ellipse2D; import java.awt.image.BufferedImage; import java.io.File; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import static mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment.Allied; @@ -118,6 +120,8 @@ private enum DrawHexType { private final Map imageCache = new HashMap<>(); + private List pendingDeployments = new ArrayList(); + /** * Constructs a StratconPanel instance, given a parent campaign GUI and a * pointer to an info area. @@ -125,7 +129,7 @@ private enum DrawHexType { public StratconPanel(CampaignGUI gui, JLabel infoArea) { campaign = gui.getCampaign(); - scenarioWizard = new StratconScenarioWizard(campaign); + scenarioWizard = new StratconScenarioWizard(campaign, this); this.infoArea = infoArea; assignmentUI = new TrackForceAssignmentUI(this); @@ -1049,6 +1053,15 @@ public void actionPerformed(ActionEvent evt) { isPrimaryForce = true; } } + if (selectedScenario != null) { + scenarioWizard.setCurrentScenario(currentTrack.getScenario(selectedCoords), + currentTrack, campaignState, isPrimaryForce); + + scenarioWizard.toFront(); + scenarioWizard.setVisible(true); + } + setPendingDeployments(new ArrayList<>()); + break; // Deliberate fall-through case RCLICK_COMMAND_MANAGE_SCENARIO: // It's possible a scenario may have been placed when deploying the force, so we @@ -1122,4 +1135,22 @@ public Dimension getPreferredSize() { return super.getPreferredSize(); } } + + public List getPendingDeployments() { + return pendingDeployments; + } + + public void setPendingDeployments(List pendingDeployments) { + this.pendingDeployments = pendingDeployments; + } + + public void processPendingDeployments() { + + for (Force force : getPendingDeployments()) { + StratconRulesManager.deployForceToCoords(getSelectedCoords(), + force.getId(), campaign, campaignState.getContract(), getCurrentTrack(), false); + } + + setPendingDeployments(new ArrayList<>()); + } } diff --git a/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java b/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java index f4bafeccd6d..2156f1fe924 100644 --- a/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java +++ b/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java @@ -36,6 +36,7 @@ import mekhq.campaign.stratcon.StratconScenario; import mekhq.campaign.stratcon.StratconTrackState; import mekhq.campaign.unit.Unit; +import mekhq.gui.StratconPanel; import mekhq.gui.utilities.JScrollPaneWithSpeed; import mekhq.utilities.MHQInternationalization; import org.apache.commons.lang3.ArrayUtils; @@ -93,8 +94,11 @@ public class StratconScenarioWizard extends JDialog { private static final MMLogger logger = MMLogger.create(StratconScenarioWizard.class); - public StratconScenarioWizard(Campaign campaign) { + private final StratconPanel parent; + + public StratconScenarioWizard(Campaign campaign, StratconPanel parent) { this.campaign = campaign; + this.parent = parent; this.setModalityType(ModalityType.APPLICATION_MODAL); for (CampaignTransportType campaignTransportType : getLeadershipDropdownVectorPair().stream().map(Pair::getValue).collect(Collectors.toSet())) { @@ -900,6 +904,11 @@ private void reinforcementConfirmDialog() { */ private void btnCommitClicked(ActionEvent evt, @Nullable Integer reinforcementTargetNumber, boolean isGMReinforcement) { + //StratconPanel parent = (StratconPanel) getParent(); + if (parent != null && !(parent.getPendingDeployments().isEmpty())) { + parent.processPendingDeployments(); + } + // go through all the force lists and add the selected forces to the scenario for (String templateID : availableForceLists.keySet()) { for (Force force : availableForceLists.get(templateID).getSelectedValuesList()) { @@ -956,7 +965,7 @@ private void btnCommitClicked(ActionEvent evt, @Nullable Integer reinforcementTa currentScenario.updateMinefieldCount(Minefield.TYPE_CONVENTIONAL, getNumMinefields()); - if (currentScenario.getCurrentState().ordinal() < REINFORCEMENTS_COMMITTED.ordinal()) { + if (currentScenario.getCurrentState().ordinal() <= REINFORCEMENTS_COMMITTED.ordinal()) { translateTemplateObjectives(currentScenario.getBackingScenario(), campaign); scaleObjectiveTimeLimits(currentScenario.getBackingScenario(), campaign); } diff --git a/MekHQ/src/mekhq/gui/stratcon/TrackForceAssignmentUI.java b/MekHQ/src/mekhq/gui/stratcon/TrackForceAssignmentUI.java index 1f9ee4b053e..6db6055a5d6 100644 --- a/MekHQ/src/mekhq/gui/stratcon/TrackForceAssignmentUI.java +++ b/MekHQ/src/mekhq/gui/stratcon/TrackForceAssignmentUI.java @@ -114,10 +114,8 @@ public void actionPerformed(ActionEvent e) { // sometimes the scenario templates take a little while to load, we don't want the user // clicking the button fifty times and getting a bunch of scenarios. btnConfirm.setEnabled(false); - for (Force force : availableForceList.getSelectedValuesList()) { - StratconRulesManager.deployForceToCoords(ownerPanel.getSelectedCoords(), - force.getId(), campaign, currentCampaignState.getContract(), ownerPanel.getCurrentTrack(), false); - } + + ownerPanel.setPendingDeployments(availableForceList.getSelectedValuesList()); setVisible(false); ownerPanel.repaint(); btnConfirm.setEnabled(true); From 24ca11045a5a3d21f8ebb7c4e419684d3173f749 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Fri, 7 Feb 2025 15:21:31 -0600 Subject: [PATCH 087/112] Added Glossary Functionality to `MHQDialogImmersive` with Clickable Hyperlink Support - Implemented a new glossary system to manage and display terms across the application. - Added glossary entries, dialog support, hyperlink parsing, and a YAML-based data structure for term definitions. - Updated UI components to handle glossary commands and provide user-friendly navigation. This PR does not include any actual glossary entries. --- MekHQ/data/universe/glossary.yml | 8 + MekHQ/src/mekhq/campaign/Campaign.java | 19 ++ .../utilities/glossary/GlossaryEntry.java | 64 ++++++ .../glossary/GlossaryEntryWrapper.java | 58 ++++++ .../utilities/glossary/GlossaryLibrary.java | 106 ++++++++++ .../baseComponents/MHQDialogImmersive.java | 63 ++++-- .../src/mekhq/gui/dialog/GlossaryDialog.java | 186 ++++++++++++++++++ .../VocationalExperienceAwardDialog.java | 24 ++- 8 files changed, 503 insertions(+), 25 deletions(-) create mode 100644 MekHQ/data/universe/glossary.yml create mode 100644 MekHQ/src/mekhq/campaign/utilities/glossary/GlossaryEntry.java create mode 100644 MekHQ/src/mekhq/campaign/utilities/glossary/GlossaryEntryWrapper.java create mode 100644 MekHQ/src/mekhq/campaign/utilities/glossary/GlossaryLibrary.java create mode 100644 MekHQ/src/mekhq/gui/dialog/GlossaryDialog.java diff --git a/MekHQ/data/universe/glossary.yml b/MekHQ/data/universe/glossary.yml new file mode 100644 index 00000000000..acf91ca10b4 --- /dev/null +++ b/MekHQ/data/universe/glossary.yml @@ -0,0 +1,8 @@ +entries: + # The map key used to retrieve the entry. This is the value included in the HTML hyperlink to + # fetch this particular glossary entry so must be unique. + EXAMPLE: + # This is the title of the entry, this will be displayed in the glossary dialog + title: "An Example Title" + # This is the specific description of the term that will be presented to the player + description: "This is an example entry. This text will be displayed to the user." diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index 1a62047b06e..ae0f4003024 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -128,6 +128,8 @@ import mekhq.campaign.universe.selectors.planetSelectors.AbstractPlanetSelector; import mekhq.campaign.universe.selectors.planetSelectors.DefaultPlanetSelector; import mekhq.campaign.universe.selectors.planetSelectors.RangedPlanetSelector; +import mekhq.campaign.utilities.glossary.GlossaryEntry; +import mekhq.campaign.utilities.glossary.GlossaryLibrary; import mekhq.campaign.work.IAcquisitionWork; import mekhq.campaign.work.IPartWork; import mekhq.gui.sorter.PersonTitleSorter; @@ -317,6 +319,12 @@ public class Campaign implements ITechManager { private boolean topUpWeekly; private PartQuality ignoreSparesUnderQuality; + // Libraries + // We deliberately don't write this data to the save file as we want it rebuilt every time the + // campaign loads. This ensures updates can be applied and there is no risk of bugs being + // permanently locked into the campaign file. + Map glossaryLibrary = new GlossaryLibrary().getGlossaryEntries(); + /** * Represents the different types of administrative specializations. * Each specialization corresponds to a distinct administrative role @@ -5941,6 +5949,17 @@ public void setAutomatedMothballUnits(List automatedMothballUnits) { this.automatedMothballUnits = automatedMothballUnits; } + /** + * Retrieves a glossary entry based on the provided key. + * + * @param key the unique identifier for the glossary entry to retrieve + * @return the {@code GlossaryEntry} object corresponding to the given key, + * or {@code null} if no entry is found for the specified key + */ + public @Nullable GlossaryEntry getGlossaryEntry(String key) { + return glossaryLibrary.get(key); + } + public void writeToXML(final PrintWriter pw) { int indent = 0; diff --git a/MekHQ/src/mekhq/campaign/utilities/glossary/GlossaryEntry.java b/MekHQ/src/mekhq/campaign/utilities/glossary/GlossaryEntry.java new file mode 100644 index 00000000000..bcf98438441 --- /dev/null +++ b/MekHQ/src/mekhq/campaign/utilities/glossary/GlossaryEntry.java @@ -0,0 +1,64 @@ +/* + * 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 as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with MekHQ. If not, see . + */ +package mekhq.campaign.utilities.glossary; + +/** + * Represents an immutable glossary entry containing details about a specific term. This record is + * designed to encapsulate the key information required to define or describe an entry in a + * glossary. + * + *

Validation is performed during construction to ensure none of the fields are {@code null} or + * blank.

+ * + *

Fields:

+ *
    + *
  • {@code title}: A short, human-friendly title for the glossary entry
  • + *
  • {@code description}: A detailed explanation or definition of the glossary entry
  • + *
+ * + * @param title The title of the glossary entry. Must not be {@code null} or blank. + * @param description The description of the glossary entry. Must not be {@code null} or blank. + */ +public record GlossaryEntry(String title, String description) { + /** + * Compact constructor for {@code GlossaryEntry}. + * Performs validation to ensure all fields are non-null and non-blank. + * + * @param title The title of the glossary entry. + * @param description The description of the glossary entry. + * @throws IllegalArgumentException If any field is {@code null} or blank. + */ + public GlossaryEntry { + validateField(title, "Title"); + validateField(description, "Description"); + } + + /** + * Validates that the provided field is neither {@code null} nor blank. + * + * @param field The field to validate. + * @param fieldName The name of the field (used in exception messages). + * @throws IllegalArgumentException If the field is {@code null} or blank. + */ + private static void validateField(String field, String fieldName) { + if (field == null || field.isBlank()) { + throw new IllegalArgumentException(fieldName + " must not be null or blank."); + } + } +} diff --git a/MekHQ/src/mekhq/campaign/utilities/glossary/GlossaryEntryWrapper.java b/MekHQ/src/mekhq/campaign/utilities/glossary/GlossaryEntryWrapper.java new file mode 100644 index 00000000000..349cc3fc36e --- /dev/null +++ b/MekHQ/src/mekhq/campaign/utilities/glossary/GlossaryEntryWrapper.java @@ -0,0 +1,58 @@ +/* + * 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 as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with MekHQ. If not, see . + */ +package mekhq.campaign.utilities.glossary; + +import java.util.Map; + +/** + * The {@code GlossaryEntryWrapper} class is a container used to model the YAML structure + * for glossary entries. It acts as an intermediary for parsing the glossary YAML file and + * mapping its terms to specific glossary entries. + * + *

+ * The YAML file is expected to define a map-like structure where the keys are glossary term + * identifiers (Strings) and the values are instances of {@link GlossaryEntry}. + *

+ */ +class GlossaryEntryWrapper { + private Map entries; + + /** + * Gets the map of glossary terms with their corresponding {@link GlossaryEntry} objects. + * + * @return A {@link Map} containing all glossary terms and their metadata. + */ + public Map getEntries() { + return entries; + } + + /** + * Sets the map of glossary terms with their corresponding {@link GlossaryEntry} records. + * + *

+ * This method is primarily used during deserialization when Jackson maps the YAML data to + * this object. It allows assigning the parsed entries to the internal map. + *

+ * + * @param entries A {@link Map} containing glossary terms and their {@link GlossaryEntry} objects. + */ + public void setEntries(Map entries) { + this.entries = entries; + } +} diff --git a/MekHQ/src/mekhq/campaign/utilities/glossary/GlossaryLibrary.java b/MekHQ/src/mekhq/campaign/utilities/glossary/GlossaryLibrary.java new file mode 100644 index 00000000000..041d79a75aa --- /dev/null +++ b/MekHQ/src/mekhq/campaign/utilities/glossary/GlossaryLibrary.java @@ -0,0 +1,106 @@ +/* + * 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 as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with MekHQ. If not, see . + */ +package mekhq.campaign.utilities.glossary; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * The {@code GlossaryLibrary} class manages the loading and retrieval of glossary entries. + * These entries are stored in YAML files and provide definitions or descriptions for various terms. + * + *

The default glossary file location is set to {@code data/universe/Glossary.yml}.

+ */ +public class GlossaryLibrary { + private final String DIRECTORY = "data/universe/"; + private final String EXTENSION = ".yml"; + private final String GLOSSARY_ADDRESS = DIRECTORY + "Glossary" + EXTENSION; + + private final Map glossaryEntries = new HashMap<>(); + + /** + * The command string used to indicate a glossary entry in other parts of the application. + * This is primarily used for recognizing glossary calls in hyperlinks. See + * {@link mekhq.gui.baseComponents.MHQDialogImmersive} for an example usage. + */ + public static final String GLOSSARY_COMMAND_STRING = "GLOSSARY"; + + /** + * Constructs a new {@code GlossaryLibrary} and immediately loads glossary entries + * from the default file path specified in {@link #GLOSSARY_ADDRESS}. + * + *

+ * Any errors encountered during loading will result in a {@link RuntimeException}. + *

+ */ + public GlossaryLibrary() { + loadGlossaryEntries(GLOSSARY_ADDRESS); + } + + /** + * Loads a map of glossary entries from a specified YAML file. + * + *

+ * The YAML file must follow the structure defined by the {@link GlossaryEntryWrapper} + * class, which wraps a map of glossary terms and their corresponding {@link GlossaryEntry} + * records. + *

+ * + *

+ * If the file cannot be read or parsed, a {@link RuntimeException} is thrown with the + * error details. + *

+ * + * @param filePath The path to the YAML file containing the glossary data. + * @throws RuntimeException if the file cannot be read or parsed. + */ + public void loadGlossaryEntries(String filePath) { + ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory()); + + try { + GlossaryEntryWrapper wrapper = objectMapper.readValue( + new File(filePath), GlossaryEntryWrapper.class + ); + + glossaryEntries.putAll(wrapper.getEntries()); + } catch (IOException e) { + throw new RuntimeException("Error reading glossary entries from file: " + filePath, e); + } + } + + /** + * Gets the map of all glossary entries that have been loaded. + * + *

+ * The keys of the map represent glossary term identifiers, and the values represent the + * corresponding {@link GlossaryEntry} objects. + *

+ * + * @return A {@link Map} containing all glossary entries, or an empty map if no entries were + * loaded. + */ + public Map getGlossaryEntries() { + return glossaryEntries; + } +} diff --git a/MekHQ/src/mekhq/gui/baseComponents/MHQDialogImmersive.java b/MekHQ/src/mekhq/gui/baseComponents/MHQDialogImmersive.java index 64f593044ed..66d7b3dddb7 100644 --- a/MekHQ/src/mekhq/gui/baseComponents/MHQDialogImmersive.java +++ b/MekHQ/src/mekhq/gui/baseComponents/MHQDialogImmersive.java @@ -26,6 +26,7 @@ import mekhq.campaign.personnel.Person; import mekhq.campaign.personnel.enums.PersonnelRole; import mekhq.campaign.unit.Unit; +import mekhq.gui.dialog.GlossaryDialog; import javax.swing.*; import javax.swing.event.HyperlinkEvent.EventType; @@ -36,6 +37,7 @@ import static megamek.client.ui.WrapLayout.wordWrap; import static megamek.client.ui.swing.util.FlatLafStyleBuilder.setFontScaling; import static mekhq.campaign.force.Force.FORCE_NONE; +import static mekhq.campaign.utilities.glossary.GlossaryLibrary.GLOSSARY_COMMAND_STRING; import static mekhq.utilities.ImageUtilities.scaleImageIconToWidth; import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; @@ -57,7 +59,7 @@ public class MHQDialogImmersive extends JDialog { private int CENTER_WIDTH = UIUtil.scaleForGUI(400); - private final int INSERT_SIZE = UIUtil.scaleForGUI(5); + private final int PADDING = UIUtil.scaleForGUI(5); protected final int IMAGE_WIDTH = 125; // This is scaled to GUI by 'scaleImageIconToWidth' private JPanel northPanel; @@ -115,7 +117,7 @@ public MHQDialogImmersive(Campaign campaign, @Nullable Person leftSpeaker, // Main Panel to hold all boxes JPanel mainPanel = new JPanel(new GridBagLayout()); GridBagConstraints constraints = new GridBagConstraints(); - constraints.insets = new Insets(INSERT_SIZE, INSERT_SIZE, INSERT_SIZE, INSERT_SIZE); + constraints.insets = new Insets(PADDING, PADDING, PADDING, PADDING); constraints.fill = GridBagConstraints.BOTH; constraints.weighty = 1; @@ -233,7 +235,7 @@ private JPanel createCenterBox(String centerMessage, List - * Usage
- * This method provides a default implementation that does nothing. Subclasses should - * override this to provide specific behavior when hyperlinks are clicked. - *

* * @param campaign The {@link Campaign} instance that contains relevant data. - * @param href The hyperlink reference (e.g., a URL or a specific identifier). + * @param reference The hyperlink reference (e.g., a specific identifier). */ - protected void handleHyperlinkClick(Campaign campaign, String href) { - logger.error("handleHyperlinkClick() was not overridden in the subclass."); + protected void handleHyperlinkClick(Campaign campaign, String reference) { + String[] splitReference = reference.split(":"); + + String commandKey = splitReference[0]; + String entryKey = splitReference[1]; + + if (commandKey.equals(GLOSSARY_COMMAND_STRING)) { + new GlossaryDialog(this, campaign, entryKey); + } } /** @@ -272,16 +276,37 @@ protected void handleHyperlinkClick(Campaign campaign, String href) { */ private void populateOutOfCharacterPanel(String outOfCharacterMessage) { JPanel pnlOutOfCharacter = new JPanel(new GridBagLayout()); - pnlOutOfCharacter.setBorder(BorderFactory.createEtchedBorder()); - JLabel lblOutOfCharacter = new JLabel( - String.format("
%s
", - CENTER_WIDTH, outOfCharacterMessage)); - lblOutOfCharacter.setBorder(BorderFactory.createEmptyBorder(INSERT_SIZE, INSERT_SIZE, - INSERT_SIZE, INSERT_SIZE)); + // Create a compound border with an etched border and padding (empty border) + pnlOutOfCharacter.setBorder( + BorderFactory.createEtchedBorder() + ); + + // Create a JEditorPane for the message + JEditorPane editorPane = new JEditorPane(); + editorPane.setContentType("text/html"); + editorPane.setEditable(false); + editorPane.setFocusable(false); + + int width = CENTER_WIDTH; + width += leftSpeaker != null ? IMAGE_WIDTH + PADDING : 0; + width += rightSpeaker != null ? IMAGE_WIDTH + PADDING : 0; + + // Use inline CSS to set font family, size, and other style properties + editorPane.setText(String.format("
%s
", width, outOfCharacterMessage)); + setFontScaling(editorPane, false, 1); + + // Add a HyperlinkListener to capture hyperlink clicks + editorPane.addHyperlinkListener(evt -> { + if (evt.getEventType() == EventType.ACTIVATED) { + handleHyperlinkClick(campaign, evt.getDescription()); + } + }); - pnlOutOfCharacter.add(lblOutOfCharacter); + // Add the editor pane to the panel + pnlOutOfCharacter.add(editorPane); + // Add the panel to the southPanel southPanel.add(pnlOutOfCharacter, BorderLayout.SOUTH); } @@ -299,7 +324,7 @@ private void populateButtonPanel(List buttons) { GridBagConstraints gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 0; - gbc.insets = new Insets(INSERT_SIZE, INSERT_SIZE, INSERT_SIZE, INSERT_SIZE); + gbc.insets = new Insets(PADDING, PADDING, PADDING, PADDING); gbc.anchor = GridBagConstraints.WEST; gbc.fill = GridBagConstraints.NONE; diff --git a/MekHQ/src/mekhq/gui/dialog/GlossaryDialog.java b/MekHQ/src/mekhq/gui/dialog/GlossaryDialog.java new file mode 100644 index 00000000000..f7baafac01d --- /dev/null +++ b/MekHQ/src/mekhq/gui/dialog/GlossaryDialog.java @@ -0,0 +1,186 @@ +/* + * 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 as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with MekHQ. If not, see . + */ +package mekhq.gui.dialog; + +import megamek.client.ui.swing.util.UIUtil; +import megamek.logging.MMLogger; +import mekhq.campaign.Campaign; +import mekhq.campaign.utilities.glossary.GlossaryEntry; + +import javax.swing.*; +import javax.swing.event.HyperlinkEvent.EventType; +import java.awt.*; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; + +import static megamek.client.ui.swing.util.FlatLafStyleBuilder.setFontScaling; +import static mekhq.campaign.utilities.glossary.GlossaryLibrary.GLOSSARY_COMMAND_STRING; + +/** + * The {@code GlossaryDialog} class represents a dialog window for displaying glossary entries. + * It displays detailed information about a glossary term, including its title and description, + * in a styled HTML format. + * + *

+ * This class uses a {@link JEditorPane} to render glossary entry content and supports hyperlink + * interactions for related glossary entries. If a related term is clicked, a new {@code GlossaryDialog} + * is opened to show its details. + *

+ * + *

+ * The dialog fetches glossary entries via the {@link Campaign} class and uses + * {@link GlossaryEntry} objects to retrieve data for display. + *

+ */ +public class GlossaryDialog extends JDialog { + private static final MMLogger logger = MMLogger.create(GlossaryDialog.class); + + private final JDialog parent; + private final Campaign campaign; + private final GlossaryEntry entry; + + private int CENTER_WIDTH = UIUtil.scaleForGUI(400); + private int CENTER_HEIGHT = UIUtil.scaleForGUI(300); + private int PADDING = UIUtil.scaleForGUI(10); + + /** + * Constructs a new {@code GlossaryDialog} to display a glossary entry. + * + *

+ * If the glossary key does not correspond to an existing entry, an error is logged, + * and the dialog is not displayed. + *

+ * + * @param parent The parent {@code JDialog} to temporarily hide while this dialog is open. + * @param campaign The {@code Campaign} instance containing the glossary data. + * @param key The key for the glossary entry to display. + */ + public GlossaryDialog(JDialog parent, Campaign campaign, String key) { + this.parent = parent; + this.campaign = campaign; + this.entry = campaign.getGlossaryEntry(key); + + if (entry != null) { + parent.setVisible(false); + displayDialog(entry.title(), entry.description()); + } else { + logger.error("No entry available for key {}", key); + } + } + + /** + * Displays the glossary dialog with the given title and description. + * + *

+ * The content is rendered using HTML in a {@link JEditorPane}, allowing for styled text + * and clickable hyperlinks. The dialog also provides a scrollable view for long content. + *

+ * + * @param title The title of the glossary entry. + * @param description The detailed description of the glossary entry. + */ + private void displayDialog(String title, String description) { + setTitle(title); + + // Create a JEditorPane for the message + JEditorPane editorPane = new JEditorPane(); + editorPane.setContentType("text/html"); + editorPane.setEditable(false); + editorPane.setFocusable(false); + + // Use inline CSS to set font family, size, and other style properties + String fontStyle = "font-family: Noto Sans;"; + editorPane.setText(String.format( + "
" + + "

%s

" + + "

%s

" + + "
", + CENTER_WIDTH, fontStyle, title, description + )); + setFontScaling(editorPane, false, 1.1); + + // Add a HyperlinkListener to capture hyperlink clicks + editorPane.addHyperlinkListener(evt -> { + if (evt.getEventType() == EventType.ACTIVATED) { + handleHyperlinkClick(campaign, evt.getDescription()); + } + }); + + // Wrap the JEditorPane in a JScrollPane + JScrollPane scrollPane = new JScrollPane(editorPane); + scrollPane.setMinimumSize(new Dimension(CENTER_WIDTH, scrollPane.getHeight())); + + // Create a JPanel with padding + JPanel paddedPanel = new JPanel(new BorderLayout()); + paddedPanel.setBorder(BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING)); + paddedPanel.add(scrollPane, BorderLayout.CENTER); + add(paddedPanel); + + // Assign close action + setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); + addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + onCloseAction(); + } + }); + + // Set dialog properties + setSize(CENTER_WIDTH + (PADDING * 2), CENTER_HEIGHT); + setLocationRelativeTo(null); + setModal(true); + setVisible(true); + } + + /** + * Handles user interactions when the dialog is closed. + * + *

+ * This method ensures the parent dialog is made visible again after the glossary + * dialog is closed. + *

+ */ + private void onCloseAction() { + dispose(); + parent.setVisible(true); + } + + /** + * Handles hyperlink clicks from the HTML content displayed in the glossary dialog. + * + *

+ * If the hyperlink points to another glossary term (via the {@code GLOSSARY} command), + * a new {@code GlossaryDialog} is opened for the referenced term. + *

+ * + * @param campaign The {@link Campaign} instance containing glossary data. + * @param reference The hyperlink reference string. Expected to be in the format + * {@code GL_COMMAND:termKey}. + */ + private void handleHyperlinkClick(Campaign campaign, String reference) { + String[] splitReference = reference.split(":"); + + String commandKey = splitReference[0]; + String entryKey = splitReference[1]; + + if (commandKey.equals(GLOSSARY_COMMAND_STRING)) { + new GlossaryDialog(this, campaign, entryKey); + } + } +} diff --git a/MekHQ/src/mekhq/gui/dialog/VocationalExperienceAwardDialog.java b/MekHQ/src/mekhq/gui/dialog/VocationalExperienceAwardDialog.java index f3951185609..fa44e0548b2 100644 --- a/MekHQ/src/mekhq/gui/dialog/VocationalExperienceAwardDialog.java +++ b/MekHQ/src/mekhq/gui/dialog/VocationalExperienceAwardDialog.java @@ -30,6 +30,7 @@ import java.util.UUID; import static mekhq.campaign.Campaign.AdministratorSpecialization.HR; +import static mekhq.campaign.utilities.glossary.GlossaryLibrary.GLOSSARY_COMMAND_STRING; import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; /** @@ -42,6 +43,8 @@ * personnel records via hyperlinks.

*/ public class VocationalExperienceAwardDialog extends MHQDialogImmersive { + private static final String PERSON_COMMAND_STRING = "PERSON"; + private static final String RESOURCE_BUNDLE = "mekhq.resources.VocationalExperienceAwardDialog"; /** @@ -67,15 +70,24 @@ public VocationalExperienceAwardDialog(Campaign campaign) { *

This method parses the hyperlink reference to focus on the personnel record identified by * the provided UUID in the campaign's graphical user interface.

* - * @param campaign the {@link Campaign} containing relevant personnel data - * @param hyperlinkReference the hyperlink reference containing the UUID of the selected character + * @param campaign the {@link Campaign} containing relevant personnel data + * @param reference the hyperlink reference containing the UUID of the selected character */ @Override - protected void handleHyperlinkClick(Campaign campaign, String hyperlinkReference) { - CampaignGUI campaignGUI = campaign.getApp().getCampaigngui(); + protected void handleHyperlinkClick(Campaign campaign, String reference) { + String[] splitReference = reference.split(":"); + + String commandKey = splitReference[0]; + String entryKey = splitReference[1]; - final UUID id = UUID.fromString(hyperlinkReference.split(":")[1]); - campaignGUI.focusOnPerson(id); + if (commandKey.equals(GLOSSARY_COMMAND_STRING)) { + new GlossaryDialog(this, campaign, entryKey); + } else if (commandKey.equals(PERSON_COMMAND_STRING)) { + CampaignGUI campaignGUI = campaign.getApp().getCampaigngui(); + + final UUID id = UUID.fromString(reference.split(":")[1]); + campaignGUI.focusOnPerson(id); + } } /** From 65591841ce207e9aafdc34b0263d0521ca3e1f28 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Fri, 7 Feb 2025 16:00:53 -0600 Subject: [PATCH 088/112] Refactored glossaryLibrary initialization in Campaign. Moved glossaryLibrary initialization to the reset method. This ensures proper reinitialization when resetting the campaign state and adheres to the intended behavior of rebuilding the glossary upon reload. --- MekHQ/src/mekhq/campaign/Campaign.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index ae0f4003024..4bae99763e2 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -323,7 +323,7 @@ public class Campaign implements ITechManager { // We deliberately don't write this data to the save file as we want it rebuilt every time the // campaign loads. This ensures updates can be applied and there is no risk of bugs being // permanently locked into the campaign file. - Map glossaryLibrary = new GlossaryLibrary().getGlossaryEntries(); + Map glossaryLibrary; /** * Represents the different types of administrative specializations. @@ -411,6 +411,7 @@ public Campaign() { topUpWeekly = false; ignoreMothballed = false; ignoreSparesUnderQuality = QUALITY_A; + glossaryLibrary = new GlossaryLibrary().getGlossaryEntries(); } From 0aba803d72bdc0fe034d195b5dcbb26dbc822bdb Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Fri, 7 Feb 2025 16:09:44 -0600 Subject: [PATCH 089/112] Refactored glossaryLibrary initialization to use an empty map. Replaced the GlossaryLibrary initialization with a HashMap to ensure flexibility in handling glossary entries. This change optimizes resource management and prepares the field for future dynamic modifications. --- MekHQ/src/mekhq/campaign/Campaign.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index 4bae99763e2..5860b4a279c 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -129,7 +129,6 @@ import mekhq.campaign.universe.selectors.planetSelectors.DefaultPlanetSelector; import mekhq.campaign.universe.selectors.planetSelectors.RangedPlanetSelector; import mekhq.campaign.utilities.glossary.GlossaryEntry; -import mekhq.campaign.utilities.glossary.GlossaryLibrary; import mekhq.campaign.work.IAcquisitionWork; import mekhq.campaign.work.IPartWork; import mekhq.gui.sorter.PersonTitleSorter; @@ -411,7 +410,7 @@ public Campaign() { topUpWeekly = false; ignoreMothballed = false; ignoreSparesUnderQuality = QUALITY_A; - glossaryLibrary = new GlossaryLibrary().getGlossaryEntries(); + glossaryLibrary = new HashMap<>(); } From 8bbf8c4264d9d85a053111d2c4a479a43d2b94ed Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Fri, 7 Feb 2025 16:11:06 -0600 Subject: [PATCH 090/112] Initialize GlossaryLibrary with error handling Replaced direct HashMap initialization with GlossaryLibrary parsing. Added a try-catch block to ensure campaign initialization proceeds even if glossary parsing fails, improving robustness. --- MekHQ/src/mekhq/campaign/Campaign.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index 5860b4a279c..da4a03bfc93 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -129,6 +129,7 @@ import mekhq.campaign.universe.selectors.planetSelectors.DefaultPlanetSelector; import mekhq.campaign.universe.selectors.planetSelectors.RangedPlanetSelector; import mekhq.campaign.utilities.glossary.GlossaryEntry; +import mekhq.campaign.utilities.glossary.GlossaryLibrary; import mekhq.campaign.work.IAcquisitionWork; import mekhq.campaign.work.IPartWork; import mekhq.gui.sorter.PersonTitleSorter; @@ -410,7 +411,15 @@ public Campaign() { topUpWeekly = false; ignoreMothballed = false; ignoreSparesUnderQuality = QUALITY_A; - glossaryLibrary = new HashMap<>(); + + // Library initialization + try { + glossaryLibrary = new GlossaryLibrary().getGlossaryEntries(); + } catch (Exception e) { + // This ensures that if we fail to parse the glossary, for whatever reason, + // the campaign will still successfully initialize. + glossaryLibrary = new HashMap<>(); + } } From c82a2abcea10803b9bee7b950a5fa06a32731549 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Fri, 7 Feb 2025 19:25:11 -0600 Subject: [PATCH 091/112] Refactored glossary system to use enums and removed YAML structure. Replaced the glossary YAML-based system with an enum-based implementation, consolidating glossary entries into the `Glossary` enum. Eliminated redundant classes (`GlossaryLibrary`, `GlossaryEntry`, and `GlossaryEntryWrapper`) and adjusted references across the codebase for the updated structure. This streamlines glossary handling and reduces dependency on external files. --- MekHQ/data/universe/glossary.yml | 8 -- MekHQ/src/mekhq/campaign/Campaign.java | 29 ----- MekHQ/src/mekhq/campaign/enums/Glossary.java | 72 ++++++++++++ .../utilities/glossary/GlossaryEntry.java | 64 ----------- .../glossary/GlossaryEntryWrapper.java | 58 ---------- .../utilities/glossary/GlossaryLibrary.java | 106 ------------------ .../baseComponents/MHQDialogImmersive.java | 2 +- .../src/mekhq/gui/dialog/GlossaryDialog.java | 19 ++-- .../VocationalExperienceAwardDialog.java | 1 - 9 files changed, 80 insertions(+), 279 deletions(-) delete mode 100644 MekHQ/data/universe/glossary.yml create mode 100644 MekHQ/src/mekhq/campaign/enums/Glossary.java delete mode 100644 MekHQ/src/mekhq/campaign/utilities/glossary/GlossaryEntry.java delete mode 100644 MekHQ/src/mekhq/campaign/utilities/glossary/GlossaryEntryWrapper.java delete mode 100644 MekHQ/src/mekhq/campaign/utilities/glossary/GlossaryLibrary.java diff --git a/MekHQ/data/universe/glossary.yml b/MekHQ/data/universe/glossary.yml deleted file mode 100644 index acf91ca10b4..00000000000 --- a/MekHQ/data/universe/glossary.yml +++ /dev/null @@ -1,8 +0,0 @@ -entries: - # The map key used to retrieve the entry. This is the value included in the HTML hyperlink to - # fetch this particular glossary entry so must be unique. - EXAMPLE: - # This is the title of the entry, this will be displayed in the glossary dialog - title: "An Example Title" - # This is the specific description of the term that will be presented to the player - description: "This is an example entry. This text will be displayed to the user." diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index da4a03bfc93..ca631617aeb 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -128,8 +128,6 @@ import mekhq.campaign.universe.selectors.planetSelectors.AbstractPlanetSelector; import mekhq.campaign.universe.selectors.planetSelectors.DefaultPlanetSelector; import mekhq.campaign.universe.selectors.planetSelectors.RangedPlanetSelector; -import mekhq.campaign.utilities.glossary.GlossaryEntry; -import mekhq.campaign.utilities.glossary.GlossaryLibrary; import mekhq.campaign.work.IAcquisitionWork; import mekhq.campaign.work.IPartWork; import mekhq.gui.sorter.PersonTitleSorter; @@ -319,12 +317,6 @@ public class Campaign implements ITechManager { private boolean topUpWeekly; private PartQuality ignoreSparesUnderQuality; - // Libraries - // We deliberately don't write this data to the save file as we want it rebuilt every time the - // campaign loads. This ensures updates can be applied and there is no risk of bugs being - // permanently locked into the campaign file. - Map glossaryLibrary; - /** * Represents the different types of administrative specializations. * Each specialization corresponds to a distinct administrative role @@ -411,16 +403,6 @@ public Campaign() { topUpWeekly = false; ignoreMothballed = false; ignoreSparesUnderQuality = QUALITY_A; - - // Library initialization - try { - glossaryLibrary = new GlossaryLibrary().getGlossaryEntries(); - } catch (Exception e) { - // This ensures that if we fail to parse the glossary, for whatever reason, - // the campaign will still successfully initialize. - glossaryLibrary = new HashMap<>(); - } - } /** @@ -5958,17 +5940,6 @@ public void setAutomatedMothballUnits(List automatedMothballUnits) { this.automatedMothballUnits = automatedMothballUnits; } - /** - * Retrieves a glossary entry based on the provided key. - * - * @param key the unique identifier for the glossary entry to retrieve - * @return the {@code GlossaryEntry} object corresponding to the given key, - * or {@code null} if no entry is found for the specified key - */ - public @Nullable GlossaryEntry getGlossaryEntry(String key) { - return glossaryLibrary.get(key); - } - public void writeToXML(final PrintWriter pw) { int indent = 0; diff --git a/MekHQ/src/mekhq/campaign/enums/Glossary.java b/MekHQ/src/mekhq/campaign/enums/Glossary.java new file mode 100644 index 00000000000..a19c4c0a3f2 --- /dev/null +++ b/MekHQ/src/mekhq/campaign/enums/Glossary.java @@ -0,0 +1,72 @@ +/* + * 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 as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with MekHQ. If not, see . + */ +package mekhq.campaign.enums; + +import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; + +public enum Glossary { + // region Enum Declarations + PRISONER_CAPACITY, + REPUTATION; + // endregion Enum Declarations + + final private String RESOURCE_BUNDLE = "mekhq.resources." + getClass().getSimpleName(); + + // region Getters + /** + * Retrieves the title associated with this enum constant. + * + *

+ * This method constructs a resource key by appending {@code ".title"} to the enum constant's + * name and looks up the corresponding title from the resource bundle. The title is then + * formatted and returned as a string. + *

+ * + * @return A formatted string representing the title for this enum constant, + * fetched from the resource bundle. + */ + public String getTitle() { + final String RESOURCE_KEY = name() + ".title"; + + return getFormattedTextAt(RESOURCE_BUNDLE, RESOURCE_KEY); + } + + /** + * Retrieves the description associated with this enum constant. + * + *

+ * This method constructs a resource key by appending {@code ".description"} to the enum constant's + * name and looks up the corresponding description from the resource bundle. The description + * is then formatted and returned as a string. + *

+ * + * @return A formatted string representing the description for this enum constant, + * fetched from the resource bundle. + */ + public String getDescription() { + final String RESOURCE_KEY = name() + ".description"; + + return getFormattedTextAt(RESOURCE_BUNDLE, RESOURCE_KEY); + } + + @Override + public String toString() { + return getTitle(); + } +} diff --git a/MekHQ/src/mekhq/campaign/utilities/glossary/GlossaryEntry.java b/MekHQ/src/mekhq/campaign/utilities/glossary/GlossaryEntry.java deleted file mode 100644 index bcf98438441..00000000000 --- a/MekHQ/src/mekhq/campaign/utilities/glossary/GlossaryEntry.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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 as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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. - * - * You should have received a copy of the GNU General Public License - * along with MekHQ. If not, see . - */ -package mekhq.campaign.utilities.glossary; - -/** - * Represents an immutable glossary entry containing details about a specific term. This record is - * designed to encapsulate the key information required to define or describe an entry in a - * glossary. - * - *

Validation is performed during construction to ensure none of the fields are {@code null} or - * blank.

- * - *

Fields:

- *
    - *
  • {@code title}: A short, human-friendly title for the glossary entry
  • - *
  • {@code description}: A detailed explanation or definition of the glossary entry
  • - *
- * - * @param title The title of the glossary entry. Must not be {@code null} or blank. - * @param description The description of the glossary entry. Must not be {@code null} or blank. - */ -public record GlossaryEntry(String title, String description) { - /** - * Compact constructor for {@code GlossaryEntry}. - * Performs validation to ensure all fields are non-null and non-blank. - * - * @param title The title of the glossary entry. - * @param description The description of the glossary entry. - * @throws IllegalArgumentException If any field is {@code null} or blank. - */ - public GlossaryEntry { - validateField(title, "Title"); - validateField(description, "Description"); - } - - /** - * Validates that the provided field is neither {@code null} nor blank. - * - * @param field The field to validate. - * @param fieldName The name of the field (used in exception messages). - * @throws IllegalArgumentException If the field is {@code null} or blank. - */ - private static void validateField(String field, String fieldName) { - if (field == null || field.isBlank()) { - throw new IllegalArgumentException(fieldName + " must not be null or blank."); - } - } -} diff --git a/MekHQ/src/mekhq/campaign/utilities/glossary/GlossaryEntryWrapper.java b/MekHQ/src/mekhq/campaign/utilities/glossary/GlossaryEntryWrapper.java deleted file mode 100644 index 349cc3fc36e..00000000000 --- a/MekHQ/src/mekhq/campaign/utilities/glossary/GlossaryEntryWrapper.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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 as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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. - * - * You should have received a copy of the GNU General Public License - * along with MekHQ. If not, see . - */ -package mekhq.campaign.utilities.glossary; - -import java.util.Map; - -/** - * The {@code GlossaryEntryWrapper} class is a container used to model the YAML structure - * for glossary entries. It acts as an intermediary for parsing the glossary YAML file and - * mapping its terms to specific glossary entries. - * - *

- * The YAML file is expected to define a map-like structure where the keys are glossary term - * identifiers (Strings) and the values are instances of {@link GlossaryEntry}. - *

- */ -class GlossaryEntryWrapper { - private Map entries; - - /** - * Gets the map of glossary terms with their corresponding {@link GlossaryEntry} objects. - * - * @return A {@link Map} containing all glossary terms and their metadata. - */ - public Map getEntries() { - return entries; - } - - /** - * Sets the map of glossary terms with their corresponding {@link GlossaryEntry} records. - * - *

- * This method is primarily used during deserialization when Jackson maps the YAML data to - * this object. It allows assigning the parsed entries to the internal map. - *

- * - * @param entries A {@link Map} containing glossary terms and their {@link GlossaryEntry} objects. - */ - public void setEntries(Map entries) { - this.entries = entries; - } -} diff --git a/MekHQ/src/mekhq/campaign/utilities/glossary/GlossaryLibrary.java b/MekHQ/src/mekhq/campaign/utilities/glossary/GlossaryLibrary.java deleted file mode 100644 index 041d79a75aa..00000000000 --- a/MekHQ/src/mekhq/campaign/utilities/glossary/GlossaryLibrary.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * 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 as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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. - * - * You should have received a copy of the GNU General Public License - * along with MekHQ. If not, see . - */ -package mekhq.campaign.utilities.glossary; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; - -import java.io.File; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -/** - * The {@code GlossaryLibrary} class manages the loading and retrieval of glossary entries. - * These entries are stored in YAML files and provide definitions or descriptions for various terms. - * - *

The default glossary file location is set to {@code data/universe/Glossary.yml}.

- */ -public class GlossaryLibrary { - private final String DIRECTORY = "data/universe/"; - private final String EXTENSION = ".yml"; - private final String GLOSSARY_ADDRESS = DIRECTORY + "Glossary" + EXTENSION; - - private final Map glossaryEntries = new HashMap<>(); - - /** - * The command string used to indicate a glossary entry in other parts of the application. - * This is primarily used for recognizing glossary calls in hyperlinks. See - * {@link mekhq.gui.baseComponents.MHQDialogImmersive} for an example usage. - */ - public static final String GLOSSARY_COMMAND_STRING = "GLOSSARY"; - - /** - * Constructs a new {@code GlossaryLibrary} and immediately loads glossary entries - * from the default file path specified in {@link #GLOSSARY_ADDRESS}. - * - *

- * Any errors encountered during loading will result in a {@link RuntimeException}. - *

- */ - public GlossaryLibrary() { - loadGlossaryEntries(GLOSSARY_ADDRESS); - } - - /** - * Loads a map of glossary entries from a specified YAML file. - * - *

- * The YAML file must follow the structure defined by the {@link GlossaryEntryWrapper} - * class, which wraps a map of glossary terms and their corresponding {@link GlossaryEntry} - * records. - *

- * - *

- * If the file cannot be read or parsed, a {@link RuntimeException} is thrown with the - * error details. - *

- * - * @param filePath The path to the YAML file containing the glossary data. - * @throws RuntimeException if the file cannot be read or parsed. - */ - public void loadGlossaryEntries(String filePath) { - ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory()); - - try { - GlossaryEntryWrapper wrapper = objectMapper.readValue( - new File(filePath), GlossaryEntryWrapper.class - ); - - glossaryEntries.putAll(wrapper.getEntries()); - } catch (IOException e) { - throw new RuntimeException("Error reading glossary entries from file: " + filePath, e); - } - } - - /** - * Gets the map of all glossary entries that have been loaded. - * - *

- * The keys of the map represent glossary term identifiers, and the values represent the - * corresponding {@link GlossaryEntry} objects. - *

- * - * @return A {@link Map} containing all glossary entries, or an empty map if no entries were - * loaded. - */ - public Map getGlossaryEntries() { - return glossaryEntries; - } -} diff --git a/MekHQ/src/mekhq/gui/baseComponents/MHQDialogImmersive.java b/MekHQ/src/mekhq/gui/baseComponents/MHQDialogImmersive.java index 66d7b3dddb7..9fb01dc0a52 100644 --- a/MekHQ/src/mekhq/gui/baseComponents/MHQDialogImmersive.java +++ b/MekHQ/src/mekhq/gui/baseComponents/MHQDialogImmersive.java @@ -37,7 +37,6 @@ import static megamek.client.ui.WrapLayout.wordWrap; import static megamek.client.ui.swing.util.FlatLafStyleBuilder.setFontScaling; import static mekhq.campaign.force.Force.FORCE_NONE; -import static mekhq.campaign.utilities.glossary.GlossaryLibrary.GLOSSARY_COMMAND_STRING; import static mekhq.utilities.ImageUtilities.scaleImageIconToWidth; import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; @@ -54,6 +53,7 @@ */ public class MHQDialogImmersive extends JDialog { private final String RESOURCE_BUNDLE = "mekhq.resources.GUI"; + public final static String GLOSSARY_COMMAND_STRING = "GLOSSARY"; private Campaign campaign; diff --git a/MekHQ/src/mekhq/gui/dialog/GlossaryDialog.java b/MekHQ/src/mekhq/gui/dialog/GlossaryDialog.java index f7baafac01d..43c2582c83a 100644 --- a/MekHQ/src/mekhq/gui/dialog/GlossaryDialog.java +++ b/MekHQ/src/mekhq/gui/dialog/GlossaryDialog.java @@ -21,7 +21,7 @@ import megamek.client.ui.swing.util.UIUtil; import megamek.logging.MMLogger; import mekhq.campaign.Campaign; -import mekhq.campaign.utilities.glossary.GlossaryEntry; +import mekhq.campaign.enums.Glossary; import javax.swing.*; import javax.swing.event.HyperlinkEvent.EventType; @@ -30,7 +30,7 @@ import java.awt.event.WindowEvent; import static megamek.client.ui.swing.util.FlatLafStyleBuilder.setFontScaling; -import static mekhq.campaign.utilities.glossary.GlossaryLibrary.GLOSSARY_COMMAND_STRING; +import static mekhq.gui.baseComponents.MHQDialogImmersive.GLOSSARY_COMMAND_STRING; /** * The {@code GlossaryDialog} class represents a dialog window for displaying glossary entries. @@ -42,18 +42,13 @@ * interactions for related glossary entries. If a related term is clicked, a new {@code GlossaryDialog} * is opened to show its details. *

- * - *

- * The dialog fetches glossary entries via the {@link Campaign} class and uses - * {@link GlossaryEntry} objects to retrieve data for display. - *

*/ public class GlossaryDialog extends JDialog { private static final MMLogger logger = MMLogger.create(GlossaryDialog.class); private final JDialog parent; private final Campaign campaign; - private final GlossaryEntry entry; + private Glossary entry; private int CENTER_WIDTH = UIUtil.scaleForGUI(400); private int CENTER_HEIGHT = UIUtil.scaleForGUI(300); @@ -74,12 +69,12 @@ public class GlossaryDialog extends JDialog { public GlossaryDialog(JDialog parent, Campaign campaign, String key) { this.parent = parent; this.campaign = campaign; - this.entry = campaign.getGlossaryEntry(key); - if (entry != null) { + try { + this.entry = Glossary.valueOf(key); parent.setVisible(false); - displayDialog(entry.title(), entry.description()); - } else { + displayDialog(entry.getTitle(), entry.getDescription()); + } catch (IllegalArgumentException e) { logger.error("No entry available for key {}", key); } } diff --git a/MekHQ/src/mekhq/gui/dialog/VocationalExperienceAwardDialog.java b/MekHQ/src/mekhq/gui/dialog/VocationalExperienceAwardDialog.java index fa44e0548b2..e238436274c 100644 --- a/MekHQ/src/mekhq/gui/dialog/VocationalExperienceAwardDialog.java +++ b/MekHQ/src/mekhq/gui/dialog/VocationalExperienceAwardDialog.java @@ -30,7 +30,6 @@ import java.util.UUID; import static mekhq.campaign.Campaign.AdministratorSpecialization.HR; -import static mekhq.campaign.utilities.glossary.GlossaryLibrary.GLOSSARY_COMMAND_STRING; import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; /** From f7cdc432ae4eea44595d079030c3620bb0084815 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Fri, 7 Feb 2025 19:29:00 -0600 Subject: [PATCH 092/112] Add glossary entries for Prisoner Capacity and Reputation Glossary entries for "Prisoner Capacity" and "Reputation" were added to provide users with clear descriptions. These include explanations and references to relevant documentation for further details. --- MekHQ/resources/mekhq/resources/Glossary.properties | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 MekHQ/resources/mekhq/resources/Glossary.properties diff --git a/MekHQ/resources/mekhq/resources/Glossary.properties b/MekHQ/resources/mekhq/resources/Glossary.properties new file mode 100644 index 00000000000..10d8e3e75ad --- /dev/null +++ b/MekHQ/resources/mekhq/resources/Glossary.properties @@ -0,0 +1,12 @@ +PRISONER_CAPACITY.title=Prisoner Capacity +PRISONER_CAPACITY.description=Prisoner Capacity is a measure of how many prisoners your Security\ + \ personnel can manage without potential crisis. Generally speaking, each prisoner takes up 1\ + \ capacity; however, some events may change this.\ +

For additional information on Prisoner Capacity and how to manage it, please see the\ + \ documentation in...\ +
MekHQ/docs/Stratcon and Against the Bot/Prisoners.pdf

+ +REPUTATION.title=Reputation +REPUTATION.description=Reputation is a measure of how well known your unit is and whether people\ + \ generally believe you can get the job done. The rules for Reputation can be found in Campaign\ + \ Operations. \ No newline at end of file From 43eb38f28cc5e71ce01de99b8ee1ee078b007edd Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Fri, 7 Feb 2025 19:34:11 -0600 Subject: [PATCH 093/112] Adjust glossary dialog size for improved visibility Increased the dialog width by 10% using Math.round to enhance readability and better accommodate content. This change ensures a more user-friendly interface while maintaining proper scaling. --- MekHQ/src/mekhq/gui/dialog/GlossaryDialog.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MekHQ/src/mekhq/gui/dialog/GlossaryDialog.java b/MekHQ/src/mekhq/gui/dialog/GlossaryDialog.java index 43c2582c83a..e383aec610d 100644 --- a/MekHQ/src/mekhq/gui/dialog/GlossaryDialog.java +++ b/MekHQ/src/mekhq/gui/dialog/GlossaryDialog.java @@ -29,6 +29,7 @@ import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; +import static java.lang.Math.round; import static megamek.client.ui.swing.util.FlatLafStyleBuilder.setFontScaling; import static mekhq.gui.baseComponents.MHQDialogImmersive.GLOSSARY_COMMAND_STRING; @@ -137,7 +138,7 @@ public void windowClosing(WindowEvent e) { }); // Set dialog properties - setSize(CENTER_WIDTH + (PADDING * 2), CENTER_HEIGHT); + setSize((int) round((CENTER_WIDTH + (PADDING * 2)) * 1.1), CENTER_HEIGHT); setLocationRelativeTo(null); setModal(true); setVisible(true); From 47687d727d90c1484295b5a155d50e4b30e7a498 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Fri, 7 Feb 2025 19:38:45 -0600 Subject: [PATCH 094/112] Add unit tests for Glossary enum in MekHQ Introduced tests for parsing, label validation, and title extension validation in the `Glossary` enum. These ensure proper behavior and resource key validity, improving code reliability and coverage. --- .../mekhq/campaign/enums/GlossaryTest.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 MekHQ/unittests/mekhq/campaign/enums/GlossaryTest.java diff --git a/MekHQ/unittests/mekhq/campaign/enums/GlossaryTest.java b/MekHQ/unittests/mekhq/campaign/enums/GlossaryTest.java new file mode 100644 index 00000000000..bb569f8def2 --- /dev/null +++ b/MekHQ/unittests/mekhq/campaign/enums/GlossaryTest.java @@ -0,0 +1,66 @@ +/* + * 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 as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with MekHQ. If not, see . + */ +package mekhq.campaign.enums; + +import org.junit.jupiter.api.Test; + +import static mekhq.campaign.enums.Glossary.PRISONER_CAPACITY; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class GlossaryTest { + @Test + public void testParseFromString_ValidStatus() { + Glossary status = Glossary.valueOf("PRISONER_CAPACITY"); + assertEquals(PRISONER_CAPACITY, status); + } + + @Test + public void testGetLabel_notInvalid() { + for (Glossary status : Glossary.values()) { + String label = status.getTitle(); + assertTrue(isResourceKeyValid(label)); + } + } + + @Test + public void testGetTitleExtension_notInvalid() { + for (Glossary status : Glossary.values()) { + String titleExtension = status.getDescription(); + assertTrue(isResourceKeyValid(titleExtension)); + } + } + + /** + * Checks if the given text is valid. A valid string does not start or end with an exclamation + * mark ('!'). + * + *

If {@link mekhq.utilities.MHQInternationalization} fails to fetch a valid return it + * returns the key between two {@code !}. So by checking the returned string doesn't begin and + * end with that punctuation, we can easily verify that all statuses have been provided results + * for the keys we're using.

+ * + * @param text The text to validate. + * @return true if the text is valid (does not start or end with an '!'); + * false otherwise. + */ + public static boolean isResourceKeyValid(String text) { + return !text.startsWith("!") && !text.endsWith("!"); + } +} From f376b964749faa6ef8093165f914a0322dd302b3 Mon Sep 17 00:00:00 2001 From: Daniel L- <103902653+IllianiCBT@users.noreply.github.com> Date: Fri, 7 Feb 2025 19:45:28 -0600 Subject: [PATCH 095/112] Update history.txt --- MekHQ/docs/history.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/MekHQ/docs/history.txt b/MekHQ/docs/history.txt index 3b7a312bbb3..e0ed466143a 100644 --- a/MekHQ/docs/history.txt +++ b/MekHQ/docs/history.txt @@ -23,6 +23,9 @@ MEKHQ VERSION HISTORY: + PR #5985: Fixed backwards "last compatible version" check + PR #5988: Feat: refactor flags to use EquipmentFlag instead + Fix #5987: local bots property empty on non-atb games #5989 ++ Fix #5922: Fixed Off-by-One Error in Mass Training Dialog ++ PR #5993: Adjusted Gray Monday Employer Dialog to Trigger on the Correct Day ++ PR #5999: Fixed Clan Ghost Bear Greeting Keys 0.50.03 (2025-02-02 2030 UTC) From 7b3a28643d2081e62174d19f694459533414fc38 Mon Sep 17 00:00:00 2001 From: psikomonkie <189469115+psikomonkie@users.noreply.github.com> Date: Sat, 8 Feb 2025 12:10:33 -0500 Subject: [PATCH 096/112] Issue 5979: Force players to use commit when deploying forces --- .../mekhq/resources/AtBStratCon.properties | 5 ++ MekHQ/src/mekhq/campaign/force/Force.java | 8 +-- MekHQ/src/mekhq/gui/StratconPanel.java | 29 ++++----- .../gui/stratcon/StratconScenarioWizard.java | 62 ++++++++++++++----- .../gui/stratcon/TrackForceAssignmentUI.java | 6 +- 5 files changed, 70 insertions(+), 40 deletions(-) diff --git a/MekHQ/resources/mekhq/resources/AtBStratCon.properties b/MekHQ/resources/mekhq/resources/AtBStratCon.properties index bd7f21eaabb..72743157d0f 100644 --- a/MekHQ/resources/mekhq/resources/AtBStratCon.properties +++ b/MekHQ/resources/mekhq/resources/AtBStratCon.properties @@ -13,6 +13,10 @@ lblLeadershipInstructions.Text=The force commander's leadership allows the
Available BV: %sTransport Type: +lblLeadershipCommitForces.text=Commit {0} and any selected auxiliary units? +lblLeadershipCommitForces.fallback.text=Commit force? +leadershipCommit.text=Commit +leadershipCancel.text=Cancel selectForceForTemplate.Text=Select a force from the list below.\
\ @@ -86,6 +90,7 @@ reinforcementConfirmation.cancelButton=Cancel unitsSelectedLabel.bv=selected (ignores crew skill) unitsSelectedLabel.count=selected + ### Support Point Negotiation supportPoints.maximum=Your Admin/Transport personnel cannot use their Administration\ \ skill to create additional Support Points for contract %s, as you have already %sreached the\ diff --git a/MekHQ/src/mekhq/campaign/force/Force.java b/MekHQ/src/mekhq/campaign/force/Force.java index e750a9fea77..0b67dc36aa0 100644 --- a/MekHQ/src/mekhq/campaign/force/Force.java +++ b/MekHQ/src/mekhq/campaign/force/Force.java @@ -413,12 +413,10 @@ public Vector getUnits() { * @return all the unit ids in this force and all of its subforces */ public Vector getAllUnits(boolean standardForcesOnly) { - Vector allUnits; + Vector allUnits = new Vector<>(); - if (standardForcesOnly && forceType.isStandard()) { - allUnits = new Vector<>(); - } else { - allUnits = new Vector<>(units); + if (!standardForcesOnly || forceType.isStandard()) { + allUnits.addAll(units); } for (Force force : subForces) { diff --git a/MekHQ/src/mekhq/gui/StratconPanel.java b/MekHQ/src/mekhq/gui/StratconPanel.java index 91b76758eb5..2488ffc36cf 100644 --- a/MekHQ/src/mekhq/gui/StratconPanel.java +++ b/MekHQ/src/mekhq/gui/StratconPanel.java @@ -120,7 +120,7 @@ private enum DrawHexType { private final Map imageCache = new HashMap<>(); - private List pendingDeployments = new ArrayList(); + private boolean commitForces = false; /** * Constructs a StratconPanel instance, given a parent campaign GUI and a @@ -1053,16 +1053,17 @@ public void actionPerformed(ActionEvent evt) { isPrimaryForce = true; } } - if (selectedScenario != null) { + if (selectedScenario != null && selectedScenario.getCurrentState() == PRIMARY_FORCES_COMMITTED) { scenarioWizard.setCurrentScenario(currentTrack.getScenario(selectedCoords), currentTrack, campaignState, isPrimaryForce); scenarioWizard.toFront(); scenarioWizard.setVisible(true); } - setPendingDeployments(new ArrayList<>()); + if (selectedScenario != null && !isCommitForces()) { + selectedScenario.resetScenario(campaign); + } break; - // Deliberate fall-through case RCLICK_COMMAND_MANAGE_SCENARIO: // It's possible a scenario may have been placed when deploying the force, so we // need to recheck @@ -1118,6 +1119,8 @@ public void actionPerformed(ActionEvent evt) { if (scenarioToReset != null) { scenarioToReset.resetScenario(campaign); } + + setCommitForces(false); break; } @@ -1136,21 +1139,11 @@ public Dimension getPreferredSize() { } } - public List getPendingDeployments() { - return pendingDeployments; - } - - public void setPendingDeployments(List pendingDeployments) { - this.pendingDeployments = pendingDeployments; + public boolean isCommitForces() { + return commitForces; } - public void processPendingDeployments() { - - for (Force force : getPendingDeployments()) { - StratconRulesManager.deployForceToCoords(getSelectedCoords(), - force.getId(), campaign, campaignState.getContract(), getCurrentTrack(), false); - } - - setPendingDeployments(new ArrayList<>()); + public void setCommitForces(boolean commitForces) { + this.commitForces = commitForces; } } diff --git a/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java b/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java index 2156f1fe924..8be5c4c5cec 100644 --- a/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java +++ b/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java @@ -91,6 +91,7 @@ public class StratconScenarioWizard extends JDialog { private JPanel contentPanel; private JButton btnCommit; + private JButton btnCancel; private static final MMLogger logger = MMLogger.create(StratconScenarioWizard.class); @@ -116,17 +117,17 @@ public StratconScenarioWizard(Campaign campaign, StratconPanel parent) { * @param trackState the {@link StratconTrackState} representing the state of the scenario's track. * @param campaignState the {@link StratconCampaignState} representing the state of the overall campaign. * @param isPrimaryForce a boolean flag indicating whether the primary force is being assigned for this scenario. + *
    + *
  • {@code true}: Indicates that the primary force is being deployed.
  • + *
  • {@code false}: Indicates that the scenario is being configured without primary force assignment.
  • + *
+ * + *

Functionality and Process:

*
    - *
  • {@code true}: Indicates that the primary force is being deployed.
  • - *
  • {@code false}: Indicates that the scenario is being configured without primary force assignment.
  • + *
  • Sets the provided scenario as the {@code currentScenario}.
  • + *
  • Updates the {@link StratconCampaignState}, {@link StratconTrackState}, and clears previous force/unit lists.
  • + *
  • Initializes the user interface by calling {@link #setUI(boolean)}, passing the {@code isPrimaryForce} parameter.
  • *
- * - *

Functionality and Process:

- *
    - *
  • Sets the provided scenario as the {@code currentScenario}.
  • - *
  • Updates the {@link StratconCampaignState}, {@link StratconTrackState}, and clears previous force/unit lists.
  • - *
  • Initializes the user interface by calling {@link #setUI(boolean)}, passing the {@code isPrimaryForce} parameter.
  • - *
*/ public void setCurrentScenario(StratconScenario scenario, StratconTrackState trackState, StratconCampaignState campaignState, boolean isPrimaryForce) { @@ -668,7 +669,7 @@ private String buildForceCost(int forceID) { */ private void setNavigationButtons(GridBagConstraints constraints, boolean isPrimaryForce) { // Create the commit button - btnCommit = new JButton("Commit"); + btnCommit = new JButton(MHQInternationalization.getTextAt(resourcePath, "leadershipCommit.text")); btnCommit.setActionCommand("COMMIT_CLICK"); if (isPrimaryForce) { btnCommit.addActionListener(evt -> btnCommitClicked(evt, @@ -678,13 +679,41 @@ private void setNavigationButtons(GridBagConstraints constraints, boolean isPrim btnCommit.setEnabled(currentCampaignState.getSupportPoints() > 0); } - // Configure layout constraints for the button - constraints.gridheight = GridBagConstraints.REMAINDER; + btnCancel = new JButton(MHQInternationalization.getTextAt(resourcePath, "leadershipCancel.text")); + btnCancel.setActionCommand("CANCEL_CLICK"); + btnCancel.addActionListener(evt -> closeWizard()); + btnCancel.setEnabled(true); + + + // Configure layout constraints for the buttons constraints.gridwidth = GridBagConstraints.REMAINDER; constraints.anchor = GridBagConstraints.CENTER; + //Final instructions: + if (isPrimaryForce) { + String instructions; + Force primaryForce = currentScenario.getBackingScenario().getForces(campaign) + .getAllSubForces().stream().findFirst().orElse(null); + if (primaryForce != null) { + instructions = MHQInternationalization.getFormattedTextAt(resourcePath, "lblLeadershipCommitForces.text", primaryForce.getName()); + } + else { instructions = MHQInternationalization.getTextAt(resourcePath, "lblLeadershipCommitForces.fallback.text"); } + + contentPanel.add(new JLabel(instructions), constraints); + } + + + // Allign and add cancel button to the content panel + constraints.gridy++; + constraints.gridheight = GridBagConstraints.REMAINDER; + constraints.anchor = GridBagConstraints.WEST; + contentPanel.add(btnCancel, constraints); + constraints.anchor = GridBagConstraints.CENTER; + // Add the commit button to the content panel contentPanel.add(btnCommit, constraints); + + } /** @@ -904,9 +933,8 @@ private void reinforcementConfirmDialog() { */ private void btnCommitClicked(ActionEvent evt, @Nullable Integer reinforcementTargetNumber, boolean isGMReinforcement) { - //StratconPanel parent = (StratconPanel) getParent(); - if (parent != null && !(parent.getPendingDeployments().isEmpty())) { - parent.processPendingDeployments(); + if (parent != null ) { + parent.setCommitForces(true); } // go through all the force lists and add the selected forces to the scenario @@ -970,6 +998,10 @@ private void btnCommitClicked(ActionEvent evt, @Nullable Integer reinforcementTa scaleObjectiveTimeLimits(currentScenario.getBackingScenario(), campaign); } + closeWizard(); + } + + private void closeWizard() { this.getParent().repaint(); dispose(); diff --git a/MekHQ/src/mekhq/gui/stratcon/TrackForceAssignmentUI.java b/MekHQ/src/mekhq/gui/stratcon/TrackForceAssignmentUI.java index 6db6055a5d6..1f9ee4b053e 100644 --- a/MekHQ/src/mekhq/gui/stratcon/TrackForceAssignmentUI.java +++ b/MekHQ/src/mekhq/gui/stratcon/TrackForceAssignmentUI.java @@ -114,8 +114,10 @@ public void actionPerformed(ActionEvent e) { // sometimes the scenario templates take a little while to load, we don't want the user // clicking the button fifty times and getting a bunch of scenarios. btnConfirm.setEnabled(false); - - ownerPanel.setPendingDeployments(availableForceList.getSelectedValuesList()); + for (Force force : availableForceList.getSelectedValuesList()) { + StratconRulesManager.deployForceToCoords(ownerPanel.getSelectedCoords(), + force.getId(), campaign, currentCampaignState.getContract(), ownerPanel.getCurrentTrack(), false); + } setVisible(false); ownerPanel.repaint(); btnConfirm.setEnabled(true); From 34d06cb96fb833c9492723947ae6ae6558d4b5f7 Mon Sep 17 00:00:00 2001 From: psikomonkie <189469115+psikomonkie@users.noreply.github.com> Date: Sat, 8 Feb 2025 12:12:54 -0500 Subject: [PATCH 097/112] Issue 5979: Force players to use commit when deploying forces --- MekHQ/src/mekhq/campaign/force/Force.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/force/Force.java b/MekHQ/src/mekhq/campaign/force/Force.java index 0b67dc36aa0..e750a9fea77 100644 --- a/MekHQ/src/mekhq/campaign/force/Force.java +++ b/MekHQ/src/mekhq/campaign/force/Force.java @@ -413,10 +413,12 @@ public Vector getUnits() { * @return all the unit ids in this force and all of its subforces */ public Vector getAllUnits(boolean standardForcesOnly) { - Vector allUnits = new Vector<>(); + Vector allUnits; - if (!standardForcesOnly || forceType.isStandard()) { - allUnits.addAll(units); + if (standardForcesOnly && forceType.isStandard()) { + allUnits = new Vector<>(); + } else { + allUnits = new Vector<>(units); } for (Force force : subForces) { From 8549e87cadd20e2e28053a22fcb71f22cf40facc Mon Sep 17 00:00:00 2001 From: psikomonkie <189469115+psikomonkie@users.noreply.github.com> Date: Sat, 8 Feb 2025 12:19:14 -0500 Subject: [PATCH 098/112] Issue 5979: Whitespace fixes --- .../mekhq/resources/AtBStratCon.properties | 1 - .../gui/stratcon/StratconScenarioWizard.java | 22 +++++++++---------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/MekHQ/resources/mekhq/resources/AtBStratCon.properties b/MekHQ/resources/mekhq/resources/AtBStratCon.properties index 72743157d0f..3f29a096dcb 100644 --- a/MekHQ/resources/mekhq/resources/AtBStratCon.properties +++ b/MekHQ/resources/mekhq/resources/AtBStratCon.properties @@ -90,7 +90,6 @@ reinforcementConfirmation.cancelButton=Cancel unitsSelectedLabel.bv=selected (ignores crew skill) unitsSelectedLabel.count=selected - ### Support Point Negotiation supportPoints.maximum=Your Admin/Transport personnel cannot use their Administration\ \ skill to create additional Support Points for contract %s, as you have already %sreached the\ diff --git a/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java b/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java index 8be5c4c5cec..3dd5a9d0c25 100644 --- a/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java +++ b/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java @@ -117,17 +117,17 @@ public StratconScenarioWizard(Campaign campaign, StratconPanel parent) { * @param trackState the {@link StratconTrackState} representing the state of the scenario's track. * @param campaignState the {@link StratconCampaignState} representing the state of the overall campaign. * @param isPrimaryForce a boolean flag indicating whether the primary force is being assigned for this scenario. - *
    - *
  • {@code true}: Indicates that the primary force is being deployed.
  • - *
  • {@code false}: Indicates that the scenario is being configured without primary force assignment.
  • - *
- * - *

Functionality and Process:

*
    - *
  • Sets the provided scenario as the {@code currentScenario}.
  • - *
  • Updates the {@link StratconCampaignState}, {@link StratconTrackState}, and clears previous force/unit lists.
  • - *
  • Initializes the user interface by calling {@link #setUI(boolean)}, passing the {@code isPrimaryForce} parameter.
  • + *
  • {@code true}: Indicates that the primary force is being deployed.
  • + *
  • {@code false}: Indicates that the scenario is being configured without primary force assignment.
  • *
+ * + *

Functionality and Process:

+ *
    + *
  • Sets the provided scenario as the {@code currentScenario}.
  • + *
  • Updates the {@link StratconCampaignState}, {@link StratconTrackState}, and clears previous force/unit lists.
  • + *
  • Initializes the user interface by calling {@link #setUI(boolean)}, passing the {@code isPrimaryForce} parameter.
  • + *
*/ public void setCurrentScenario(StratconScenario scenario, StratconTrackState trackState, StratconCampaignState campaignState, boolean isPrimaryForce) { @@ -684,7 +684,6 @@ private void setNavigationButtons(GridBagConstraints constraints, boolean isPrim btnCancel.addActionListener(evt -> closeWizard()); btnCancel.setEnabled(true); - // Configure layout constraints for the buttons constraints.gridwidth = GridBagConstraints.REMAINDER; constraints.anchor = GridBagConstraints.CENTER; @@ -702,7 +701,6 @@ private void setNavigationButtons(GridBagConstraints constraints, boolean isPrim contentPanel.add(new JLabel(instructions), constraints); } - // Allign and add cancel button to the content panel constraints.gridy++; constraints.gridheight = GridBagConstraints.REMAINDER; @@ -993,7 +991,7 @@ private void btnCommitClicked(ActionEvent evt, @Nullable Integer reinforcementTa currentScenario.updateMinefieldCount(Minefield.TYPE_CONVENTIONAL, getNumMinefields()); - if (currentScenario.getCurrentState().ordinal() <= REINFORCEMENTS_COMMITTED.ordinal()) { + if (currentScenario.getCurrentState().ordinal() < REINFORCEMENTS_COMMITTED.ordinal()) { translateTemplateObjectives(currentScenario.getBackingScenario(), campaign); scaleObjectiveTimeLimits(currentScenario.getBackingScenario(), campaign); } From 51cc8cf3c0eb812f6b6d8a2a9661eb4c767847ef Mon Sep 17 00:00:00 2001 From: psikomonkie <189469115+psikomonkie@users.noreply.github.com> Date: Sat, 8 Feb 2025 12:22:14 -0500 Subject: [PATCH 099/112] Issue 5979: More whitespace fixes --- MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java b/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java index 3dd5a9d0c25..fcbc8acf23c 100644 --- a/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java +++ b/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java @@ -710,8 +710,6 @@ private void setNavigationButtons(GridBagConstraints constraints, boolean isPrim // Add the commit button to the content panel contentPanel.add(btnCommit, constraints); - - } /** From f9cf10a48754aee5dfae47e8e9ff94ac1d345fb3 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sat, 8 Feb 2025 13:42:51 -0600 Subject: [PATCH 100/112] Refactored glossary handling and removed Glossary.java Removed the unused Glossary enum and its related test class to streamline code maintenance. Refactored glossary handling to use resource keys directly and moved validation logic to MHQInternationalization for consistent key validation across the application. --- MekHQ/src/mekhq/campaign/enums/Glossary.java | 72 ------------------- .../baseComponents/MHQDialogImmersive.java | 2 +- .../src/mekhq/gui/dialog/GlossaryDialog.java | 60 +++++++++------- .../VocationalExperienceAwardDialog.java | 1 + .../utilities/MHQInternationalization.java | 21 +++++- .../mekhq/campaign/enums/GlossaryTest.java | 66 ----------------- .../prisoners/enums/PrisonerStatusTest.java | 22 +----- 7 files changed, 60 insertions(+), 184 deletions(-) delete mode 100644 MekHQ/src/mekhq/campaign/enums/Glossary.java delete mode 100644 MekHQ/unittests/mekhq/campaign/enums/GlossaryTest.java diff --git a/MekHQ/src/mekhq/campaign/enums/Glossary.java b/MekHQ/src/mekhq/campaign/enums/Glossary.java deleted file mode 100644 index a19c4c0a3f2..00000000000 --- a/MekHQ/src/mekhq/campaign/enums/Glossary.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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 as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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. - * - * You should have received a copy of the GNU General Public License - * along with MekHQ. If not, see . - */ -package mekhq.campaign.enums; - -import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; - -public enum Glossary { - // region Enum Declarations - PRISONER_CAPACITY, - REPUTATION; - // endregion Enum Declarations - - final private String RESOURCE_BUNDLE = "mekhq.resources." + getClass().getSimpleName(); - - // region Getters - /** - * Retrieves the title associated with this enum constant. - * - *

- * This method constructs a resource key by appending {@code ".title"} to the enum constant's - * name and looks up the corresponding title from the resource bundle. The title is then - * formatted and returned as a string. - *

- * - * @return A formatted string representing the title for this enum constant, - * fetched from the resource bundle. - */ - public String getTitle() { - final String RESOURCE_KEY = name() + ".title"; - - return getFormattedTextAt(RESOURCE_BUNDLE, RESOURCE_KEY); - } - - /** - * Retrieves the description associated with this enum constant. - * - *

- * This method constructs a resource key by appending {@code ".description"} to the enum constant's - * name and looks up the corresponding description from the resource bundle. The description - * is then formatted and returned as a string. - *

- * - * @return A formatted string representing the description for this enum constant, - * fetched from the resource bundle. - */ - public String getDescription() { - final String RESOURCE_KEY = name() + ".description"; - - return getFormattedTextAt(RESOURCE_BUNDLE, RESOURCE_KEY); - } - - @Override - public String toString() { - return getTitle(); - } -} diff --git a/MekHQ/src/mekhq/gui/baseComponents/MHQDialogImmersive.java b/MekHQ/src/mekhq/gui/baseComponents/MHQDialogImmersive.java index 9fb01dc0a52..d795bb62a12 100644 --- a/MekHQ/src/mekhq/gui/baseComponents/MHQDialogImmersive.java +++ b/MekHQ/src/mekhq/gui/baseComponents/MHQDialogImmersive.java @@ -37,6 +37,7 @@ import static megamek.client.ui.WrapLayout.wordWrap; import static megamek.client.ui.swing.util.FlatLafStyleBuilder.setFontScaling; import static mekhq.campaign.force.Force.FORCE_NONE; +import static mekhq.gui.dialog.GlossaryDialog.GLOSSARY_COMMAND_STRING; import static mekhq.utilities.ImageUtilities.scaleImageIconToWidth; import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; @@ -53,7 +54,6 @@ */ public class MHQDialogImmersive extends JDialog { private final String RESOURCE_BUNDLE = "mekhq.resources.GUI"; - public final static String GLOSSARY_COMMAND_STRING = "GLOSSARY"; private Campaign campaign; diff --git a/MekHQ/src/mekhq/gui/dialog/GlossaryDialog.java b/MekHQ/src/mekhq/gui/dialog/GlossaryDialog.java index e383aec610d..96f41b8308c 100644 --- a/MekHQ/src/mekhq/gui/dialog/GlossaryDialog.java +++ b/MekHQ/src/mekhq/gui/dialog/GlossaryDialog.java @@ -21,7 +21,6 @@ import megamek.client.ui.swing.util.UIUtil; import megamek.logging.MMLogger; import mekhq.campaign.Campaign; -import mekhq.campaign.enums.Glossary; import javax.swing.*; import javax.swing.event.HyperlinkEvent.EventType; @@ -30,8 +29,10 @@ import java.awt.event.WindowEvent; import static java.lang.Math.round; +import static javax.swing.BorderFactory.createEmptyBorder; import static megamek.client.ui.swing.util.FlatLafStyleBuilder.setFontScaling; -import static mekhq.gui.baseComponents.MHQDialogImmersive.GLOSSARY_COMMAND_STRING; +import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; +import static mekhq.utilities.MHQInternationalization.isResourceKeyValid; /** * The {@code GlossaryDialog} class represents a dialog window for displaying glossary entries. @@ -49,49 +50,59 @@ public class GlossaryDialog extends JDialog { private final JDialog parent; private final Campaign campaign; - private Glossary entry; private int CENTER_WIDTH = UIUtil.scaleForGUI(400); private int CENTER_HEIGHT = UIUtil.scaleForGUI(300); private int PADDING = UIUtil.scaleForGUI(10); + private final String GLOSSARY_BUNDLE = "mekhq.resources.Glossary"; + public final static String GLOSSARY_COMMAND_STRING = "GLOSSARY"; + /** - * Constructs a new {@code GlossaryDialog} to display a glossary entry. + * Creates a new {@code GlossaryDialog} instance to display information about a glossary term. * *

- * If the glossary key does not correspond to an existing entry, an error is logged, - * and the dialog is not displayed. + * The dialog retrieves the glossary term's title and description using the provided key + * and displays the content in a styled format. During its construction, the parent dialog + * is hidden to ensure that only this dialog is visible to the user. *

* - * @param parent The parent {@code JDialog} to temporarily hide while this dialog is open. - * @param campaign The {@code Campaign} instance containing the glossary data. - * @param key The key for the glossary entry to display. + * @param parent The parent {@link JDialog} that is temporarily hidden while this dialog is displayed. + * @param campaign The {@link Campaign} object containing resources and glossary entries. + * @param key The unique identifier for the glossary term to be displayed. */ public GlossaryDialog(JDialog parent, Campaign campaign, String key) { this.parent = parent; this.campaign = campaign; - try { - this.entry = Glossary.valueOf(key); - parent.setVisible(false); - displayDialog(entry.getTitle(), entry.getDescription()); - } catch (IllegalArgumentException e) { - logger.error("No entry available for key {}", key); - } + parent.setVisible(false); + buildDialog(key); } /** - * Displays the glossary dialog with the given title and description. + * Builds the Glossary Dialog by setting its title and description based on the key provided. * *

- * The content is rendered using HTML in a {@link JEditorPane}, allowing for styled text - * and clickable hyperlinks. The dialog also provides a scrollable view for long content. + * This method fetches the title and description strings for the glossary term from the + * resource bundle. If the title is invalid (i.e., the resource key is not found), + * it logs an error and terminates the dialog building process. *

* - * @param title The title of the glossary entry. - * @param description The detailed description of the glossary entry. + * @param key The resource key used to retrieve the glossary term's title and description. */ - private void displayDialog(String title, String description) { + private void buildDialog(String key) { + String title = getFormattedTextAt(GLOSSARY_BUNDLE, key + ".title"); + if (!isResourceKeyValid(title)) { + logger.error("No valid title for {}", key); + return; + } + + String description = getFormattedTextAt(GLOSSARY_BUNDLE, key + ".description"); + if (!isResourceKeyValid(description)) { + logger.error("No valid description for {}", key); + return; + } + setTitle(title); // Create a JEditorPane for the message @@ -105,8 +116,7 @@ private void displayDialog(String title, String description) { editorPane.setText(String.format( "
" + "

%s

" - + "

%s

" - + "
", + + "%s", CENTER_WIDTH, fontStyle, title, description )); setFontScaling(editorPane, false, 1.1); @@ -124,7 +134,7 @@ private void displayDialog(String title, String description) { // Create a JPanel with padding JPanel paddedPanel = new JPanel(new BorderLayout()); - paddedPanel.setBorder(BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING)); + paddedPanel.setBorder(createEmptyBorder(PADDING, PADDING, PADDING, PADDING)); paddedPanel.add(scrollPane, BorderLayout.CENTER); add(paddedPanel); diff --git a/MekHQ/src/mekhq/gui/dialog/VocationalExperienceAwardDialog.java b/MekHQ/src/mekhq/gui/dialog/VocationalExperienceAwardDialog.java index e238436274c..4640fca7139 100644 --- a/MekHQ/src/mekhq/gui/dialog/VocationalExperienceAwardDialog.java +++ b/MekHQ/src/mekhq/gui/dialog/VocationalExperienceAwardDialog.java @@ -30,6 +30,7 @@ import java.util.UUID; import static mekhq.campaign.Campaign.AdministratorSpecialization.HR; +import static mekhq.gui.dialog.GlossaryDialog.GLOSSARY_COMMAND_STRING; import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; /** diff --git a/MekHQ/src/mekhq/utilities/MHQInternationalization.java b/MekHQ/src/mekhq/utilities/MHQInternationalization.java index 400980bba1a..e14aeac4178 100644 --- a/MekHQ/src/mekhq/utilities/MHQInternationalization.java +++ b/MekHQ/src/mekhq/utilities/MHQInternationalization.java @@ -37,6 +37,7 @@ * It makes use of some short names to make it easier to use since it is used in many places */ public class MHQInternationalization { + private static String MISSING_RESOURCE_TAG = "!"; private final String defaultBundle; private final ConcurrentHashMap resourceBundles = new ConcurrentHashMap<>(); @@ -82,7 +83,7 @@ public static String getTextAt(String bundleName, String key) { if (getInstance().getResourceBundle(bundleName).containsKey(key)) { return getInstance().getResourceBundle(bundleName).getString(key); } - return "!" + key + "!"; + return MISSING_RESOURCE_TAG + key + MISSING_RESOURCE_TAG; } /** @@ -115,4 +116,22 @@ public static String getFormattedTextAt(String bundleName, String key, Object... return MessageFormat.format(getTextAt(bundleName, key), args); } + + + /** + * Checks if the given text is valid. A valid string does not start or end with an exclamation + * mark ('!'). + * + *

If {@link MHQInternationalization} fails to fetch a valid return it returns the key + * between two {@code !}. So by checking the returned string doesn't begin and end with that + * punctuation, we can easily verify that all statuses have been provided results for the key + * s we're using.

+ * + * @param text The text to validate. + * @return {@code true} if the text is valid (does not start or end with an '!'); {@code false} + * otherwise. + */ + public static boolean isResourceKeyValid(String text) { + return !text.startsWith(MISSING_RESOURCE_TAG) && !text.endsWith(MISSING_RESOURCE_TAG); + } } diff --git a/MekHQ/unittests/mekhq/campaign/enums/GlossaryTest.java b/MekHQ/unittests/mekhq/campaign/enums/GlossaryTest.java deleted file mode 100644 index bb569f8def2..00000000000 --- a/MekHQ/unittests/mekhq/campaign/enums/GlossaryTest.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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 as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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. - * - * You should have received a copy of the GNU General Public License - * along with MekHQ. If not, see . - */ -package mekhq.campaign.enums; - -import org.junit.jupiter.api.Test; - -import static mekhq.campaign.enums.Glossary.PRISONER_CAPACITY; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class GlossaryTest { - @Test - public void testParseFromString_ValidStatus() { - Glossary status = Glossary.valueOf("PRISONER_CAPACITY"); - assertEquals(PRISONER_CAPACITY, status); - } - - @Test - public void testGetLabel_notInvalid() { - for (Glossary status : Glossary.values()) { - String label = status.getTitle(); - assertTrue(isResourceKeyValid(label)); - } - } - - @Test - public void testGetTitleExtension_notInvalid() { - for (Glossary status : Glossary.values()) { - String titleExtension = status.getDescription(); - assertTrue(isResourceKeyValid(titleExtension)); - } - } - - /** - * Checks if the given text is valid. A valid string does not start or end with an exclamation - * mark ('!'). - * - *

If {@link mekhq.utilities.MHQInternationalization} fails to fetch a valid return it - * returns the key between two {@code !}. So by checking the returned string doesn't begin and - * end with that punctuation, we can easily verify that all statuses have been provided results - * for the keys we're using.

- * - * @param text The text to validate. - * @return true if the text is valid (does not start or end with an '!'); - * false otherwise. - */ - public static boolean isResourceKeyValid(String text) { - return !text.startsWith("!") && !text.endsWith("!"); - } -} diff --git a/MekHQ/unittests/mekhq/campaign/randomEvents/prisoners/enums/PrisonerStatusTest.java b/MekHQ/unittests/mekhq/campaign/randomEvents/prisoners/enums/PrisonerStatusTest.java index 480ca1cb2f6..784148a2bf1 100644 --- a/MekHQ/unittests/mekhq/campaign/randomEvents/prisoners/enums/PrisonerStatusTest.java +++ b/MekHQ/unittests/mekhq/campaign/randomEvents/prisoners/enums/PrisonerStatusTest.java @@ -22,6 +22,7 @@ import static mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus.FREE; import static mekhq.campaign.randomEvents.prisoners.enums.PrisonerStatus.PRISONER; +import static mekhq.utilities.MHQInternationalization.isResourceKeyValid; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -57,7 +58,7 @@ public void testParseFromString_EmptyString() { public void testGetLabel_notInvalid() { for (PrisonerStatus status : PrisonerStatus.values()) { String label = status.getLabel(); - assertTrue(isTitleExtensionValid(label)); + assertTrue(isResourceKeyValid(label)); } } @@ -65,24 +66,7 @@ public void testGetLabel_notInvalid() { public void testGetTitleExtension_notInvalid() { for (PrisonerStatus status : PrisonerStatus.values()) { String titleExtension = status.getTitleExtension(); - assertTrue(isTitleExtensionValid(titleExtension)); + assertTrue(isResourceKeyValid(titleExtension)); } } - - /** - * Checks if the given text is a valid title extension. A valid title extension - * does not start or end with an exclamation mark ('!'). - * - *

If {@link mekhq.utilities.MHQInternationalization} fails to fetch a valid return it - * returns the key between two {@code !}. So by checking the returned string doesn't begin and - * end with that punctuation, we can easily verify that all statuses have been provided results - * for the keys we're using.

- * - * @param text The text to validate as a title extension. - * @return true if the text is valid (does not start or end with an '!'); - * false otherwise. - */ - public static boolean isTitleExtensionValid(String text) { - return !text.startsWith("!") && !text.endsWith("!"); - } } From 7ec30c02fa7c065001b7b4b50643459d4dc20cec Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sat, 8 Feb 2025 13:54:03 -0600 Subject: [PATCH 101/112] Update glossary terminology from 'description' to 'definition' Revised glossary-related code and properties to replace the term 'description' with 'definition' for consistency. Updated variable names, logging messages, method comments, and resource strings accordingly. This enhances clarity and aligns terminology across the project. --- MekHQ/resources/mekhq/resources/Glossary.properties | 4 ++-- MekHQ/src/mekhq/campaign/Campaign.java | 1 + MekHQ/src/mekhq/gui/dialog/GlossaryDialog.java | 10 +++++----- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/MekHQ/resources/mekhq/resources/Glossary.properties b/MekHQ/resources/mekhq/resources/Glossary.properties index 10d8e3e75ad..3f1b37ea6ed 100644 --- a/MekHQ/resources/mekhq/resources/Glossary.properties +++ b/MekHQ/resources/mekhq/resources/Glossary.properties @@ -1,5 +1,5 @@ PRISONER_CAPACITY.title=Prisoner Capacity -PRISONER_CAPACITY.description=Prisoner Capacity is a measure of how many prisoners your Security\ +PRISONER_CAPACITY.definition=Prisoner Capacity is a measure of how many prisoners your Security\ \ personnel can manage without potential crisis. Generally speaking, each prisoner takes up 1\ \ capacity; however, some events may change this.\

For additional information on Prisoner Capacity and how to manage it, please see the\ @@ -7,6 +7,6 @@ PRISONER_CAPACITY.description=Prisoner Capacity is a measure of how many prisone
MekHQ/docs/Stratcon and Against the Bot/Prisoners.pdf

REPUTATION.title=Reputation -REPUTATION.description=Reputation is a measure of how well known your unit is and whether people\ +REPUTATION.definition=Reputation is a measure of how well known your unit is and whether people\ \ generally believe you can get the job done. The rules for Reputation can be found in Campaign\ \ Operations. \ No newline at end of file diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index ca631617aeb..1a62047b06e 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -403,6 +403,7 @@ public Campaign() { topUpWeekly = false; ignoreMothballed = false; ignoreSparesUnderQuality = QUALITY_A; + } /** diff --git a/MekHQ/src/mekhq/gui/dialog/GlossaryDialog.java b/MekHQ/src/mekhq/gui/dialog/GlossaryDialog.java index 96f41b8308c..be30527d127 100644 --- a/MekHQ/src/mekhq/gui/dialog/GlossaryDialog.java +++ b/MekHQ/src/mekhq/gui/dialog/GlossaryDialog.java @@ -80,15 +80,15 @@ public GlossaryDialog(JDialog parent, Campaign campaign, String key) { } /** - * Builds the Glossary Dialog by setting its title and description based on the key provided. + * Builds the Glossary Dialog by setting its title and definition based on the key provided. * *

- * This method fetches the title and description strings for the glossary term from the + * This method fetches the title and definition strings for the glossary term from the * resource bundle. If the title is invalid (i.e., the resource key is not found), * it logs an error and terminates the dialog building process. *

* - * @param key The resource key used to retrieve the glossary term's title and description. + * @param key The resource key used to retrieve the glossary term's title and definition. */ private void buildDialog(String key) { String title = getFormattedTextAt(GLOSSARY_BUNDLE, key + ".title"); @@ -97,9 +97,9 @@ private void buildDialog(String key) { return; } - String description = getFormattedTextAt(GLOSSARY_BUNDLE, key + ".description"); + String description = getFormattedTextAt(GLOSSARY_BUNDLE, key + ".definition"); if (!isResourceKeyValid(description)) { - logger.error("No valid description for {}", key); + logger.error("No valid definition for {}", key); return; } From 8d014d8924aa5e11bd43c0a342e556de581b708c Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sat, 8 Feb 2025 15:04:34 -0600 Subject: [PATCH 102/112] Refactored hyperlink handling logic into reusable method Moved hyperlink click handling to a static method in `MHQDialogImmersive` for centralized logic and reusability. Adjusted related dialog classes to utilize the new method, reducing redundancy and improving maintainability. --- .../baseComponents/MHQDialogImmersive.java | 45 +++++++++++++++---- .../src/mekhq/gui/dialog/GlossaryDialog.java | 27 +---------- .../VocationalExperienceAwardDialog.java | 29 ------------ 3 files changed, 39 insertions(+), 62 deletions(-) diff --git a/MekHQ/src/mekhq/gui/baseComponents/MHQDialogImmersive.java b/MekHQ/src/mekhq/gui/baseComponents/MHQDialogImmersive.java index d795bb62a12..7afe1c4b062 100644 --- a/MekHQ/src/mekhq/gui/baseComponents/MHQDialogImmersive.java +++ b/MekHQ/src/mekhq/gui/baseComponents/MHQDialogImmersive.java @@ -26,18 +26,19 @@ import mekhq.campaign.personnel.Person; import mekhq.campaign.personnel.enums.PersonnelRole; import mekhq.campaign.unit.Unit; +import mekhq.gui.CampaignGUI; import mekhq.gui.dialog.GlossaryDialog; import javax.swing.*; import javax.swing.event.HyperlinkEvent.EventType; import java.awt.*; import java.util.List; +import java.util.UUID; import static java.lang.Math.max; import static megamek.client.ui.WrapLayout.wordWrap; import static megamek.client.ui.swing.util.FlatLafStyleBuilder.setFontScaling; import static mekhq.campaign.force.Force.FORCE_NONE; -import static mekhq.gui.dialog.GlossaryDialog.GLOSSARY_COMMAND_STRING; import static mekhq.utilities.ImageUtilities.scaleImageIconToWidth; import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; @@ -54,6 +55,8 @@ */ public class MHQDialogImmersive extends JDialog { private final String RESOURCE_BUNDLE = "mekhq.resources.GUI"; + public final static String GLOSSARY_COMMAND_STRING = "GLOSSARY"; + public final static String PERSON_COMMAND_STRING = "PERSON"; private Campaign campaign; @@ -224,7 +227,7 @@ private JPanel createCenterBox(String centerMessage, List { if (evt.getEventType() == EventType.ACTIVATED) { - handleHyperlinkClick(campaign, evt.getDescription()); + handleImmersiveHyperlinkClick(this, campaign, evt.getDescription()); } }); @@ -250,19 +253,45 @@ private JPanel createCenterBox(String centerMessage, List + * This method processes the provided hyperlink reference to determine the type of command + * and executes the appropriate action. It supports commands for displaying a glossary + * entry or focusing on a specific person in the campaign. + *

+ * + * Supported Commands: + *
    + *
  • {@code GLOSSARY_COMMAND_STRING}: Opens a new {@link GlossaryDialog} to display the + * referenced glossary entry.
  • + *
  • {@code PERSON_COMMAND_STRING}: Focuses on a specific person in the campaign using + * their unique identifier (UUID). If using this, you will need to ensure your dialog has + * modal set to {@code false}
  • + *
+ * + *

+ * If the command is not recognized, no action is performed. + *

+ * + * @param parent The parent {@link JDialog} instance to associate with the new dialog, if created. + * @param campaign The {@link Campaign} instance that contains application and campaign data. + * @param reference The hyperlink reference used to determine the command and additional + * information (e.g., a specific glossary term key or a person's UUID). */ - protected void handleHyperlinkClick(Campaign campaign, String reference) { + public static void handleImmersiveHyperlinkClick(JDialog parent, Campaign campaign, String reference) { String[] splitReference = reference.split(":"); String commandKey = splitReference[0]; String entryKey = splitReference[1]; if (commandKey.equals(GLOSSARY_COMMAND_STRING)) { - new GlossaryDialog(this, campaign, entryKey); + new GlossaryDialog(parent, campaign, entryKey); + } else if (commandKey.equals(PERSON_COMMAND_STRING)) { + CampaignGUI campaignGUI = campaign.getApp().getCampaigngui(); + + final UUID id = UUID.fromString(reference.split(":")[1]); + campaignGUI.focusOnPerson(id); } } @@ -299,7 +328,7 @@ private void populateOutOfCharacterPanel(String outOfCharacterMessage) { // Add a HyperlinkListener to capture hyperlink clicks editorPane.addHyperlinkListener(evt -> { if (evt.getEventType() == EventType.ACTIVATED) { - handleHyperlinkClick(campaign, evt.getDescription()); + handleImmersiveHyperlinkClick(this, campaign, evt.getDescription()); } }); diff --git a/MekHQ/src/mekhq/gui/dialog/GlossaryDialog.java b/MekHQ/src/mekhq/gui/dialog/GlossaryDialog.java index be30527d127..c55b00f7a19 100644 --- a/MekHQ/src/mekhq/gui/dialog/GlossaryDialog.java +++ b/MekHQ/src/mekhq/gui/dialog/GlossaryDialog.java @@ -31,6 +31,7 @@ import static java.lang.Math.round; import static javax.swing.BorderFactory.createEmptyBorder; import static megamek.client.ui.swing.util.FlatLafStyleBuilder.setFontScaling; +import static mekhq.gui.baseComponents.MHQDialogImmersive.handleImmersiveHyperlinkClick; import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; import static mekhq.utilities.MHQInternationalization.isResourceKeyValid; @@ -56,7 +57,6 @@ public class GlossaryDialog extends JDialog { private int PADDING = UIUtil.scaleForGUI(10); private final String GLOSSARY_BUNDLE = "mekhq.resources.Glossary"; - public final static String GLOSSARY_COMMAND_STRING = "GLOSSARY"; /** * Creates a new {@code GlossaryDialog} instance to display information about a glossary term. @@ -124,7 +124,7 @@ private void buildDialog(String key) { // Add a HyperlinkListener to capture hyperlink clicks editorPane.addHyperlinkListener(evt -> { if (evt.getEventType() == EventType.ACTIVATED) { - handleHyperlinkClick(campaign, evt.getDescription()); + handleImmersiveHyperlinkClick(parent, campaign, evt.getDescription()); } }); @@ -166,27 +166,4 @@ private void onCloseAction() { dispose(); parent.setVisible(true); } - - /** - * Handles hyperlink clicks from the HTML content displayed in the glossary dialog. - * - *

- * If the hyperlink points to another glossary term (via the {@code GLOSSARY} command), - * a new {@code GlossaryDialog} is opened for the referenced term. - *

- * - * @param campaign The {@link Campaign} instance containing glossary data. - * @param reference The hyperlink reference string. Expected to be in the format - * {@code GL_COMMAND:termKey}. - */ - private void handleHyperlinkClick(Campaign campaign, String reference) { - String[] splitReference = reference.split(":"); - - String commandKey = splitReference[0]; - String entryKey = splitReference[1]; - - if (commandKey.equals(GLOSSARY_COMMAND_STRING)) { - new GlossaryDialog(this, campaign, entryKey); - } - } } diff --git a/MekHQ/src/mekhq/gui/dialog/VocationalExperienceAwardDialog.java b/MekHQ/src/mekhq/gui/dialog/VocationalExperienceAwardDialog.java index 4640fca7139..73aac35273c 100644 --- a/MekHQ/src/mekhq/gui/dialog/VocationalExperienceAwardDialog.java +++ b/MekHQ/src/mekhq/gui/dialog/VocationalExperienceAwardDialog.java @@ -23,14 +23,11 @@ import mekhq.campaign.CampaignOptions; import mekhq.campaign.mission.AtBContract; import mekhq.campaign.personnel.Person; -import mekhq.gui.CampaignGUI; import mekhq.gui.baseComponents.MHQDialogImmersive; import java.util.List; -import java.util.UUID; import static mekhq.campaign.Campaign.AdministratorSpecialization.HR; -import static mekhq.gui.dialog.GlossaryDialog.GLOSSARY_COMMAND_STRING; import static mekhq.utilities.MHQInternationalization.getFormattedTextAt; /** @@ -64,32 +61,6 @@ public VocationalExperienceAwardDialog(Campaign campaign) { setAlwaysOnTop(true); } - /** - * Handles the hyperlink click event in the dialog. - * - *

This method parses the hyperlink reference to focus on the personnel record identified by - * the provided UUID in the campaign's graphical user interface.

- * - * @param campaign the {@link Campaign} containing relevant personnel data - * @param reference the hyperlink reference containing the UUID of the selected character - */ - @Override - protected void handleHyperlinkClick(Campaign campaign, String reference) { - String[] splitReference = reference.split(":"); - - String commandKey = splitReference[0]; - String entryKey = splitReference[1]; - - if (commandKey.equals(GLOSSARY_COMMAND_STRING)) { - new GlossaryDialog(this, campaign, entryKey); - } else if (commandKey.equals(PERSON_COMMAND_STRING)) { - CampaignGUI campaignGUI = campaign.getApp().getCampaigngui(); - - final UUID id = UUID.fromString(reference.split(":")[1]); - campaignGUI.focusOnPerson(id); - } - } - /** * Creates the list of buttons to be displayed in the dialog. * From 212f2213a4fa5c7eed4a982ebaca5e40a70fbcd3 Mon Sep 17 00:00:00 2001 From: Daniel L- <103902653+IllianiCBT@users.noreply.github.com> Date: Sat, 8 Feb 2025 15:13:15 -0600 Subject: [PATCH 103/112] Update history.txt --- MekHQ/docs/history.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MekHQ/docs/history.txt b/MekHQ/docs/history.txt index e0ed466143a..086c9204238 100644 --- a/MekHQ/docs/history.txt +++ b/MekHQ/docs/history.txt @@ -26,6 +26,8 @@ MEKHQ VERSION HISTORY: + Fix #5922: Fixed Off-by-One Error in Mass Training Dialog + PR #5993: Adjusted Gray Monday Employer Dialog to Trigger on the Correct Day + PR #5999: Fixed Clan Ghost Bear Greeting Keys ++ Fix #5979: Force players to use commit when deploying forces ++ PR #6001: Added Glossary Functionality to MHQDialogImmersive with Clickable Hyperlink Support 0.50.03 (2025-02-02 2030 UTC) From 60b513072f8b28aa04bc4945e9aaa2fed1a7cba0 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sat, 8 Feb 2025 15:37:01 -0600 Subject: [PATCH 104/112] Refactored support point modification method usage Replaced calls to `addSupportPoints` with `changeSupportPoints` across relevant classes for clarity and consistency. Updated the method to support both positive and negative changes, improving code readability and maintainability. Also updated copyright years where applicable. --- .../mekhq/campaign/mission/AtBContract.java | 2 +- .../mission/ScenarioObjectiveProcessor.java | 4 ++-- .../stratcon/StratconCampaignState.java | 18 ++++++++++++++--- .../stratcon/StratconRulesManager.java | 6 +++--- .../stratcon/SupportPointNegotiation.java | 20 ++++++++++++++++++- .../stratcon/CampaignManagementDialog.java | 4 ++-- 6 files changed, 42 insertions(+), 12 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/mission/AtBContract.java b/MekHQ/src/mekhq/campaign/mission/AtBContract.java index fd8b25d2a50..d5e86ee2385 100644 --- a/MekHQ/src/mekhq/campaign/mission/AtBContract.java +++ b/MekHQ/src/mekhq/campaign/mission/AtBContract.java @@ -1025,7 +1025,7 @@ public void checkEvents(Campaign campaign) { if (campaignState != null) { text += ": -1 Support Point"; - campaignState.addSupportPoints(-1); + campaignState.changeSupportPoints(-1); } } break; diff --git a/MekHQ/src/mekhq/campaign/mission/ScenarioObjectiveProcessor.java b/MekHQ/src/mekhq/campaign/mission/ScenarioObjectiveProcessor.java index 08d6e99664f..b6c864289a9 100644 --- a/MekHQ/src/mekhq/campaign/mission/ScenarioObjectiveProcessor.java +++ b/MekHQ/src/mekhq/campaign/mission/ScenarioObjectiveProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2024 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2019-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * @@ -399,7 +399,7 @@ private String processObjectiveEffect(Campaign campaign, ObjectiveEffect effect, if (dryRun) { return String.format("%d support points will be added", numSupportPoints); } else { - contract.getStratconCampaignState().addSupportPoints(numSupportPoints); + contract.getStratconCampaignState().changeSupportPoints(numSupportPoints); } } } diff --git a/MekHQ/src/mekhq/campaign/stratcon/StratconCampaignState.java b/MekHQ/src/mekhq/campaign/stratcon/StratconCampaignState.java index 8a65904e1ef..aef3acd83c4 100644 --- a/MekHQ/src/mekhq/campaign/stratcon/StratconCampaignState.java +++ b/MekHQ/src/mekhq/campaign/stratcon/StratconCampaignState.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2024 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2019-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * @@ -131,8 +131,20 @@ public int getSupportPoints() { return supportPoints; } - public void addSupportPoints(int number) { - supportPoints += number; + /** + * Modifies the current support points by the specified amount. + * + *

+ * This method increases or decreases the support points by the given number. + * It adds the value of {@code change} to the existing support points total. + * This can be used to reflect changes due to various gameplay events or actions. + *

+ * + * @param change The amount to adjust the support points by. Positive values will + * increase the support points, while negative values will decrease them. + */ + public void changeSupportPoints(int change) { + supportPoints += change; } public void setSupportPoints(int supportPoints) { diff --git a/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java b/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java index 3c426faa479..3a33e8390ff 100644 --- a/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java +++ b/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2024 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2019-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * @@ -1224,7 +1224,7 @@ private static void processFacilityEffects(StratconTrackState track, StratconCampaignState campaignState, boolean isStartOfMonth) { for (StratconFacility facility : track.getFacilities().values()) { if (isStartOfMonth) { - campaignState.addSupportPoints(facility.getMonthlySPModifier()); + campaignState.changeSupportPoints(facility.getMonthlySPModifier()); } } } @@ -2652,7 +2652,7 @@ public static void processScenarioCompletion(ResolveScenarioTracker tracker) { /** * Processes completion of a Stratcon scenario that is linked to another scenario * pulls forces off completed scenario and moves them to linked one. - * + * * Should only be used after a scenario is resolved */ public static void linkedScenarioProcessing(ResolveScenarioTracker tracker, List forces) { diff --git a/MekHQ/src/mekhq/campaign/stratcon/SupportPointNegotiation.java b/MekHQ/src/mekhq/campaign/stratcon/SupportPointNegotiation.java index 4691bf29bab..aa0a382c2c7 100644 --- a/MekHQ/src/mekhq/campaign/stratcon/SupportPointNegotiation.java +++ b/MekHQ/src/mekhq/campaign/stratcon/SupportPointNegotiation.java @@ -1,3 +1,21 @@ +/* + * Copyright (c) 2024-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 as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with MekHQ. If not, see . + */ package mekhq.campaign.stratcon; import megamek.common.Compute; @@ -159,7 +177,7 @@ private static void processContractSupportPoints(Campaign campaign, AtBContract // Add points to the contract if positive if (negotiatedSupportPoints > 0) { - campaignState.addSupportPoints(negotiatedSupportPoints); + campaignState.changeSupportPoints(negotiatedSupportPoints); } // Add a report diff --git a/MekHQ/src/mekhq/gui/stratcon/CampaignManagementDialog.java b/MekHQ/src/mekhq/gui/stratcon/CampaignManagementDialog.java index 22f73b6481f..6ecdb08f32a 100644 --- a/MekHQ/src/mekhq/gui/stratcon/CampaignManagementDialog.java +++ b/MekHQ/src/mekhq/gui/stratcon/CampaignManagementDialog.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2024 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2021-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * @@ -160,7 +160,7 @@ private void gmAddVPHandler(ActionEvent e) { } private void gmAddSPHandler(ActionEvent e) { - currentCampaignState.addSupportPoints(1); + currentCampaignState.changeSupportPoints(1); btnGMRemoveSP.setEnabled(currentCampaignState.getSupportPoints() > 0); parent.updateCampaignState(); } From 34c108bd77fc2dbe8f0c1a06363a93ca18dac6e6 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sat, 8 Feb 2025 15:43:42 -0600 Subject: [PATCH 105/112] Refactored fatigue adjustment method across the codebase. Replaced the `increaseFatigue` method with a more versatile `changeFatigue` method to handle both increases and decreases in fatigue. Updated calls and documentation to reflect this change, improving code clarity and functionality. Adjusted copyright headers for 2025. --- .../mekhq/campaign/ResolveScenarioTracker.java | 4 ++-- MekHQ/src/mekhq/campaign/personnel/Person.java | 16 ++++++++++++++-- .../personnel/turnoverAndRetention/Fatigue.java | 14 +++++++------- .../campaign/stratcon/StratconRulesManager.java | 6 +++--- 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/ResolveScenarioTracker.java b/MekHQ/src/mekhq/campaign/ResolveScenarioTracker.java index 364609e6901..cf22bffd5f2 100644 --- a/MekHQ/src/mekhq/campaign/ResolveScenarioTracker.java +++ b/MekHQ/src/mekhq/campaign/ResolveScenarioTracker.java @@ -1479,7 +1479,7 @@ public void resolveScenario(ScenarioStatus resolution, String report) { MekHQ.triggerEvent(new PersonBattleFinishedEvent(person, status)); if (status.getHits() > person.getHits()) { if (campaign.getCampaignOptions().isUseInjuryFatigue()) { - person.increaseFatigue( + person.changeFatigue( campaign.getCampaignOptions().getFatigueRate() * (status.getHits() - person.getHits())); } @@ -1506,7 +1506,7 @@ public void resolveScenario(ScenarioStatus resolution, String report) { } if (!status.isDead()) { - person.increaseFatigue(campaign.getCampaignOptions().getFatigueRate()); + person.changeFatigue(campaign.getCampaignOptions().getFatigueRate()); if (campaign.getCampaignOptions().isUseFatigue()) { Fatigue.processFatigueActions(campaign, person); diff --git a/MekHQ/src/mekhq/campaign/personnel/Person.java b/MekHQ/src/mekhq/campaign/personnel/Person.java index 06e9c7ebeaa..2e2a05cdb34 100644 --- a/MekHQ/src/mekhq/campaign/personnel/Person.java +++ b/MekHQ/src/mekhq/campaign/personnel/Person.java @@ -1578,8 +1578,20 @@ public void setFatigue(final int fatigue) { this.fatigue = fatigue; } - public void increaseFatigue(final int fatigue) { - this.fatigue = this.fatigue + fatigue; + /** + * Adjusts the current fatigue level by the specified amount. + * + *

+ * This method modifies the fatigue level by adding the value of {@code change} + * to the current fatigue. Positive values will increase the fatigue, while + * negative values will decrease it. + *

+ * + * @param change The amount to adjust the fatigue by. Positive values increase fatigue, + * and negative values decrease it. + */ + public void changeFatigue(final int change) { + this.fatigue += change; } public boolean getIsRecoveringFromFatigue() { diff --git a/MekHQ/src/mekhq/campaign/personnel/turnoverAndRetention/Fatigue.java b/MekHQ/src/mekhq/campaign/personnel/turnoverAndRetention/Fatigue.java index 33331507139..6e5b7a8458a 100644 --- a/MekHQ/src/mekhq/campaign/personnel/turnoverAndRetention/Fatigue.java +++ b/MekHQ/src/mekhq/campaign/personnel/turnoverAndRetention/Fatigue.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2024-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * @@ -18,11 +18,6 @@ */ package mekhq.campaign.personnel.turnoverAndRetention; -import java.time.DayOfWeek; -import java.util.Collection; -import java.util.List; -import java.util.ResourceBundle; - import megamek.common.MiscType; import megamek.common.equipment.MiscMounted; import mekhq.MekHQ; @@ -32,6 +27,11 @@ import mekhq.campaign.unit.Unit; import mekhq.utilities.ReportingUtilities; +import java.time.DayOfWeek; +import java.util.Collection; +import java.util.List; +import java.util.ResourceBundle; + /** * The Fatigue class contains static methods for calculating and processing * fatigue levels and actions. @@ -156,7 +156,7 @@ public static void processFatigueRecovery(Campaign campaign) { fatigueAdjustment++; } - person.increaseFatigue(-fatigueAdjustment); + person.changeFatigue(-fatigueAdjustment); if (person.getFatigue() < 0) { person.setFatigue(0); diff --git a/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java b/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java index 3c426faa479..1372cbb3ec1 100644 --- a/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java +++ b/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2024 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2019-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * @@ -1368,7 +1368,7 @@ public static void processForceDeployment(StratconCoords coords, int forceID, Ca private static void increaseFatigue(int forceID, Campaign campaign) { for (UUID unit : campaign.getForce(forceID).getAllUnits(false)) { for (Person person : campaign.getUnit(unit).getCrew()) { - person.increaseFatigue(campaign.getCampaignOptions().getFatigueRate()); + person.changeFatigue(campaign.getCampaignOptions().getFatigueRate()); if (campaign.getCampaignOptions().isUseFatigue()) { Fatigue.processFatigueActions(campaign, person); @@ -2652,7 +2652,7 @@ public static void processScenarioCompletion(ResolveScenarioTracker tracker) { /** * Processes completion of a Stratcon scenario that is linked to another scenario * pulls forces off completed scenario and moves them to linked one. - * + * * Should only be used after a scenario is resolved */ public static void linkedScenarioProcessing(ResolveScenarioTracker tracker, List forces) { From 76dc39f23e300f754adf04f7bdb4977cd1da8f39 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sat, 8 Feb 2025 15:43:42 -0600 Subject: [PATCH 106/112] Refactored fatigue adjustment method across the codebase. Replaced the `increaseFatigue` method with a more versatile `changeFatigue` method to handle both increases and decreases in fatigue. Updated calls and documentation to reflect this change, improving code clarity and functionality. Adjusted copyright headers for 2025. --- .../mekhq/campaign/ResolveScenarioTracker.java | 4 ++-- MekHQ/src/mekhq/campaign/personnel/Person.java | 16 ++++++++++++++-- .../personnel/turnoverAndRetention/Fatigue.java | 14 +++++++------- .../campaign/stratcon/StratconRulesManager.java | 6 +++--- 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/ResolveScenarioTracker.java b/MekHQ/src/mekhq/campaign/ResolveScenarioTracker.java index 364609e6901..cf22bffd5f2 100644 --- a/MekHQ/src/mekhq/campaign/ResolveScenarioTracker.java +++ b/MekHQ/src/mekhq/campaign/ResolveScenarioTracker.java @@ -1479,7 +1479,7 @@ public void resolveScenario(ScenarioStatus resolution, String report) { MekHQ.triggerEvent(new PersonBattleFinishedEvent(person, status)); if (status.getHits() > person.getHits()) { if (campaign.getCampaignOptions().isUseInjuryFatigue()) { - person.increaseFatigue( + person.changeFatigue( campaign.getCampaignOptions().getFatigueRate() * (status.getHits() - person.getHits())); } @@ -1506,7 +1506,7 @@ public void resolveScenario(ScenarioStatus resolution, String report) { } if (!status.isDead()) { - person.increaseFatigue(campaign.getCampaignOptions().getFatigueRate()); + person.changeFatigue(campaign.getCampaignOptions().getFatigueRate()); if (campaign.getCampaignOptions().isUseFatigue()) { Fatigue.processFatigueActions(campaign, person); diff --git a/MekHQ/src/mekhq/campaign/personnel/Person.java b/MekHQ/src/mekhq/campaign/personnel/Person.java index 06e9c7ebeaa..b99531793da 100644 --- a/MekHQ/src/mekhq/campaign/personnel/Person.java +++ b/MekHQ/src/mekhq/campaign/personnel/Person.java @@ -1578,8 +1578,20 @@ public void setFatigue(final int fatigue) { this.fatigue = fatigue; } - public void increaseFatigue(final int fatigue) { - this.fatigue = this.fatigue + fatigue; + /** + * Adjusts the current fatigue level by the specified amount. + * + *

+ * This method modifies the fatigue level by adding the value of {@code change} + * to the current fatigue. Positive values will increase the fatigue, while + * negative values will decrease it. + *

+ * + * @param change The amount to adjust the fatigue by. Positive values increase fatigue, + * and negative values decrease it. + */ + public void changeFatigue(final int change) { + this.fatigue = this.fatigue + change; } public boolean getIsRecoveringFromFatigue() { diff --git a/MekHQ/src/mekhq/campaign/personnel/turnoverAndRetention/Fatigue.java b/MekHQ/src/mekhq/campaign/personnel/turnoverAndRetention/Fatigue.java index 33331507139..6e5b7a8458a 100644 --- a/MekHQ/src/mekhq/campaign/personnel/turnoverAndRetention/Fatigue.java +++ b/MekHQ/src/mekhq/campaign/personnel/turnoverAndRetention/Fatigue.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2024-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * @@ -18,11 +18,6 @@ */ package mekhq.campaign.personnel.turnoverAndRetention; -import java.time.DayOfWeek; -import java.util.Collection; -import java.util.List; -import java.util.ResourceBundle; - import megamek.common.MiscType; import megamek.common.equipment.MiscMounted; import mekhq.MekHQ; @@ -32,6 +27,11 @@ import mekhq.campaign.unit.Unit; import mekhq.utilities.ReportingUtilities; +import java.time.DayOfWeek; +import java.util.Collection; +import java.util.List; +import java.util.ResourceBundle; + /** * The Fatigue class contains static methods for calculating and processing * fatigue levels and actions. @@ -156,7 +156,7 @@ public static void processFatigueRecovery(Campaign campaign) { fatigueAdjustment++; } - person.increaseFatigue(-fatigueAdjustment); + person.changeFatigue(-fatigueAdjustment); if (person.getFatigue() < 0) { person.setFatigue(0); diff --git a/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java b/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java index 3c426faa479..1372cbb3ec1 100644 --- a/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java +++ b/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2024 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2019-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * @@ -1368,7 +1368,7 @@ public static void processForceDeployment(StratconCoords coords, int forceID, Ca private static void increaseFatigue(int forceID, Campaign campaign) { for (UUID unit : campaign.getForce(forceID).getAllUnits(false)) { for (Person person : campaign.getUnit(unit).getCrew()) { - person.increaseFatigue(campaign.getCampaignOptions().getFatigueRate()); + person.changeFatigue(campaign.getCampaignOptions().getFatigueRate()); if (campaign.getCampaignOptions().isUseFatigue()) { Fatigue.processFatigueActions(campaign, person); @@ -2652,7 +2652,7 @@ public static void processScenarioCompletion(ResolveScenarioTracker tracker) { /** * Processes completion of a Stratcon scenario that is linked to another scenario * pulls forces off completed scenario and moves them to linked one. - * + * * Should only be used after a scenario is resolved */ public static void linkedScenarioProcessing(ResolveScenarioTracker tracker, List forces) { From 2b5db3af6ef3dca91de5e629a4ef7211a45248c9 Mon Sep 17 00:00:00 2001 From: Daniel L- <103902653+IllianiCBT@users.noreply.github.com> Date: Sat, 8 Feb 2025 15:54:13 -0600 Subject: [PATCH 107/112] Update history.txt --- MekHQ/docs/history.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MekHQ/docs/history.txt b/MekHQ/docs/history.txt index 086c9204238..98b2501f208 100644 --- a/MekHQ/docs/history.txt +++ b/MekHQ/docs/history.txt @@ -28,6 +28,8 @@ MEKHQ VERSION HISTORY: + PR #5999: Fixed Clan Ghost Bear Greeting Keys + Fix #5979: Force players to use commit when deploying forces + PR #6001: Added Glossary Functionality to MHQDialogImmersive with Clickable Hyperlink Support ++ PR #6004: Refactored Support Point Modification Method Name ++ PR #6005: Refactored Fatigue Modification Method Name 0.50.03 (2025-02-02 2030 UTC) From d4e90eec28bc220a49a1ee06a60dd052c3f09ead Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sat, 8 Feb 2025 16:07:11 -0600 Subject: [PATCH 108/112] Add method to check if a unit is battle armor Introduced a new `isBattleArmor` method to determine if the associated entity is classified as battle armor. This complements existing classification methods and ensures consistent entity type checks. --- MekHQ/src/mekhq/campaign/unit/Unit.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/MekHQ/src/mekhq/campaign/unit/Unit.java b/MekHQ/src/mekhq/campaign/unit/Unit.java index d0e2c69d1ee..10915a2d41b 100644 --- a/MekHQ/src/mekhq/campaign/unit/Unit.java +++ b/MekHQ/src/mekhq/campaign/unit/Unit.java @@ -6044,6 +6044,22 @@ public boolean isConventionalInfantry() { return (getEntity() != null) && getEntity().isConventionalInfantry(); } + /** + * Checks if the associated entity is classified as battle armor. + * + *

+ * This method determines whether the entity linked to this object is + * considered battle armor. It first verifies that the entity is not null, + * and then checks if the entity meets the criteria for battle armor. + *

+ * + * @return {@code true} if the entity is classified as battle armor and is not null, + * otherwise {@code false}. + */ + public boolean isBattleArmor() { + return (getEntity() != null) && getEntity().isBattleArmor(); + } + public boolean isIntroducedBy(int year) { return null != entity && entity.getYear() <= year; } From 354eb5d0847191d68eba6e5df3ffa24a8698e593 Mon Sep 17 00:00:00 2001 From: Daniel L- <103902653+IllianiCBT@users.noreply.github.com> Date: Sat, 8 Feb 2025 16:20:12 -0600 Subject: [PATCH 109/112] Update history.txt --- MekHQ/docs/history.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/MekHQ/docs/history.txt b/MekHQ/docs/history.txt index 98b2501f208..90dc6a3cdb7 100644 --- a/MekHQ/docs/history.txt +++ b/MekHQ/docs/history.txt @@ -30,6 +30,7 @@ MEKHQ VERSION HISTORY: + PR #6001: Added Glossary Functionality to MHQDialogImmersive with Clickable Hyperlink Support + PR #6004: Refactored Support Point Modification Method Name + PR #6005: Refactored Fatigue Modification Method Name ++ PR #6006: Added Shortcut Method to Check if a Unit is Battle Armor 0.50.03 (2025-02-02 2030 UTC) From a0d1b331c98cdee6d2156246d748a7c0378e643a Mon Sep 17 00:00:00 2001 From: psikomonkie <189469115+psikomonkie@users.noreply.github.com> Date: Sun, 9 Feb 2025 15:07:32 -0500 Subject: [PATCH 110/112] Issue 6009: Improve Stratcon deployment logic to ensure units aren't deployed twice --- MekHQ/src/mekhq/gui/StratconPanel.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/MekHQ/src/mekhq/gui/StratconPanel.java b/MekHQ/src/mekhq/gui/StratconPanel.java index 2488ffc36cf..4f617ba5056 100644 --- a/MekHQ/src/mekhq/gui/StratconPanel.java +++ b/MekHQ/src/mekhq/gui/StratconPanel.java @@ -1053,6 +1053,10 @@ public void actionPerformed(ActionEvent evt) { isPrimaryForce = true; } } + + // Let's reload the scenario in case it updated + selectedScenario = currentTrack.getScenario(selectedCoords); + if (selectedScenario != null && selectedScenario.getCurrentState() == PRIMARY_FORCES_COMMITTED) { scenarioWizard.setCurrentScenario(currentTrack.getScenario(selectedCoords), currentTrack, campaignState, isPrimaryForce); @@ -1063,6 +1067,8 @@ public void actionPerformed(ActionEvent evt) { if (selectedScenario != null && !isCommitForces()) { selectedScenario.resetScenario(campaign); } + + setCommitForces(false); break; case RCLICK_COMMAND_MANAGE_SCENARIO: // It's possible a scenario may have been placed when deploying the force, so we @@ -1119,8 +1125,6 @@ public void actionPerformed(ActionEvent evt) { if (scenarioToReset != null) { scenarioToReset.resetScenario(campaign); } - - setCommitForces(false); break; } From 078a894c4d5c7aec8de92a9333aa81cbfe22809d Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sun, 9 Feb 2025 14:35:16 -0600 Subject: [PATCH 111/112] Fixed Multiple Resupply Bugs - Updated `isProhibitedUnitType` to include additional exclusion flags for super heavy units and adjusted method calls accordingly. - Corrected handling of Clan and ineligible parts --- MekHQ/src/mekhq/campaign/Campaign.java | 2 +- .../mission/resupplyAndCaches/Resupply.java | 88 +++++++++---------- .../DialogContractStart.java | 2 +- 3 files changed, 42 insertions(+), 50 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index 1a62047b06e..9a41b7ff210 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -2625,7 +2625,7 @@ public Set getPartsInUse(boolean ignoreMothballedUnits, } if (entity != null) { - if (isProhibitedUnitType(entity, false)) { + if (isProhibitedUnitType(entity, false, false)) { return; } } diff --git a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java index 4aa083aeed9..bff117eb31c 100644 --- a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java +++ b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java @@ -19,7 +19,6 @@ package mekhq.campaign.mission.resupplyAndCaches; import megamek.common.Entity; -import megamek.common.EquipmentFlag; import megamek.common.Mek; import megamek.logging.MMLogger; import mekhq.campaign.Campaign; @@ -37,7 +36,6 @@ import mekhq.campaign.unit.Unit; import mekhq.campaign.universe.Faction; -import java.math.BigInteger; import java.time.LocalDate; import java.util.*; @@ -404,7 +402,7 @@ static int calculateTargetCargoTonnage(Campaign campaign, AtBContract contract) continue; } - if (isProhibitedUnitType(entity, false)) { + if (isProhibitedUnitType(entity, false, false)) { continue; } @@ -428,26 +426,29 @@ static int calculateTargetCargoTonnage(Campaign campaign, AtBContract contract) } /** - * Checks whether a unit type is prohibited from resupply based on its characteristics. - * Some units, such as large craft, super-heavy units, and conventional infantry, may - * be excluded. - *

- * If {@code excludeDropShipsFromCheck} is {@code true} DropShips will not be considered a - * prohibited unit + * Determines if the given entity is a prohibited unit type based on specific criteria. * - * @param entity The entity being checked. - * @param excludeDropShipsFromCheck {@code true} to exclude DropShips from prohibited checks, - * {@code false} otherwise. - * @return {@code true} if the unit type is prohibited, {@code false} otherwise. + * @param entity the entity to check for prohibited unit type + * @param excludeDropShipsFromCheck if true, DropShip entities are excluded from being + * considered prohibited + * @param excludeSuperHeaviesFromCheck if true, Super Heavy entities are excluded from being + * considered prohibited + * @return {@code true} if the entity is a prohibited unit type such as Small Craft, Large + * Craft, or Conventional Infantry, and not excluded by the specified parameters; {@code false} + * otherwise */ - public static boolean isProhibitedUnitType(Entity entity, boolean excludeDropShipsFromCheck) { + public static boolean isProhibitedUnitType(Entity entity, boolean excludeDropShipsFromCheck, + boolean excludeSuperHeaviesFromCheck) { if (entity.isDropShip() && excludeDropShipsFromCheck) { return false; } + if (entity.isSuperHeavy() && excludeSuperHeaviesFromCheck) { + return false; + } + return entity.isSmallCraft() || entity.isLargeCraft() - || entity.isSuperHeavy() || entity.isConventionalInfantry(); } @@ -516,32 +517,26 @@ private Map collectParts() { Set partsInUse = campaign.getPartsInUse(true, true, PartQuality.QUALITY_A); - return applyWarehouseWeightModifiers(partsInUse); - } + Faction campaignFaction = campaign.getFaction(); + LocalDate today = campaign.getLocalDate(); + boolean removeClan = !campaignFaction.isClan() && today.isBefore(BATTLE_OF_TUKAYYID); - /** - * 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(); + Set partsToRemove = new HashSet<>(); + for (PartInUse partInUse : partsInUse) { + Part part = partInUse.getPartToBuy().getAcquisitionPart(); + if (removeClan && (part.isClan() || part.isMixedTech())) { + partsToRemove.add(partInUse); + continue; + } - if (part instanceof AmmoBin) { - partTonnage = RESUPPLY_AMMO_TONNAGE; - } else if (part instanceof Armor) { - partTonnage = RESUPPLY_ARMOR_TONNAGE; + if (isIneligiblePart(part)) { + partsToRemove.add(partInUse); + } } - return partName + ':' + partTonnage; + partsInUse.removeAll(partsToRemove); + + return applyWarehouseWeightModifiers(partsInUse); } /** @@ -549,12 +544,11 @@ private static String getPartKey(Part part) { * determined based on exclusion lists, unit structure compatibility, and transporter checks. * * @param part The part being checked. - * @param unit The unit to which the part belongs. * @return {@code true} if the part is ineligible, {@code false} otherwise. */ - private boolean isIneligiblePart(Part part, Unit unit) { + private boolean isIneligiblePart(Part part) { return checkExclusionList(part) - || checkMekLocation(part, unit) + || checkMekLocation(part) || checkTankLocation(part) || checkMotiveSystem(part) || checkTransporter(part); @@ -586,19 +580,17 @@ private boolean checkMotiveSystem(Part part) { } /** - * Checks if a part belonging to a 'Mek' unit is eligible for resupply, based on its location - * or whether the unit is considered extinct. For example, parts located in the center torso - * or parts from extinct units are deemed ineligible. + * Checks if a part belonging to a 'Mek' unit is eligible for resupply, based on its location. + * For example, parts located in the center torso or parts from extinct units are deemed + * ineligible. * * @param part The part to check. - * @param mek The unit to which the part belongs. * @return {@code true} if the part is ineligible due to its location or extinction, * {@code false} otherwise. */ - private boolean checkMekLocation(Part part, Unit mek) { + private boolean checkMekLocation(Part part) { return part instanceof MekLocation && - (((MekLocation) part).getLoc() == Mek.LOC_CT - || mek.isExtinct(currentYear, employerIsClan, employerTechCode)); + (((MekLocation) part).getLoc() == Mek.LOC_CT); } /** @@ -799,7 +791,7 @@ private void calculatePlayerConvoyValues() { if (unit.isDamaged() || !unit.isFullyCrewed() - || isProhibitedUnitType(entity, true)) { + || isProhibitedUnitType(entity, true, true)) { continue; } diff --git a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogContractStart.java b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogContractStart.java index b0884cb94d8..d7972f32eab 100644 --- a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogContractStart.java +++ b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogContractStart.java @@ -213,7 +213,7 @@ private static String generateContractStartMessage(Campaign campaign, AtBContrac if (unit.isDamaged() || !unit.isFullyCrewed() - || isProhibitedUnitType(entity, true)) { + || isProhibitedUnitType(entity, true, true)) { continue; } From 864743308beb7d6b024ac230cd22e0da1cf7b48c Mon Sep 17 00:00:00 2001 From: HammerGS Date: Sun, 9 Feb 2025 14:15:57 -0700 Subject: [PATCH 112/112] History Text updates, --- MekHQ/docs/history.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MekHQ/docs/history.txt b/MekHQ/docs/history.txt index 90dc6a3cdb7..3c78e2e89cf 100644 --- a/MekHQ/docs/history.txt +++ b/MekHQ/docs/history.txt @@ -31,6 +31,8 @@ MEKHQ VERSION HISTORY: + PR #6004: Refactored Support Point Modification Method Name + PR #6005: Refactored Fatigue Modification Method Name + PR #6006: Added Shortcut Method to Check if a Unit is Battle Armor ++ Fix #6009: Improve Stratcon deployment logic to ensure units aren't deployed twice ++ PR #6012: Fixed Multiple Resupply Bugs 0.50.03 (2025-02-02 2030 UTC)