diff --git a/MekHQ/resources/mekhq/resources/Mission.properties b/MekHQ/resources/mekhq/resources/Mission.properties index 6b82d8b25b..b1572c3fc6 100644 --- a/MekHQ/resources/mekhq/resources/Mission.properties +++ b/MekHQ/resources/mekhq/resources/Mission.properties @@ -64,7 +64,7 @@ ContractCommandRights.INTEGRATED.toolTipText=-
- No map scouting (StratCon).\
\
Integrated command rights, standard for government forces, streamline command and control, particularly in large-scale operations involving multiple forces, ensuring effective collaboration without inter-service rivalry or confusion over command authority. -ContractCommandRights.INTEGRATED.stratConText=The employer will make Lance assignments. Complete required scenarios to fulfill contract conditions. +ContractCommandRights.INTEGRATED.stratConText=The employer will make Lance assignments.
Complete required scenarios to fulfill contract conditions. ContractCommandRights.HOUSE.text=House ContractCommandRights.HOUSE.toolTipText=- Keep your Campaign Victory Points (CVP) positive. Winning a non-initiated scenario: +1 CVP. Losing a non-initiated scenario: -1 CVP.\ diff --git a/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenario.java b/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenario.java index b5bafb3cd2..32be95056e 100644 --- a/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenario.java +++ b/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenario.java @@ -22,13 +22,17 @@ import megamek.common.Entity; import megamek.common.annotations.Nullable; import megamek.common.enums.SkillLevel; +import megamek.logging.MMLogger; import mekhq.campaign.Campaign; +import mekhq.campaign.force.Force; import mekhq.campaign.force.StrategicFormation; +import mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment; import mekhq.campaign.mission.ScenarioForceTemplate.ForceGenerationMethod; import mekhq.campaign.mission.atb.AtBScenarioModifier; import mekhq.campaign.personnel.Person; import mekhq.campaign.personnel.SkillType; import mekhq.campaign.rating.IUnitRating; +import mekhq.campaign.unit.Unit; import mekhq.utilities.MHQXMLUtility; import org.apache.commons.lang3.StringUtils; import org.w3c.dom.Element; @@ -39,6 +43,11 @@ import java.text.ParseException; import java.util.*; +import static mekhq.campaign.mission.AtBDynamicScenarioFactory.getPlanetOwnerAlignment; +import static mekhq.campaign.mission.AtBDynamicScenarioFactory.getPlanetOwnerFaction; +import static mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment.Allied; +import static mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment.PlanetOwner; + /** * Data structure intended to hold data relevant to AtB Dynamic Scenarios (AtB 3.0) * @author NickAragua @@ -86,6 +95,9 @@ public static class BenchedEntityData { private transient Map playerUnitTemplates; private transient List scenarioModifiers; + private static final MMLogger logger = MMLogger.create(AtBDynamicScenario.class); + + public AtBDynamicScenario() { super(); @@ -594,4 +606,64 @@ public void clearAllForcesAndPersonnel(Campaign campaign) { public String getBattlefieldControlDescription() { return ""; } + + /** + * Returns the total battle value (BV) either for allied forces or opposing forces in + * a given contract campaign, as per the parameter {@code isAllied}. + *

+ * If {@code isAllied} is {@code true}, the method calculates the total BV for the allied + * forces inclusive of player forces. If {@code isAllied} is {@code false}, the total BV for + * opposing forces is calculated. + *

+ * The calculation is done based on Bot forces attributed to each side. In the case of + * PlanetOwner, the alignment of the owner faction is considered to determine the ownership of + * Bot forces. + * + * @param campaign The campaign in which the forces are participating. + * @param isAllied A boolean value indicating whether to calculate the total BV for + * allied forces (if true) or opposing forces (if false). + * @return The total battle value (BV) either for the allied forces or + * opposing forces, as specified by the parameter isAllied. + */ + public int getTeamTotalBattleValue(Campaign campaign, boolean isAllied) { + AtBContract contract = getContract(campaign); + int totalBattleValue = 0; + + for (BotForce botForce : getBotForces()) { + int battleValue = botForce.getTotalBV(campaign); + + int team = botForce.getTeam(); + + if (team == PlanetOwner.ordinal()) { + String planetOwnerFaction = getPlanetOwnerFaction(contract, campaign.getLocalDate()); + ForceAlignment forceAlignment = getPlanetOwnerAlignment(contract, planetOwnerFaction, campaign.getLocalDate()); + team = forceAlignment.ordinal(); + } + + if (team <= Allied.ordinal()) { + if (isAllied) { + totalBattleValue += battleValue; + } + } else if (!isAllied) { + totalBattleValue += battleValue; + } + } + + if (isAllied) { + Force playerForces = this.getForces(campaign); + + for (UUID unitID : playerForces.getAllUnits(false)) { + try { + Unit unit = campaign.getUnit(unitID); + Entity entity = unit.getEntity(); + + totalBattleValue += entity.calculateBattleValue(); + } catch (Exception ex) { + logger.warn(ex.getMessage(), ex); + } + } + } + + return totalBattleValue; + } } diff --git a/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java b/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java index f78e1bf6bd..20f56264b9 100644 --- a/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java +++ b/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java @@ -3909,7 +3909,7 @@ private static void correctNonAeroFlyerBehavior(List entityList, int boa * @param currentDate Current date. * @return Faction code. */ - private static String getPlanetOwnerFaction(AtBContract contract, LocalDate currentDate) { + static String getPlanetOwnerFaction(AtBContract contract, LocalDate currentDate) { String factionCode = "MERC"; // planet owner is the first of the factions that owns the current planet. @@ -3935,8 +3935,7 @@ private static String getPlanetOwnerFaction(AtBContract contract, LocalDate curr * @param currentDate Current date. * @return ForceAlignment. */ - private static ForceAlignment getPlanetOwnerAlignment(AtBContract contract, String factionCode, - LocalDate currentDate) { + static ForceAlignment getPlanetOwnerAlignment(AtBContract contract, String factionCode, LocalDate currentDate) { // if the faction is one of the planet owners, see if it's either the employer // or opfor. If it's not, third-party. if (contract.getSystem().getFactions(currentDate).contains(factionCode)) { diff --git a/MekHQ/src/mekhq/campaign/stratcon/StratconScenario.java b/MekHQ/src/mekhq/campaign/stratcon/StratconScenario.java index d2a75d0940..4298d8226c 100644 --- a/MekHQ/src/mekhq/campaign/stratcon/StratconScenario.java +++ b/MekHQ/src/mekhq/campaign/stratcon/StratconScenario.java @@ -15,6 +15,7 @@ import jakarta.xml.bind.annotation.XmlTransient; import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import megamek.common.annotations.Nullable; import mekhq.MekHQ; import mekhq.adapter.DateAdapter; import mekhq.campaign.Campaign; @@ -166,10 +167,10 @@ public void setCurrentState(ScenarioState state) { @Override public String getInfo() { - return getInfo(true); + return getInfo(null, true); } - public String getInfo(boolean html) { + public String getInfo(@Nullable Campaign campaign, boolean html) { StringBuilder stateBuilder = new StringBuilder(); if (isStrategicObjective()) { @@ -218,6 +219,17 @@ public String getInfo(boolean html) { .append("
"); } + if (campaign != null) { + AtBDynamicScenario backingScenario = getBackingScenario(); + + if (backingScenario != null) { + stateBuilder.append(String.format("Hostile BV: %d
", + backingScenario.getTeamTotalBattleValue(campaign, false))); + stateBuilder.append(String.format("Allied BV: %d", + backingScenario.getTeamTotalBattleValue(campaign, true))); + } + } + stateBuilder.append(""); return stateBuilder.toString(); } diff --git a/MekHQ/src/mekhq/gui/StratconPanel.java b/MekHQ/src/mekhq/gui/StratconPanel.java index 389ca18b62..fc6326052c 100644 --- a/MekHQ/src/mekhq/gui/StratconPanel.java +++ b/MekHQ/src/mekhq/gui/StratconPanel.java @@ -18,7 +18,6 @@ import mekhq.MekHQ; import mekhq.campaign.Campaign; import mekhq.campaign.force.Force; -import mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment; import mekhq.campaign.stratcon.*; import mekhq.campaign.stratcon.StratconBiomeManifest.ImageType; import mekhq.gui.stratcon.StratconScenarioWizard; @@ -40,6 +39,8 @@ import java.util.HashMap; import java.util.Map; +import static mekhq.campaign.mission.ScenarioForceTemplate.ForceAlignment.Allied; + /** * This panel handles AtB-Stratcon GUI interactions with a specific scenario * track. @@ -144,7 +145,7 @@ public void selectTrack(StratconCampaignState campaignState, StratconTrackState // clear hex selection boardState.selectedX = null; boardState.selectedY = null; - infoArea.setText(buildSelectedHexInfo()); + infoArea.setText(buildSelectedHexInfo(campaign)); repaint(); } @@ -471,7 +472,7 @@ private boolean imageLoaded(String imageKey) { } private BufferedImage getFacilityImage(StratconFacility facility) { - String imageKeyPrefix = facility.getOwner() == ForceAlignment.Allied ? StratconBiomeManifest.FACILITY_ALLIED + String imageKeyPrefix = facility.getOwner() == Allied ? StratconBiomeManifest.FACILITY_ALLIED : StratconBiomeManifest.FACILITY_HOSTILE; String imageKey = imageKeyPrefix + facility.getFacilityType().name(); @@ -569,7 +570,7 @@ private void drawScenarios(Graphics2D g2D) { if (currentTrack.getFacility(currentCoords) == null) { drawTextEffect(g2D, scenarioMarker, "Hostile Force Detected", currentCoords); - } else if (currentTrack.getFacility(currentCoords).getOwner() == ForceAlignment.Allied) { + } else if (currentTrack.getFacility(currentCoords).getOwner() == Allied) { drawTextEffect(g2D, scenarioMarker, "Under Attack!", currentCoords); } } @@ -610,7 +611,7 @@ private void drawFacilities(Graphics2D g2D) { StratconFacility facility = currentTrack.getFacility(currentCoords); if ((facility != null) && (facility.isVisible() || trackRevealed || currentTrack.isGmRevealed())) { - g2D.setColor(facility.getOwner() == ForceAlignment.Allied ? Color.CYAN : Color.RED); + g2D.setColor(facility.getOwner() == Allied ? Color.CYAN : Color.RED); BufferedImage facilityImage = getFacilityImage(facility); @@ -813,7 +814,7 @@ public void mouseReleasedHandler(MouseEvent e) { boolean pointFoundOnBoard = detectClickedHex(); if (pointFoundOnBoard) { - infoArea.setText(buildSelectedHexInfo()); + infoArea.setText(buildSelectedHexInfo(campaign)); } repaint(); @@ -850,7 +851,7 @@ public StratconCoords getSelectedCoords() { * containing info such as whether it's been revealed, assigned forces, * scenarios, facilities, etc. */ - private String buildSelectedHexInfo() { + private String buildSelectedHexInfo(Campaign campaign) { StringBuilder infoBuilder = new StringBuilder(); infoBuilder.append("
"); @@ -864,13 +865,13 @@ private String buildSelectedHexInfo() { boolean coordsRevealed = currentTrack.hasActiveTrackReveal() || currentTrack.getRevealedCoords().contains(boardState.getSelectedCoords()); if (coordsRevealed) { - infoBuilder.append("Recon complete
"); + infoBuilder.append("Recon Complete
"); } if (currentTrack.getAssignedCoordForces().containsKey(boardState.getSelectedCoords())) { for (int forceID : currentTrack.getAssignedCoordForces().get(boardState.getSelectedCoords())) { - Force force = campaign.getForce(forceID); + Force force = this.campaign.getForce(forceID); infoBuilder.append(force.getName()).append(" assigned"); if (currentTrack.getStickyForces().contains(forceID)) { @@ -890,12 +891,12 @@ private String buildSelectedHexInfo() { if ((facility != null) && (facility.getFacilityType() != null)) { if (facility.isStrategicObjective()) { infoBuilder.append(String.format("
Contract objective located", - facility.getOwner() == ForceAlignment.Allied + facility.getOwner() == Allied ? MekHQ.getMHQOptions().getFontColorPositiveHexColor() : MekHQ.getMHQOptions().getFontColorNegativeHexColor())); } infoBuilder.append("") @@ -912,14 +913,14 @@ private String buildSelectedHexInfo() { } else { infoBuilder.append("Recon incomplete"); + .append("'>Recon Incomplete"); } infoBuilder.append("
"); StratconScenario selectedScenario = getSelectedScenario(); if ((selectedScenario != null) && ((selectedScenario.getDeploymentDate() != null) || currentTrack.isGmRevealed())) { - infoBuilder.append(selectedScenario.getInfo()); + infoBuilder.append(selectedScenario.getInfo(campaign, true)); } infoBuilder.append(""); diff --git a/MekHQ/src/mekhq/gui/StratconTab.java b/MekHQ/src/mekhq/gui/StratconTab.java index 5999cfc3d3..896caa2fba 100644 --- a/MekHQ/src/mekhq/gui/StratconTab.java +++ b/MekHQ/src/mekhq/gui/StratconTab.java @@ -13,6 +13,7 @@ */ package mekhq.gui; +import megamek.client.ui.swing.util.UIUtil; import megamek.common.event.Subscribe; import mekhq.MekHQ; import mekhq.campaign.event.MissionCompletedEvent; @@ -48,7 +49,8 @@ public class StratconTab extends CampaignGuiTab { private StratconPanel stratconPanel; private JPanel infoPanel; - private JComboBox cboCurrentTrack; + private DefaultListModel listModel = new DefaultListModel<>(); + private JList listCurrentTrack; private JLabel infoPanelText; private JLabel campaignStatusText; private JLabel objectiveStatusText; @@ -87,7 +89,7 @@ public void initTab() { objectiveStatusText.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent me) { - TrackDropdownItem currentTDI = (TrackDropdownItem) cboCurrentTrack.getSelectedItem(); + TrackDropdownItem currentTDI = listCurrentTrack.getSelectedValue(); StratconCampaignState campaignState = currentTDI.contract.getStratconCampaignState(); objectivesCollapsed = !objectivesCollapsed; objectiveStatusText.setText(getStrategicObjectiveText(campaignState)); @@ -116,47 +118,54 @@ public void mousePressed(MouseEvent me) { * Worker function that sets up the layout of the right-side info panel. */ private void initializeInfoPanel() { - infoPanel = new JPanel(); - infoPanel.setLayout(new BoxLayout(infoPanel, BoxLayout.PAGE_AXIS)); + int gridY = 0; + infoPanel = new JPanel(new GridBagLayout()); + GridBagConstraints constraints = new GridBagConstraints(); + constraints.anchor = GridBagConstraints.NORTHWEST; + constraints.gridx = gridY++; - infoPanel.add(new JLabel("Current Campaign Status:")); - infoPanel.add(campaignStatusText); + infoPanel.add(new JLabel("Current Campaign Status:"), constraints); + + constraints.gridy = gridY++; + infoPanel.add(campaignStatusText, constraints); JButton btnManageCampaignState = new JButton("Manage SP/CVP"); - btnManageCampaignState.setHorizontalAlignment(SwingConstants.LEFT); - btnManageCampaignState.setVerticalAlignment(SwingConstants.TOP); btnManageCampaignState.addActionListener(this::showCampaignStateManagement); - infoPanel.add(btnManageCampaignState); + constraints.gridy = gridY++; + infoPanel.add(btnManageCampaignState, constraints); expandedObjectivePanel = new JScrollPaneWithSpeed(objectiveStatusText); - expandedObjectivePanel.setMaximumSize(new Dimension(400, 300)); - expandedObjectivePanel.setAlignmentX(LEFT_ALIGNMENT); - infoPanel.add(expandedObjectivePanel); - - JLabel lblCurrentTrack = new JLabel("Current Sector:"); - infoPanel.add(lblCurrentTrack); - - cboCurrentTrack = new JComboBox<>(); - cboCurrentTrack.setAlignmentX(LEFT_ALIGNMENT); - cboCurrentTrack.setMaximumSize(new Dimension(320, 20)); + expandedObjectivePanel.setPreferredSize(new Dimension(400, 300)); + constraints.gridy = gridY++; + infoPanel.add(expandedObjectivePanel, constraints); + + JLabel lblCurrentTrack = new JLabel("Assigned Sectors:"); + constraints.gridy = gridY++; + infoPanel.add(lblCurrentTrack, constraints); + + listModel = new DefaultListModel<>(); + listCurrentTrack = new JList<>(listModel); + listCurrentTrack.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + listCurrentTrack.setFixedCellHeight(UIUtil.scaleForGUI(20)); repopulateTrackList(); - cboCurrentTrack.addItemListener(evt -> trackSelectionHandler()); + listCurrentTrack.addListSelectionListener(evt -> trackSelectionHandler()); - infoPanel.add(cboCurrentTrack); + JScrollPane scrollPane = new JScrollPane(listCurrentTrack); + scrollPane.setPreferredSize(new Dimension(UIUtil.scaleForGUI(400), + listCurrentTrack.getFixedCellHeight() * 10)); + constraints.gridy = gridY++; - // have a default selected - if (cboCurrentTrack.getItemCount() > 0) { - trackSelectionHandler(); - } - - infoPanel.add(infoPanelText); + infoPanel.add(scrollPane, constraints); + constraints.gridx = 2; + constraints.gridheight = 2; + infoPanel.add(infoPanelText, constraints); } /** * Worker that handles track selection. */ private void trackSelectionHandler() { - TrackDropdownItem tdi = (TrackDropdownItem) cboCurrentTrack.getSelectedItem(); + TrackDropdownItem tdi = listCurrentTrack.getSelectedValue(); if (tdi != null) { stratconPanel.selectTrack(tdi.contract.getStratconCampaignState(), tdi.track); updateCampaignState(); @@ -185,7 +194,7 @@ public MHQTabType tabType() { * with such info as current objective status, VP/SP totals, etc. */ public void updateCampaignState() { - if ((cboCurrentTrack == null) || (campaignStatusText == null)) { + if ((listCurrentTrack == null) || (campaignStatusText == null)) { return; } @@ -193,7 +202,7 @@ public void updateCampaignState() { // list of remaining objectives, percentage remaining // current VP // current support points - TrackDropdownItem currentTDI = (TrackDropdownItem) cboCurrentTrack.getSelectedItem(); + TrackDropdownItem currentTDI = listCurrentTrack.getSelectedValue(); if (currentTDI == null) { campaignStatusText.setText("No active contract selected, or contract has not started."); expandedObjectivePanel.setVisible(false); @@ -387,40 +396,38 @@ private String buildStrategicObjectiveText(StratconCampaignState campaignState) * Refreshes the list of tracks */ private void repopulateTrackList() { - TrackDropdownItem currentTDI = (TrackDropdownItem) cboCurrentTrack.getSelectedItem(); - cboCurrentTrack.removeAllItems(); + int currentTrackIndex = listCurrentTrack.getSelectedIndex(); + listModel.clear(); - // track dropdown is populated with all tracks across all active contracts for (AtBContract contract : getCampaignGui().getCampaign().getActiveAtBContracts(true)) { - if (contract.getStratconCampaignState() != null) { - for (StratconTrackState track : contract.getStratconCampaignState().getTracks()) { - TrackDropdownItem tdi = new TrackDropdownItem(contract, track); - cboCurrentTrack.addItem(tdi); - - if ((currentTDI != null) && currentTDI.equals(tdi)) { - currentTDI = tdi; - cboCurrentTrack.setSelectedItem(tdi); - } else if (currentTDI == null) { - currentTDI = tdi; - cboCurrentTrack.setSelectedItem(tdi); - } + StratconCampaignState campaignState = contract.getStratconCampaignState(); + if (campaignState != null) { + for (StratconTrackState track : campaignState.getTracks()) { + TrackDropdownItem trackItem = new TrackDropdownItem(contract, track); + listModel.addElement(trackItem); } } } - if ((cboCurrentTrack.getItemCount() > 0) && (currentTDI != null) && (currentTDI.contract != null)) { - TrackDropdownItem selectedTrack = (TrackDropdownItem) cboCurrentTrack.getSelectedItem(); + listCurrentTrack.setModel(listModel); + listCurrentTrack.setSelectedIndex(currentTrackIndex); + + if (listCurrentTrack.getSelectedValue() == null) { + listCurrentTrack.setSelectedIndex(0); + } - stratconPanel.selectTrack(selectedTrack.contract.getStratconCampaignState(), currentTDI.track); + if (listCurrentTrack.getSelectedValue() != null) { + TrackDropdownItem selectedTrack = listCurrentTrack.getSelectedValue(); + stratconPanel.selectTrack(selectedTrack.contract.getStratconCampaignState(), selectedTrack.track); stratconPanel.setVisible(true); } else { - infoPanelText.setText("No active sectors"); + infoPanelText.setText(""); stratconPanel.setVisible(false); } } private void showCampaignStateManagement(ActionEvent e) { - TrackDropdownItem selectedTrack = (TrackDropdownItem) cboCurrentTrack.getSelectedItem(); + TrackDropdownItem selectedTrack = listCurrentTrack.getSelectedValue(); if (selectedTrack == null) { return; } diff --git a/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java b/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java index ebcdd157ec..c1f2ab8c3e 100644 --- a/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java +++ b/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java @@ -193,7 +193,7 @@ private void setInstructions(GridBagConstraints gbc) { if (currentTrackState.isGmRevealed() || currentTrackState.getRevealedCoords().contains(currentScenario.getCoords()) || (currentScenario.getDeploymentDate() != null)) { - labelBuilder.append(currentScenario.getInfo()); + labelBuilder.append(currentScenario.getInfo(campaign, true)); } if (Objects.requireNonNull(currentScenario.getCurrentState()) == ScenarioState.UNRESOLVED) {