diff --git a/MekHQ/data/images/misc/acar_splash_hd.png b/MekHQ/data/images/misc/acar_splash_hd.png new file mode 100644 index 0000000000..2f73610390 Binary files /dev/null and b/MekHQ/data/images/misc/acar_splash_hd.png differ diff --git a/MekHQ/data/scenariomodifiers/airBattleModifiers.xml b/MekHQ/data/scenariomodifiers/airBattleModifiers.xml index 14ed618218..a467550236 100644 --- a/MekHQ/data/scenariomodifiers/airBattleModifiers.xml +++ b/MekHQ/data/scenariomodifiers/airBattleModifiers.xml @@ -26,9 +26,10 @@ Need ability to allow player to specifically deploy DropShips HotDrop.xml - + GoodIntel.xml diff --git a/MekHQ/data/scenariomodifiers/groundBattleModifiers.xml b/MekHQ/data/scenariomodifiers/groundBattleModifiers.xml index 82fc306eef..6c9280ce92 100644 --- a/MekHQ/data/scenariomodifiers/groundBattleModifiers.xml +++ b/MekHQ/data/scenariomodifiers/groundBattleModifiers.xml @@ -56,12 +56,14 @@ EnemyHotDrop.xml - + + GoodIntel.xml diff --git a/MekHQ/data/scenariomodifiers/modifiermanifest.xml b/MekHQ/data/scenariomodifiers/modifiermanifest.xml index 43d4d7a16a..a22200a4f3 100644 --- a/MekHQ/data/scenariomodifiers/modifiermanifest.xml +++ b/MekHQ/data/scenariomodifiers/modifiermanifest.xml @@ -62,12 +62,14 @@ EnemyHotDrop.xml - + + GoodIntel.xml diff --git a/MekHQ/resources/mekhq/resources/CampaignGUI.properties b/MekHQ/resources/mekhq/resources/CampaignGUI.properties index 941f8120c4..d0e434490c 100644 --- a/MekHQ/resources/mekhq/resources/CampaignGUI.properties +++ b/MekHQ/resources/mekhq/resources/CampaignGUI.properties @@ -179,8 +179,9 @@ btnPrintRS.toolTipText=Print record sheets for all currently assigned units. btnPrintRS.text=Print Sheets btnGetMul.toolTipText=Get a MUL file of all assigned units that can be loaded into MegaMek btnGetMul.text=Export MUL File -btnClearAssignedUnits.toolTipText=Clear all assigned units for this scenario -btnClearAssignedUnits.text=Clear Units +btnClearAssignedUnits.toolTipText=Clear all assigned units for this scenario. Restricted to GM Mode\ + \ if it is a scenario assigned to the Area of Operations. +btnClearAssignedUnits.text=Reset Deployment btnResolveScenario.toolTipText=Bring up a wizard that will guide you through the process of resolving this scenario either by MUL files from a MegaMek game or by manually editing for tabletop games. btnResolveScenario.text=Resolve Manually btnAutoResolveScenario.toolTipText=Start a game of MegaMek with all the assigned units played by bots.
At the game's conclusion, you will be presented with a series of dialogs for resolving the scenario. diff --git a/MekHQ/resources/mekhq/resources/ScenarioTableModel.properties b/MekHQ/resources/mekhq/resources/ScenarioTableModel.properties index 66033b477b..39fffa2833 100644 --- a/MekHQ/resources/mekhq/resources/ScenarioTableModel.properties +++ b/MekHQ/resources/mekhq/resources/ScenarioTableModel.properties @@ -2,5 +2,5 @@ col_name.text=Scenario Name col_status.text=Resolution col_date.text=Date col_assign.text=Units Assigned -col_sector.text=Sector +col_sector.text=Grid Reference col_unknown.text=? diff --git a/MekHQ/src/mekhq/MekHQ.java b/MekHQ/src/mekhq/MekHQ.java index 19d9eb3e60..9c56ef7a99 100644 --- a/MekHQ/src/mekhq/MekHQ.java +++ b/MekHQ/src/mekhq/MekHQ.java @@ -27,6 +27,8 @@ import megamek.SuiteConstants; import megamek.client.Client; import megamek.client.bot.princess.BehaviorSettings; +import megamek.client.ui.dialogs.AutoResolveChanceDialog; +import megamek.client.ui.dialogs.AutoResolveProgressDialog; import megamek.client.ui.dialogs.AutoResolveSimulationLogDialog; import megamek.client.ui.preferences.PreferencesNode; import megamek.client.ui.preferences.SuitePreferences; @@ -37,6 +39,7 @@ import megamek.common.Board; import megamek.common.annotations.Nullable; import megamek.common.autoresolve.acar.SimulatedClient; +import megamek.common.autoresolve.converter.SingletonForces; import megamek.common.autoresolve.event.AutoResolveConcludedEvent; import megamek.common.event.*; import megamek.common.net.marshalling.SanityInputFilter; @@ -645,25 +648,25 @@ public void startAutoResolve(AtBScenario scenario, List units) { this.autosaveService.requestBeforeMissionAutosave(getCampaign()); if (getCampaign().getCampaignOptions().isAutoResolveVictoryChanceEnabled()) { - var proceed = megamek.client.ui.dialogs.AutoResolveChanceDialog + var proceed = AutoResolveChanceDialog .showDialog( - getCampaigngui().getFrame(), + campaignGUI.getFrame(), getCampaign().getCampaignOptions().getAutoResolveNumberOfScenarios(), Runtime.getRuntime().availableProcessors(), - getCampaign().getPlayer().getTeam(), - new AtBSetupForces(getCampaign(), units, scenario), + 1, + new AtBSetupForces(getCampaign(), units, scenario, new SingletonForces()), new Board(scenario.getBaseMapX(), scenario.getBaseMapY())) == JOptionPane.YES_OPTION; if (!proceed) { return; } } - var event = megamek.client.ui.dialogs.AutoResolveProgressDialog.showDialog( - getCampaigngui().getFrame(), - new AtBSetupForces(getCampaign(), units, scenario), + var event = AutoResolveProgressDialog.showDialog( + campaignGUI.getFrame(), + new AtBSetupForces(getCampaign(), units, scenario, new SingletonForces()), new Board(scenario.getBaseMapX(), scenario.getBaseMapY())); - var autoResolveBattleReport = new AutoResolveSimulationLogDialog(getCampaigngui().getFrame(), event.getLogFile()); + var autoResolveBattleReport = new AutoResolveSimulationLogDialog(campaignGUI.getFrame(), event.getLogFile()); autoResolveBattleReport.setModal(true); autoResolveBattleReport.setVisible(true); diff --git a/MekHQ/src/mekhq/campaign/CampaignFactory.java b/MekHQ/src/mekhq/campaign/CampaignFactory.java index 294024bc3a..59f0afacc5 100644 --- a/MekHQ/src/mekhq/campaign/CampaignFactory.java +++ b/MekHQ/src/mekhq/campaign/CampaignFactory.java @@ -79,7 +79,6 @@ public Campaign createCampaign(InputStream is) // ...otherwise, assume we're an XML file. CampaignXmlParser parser = new CampaignXmlParser(is, this.app); - return parser.parse(); } @@ -91,4 +90,5 @@ private byte[] readHeader(InputStream is) throws IOException { return header; } + } diff --git a/MekHQ/src/mekhq/campaign/autoresolve/AtBSetupForces.java b/MekHQ/src/mekhq/campaign/autoresolve/AtBSetupForces.java index e0b5bc4938..aee7980a77 100644 --- a/MekHQ/src/mekhq/campaign/autoresolve/AtBSetupForces.java +++ b/MekHQ/src/mekhq/campaign/autoresolve/AtBSetupForces.java @@ -19,10 +19,7 @@ import megamek.common.*; import megamek.common.alphaStrike.conversion.ASConverter; import megamek.common.autoresolve.acar.SimulationContext; -import megamek.common.autoresolve.converter.ConsolidateForces; -import megamek.common.autoresolve.converter.ForceToFormationConverter; -import megamek.common.autoresolve.converter.SetupForces; -import megamek.common.autoresolve.converter.SingleElementConsolidateForces; +import megamek.common.autoresolve.converter.*; import megamek.common.copy.CrewRefBreak; import megamek.common.force.Forces; import megamek.common.options.OptionsConstants; @@ -41,6 +38,8 @@ import java.io.InputStream; import java.util.*; +import static megamek.common.force.Force.NO_FORCE; + /** * @author Luana Coppio */ @@ -50,11 +49,20 @@ public class AtBSetupForces extends SetupForces { private final Campaign campaign; private final List units; private final AtBScenario scenario; + private final ForceConsolidation forceConsolidationMethod; + + private final OrderFactory orderFactory; + + public AtBSetupForces(Campaign campaign, List units, AtBScenario scenario, ForceConsolidation forceConsolidationMethod) { + this(campaign, units, scenario, forceConsolidationMethod, new OrderFactory(campaign, scenario)); + } - public AtBSetupForces(Campaign campaign, List units, AtBScenario scenario) { + public AtBSetupForces(Campaign campaign, List units, AtBScenario scenario, ForceConsolidation forceConsolidationMethod, OrderFactory orderFactory) { this.campaign = campaign; this.units = units; this.scenario = scenario; + this.forceConsolidationMethod = forceConsolidationMethod; + this.orderFactory = orderFactory; } /** @@ -64,10 +72,18 @@ public AtBSetupForces(Campaign campaign, List units, AtBScenario scenario) public void createForcesOnSimulation(SimulationContext game) { setupPlayer(game); setupBots(game); - ConsolidateForces.consolidateForces(game, new SingleElementConsolidateForces()); + forceConsolidationMethod.consolidateForces(game); convertForcesIntoFormations(game); } + @Override + public void addOrdersToForces(SimulationContext context) { + var orders = orderFactory.getOrders(); + context.getOrders().clear(); + context.getOrders().addAll(orders); + context.getOrders().resetOrders(); + } + private static class FailedToConvertForceToFormationException extends RuntimeException { public FailedToConvertForceToFormationException(Throwable cause) { super(cause); @@ -80,10 +96,10 @@ public FailedToConvertForceToFormationException(Throwable cause) { * and used in the auto resolve in place of the original entities * @param game The game object to convert the forces in */ - private static void convertForcesIntoFormations(SimulationContext game) { + private void convertForcesIntoFormations(SimulationContext game) { for(var force : game.getForces().getTopLevelForces()) { try { - var formation = new ForceToFormationConverter(force, game).convert(); + var formation = new EntityAsUnit(force, game).convert(); formation.setTargetFormationId(Entity.NONE); formation.setOwnerId(force.getOwnerId()); game.addUnit(formation); @@ -413,33 +429,14 @@ private PlanetaryConditions getPlanetaryConditions() { private void sendEntities(List entities, SimulationContext game) { Map forceMapping = new HashMap<>(); for (final Entity entity : new ArrayList<>(entities)) { - if (entity instanceof ProtoMek) { - int numPlayerProtos = game.getSelectedEntityCount(new EntitySelector() { - private final int ownerId = entity.getOwnerId(); - @Override - public boolean accept(Entity entity) { - return (entity instanceof ProtoMek) && (ownerId == entity.getOwnerId()); - } - }); - - entity.setUnitNumber((short) (numPlayerProtos / 5)); - } - - if (Entity.NONE == entity.getId()) { - entity.setId(game.getNextEntityId()); - } - - // Give the unit a spotlight, if it has the spotlight quirk - entity.setExternalSearchlight(entity.hasExternalSearchlight() - || entity.hasQuirk(OptionsConstants.QUIRK_POS_SEARCHLIGHT)); - + lastTouchesBeforeSendingEntity(game, entity); game.getPlayer(entity.getOwnerId()).changeInitialEntityCount(1); // Restore forces from MULs or other external sources from the forceString, if // any if (!entity.getForceString().isBlank()) { List forceList = Forces.parseForceString(entity); - int realId = megamek.common.force.Force.NO_FORCE; + int realId = NO_FORCE; boolean topLevel = true; for (megamek.common.force.Force force : forceList) { @@ -463,4 +460,26 @@ public boolean accept(Entity entity) { } } } + + private static void lastTouchesBeforeSendingEntity(SimulationContext game, Entity entity) { + if (entity instanceof ProtoMek) { + int numPlayerProtos = game.getSelectedEntityCount(new EntitySelector() { + private final int ownerId = entity.getOwnerId(); + @Override + public boolean accept(Entity entity) { + return (entity instanceof ProtoMek) && (ownerId == entity.getOwnerId()); + } + }); + + entity.setUnitNumber((short) (numPlayerProtos / 5)); + } + + if (Entity.NONE == entity.getId()) { + entity.setId(game.getNextEntityId()); + } + + // Give the unit a spotlight, if it has the spotlight quirk + entity.setExternalSearchlight(entity.hasExternalSearchlight() + || entity.hasQuirk(OptionsConstants.QUIRK_POS_SEARCHLIGHT)); + } } diff --git a/MekHQ/src/mekhq/campaign/autoresolve/OrderFactory.java b/MekHQ/src/mekhq/campaign/autoresolve/OrderFactory.java new file mode 100644 index 0000000000..b1b3e005d8 --- /dev/null +++ b/MekHQ/src/mekhq/campaign/autoresolve/OrderFactory.java @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2024-2025 - The MegaMek Team. All Rights Reserved. + * + * This program 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 2 of the License, or (at your option) + * any later version. + * + * This program 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. + * + */ + +package mekhq.campaign.autoresolve; + +import megamek.common.Entity; +import megamek.common.OffBoardDirection; +import megamek.common.autoresolve.acar.order.Condition; +import megamek.common.autoresolve.acar.order.Order; +import megamek.common.autoresolve.acar.order.OrderType; +import megamek.common.autoresolve.acar.order.Orders; +import megamek.logging.MMLogger; +import mekhq.campaign.Campaign; +import mekhq.campaign.mission.Scenario; +import mekhq.campaign.mission.ScenarioObjective; + +import java.util.Set; + +/** + * @author Luana Coppio + */ +public class OrderFactory { + private static final MMLogger logger = MMLogger.create(OrderFactory.class); + + private final Campaign campaign; + private final Scenario scenario; + private Orders orders; + + public OrderFactory(Campaign campaign, Scenario scenario) { + this.campaign = campaign; + this.scenario = scenario; + } + + public Orders getOrders() { + this.orders = new Orders(); + var scenarioObjectives = scenario.getScenarioObjectives(); + for (var objective : scenarioObjectives) { + createOrderFromObjective(campaign.getPlayer().getId(), objective); + } + return orders; + } + + private void addOrder(Order order) { + logger.info("Adding order {}", order); + this.orders.add(order); + } + + private void createOrderFromObjective(int ownerId, ScenarioObjective objective) { + + switch (objective.getObjectiveCriterion()) { + case Capture: // no capture, only destroy! + case Destroy: + createDestroyOrder(ownerId, objective); + break; + case Preserve: + createPreserveOrder(ownerId, objective); + break; + case ReachMapEdge: + createReachMapEdgeOrder(ownerId, objective); + break; + case ForceWithdraw: + createForceWithdrawOrder(ownerId, objective); + break; + case PreventReachMapEdge: + createPreventReachMapEdgeOrder(ownerId, objective); + break; + case Custom: + default: + // do nothing for custom bc what can be done? + break; + }; + } + // Make the enemy withdraw + private void createForceWithdrawOrder(int ownerId, ScenarioObjective objective) { + var orderBuilder = Order.OrderBuilder.anOrder(ownerId, OrderType.ATTACK_TARGET_NOT_WITHDRAWING) + .withCondition(Condition.alwaysTrue()); + if (objective.getTimeLimitType() != ScenarioObjective.TimeLimitType.None) { + orderBuilder.withCondition(context -> context.getCurrentRound() <= objective.getTimeLimit()); + } + addOrder(orderBuilder.build()); + } + + private void createReachMapEdgeOrder(int ownerId, ScenarioObjective objective) { + if (objective.getDestinationEdge() != null && objective.getDestinationEdge() != OffBoardDirection.NONE) { + var northSide = Set.of(OffBoardDirection.NORTH, OffBoardDirection.EAST); + var orderType = northSide.contains(objective.getDestinationEdge()) ? OrderType.FLEE_NORTH : OrderType.FLEE_SOUTH; + var orderBuilder = Order.OrderBuilder.anOrder(ownerId, orderType) + .withCondition(Condition.alwaysTrue()); + orders.add(orderBuilder.build()); + } else { + var orderBuilder = Order.OrderBuilder.anOrder(ownerId, scenario.getStartingPos() > 4 ? OrderType.FLEE_NORTH : OrderType.FLEE_SOUTH) + .withCondition(Condition.alwaysTrue()); + addOrder(orderBuilder.build()); + } + } + + private void createPreventReachMapEdgeOrder(int ownerId, ScenarioObjective objective) { + var orderBuilder = Order.OrderBuilder.anOrder(ownerId, OrderType.ATTACK_TARGET_WITHDRAWING) + .withCondition(Condition.alwaysTrue()); + if (objective.getTimeLimitType() != ScenarioObjective.TimeLimitType.None) { + orderBuilder.withCondition(context -> context.getCurrentRound() <= objective.getTimeLimit()); + } + addOrder(orderBuilder.build()); + } + + private void createPreserveOrder(int ownerId, ScenarioObjective objective) { + if (!objective.getAssociatedForceNames().isEmpty()) { + var order = createOrderForPreserveAssociatedForces(ownerId, objective); + addOrder(order); + } + if (!objective.getAssociatedUnitIDs().isEmpty()) { + var order = createOrderForPreserveAssociatedUnits(ownerId, objective); + addOrder(order); + } + } + + private Order createOrderForPreserveAssociatedForces(int ownerId, ScenarioObjective objective) { + var orderBuilder = Order.OrderBuilder.anOrder(ownerId, OrderType.WITHDRAW_IF_CONDITION_IS_MET) + .withCondition(context -> { + var attackTypeOrders = Set.of(OrderType.ATTACK_TARGET, OrderType.ATTACK_TARGET_NOT_WITHDRAWING, OrderType.ATTACK_TARGET_WITHDRAWING); + var hasOutstandingAttackOrders = context.getOrders().getOrders(ownerId).stream() + .filter(o -> attackTypeOrders.contains(o.getOrderType())).anyMatch(order -> order.isEligible(context)); + int totalUnits = 0; + var currentUnits = 0; + for (var forceName : objective.getAssociatedForceNames()) { + for (var force : context.getForces().getTopLevelForces()) { + if (force.getName().equals(forceName)) { + totalUnits += force.getEntities().size(); + for (var entityId : force.getEntities()) { + currentUnits += context.getSelectedEntityCount(entity -> entity.getId() == entityId); + } + } + } + } + + if (totalUnits == 0) { + return true; + } else { + + var timeLimit = false; + if (objective.getTimeLimitType() != ScenarioObjective.TimeLimitType.None && objective.isTimeLimitAtMost()) { + timeLimit = context.getCurrentRound() >= objective.getTimeLimit(); + } + if (!hasOutstandingAttackOrders) { + if (objective.getAmountType().equals(ScenarioObjective.ObjectiveAmountType.Fixed)) { + return currentUnits < objective.getFixedAmount() || timeLimit; + } else { + int percent = (int) ((totalUnits / (double) currentUnits) * 100); + return percent < objective.getPercentage() || timeLimit; + } + } else { + if (timeLimit) { + return context.getCurrentRound() >= objective.getTimeLimit(); + } else { + return true; + } + } + } + }); + + return orderBuilder.build(); + } + + private Order createOrderForPreserveAssociatedUnits(int ownerId, ScenarioObjective objective) { + var orderBuilder = Order.OrderBuilder.anOrder(ownerId, OrderType.WITHDRAW_IF_CONDITION_IS_MET) + .withCondition(context -> { + var inGameObjects = context.getInGameObjects(); + var unitIds = objective.getAssociatedUnitIDs(); + var entitiesOfInterest = inGameObjects.stream().filter(Entity.class::isInstance).map(Entity.class::cast) + .filter(e -> e.getOwnerId() == ownerId).toList(); + var currentUnits = 0; + var totalUnits = unitIds.size(); + if (totalUnits == 0) { + return true; + } + for (var unit : entitiesOfInterest) { + if (unitIds.contains(unit.getExternalIdAsString())) { + currentUnits++; + } + } + var timeLimit = false; + if (objective.getTimeLimitType() != ScenarioObjective.TimeLimitType.None && objective.isTimeLimitAtMost()) { + timeLimit = context.getCurrentRound() >= objective.getTimeLimit(); + } + if (objective.getAmountType().equals(ScenarioObjective.ObjectiveAmountType.Fixed)) { + return currentUnits < objective.getFixedAmount() || timeLimit; + } else { + int percent = (int) ((totalUnits / (double) currentUnits) * 100); + return percent < objective.getPercentage() || timeLimit; + } + }); + return orderBuilder.build(); + } + + private void createDestroyOrder(int ownerId, ScenarioObjective objective) { + var orderBuilder = Order.OrderBuilder.anOrder(ownerId, OrderType.ATTACK_TARGET); + if (objective.getPercentage() == 100) { + orderBuilder.withCondition(Condition.alwaysTrue()); + } else { + orderBuilder.withCondition(context -> { + var enemyPlayers = context.getPlayersList().stream().filter(p -> p.isEnemyOf(campaign.getPlayer())).toList(); + var totalUnits = 0; + var currentUnits = 0; + for (var player: enemyPlayers) { + totalUnits += context.getStartingNumberOfUnits(player.getId()); + currentUnits += context.getActiveFormations(player).size(); + } + + if (totalUnits == 0) { + return true; + } + if (objective.getAmountType().equals(ScenarioObjective.ObjectiveAmountType.Fixed)) { + return (totalUnits - currentUnits) < objective.getFixedAmount(); + } else { + var currentPercent = 100 - (int) (currentUnits / (double) totalUnits); + return (objective.getPercentage() - currentPercent) > 0; + } + }); + } + addOrder(orderBuilder.build()); + } +} diff --git a/MekHQ/src/mekhq/campaign/force/CombatTeam.java b/MekHQ/src/mekhq/campaign/force/CombatTeam.java index f2da4f28d9..d1dd6a608f 100644 --- a/MekHQ/src/mekhq/campaign/force/CombatTeam.java +++ b/MekHQ/src/mekhq/campaign/force/CombatTeam.java @@ -371,6 +371,15 @@ size > getStandardForceSize(campaign.getFaction()) + 2) { force.setCombatTeamStatus(false); return false; } + + if (parentForce.isConvoyForce()) { + force.setCombatTeamStatus(false); + return false; + } + + if (!parentForce.isCombatForce()) { + return false; + } } force.setCombatTeamStatus(true); diff --git a/MekHQ/src/mekhq/campaign/mission/AtBScenario.java b/MekHQ/src/mekhq/campaign/mission/AtBScenario.java index 0e96989611..6a2cabc31d 100644 --- a/MekHQ/src/mekhq/campaign/mission/AtBScenario.java +++ b/MekHQ/src/mekhq/campaign/mission/AtBScenario.java @@ -50,6 +50,9 @@ import mekhq.campaign.rating.IUnitRating; import mekhq.campaign.stratcon.StratconBiomeManifest; import mekhq.campaign.stratcon.StratconBiomeManifest.MapTypeList; +import mekhq.campaign.stratcon.StratconCampaignState; +import mekhq.campaign.stratcon.StratconScenario; +import mekhq.campaign.stratcon.StratconTrackState; import mekhq.campaign.unit.Unit; import mekhq.campaign.universe.*; import mekhq.utilities.MHQXMLUtility; @@ -2116,4 +2119,51 @@ public String getDeploymentInstructions() { public boolean canStartScenario(Campaign c) { return c.getLocalDate().equals(getDate()) && super.canStartScenario(c); } + + /** + * Retrieves the {@link StratconScenario} associated with the current mission ID. + * + *

The method first retrieves the {@link AtBContract} from the given campaign. + * If the contract, its {@link StratconCampaignState}, or any required track data + * is unavailable, the method returns {@code null}. It iterates through all + * {@link StratconTrackState} objects in the campaign state and their associated scenarios. + * If a {@link StratconScenario} contains a non-null {@link AtBDynamicScenario} whose + * mission ID matches the current mission ID, it is returned. + * + * @param campaign the {@link Campaign} instance being queried for the scenario + * @return the matching {@link StratconScenario} if found, or {@code null} if no match + * is found or any required data is missing + * @throws NullPointerException if {@code campaign} is {@code null} + */ + public @Nullable StratconScenario getStratconScenario(Campaign campaign) { + // Get contract + AtBContract contract = getContract(campaign); + if (contract == null) { + return null; + } + + // Fetch campaign state + StratconCampaignState campaignState = contract.getStratconCampaignState(); + if (campaignState == null) { + return null; + } + + // Find associated StratCon Scenario, if any + for (StratconTrackState track : campaignState.getTracks()) { + Collection trackScenarios = track.getScenarios().values(); + + for (StratconScenario scenario : trackScenarios) { + AtBDynamicScenario backingScenario = scenario.getBackingScenario(); + if (backingScenario == null) { + continue; + } + + if (backingScenario.getMissionId() == getMissionId()) { + return scenario; + } + } + } + + return null; + } } diff --git a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java index 4e98dab774..14adb1e019 100644 --- a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java +++ b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java @@ -19,6 +19,7 @@ import mekhq.campaign.universe.Faction; import java.math.BigInteger; +import java.time.LocalDate; import java.util.*; import static java.lang.Math.floor; @@ -68,6 +69,7 @@ public class Resupply { public static final int CARGO_MULTIPLIER = 4; public static final int RESUPPLY_AMMO_TONNAGE = 1; public static final int RESUPPLY_ARMOR_TONNAGE = 5; + final LocalDate BATTLE_OF_TUKAYYID = LocalDate.of(3052, 5, 21); private static final MMLogger logger = MMLogger.create(Resupply.class); @@ -522,6 +524,10 @@ private Map collectParts() { final Collection unitIds = campaign.getForce(0).getAllUnits(true); Map processedParts = new HashMap<>(); + boolean allowClan = (employerIsClan || campaign.getLocalDate().isAfter(BATTLE_OF_TUKAYYID)) + && campaign.getCampaignOptions().isAllowClanPurchases(); + boolean allowInnerSphere = campaign.getCampaignOptions().isAllowISPurchases(); + try { for (UUID unitId : unitIds) { Unit unit = campaign.getUnit(unitId); @@ -537,12 +543,23 @@ private Map collectParts() { } if (isProhibitedUnitType(entity, false)) { + logger.info("skipping " + unit.getName() + " as it is prohibited."); continue; } if (!unit.isSalvage() && (unit.isAvailable() || unit.isDeployed())) { List parts = unit.getParts(); for (Part part : parts) { + if (part.isClan()) { + if (!allowClan) { + continue; + } + } else { + if (!allowInnerSphere) { + continue; + } + } + if (isIneligiblePart(part, unit)) { continue; } @@ -711,8 +728,12 @@ private void applyWarehouseWeightModifiers(Map partsList) { continue; } - PartDetails partDetails = new PartDetails(part, weight); + // This prevents us accidentally adding new items to the pool + if (!partsList.containsKey(getPartKey(part))) { + continue; + } + PartDetails partDetails = new PartDetails(part, weight); partsList.merge(getPartKey(part), partDetails, (oldValue, newValue) -> { oldValue.setWeight(oldValue.getWeight() - newValue.getWeight()); return oldValue; diff --git a/MekHQ/src/mekhq/campaign/personnel/education/EducationController.java b/MekHQ/src/mekhq/campaign/personnel/education/EducationController.java index d58c104170..55e07e1dda 100644 --- a/MekHQ/src/mekhq/campaign/personnel/education/EducationController.java +++ b/MekHQ/src/mekhq/campaign/personnel/education/EducationController.java @@ -31,6 +31,7 @@ import mekhq.campaign.personnel.enums.PersonnelStatus; import mekhq.campaign.personnel.enums.education.EducationLevel; import mekhq.campaign.personnel.enums.education.EducationStage; +import mekhq.campaign.personnel.familyTree.Genealogy; import mekhq.campaign.personnel.randomEvents.enums.personalities.Intelligence; import mekhq.utilities.ReportingUtilities; @@ -247,11 +248,29 @@ public static void enrollPerson(Campaign campaign, Person person, Academy academ person.setEduJourneyTime(campaign.getSimplifiedTravelTime(campaign.getSystemById(campus))); person.setEduAcademySystem(campus); } + } + + Genealogy genealogy = person.getGenealogy(); + Person spouse = genealogy.getSpouse(); + List children = genealogy.getChildren(); + + boolean hasActiveParent = false; + if (spouse != null) { + if (spouse.getStatus().isActive() && spouse.isDependent()) { + person.addEduTagAlong(spouse.getId()); + spouse.changeStatus(campaign, campaign.getLocalDate(), PersonnelStatus.ON_LEAVE); + } - for (Person child : person.getGenealogy().getChildren()) { - if ((child.getStatus().isActive()) && (child.isChild(campaign.getLocalDate()))) { - person.addEduTagAlong(child.getId()); - child.changeStatus(campaign, campaign.getLocalDate(), PersonnelStatus.ON_LEAVE); + hasActiveParent = spouse.getStatus().isActive(); + } + + if (!hasActiveParent) { + for (Person child : children) { + if (child.getStatus().isActive()) { + if (child.isChild(campaign.getLocalDate())) { + person.addEduTagAlong(child.getId()); + child.changeStatus(campaign, campaign.getLocalDate(), PersonnelStatus.ON_LEAVE); + } } } } diff --git a/MekHQ/src/mekhq/campaign/personnel/procreation/AbstractProcreation.java b/MekHQ/src/mekhq/campaign/personnel/procreation/AbstractProcreation.java index f7789ef3aa..9ef0d7b5cd 100644 --- a/MekHQ/src/mekhq/campaign/personnel/procreation/AbstractProcreation.java +++ b/MekHQ/src/mekhq/campaign/personnel/procreation/AbstractProcreation.java @@ -33,7 +33,6 @@ import mekhq.campaign.log.PersonalLogger; import mekhq.campaign.personnel.Person; import mekhq.campaign.personnel.PersonnelOptions; -import mekhq.campaign.personnel.education.EducationController; import mekhq.campaign.personnel.enums.*; import mekhq.campaign.personnel.enums.education.EducationLevel; import mekhq.campaign.universe.Faction; @@ -371,9 +370,7 @@ public void birth(final Campaign campaign, final LocalDate today, final Person m campaign.recruitPerson(baby, prisonerStatus, true, true); // if the mother is at school, add the baby to the list of tag alongs - if ((mother.getEduAcademyName() != null) - && (!EducationController.getAcademy(mother.getEduAcademySet(), mother.getEduAcademyNameInSet()).isHomeSchool())) { - + if (mother.getStatus().isStudent()) { mother.addEduTagAlong(baby.getId()); baby.changeStatus(campaign, today, PersonnelStatus.ON_LEAVE); } diff --git a/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java b/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java index 2d6914f714..64bacbf937 100644 --- a/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java +++ b/MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java @@ -1061,8 +1061,9 @@ public static void deployForceToCoords(StratconCoords coords, int forceID, Campa */ private static @Nullable StratconCoords getUnoccupiedAdjacentCoords(StratconCoords originCoords, StratconTrackState trackState) { - final int trackWidth = trackState.getWidth(); - final int trackHeight = trackState.getHeight(); + // We need to reduce width/height by one because coordinates index from 0, not 1 + final int trackWidth = trackState.getWidth() - 1; + final int trackHeight = trackState.getHeight() - 1; List suitableCoords = new ArrayList<>(); for (int direction : ALL_DIRECTIONS) { diff --git a/MekHQ/src/mekhq/gui/BriefingTab.java b/MekHQ/src/mekhq/gui/BriefingTab.java index f896d3b690..165241e711 100644 --- a/MekHQ/src/mekhq/gui/BriefingTab.java +++ b/MekHQ/src/mekhq/gui/BriefingTab.java @@ -46,6 +46,7 @@ import mekhq.campaign.personnel.autoAwards.AutoAwardsController; import mekhq.campaign.personnel.enums.PersonnelRole; import mekhq.campaign.personnel.enums.PersonnelStatus; +import mekhq.campaign.stratcon.StratconScenario; import mekhq.campaign.unit.Unit; import mekhq.campaign.universe.Faction; import mekhq.campaign.universe.Factions; @@ -676,13 +677,27 @@ private void addScenario() { } private void clearAssignedUnits() { - if (0 == JOptionPane.showConfirmDialog(null, "Do you really want to remove all units from this scenario?", + if (0 == JOptionPane.showConfirmDialog(null, + "Do you really want to remove all units from this scenario?", "Clear Units?", JOptionPane.YES_NO_OPTION)) { int row = scenarioTable.getSelectedRow(); Scenario scenario = scenarioModel.getScenario(scenarioTable.convertRowIndexToModel(row)); - if (null == scenario) { + + if (scenario == null) { return; } + + // This handles StratCon undeployment + if (scenario instanceof AtBScenario) { + StratconScenario stratConScenario = ((AtBScenario) scenario).getStratconScenario(getCampaign()); + + if (stratConScenario != null) { + stratConScenario.resetScenario(getCampaign()); + return; + } + } + + // This handles Legacy AtB undeployment scenario.clearAllForcesAndPersonnel(getCampaign()); } } @@ -1282,11 +1297,19 @@ public void refreshScenarioView() { SwingUtilities.invokeLater(() -> scrollScenarioView.getVerticalScrollBar().setValue(0)); final boolean canStartGame = scenario.canStartScenario(getCampaign()); + btnStartGame.setEnabled(canStartGame); btnJoinGame.setEnabled(canStartGame); btnLoadGame.setEnabled(canStartGame); btnGetMul.setEnabled(canStartGame); - btnClearAssignedUnits.setEnabled(canStartGame); + + final boolean hasTrack = scenario.getHasTrack(); + if (hasTrack) { + btnClearAssignedUnits.setEnabled(canStartGame && getCampaign().isGM()); + } else { + btnClearAssignedUnits.setEnabled(canStartGame); + } + btnResolveScenario.setEnabled(canStartGame); if (scenario instanceof AtBScenario) { btnAutoResolveScenario.setEnabled(canStartGame); diff --git a/MekHQ/src/mekhq/gui/model/ScenarioTableModel.java b/MekHQ/src/mekhq/gui/model/ScenarioTableModel.java index 71c6517f51..6daf8ddf4b 100644 --- a/MekHQ/src/mekhq/gui/model/ScenarioTableModel.java +++ b/MekHQ/src/mekhq/gui/model/ScenarioTableModel.java @@ -25,6 +25,7 @@ import mekhq.campaign.mission.Scenario; import mekhq.campaign.mission.enums.ScenarioStatus; import mekhq.campaign.stratcon.StratconCampaignState; +import mekhq.campaign.stratcon.StratconCoords; import mekhq.campaign.stratcon.StratconScenario; import mekhq.campaign.stratcon.StratconTrackState; import mekhq.gui.utilities.MekHqTableCellRenderer; @@ -129,7 +130,13 @@ public Object getValueAt(int row, int col) { for (StratconTrackState track : campaignState.getTracks()) { for (StratconScenario stratconScenario : track.getScenarios().values()) { if (Objects.equals(stratconScenario.getBackingScenario(), scenario)) { - return track.getDisplayableName(); + StratconCoords coords = stratconScenario.getCoords(); + + if (coords == null) { + return track.getDisplayableName(); + } else { + return track.getDisplayableName() + '-' + coords.toBTString(); + } } } } diff --git a/MekHQ/unittests/mekhq/campaign/autoresolve/ResolverTest.java b/MekHQ/unittests/mekhq/campaign/autoresolve/ResolverTest.java index 5933f0484a..3aabde9a8d 100644 --- a/MekHQ/unittests/mekhq/campaign/autoresolve/ResolverTest.java +++ b/MekHQ/unittests/mekhq/campaign/autoresolve/ResolverTest.java @@ -23,6 +23,7 @@ import megamek.common.*; import megamek.common.autoresolve.Resolver; import megamek.common.autoresolve.acar.SimulationOptions; +import megamek.common.autoresolve.converter.FlattenForces; import megamek.common.autoresolve.event.AutoResolveConcludedEvent; import megamek.common.enums.Gender; import megamek.common.enums.SkillLevel; @@ -48,7 +49,6 @@ import org.junit.jupiter.api.*; import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -191,16 +191,14 @@ AtBScenario createScenario(Campaign campaign) { Campaign createCampaign() { var campaign = new Campaign(); - + campaign.setName("Test Player"); var reputationController = mock(ReputationController.class); when(reputationController.getAverageSkillLevel()).thenReturn(SkillLevel.REGULAR); - campaign.setReputation(reputationController); - var force = new Force("Heroes"); - campaign.addForce(force, campaign.getForce(0)); + campaign.addForce(force, campaign.getForce(0)); return campaign; } @@ -354,7 +352,7 @@ void autoResolve(Consumer autoResolveConcludedEvent) when(botForce.getTeam()).thenReturn(2); when(botForce.getFullEntityList(any())).thenReturn(entities); - var resolver = Resolver.simulationRun(new AtBSetupForces(campaign, units, scenario), SimulationOptions.empty(), new Board(30, 30)); + var resolver = Resolver.simulationRun(new AtBSetupForces(campaign, units, scenario, new FlattenForces()), SimulationOptions.empty(), new Board(30, 30)); autoResolveConcludedEvent.accept(resolver.resolveSimulation()); }