-
Notifications
You must be signed in to change notification settings - Fork 177
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added Contract Automation #5172
Changes from 6 commits
4c41623
84fa227
361393a
a2c8ad5
a87ea8e
9f4df35
dfef5ae
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
# General | ||
generalTitle.text=++ INCOMING TRANSMISSION ++ | ||
generalFallbackAddress.text=Commander | ||
generalConfirm.text=Accept | ||
generalDecline.text=Decline | ||
generalNonClan.text=The %s | ||
|
||
# Messages | ||
mothballDescription.text=%s, our employer has offered to assist our personnel in getting our\ | ||
\ equipment ready for transport.\ | ||
<br>\ | ||
<br>Prior to loading, all equipment in our TO&E will be <b>mothballed</b>. This will prevent us\ | ||
\ needing to maintain it while in transit and the units will be <b>unmothballed</b> prior to\ | ||
\ arrival. Equipment outside our TO&E will be unaffected.\ | ||
<br>\ | ||
<br>Will we be accepting their offer?\ | ||
<br>\ | ||
<br>If we decline, you will still be able to manually order mothballing of equipment, but this\ | ||
\ will need to be arranged by right-clicking the unit in the Hangar panel of your command console,\ | ||
\ and selecting 'mothball.' Our techs will then take care of it as time becomes available. | ||
|
||
transitDescription.text=Our target system is <b>%s</b>. %s has already calculated the best route\ | ||
\ for us.\ | ||
<br>\ | ||
<br>This journey will take us <b>%s</b> days and cost <b>%s</b>. Would you like to accept this\ | ||
\ route and begin transit?\ | ||
<br>\ | ||
<br>If we decline, you will need to manually order route calculation from the Interstellar Map.\ | ||
\ This can be done by selecting the target system in your Briefing Room, followed by 'Calculate\ | ||
\ Jump Path' and 'Begin Transit.' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,338 @@ | ||
/* | ||
* ContractAutomation.java | ||
* | ||
* Copyright (c) 2024 - 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 <http://www.gnu.org/licenses/>. | ||
*/ | ||
package mekhq.campaign.market.contractMarket; | ||
|
||
import megamek.client.ui.swing.util.UIUtil; | ||
import megamek.common.annotations.Nullable; | ||
import mekhq.MekHQ; | ||
import mekhq.campaign.Campaign; | ||
import mekhq.campaign.JumpPath; | ||
import mekhq.campaign.event.UnitChangedEvent; | ||
import mekhq.campaign.finances.Money; | ||
import mekhq.campaign.force.Force; | ||
import mekhq.campaign.mission.Contract; | ||
import mekhq.campaign.personnel.Person; | ||
import mekhq.campaign.unit.Unit; | ||
import mekhq.campaign.unit.actions.ActivateUnitAction; | ||
import mekhq.campaign.unit.actions.MothballUnitAction; | ||
import mekhq.campaign.universe.Factions; | ||
|
||
import javax.swing.*; | ||
import java.awt.*; | ||
import java.util.List; | ||
import java.util.*; | ||
|
||
import static megamek.common.icons.AbstractIcon.DEFAULT_ICON_FILENAME; | ||
|
||
/** | ||
* The ContractAutomation class provides a suite of methods | ||
* used in automating actions when a contract starts. | ||
* This includes actions like mothballing of units, | ||
* transit to mission location and the automated activation of units when arriving in system. | ||
*/ | ||
public class ContractAutomation { | ||
private final static ResourceBundle resources = ResourceBundle.getBundle("mekhq.resources.ContractAutomation"); | ||
|
||
/** | ||
* Main function to initiate a sequence of automated tasks when a contract is started. | ||
* The tasks include prompt and execution for unit mothballing, calculating and starting the | ||
* journey to the target system. | ||
* | ||
* @param campaign The current campaign. | ||
* @param contract Selected contract. | ||
*/ | ||
public static void contractStartPrompt(Campaign campaign, Contract contract) { | ||
// If we're already in the right system there is no need to automate these actions | ||
if (Objects.equals(campaign.getLocation().getCurrentSystem(), contract.getSystem())) { | ||
return; | ||
} | ||
|
||
// Initial setup | ||
final Person speaker = getSpeaker(campaign); | ||
final String speakerName = getSpeakerName(campaign, speaker); | ||
final ImageIcon speakerIcon = getSpeakerIcon(campaign, speaker); | ||
|
||
final String commanderAddress = getCommanderAddress(campaign); | ||
|
||
// Mothballing | ||
String message = String.format(resources.getString("mothballDescription.text"), commanderAddress); | ||
|
||
if (createDialog(speakerName, speakerIcon, message)) { | ||
campaign.setAutomatedMothballUnits(performAutomatedMothballing(campaign)); | ||
} | ||
|
||
// Transit | ||
String targetSystem = contract.getSystemName(campaign.getLocalDate()); | ||
String employerName = contract.getEmployer(); | ||
|
||
if (!employerName.contains("Clan")) { | ||
employerName = String.format(resources.getString("generalNonClan.text"), employerName); | ||
} | ||
|
||
JumpPath jumpPath = contract.getJumpPath(campaign); | ||
int travelDays = contract.getTravelDays(campaign); | ||
|
||
Money costPerJump = campaign.calculateCostPerJump(true, | ||
campaign.getCampaignOptions().isEquipmentContractBase()); | ||
String totalCost = costPerJump.multipliedBy(jumpPath.getJumps()).toAmountAndSymbolString(); | ||
|
||
message = String.format(resources.getString("transitDescription.text"), | ||
targetSystem, employerName, travelDays, totalCost); | ||
if (createDialog(speakerName, speakerIcon, message)) { | ||
campaign.getLocation().setJumpPath(jumpPath); | ||
campaign.getUnits().forEach(unit -> unit.setSite(Unit.SITE_FACILITY_BASIC)); | ||
campaign.getApp().getCampaigngui().refreshAllTabs(); | ||
campaign.getApp().getCampaigngui().refreshLocation(); | ||
} | ||
} | ||
|
||
/** | ||
* @param campaign The current campaign | ||
* @return The highest ranking Admin/Transport character. If none are found, returns {@code null}. | ||
*/ | ||
private static @Nullable Person getSpeaker(Campaign campaign) { | ||
List<Person> admins = campaign.getAdmins(); | ||
|
||
if (admins.isEmpty()) { | ||
return null; | ||
} | ||
|
||
List<Person> transportAdmins = new ArrayList<>(); | ||
|
||
for (Person admin : admins) { | ||
if (admin.getPrimaryRole().isAdministratorTransport() | ||
|| admin.getSecondaryRole().isAdministratorTransport()) { | ||
transportAdmins.add(admin); | ||
} | ||
} | ||
|
||
if (transportAdmins.isEmpty()) { | ||
return null; | ||
} | ||
|
||
Person speaker = transportAdmins.get(0); | ||
|
||
for (Person admin : transportAdmins) { | ||
if (admin.outRanksUsingSkillTiebreaker(campaign, speaker)) { | ||
speaker = admin; | ||
} | ||
} | ||
|
||
return speaker; | ||
} | ||
|
||
/** | ||
* Gets the name of the individual to be displayed in the dialog. | ||
* If the person is {@code null}, it uses the campaign's name. | ||
* | ||
* @param campaign The current campaign | ||
* @param speaker The person who will be speaking, or {@code null}. | ||
* @return The name to be displayed. | ||
*/ | ||
private static String getSpeakerName(Campaign campaign, @Nullable Person speaker) { | ||
if (speaker == null) { | ||
return campaign.getName(); | ||
} else { | ||
return speaker.getFullTitle(); | ||
} | ||
} | ||
|
||
/** | ||
* Gets the icon representing the speaker. | ||
* If the speaker is {@code null}, it defaults to displaying the campaign's icon, or the | ||
* campaign's faction icon. | ||
* | ||
* @param campaign The current campaign | ||
* @param speaker The person who is speaking, or {@code null}. | ||
* @return The icon of the speaker, campaign, or faction. | ||
*/ | ||
private static ImageIcon getSpeakerIcon(Campaign campaign, @Nullable Person speaker) { | ||
ImageIcon icon; | ||
|
||
if (speaker == null) { | ||
String fallbackIconFilename = campaign.getUnitIcon().getFilename(); | ||
|
||
if (fallbackIconFilename == null || fallbackIconFilename.equals(DEFAULT_ICON_FILENAME)) { | ||
icon = Factions.getFactionLogo(campaign, campaign.getFaction().getShortName(), true); | ||
} else { | ||
icon = campaign.getUnitIcon().getImageIcon(); | ||
} | ||
} else { | ||
icon = speaker.getPortrait().getImageIcon(); | ||
} | ||
|
||
Image originalImage = icon.getImage(); | ||
Image scaledImage = originalImage.getScaledInstance(100, -1, Image.SCALE_SMOOTH); | ||
return new ImageIcon(scaledImage); | ||
} | ||
|
||
/** | ||
* Gets a string to use for addressing the commander. | ||
* If no commander is flagged, returns a default address. | ||
* | ||
* @param campaign The current campaign | ||
* @return The title of the commander, or a default string if no commander. | ||
*/ | ||
private static String getCommanderAddress(Campaign campaign) { | ||
Person commander = campaign.getFlaggedCommander(); | ||
|
||
if (commander == null) { | ||
return resources.getString("generalFallbackAddress.text"); | ||
} | ||
|
||
String commanderRank = commander.getRankName(); | ||
|
||
if (commanderRank.equalsIgnoreCase("None") || commanderRank.isBlank()) { | ||
return commander.getFullName(); | ||
} | ||
|
||
return commanderRank; | ||
} | ||
|
||
/** | ||
* Displays a dialog for user interaction. | ||
* The dialog uses a custom formatted message and includes options for user to confirm or decline. | ||
* | ||
* @param speakerName The title of the speaker to be displayed. | ||
* @param speakerIcon The {@link ImageIcon} of the person speaking. | ||
* @param message The message to be displayed in the dialog. | ||
* @return {@code true} if the user confirms, {@code false} otherwise. | ||
*/ | ||
private static boolean createDialog(String speakerName, ImageIcon speakerIcon, String message) { | ||
final int WIDTH = UIUtil.scaleForGUI(400); | ||
|
||
// Custom button text | ||
Object[] options = { | ||
resources.getString("generalConfirm.text"), | ||
resources.getString("generalDecline.text") | ||
}; | ||
|
||
// Create a custom message with a border | ||
String descriptionTitle = String.format("<html><b>%s</b></html>", speakerName); | ||
|
||
// Create ImageIcon JLabel | ||
JLabel iconLabel = new JLabel(speakerIcon); | ||
iconLabel.setHorizontalAlignment(JLabel.CENTER); | ||
|
||
// Create description JPanel | ||
JPanel descriptionPanel = new JPanel(); | ||
descriptionPanel.setLayout(new BoxLayout(descriptionPanel, BoxLayout.PAGE_AXIS)); | ||
JLabel description = new JLabel(String.format("<html><div style='width: %s; text-align:justify;'>%s</div></html>", | ||
WIDTH, message)); | ||
description.setBorder(BorderFactory.createTitledBorder(descriptionTitle)); | ||
descriptionPanel.add(description); | ||
|
||
// Create main JPanel and add icon and description | ||
JPanel mainPanel = new JPanel(new BorderLayout()); | ||
mainPanel.add(iconLabel, BorderLayout.NORTH); | ||
mainPanel.add(descriptionPanel, BorderLayout.CENTER); | ||
|
||
// Create JOptionPane | ||
JOptionPane optionPane = new JOptionPane(mainPanel, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you are probably double-dialoging this because JOptionPane is not flexible, right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That was the intent, but on a second viewing I don't think it's necessary and double-dialoging isn't doing anything beneficial here, just unnecessarily increasing complexity. Removed. |
||
JOptionPane.PLAIN_MESSAGE, | ||
JOptionPane.YES_NO_OPTION, | ||
null, | ||
options, | ||
options[0]); | ||
|
||
// Create JDialog | ||
JDialog dialog = new JDialog(); | ||
dialog.setTitle(resources.getString("generalTitle.text")); | ||
dialog.setModal(true); | ||
dialog.setContentPane(optionPane); | ||
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); | ||
dialog.pack(); | ||
dialog.setLocationRelativeTo(null); | ||
|
||
optionPane.addPropertyChangeListener(evt -> { | ||
if (JOptionPane.VALUE_PROPERTY.equals(evt.getPropertyName())) { | ||
dialog.dispose(); | ||
} | ||
}); | ||
|
||
dialog.setVisible(true); | ||
|
||
int response = (Objects.equals(optionPane.getValue(), options[0]) ? | ||
JOptionPane.YES_OPTION : JOptionPane.NO_OPTION); | ||
|
||
return (response == JOptionPane.YES_OPTION); | ||
} | ||
|
||
/** | ||
* This method identifies all non-mothballed units within a campaign that are currently | ||
* assigned to a {@code Force}. Those units are then GM Mothballed. | ||
* | ||
* @param campaign The current campaign. | ||
* @return A list of all newly mothballed units. | ||
*/ | ||
private static List<Unit> performAutomatedMothballing(Campaign campaign) { | ||
List<Unit> mothballTargets = new ArrayList<>(); | ||
List<Unit> mothballedUnits = new ArrayList<>(); | ||
MothballUnitAction mothballUnitAction = new MothballUnitAction(null, true); | ||
|
||
for (Force force : campaign.getAllForces()) { | ||
for (UUID unitId : force.getUnits()) { | ||
Unit unit = campaign.getUnit(unitId); | ||
|
||
if (unit != null) { | ||
if (unit.isAvailable(false) && !unit.isUnderRepair()) { | ||
mothballTargets.add(unit); | ||
} | ||
} | ||
} | ||
} | ||
|
||
// This needs to be a separate list as the act of mothballing the unit removes it from the | ||
// list of units attached to the relevant force, resulting in a ConcurrentModificationException | ||
for (Unit unit : mothballTargets) { | ||
mothballUnitAction.execute(campaign, unit); | ||
MekHQ.triggerEvent(new UnitChangedEvent(unit)); | ||
mothballedUnits.add(unit); | ||
} | ||
|
||
return mothballedUnits; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. might simply return mothballTargets (?) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point well made |
||
} | ||
|
||
/** | ||
* Perform automated activation of units. | ||
* Identifies all units that were mothballed previously and are now needing activation. | ||
* The activation action is executed for each unit, and they are returned to their prior Force | ||
* if it still exists. | ||
* | ||
* @param campaign The current campaign. | ||
*/ | ||
public static void performAutomatedActivation(Campaign campaign) { | ||
List<Unit> units = campaign.getAutomatedMothballUnits(); | ||
|
||
if (units.isEmpty()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it feels like this is unnecessary, the loop will simply skip if campaign.getAutomatedMothballUnits() is empty. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was a relic from an earlier iteration and should have been removed. Thanks for pointing it out. |
||
return; | ||
} | ||
|
||
ActivateUnitAction activateUnitAction = new ActivateUnitAction(null, true); | ||
|
||
for (Unit unit : units) { | ||
if (unit.isMothballed()) { | ||
activateUnitAction.execute(campaign, unit); | ||
MekHQ.triggerEvent(new UnitChangedEvent(unit)); | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should this method clean up the automothball list? If getAutomatedMothballUnits() is not a copy and is modifiable: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for mentioning this, that was an oversight |
||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It feels like this can't be the first time this is done (or it might not be the last)... same for the speaker icon. But I wouldnt know. There is this "this contract is too difficult for your wimpy forces" dialog. Might be something to unify in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, moved it to
Campaign.java