diff --git a/megamek/src/megamek/client/ui/dialogs/WeightDisplayDialog.java b/megamek/src/megamek/client/ui/dialogs/WeightDisplayDialog.java new file mode 100644 index 00000000000..40013fe8129 --- /dev/null +++ b/megamek/src/megamek/client/ui/dialogs/WeightDisplayDialog.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This file is part of MegaMek. + * + * MegaMek 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. + * + * MegaMek 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 MegaMek. If not, see . + */ +package megamek.client.ui.dialogs; + +import megamek.client.ui.baseComponents.AbstractDialog; +import megamek.client.ui.swing.calculationReport.FlexibleCalculationReport; +import megamek.client.ui.swing.util.UIUtil; +import megamek.common.Entity; +import megamek.common.Infantry; +import megamek.common.verifier.TestEntity; +import megamek.common.verifier.TestInfantry; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import javax.swing.text.DefaultCaret; +import java.awt.*; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.StringSelection; +import java.util.Objects; + +public class WeightDisplayDialog extends AbstractDialog { + + private final Entity entity; + + public WeightDisplayDialog(final JFrame frame, final Entity entity) { + this(frame, false, entity); + } + + public WeightDisplayDialog(final JFrame frame, final boolean modal, final Entity entity) { + super(frame, modal, "BVDisplayDialog", "BVDisplayDialog.title"); + this.entity = Objects.requireNonNull(entity); + initialize(); + } + + @Override + protected void finalizeInitialization() throws Exception { + super.finalizeInitialization(); + setTitle(getTitle() + " (" + entity.getShortName() + ")"); + adaptToGUIScale(); + pack(); + Dimension screenSize = UIUtil.getScaledScreenSize(this); + setSize(new Dimension(getSize().width, Math.min(getHeight(), (int) (screenSize.getHeight() * 0.8)))); + } + + public Entity getEntity() { + return entity; + } + + @Override + protected Container createCenterPane() { + var scrollPane = new JScrollPane(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, + ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + String textReport; + if (entity.isConventionalInfantry()) { + FlexibleCalculationReport weightReport = new FlexibleCalculationReport(); + TestInfantry.getWeightExact((Infantry) entity, weightReport); + scrollPane.setViewportView(weightReport.toJComponent()); + textReport = weightReport.getTextReport().toString(); + } else { + TestEntity testEntity = TestEntity.getEntityVerifier(entity); + textReport = testEntity.printEntity().toString(); + JTextPane textPane = new JTextPane(); + textPane.setText(textReport); + textPane.setEditable(false); + textPane.setCaret(new DefaultCaret()); + scrollPane.setViewportView(textPane); + } + + JButton exportText = new JButton("Copy as Text"); + exportText.addActionListener(evt -> copyToClipboard(textReport)); + + scrollPane.getVerticalScrollBar().setUnitIncrement(16); + scrollPane.setBorder(new EmptyBorder(10, 0, 0, 0)); + + Box centerPanel = Box.createVerticalBox(); + centerPanel.setBorder(new EmptyBorder(25, 15, 25, 15)); + JPanel buttonPanel = new UIUtil.FixedYPanel(new FlowLayout(FlowLayout.LEFT)); + + buttonPanel.add(exportText); + centerPanel.add(buttonPanel); + centerPanel.add(scrollPane); + return centerPanel; + } + + private void copyToClipboard(String reportString) { + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + clipboard.setContents(new StringSelection(reportString), null); + } + + private void adaptToGUIScale() { + UIUtil.adjustDialog(this, UIUtil.FONT_SCALE1); + } +} \ No newline at end of file diff --git a/megamek/src/megamek/common/Infantry.java b/megamek/src/megamek/common/Infantry.java index 54376f74589..6de0b17936c 100644 --- a/megamek/src/megamek/common/Infantry.java +++ b/megamek/src/megamek/common/Infantry.java @@ -26,6 +26,7 @@ import megamek.common.enums.AimingMode; import megamek.common.enums.GamePhase; import megamek.common.options.OptionsConstants; +import megamek.common.verifier.TestInfantry; import megamek.common.weapons.infantry.InfantryWeapon; import org.apache.logging.log4j.LogManager; @@ -1693,64 +1694,7 @@ public boolean canMakeAntiMekAttacks() { @Override public double getWeight() { - if (mount != null) { - if (mount.getSize().troopsPerCreature > 1) { - return (mount.getWeight() + 0.2 * getSquadSize()) * getSquadCount(); - } else { - return (mount.getWeight() + 0.2) * activeTroopers; - } - } - double mult; - switch (getMovementMode()) { - case INF_MOTORIZED: - mult = 0.195; - break; - case HOVER: - case TRACKED: - case WHEELED: - mult = 1.0; - break; - case VTOL: - mult = hasMicrolite() ? 1.4 : 1.9; - break; - case INF_JUMP: - mult = 0.165; - break; - case INF_UMU: - if (getActiveUMUCount() > 1) { - mult = 0.295; // motorized + 0.1 for motorized scuba - } else { - mult = 0.135; // foot + 0.05 for scuba - } - break; - case SUBMARINE: - mult = 0.9; - break; - case INF_LEG: - default: - mult = 0.085; - } - - if (hasSpecialization(COMBAT_ENGINEERS)) { - mult += 0.1; - } - - if (hasSpecialization(PARATROOPS)) { - mult += 0.05; - } - - if (hasSpecialization(PARAMEDICS)) { - mult += 0.05; - } - - if (isAntiMekTrained()) { - mult +=.015; - } - - double ton = activeTroopers * mult; - ton += activeFieldWeapons().stream().mapToDouble(Mounted::getTonnage).sum(); - ton += getAmmo().stream().mapToDouble(Mounted::getTonnage).sum(); - return RoundWeight.nearestHalfTon(ton); + return TestInfantry.getWeight(this); } public String getArmorDesc() { diff --git a/megamek/src/megamek/common/verifier/TestEntity.java b/megamek/src/megamek/common/verifier/TestEntity.java index a74aba999e3..c0b6d8aaf60 100755 --- a/megamek/src/megamek/common/verifier/TestEntity.java +++ b/megamek/src/megamek/common/verifier/TestEntity.java @@ -19,6 +19,7 @@ import megamek.common.enums.MPBoosters; import megamek.common.util.StringUtil; +import java.io.File; import java.text.DecimalFormat; import java.util.*; import java.util.function.Predicate; @@ -83,6 +84,37 @@ private Ceil(double mult) { public String fileString = null; // where the unit came from + /** + * @param unit The entity the supplied entity + * @return a TestEntity instance for the supplied Entity. + */ + public static TestEntity getEntityVerifier(Entity unit) { + EntityVerifier entityVerifier = EntityVerifier.getInstance(new File( + "data/mechfiles/UnitVerifierOptions.xml")); // TODO : Remove inline file path + TestEntity testEntity = null; + + if (unit.hasETypeFlag(Entity.ETYPE_MECH)) { + testEntity = new TestMech((Mech) unit, entityVerifier.mechOption, null); + } else if (unit.hasETypeFlag(Entity.ETYPE_PROTOMECH)) { + testEntity = new TestProtomech((Protomech) unit, entityVerifier.protomechOption, null); + } else if (unit.isSupportVehicle()) { + testEntity = new TestSupportVehicle(unit, entityVerifier.tankOption, null); + } else if (unit.hasETypeFlag(Entity.ETYPE_TANK)) { + testEntity = new TestTank((Tank) unit, entityVerifier.tankOption, null); + } else if (unit.hasETypeFlag(Entity.ETYPE_SMALL_CRAFT)) { + testEntity = new TestSmallCraft((SmallCraft) unit, entityVerifier.aeroOption, null); + } else if (unit.hasETypeFlag(Entity.ETYPE_JUMPSHIP)) { + testEntity = new TestAdvancedAerospace((Jumpship) unit, entityVerifier.aeroOption, null); + } else if (unit.hasETypeFlag(Entity.ETYPE_AERO)) { + testEntity = new TestAero((Aero) unit, entityVerifier.aeroOption, null); + } else if (unit.hasETypeFlag(Entity.ETYPE_BATTLEARMOR)) { + testEntity = new TestBattleArmor((BattleArmor) unit, entityVerifier.baOption, null); + } else if (unit.hasETypeFlag(Entity.ETYPE_INFANTRY)) { + testEntity = new TestInfantry((Infantry)unit, entityVerifier.infOption, null); + } + return testEntity; + } + public TestEntity(TestEntityOption options, Engine engine, Armor[] armor, Structure structure) { this.options = options; diff --git a/megamek/src/megamek/common/verifier/TestInfantry.java b/megamek/src/megamek/common/verifier/TestInfantry.java index 2f589b8170d..66cafaa7709 100644 --- a/megamek/src/megamek/common/verifier/TestInfantry.java +++ b/megamek/src/megamek/common/verifier/TestInfantry.java @@ -14,13 +14,15 @@ */ package megamek.common.verifier; -import megamek.common.Entity; -import megamek.common.EntityMovementMode; -import megamek.common.Infantry; -import megamek.common.InfantryMount; +import megamek.client.ui.swing.calculationReport.CalculationReport; +import megamek.client.ui.swing.calculationReport.DummyCalculationReport; +import megamek.client.ui.swing.calculationReport.TextCalculationReport; +import megamek.common.*; import megamek.common.annotations.Nullable; import megamek.common.options.OptionsConstants; +import static megamek.client.ui.swing.calculationReport.CalculationReport.formatForReport; + /** * @author Jay Lawson (Taharqa) */ @@ -110,12 +112,22 @@ public int getCountHeatSinks() { @Override public String printWeightMisc() { - return null; + return ""; } @Override public String printWeightControls() { - return null; + return ""; + } + + @Override + public String printWeightStructure() { + return ""; + } + + @Override + public String printWeightArmor() { + return ""; } @Override @@ -275,7 +287,26 @@ public static int maxUnitSize(EntityMovementMode movementMode, boolean alt, bool @Override public StringBuffer printEntity() { - return null; + StringBuffer buff = new StringBuffer(); + buff.append("Mech: ").append(infantry.getDisplayName()).append("\n"); + buff.append("Found in: ").append(fileString).append("\n"); + buff.append(printTechLevel()); + buff.append("Intro year: ").append(infantry.getYear()).append("\n"); + buff.append(printSource()); + buff.append(printShortMovement()); + if (correctWeight(buff, true, true)) { + buff.append("Weight: ").append(getWeight()).append("\n"); + } + buff.append(printWeightCalculation()).append("\n"); + printFailedEquipment(buff); + return buff; + } + + @Override + public String printWeightCalculation() { + TextCalculationReport weightReport = new TextCalculationReport(); + getWeightExact(infantry, weightReport); + return weightReport.toString(); } @Override @@ -290,6 +321,138 @@ public double getWeightPowerAmp() { @Override public double calculateWeightExact() { - return infantry.getWeight(); + return getWeightExact(infantry, new DummyCalculationReport()); + } + + /** + * Calculates the weight of the given Conventional Infantry unit. Infantry weight + * is not fixed as in Meks and Vehicles but calculated from the infantry configuration. + * + * @param infantry The conventional infantry + * @return The rounded weight in tons + */ + public static double getWeight(Infantry infantry) { + double weight = getWeightExact(infantry, new DummyCalculationReport()); + return ceil(weight, Ceil.HALFTON); + } + + /** + * Calculates the weight of the given Conventional Infantry unit. Infantry weight + * is not fixed as in Meks and Vehicles but calculated from the infantry configuration. + * The given CalculationReport will be filled in with the weight calculation (the + * report includes the final rounding step but the returned result does not). + * + * @param infantry The conventional infantry + * @param report A CalculationReport to fill in + * @return The exact weight in tons + */ + public static double getWeightExact(Infantry infantry, CalculationReport report) { + String header = "Weight Calculation for "; + String fullName = infantry.getChassis() + " " + infantry.getModel(); + if (fullName.length() < 20) { + report.addHeader(header + fullName); + } else if (fullName.length() < 50) { + report.addHeader(header); + report.addHeader(fullName); + } else { + report.addHeader(header); + report.addHeader(infantry.getChassis()); + report.addHeader(infantry.getModel()); + } + report.addEmptyLine(); + + InfantryMount mount = infantry.getMount(); + int activeTroopers = infantry.getInternal(Infantry.LOC_INFANTRY); + double weight; + + if (mount != null) { + String calculation; + report.addLine("Mounted: " + mount.getName() + ", " + + mount.getSize().troopsPerCreature + " trooper(s) per mount", ""); + if (mount.getSize().troopsPerCreature > 1) { + weight = (mount.getWeight() + 0.2 * infantry.getSquadSize()) * infantry.getSquadCount(); + calculation = "(" + formatForReport(mount.getWeight()) + " + 0.2 x " + + infantry.getSquadSize() + ") x " + infantry.getSquadCount(); + } else { + weight = (mount.getWeight() + 0.2) * activeTroopers; + calculation = "(" + formatForReport(mount.getWeight()) + " + 0.2) x " + activeTroopers; + } + report.addLine("", calculation, formatForReport(weight) + " t"); + + } else { // not beast-mounted + double mult; + switch (infantry.getMovementMode()) { + case INF_MOTORIZED: + mult = 0.195; + break; + case HOVER: + case TRACKED: + case WHEELED: + mult = 1.0; + break; + case VTOL: + mult = infantry.hasMicrolite() ? 1.4 : 1.9; + break; + case INF_JUMP: + mult = 0.165; + break; + case INF_UMU: + if (infantry.getActiveUMUCount() > 1) { + mult = 0.295; // motorized + 0.1 for motorized scuba + } else { + mult = 0.135; // foot + 0.05 for scuba + } + break; + case SUBMARINE: + mult = 0.9; + break; + case INF_LEG: + default: + mult = 0.085; + } + report.addLine("Weight multiplier: ", infantry.getMovementModeAsString(), formatForReport(mult)); + + if (infantry.hasSpecialization(Infantry.COMBAT_ENGINEERS)) { + mult += 0.1; + report.addLine("", "Combat Engineers", "+ 0.1"); + + } + + if (infantry.hasSpecialization(Infantry.PARATROOPS)) { + mult += 0.05; + report.addLine("", "Paratroopers", "+ 0.05"); + } + + if (infantry.hasSpecialization(Infantry.PARAMEDICS)) { + mult += 0.05; + report.addLine("", "Paramedics", "+ 0.05"); + } + + if (infantry.isAntiMekTrained()) { + mult += .015; + report.addLine("", "Anti-Mek Training", "+ 0.015"); + } + + weight = activeTroopers * mult; + report.addLine("Trooper Weight:", activeTroopers + " x " + formatForReport(mult), + formatForReport(weight) + " t"); + + weight += infantry.activeFieldWeapons().stream().mapToDouble(Mounted::getTonnage).sum(); + weight += infantry.getAmmo().stream().mapToDouble(Mounted::getTonnage).sum(); + + infantry.activeFieldWeapons().forEach(mounted -> + report.addLine(mounted.getName(), "", + "+ " + formatForReport(mounted.getTonnage()) + " t")); + infantry.getAmmo().forEach(mounted -> + report.addLine(mounted.getName(), "", + "+ " + formatForReport(mounted.getTonnage()) + " t")); + } + + report.addEmptyLine(); + // Intentional: Add the final rounding to the report, but return the exact weight + double roundedWeight = ceil(weight, Ceil.HALFTON); + report.addLine("Final Weight:", "round up to nearest half ton", + formatForReport(roundedWeight) + " t"); + return weight; } } \ No newline at end of file