From c0748e2a3e3584a185e02ab42a6cc938d0d4acdb Mon Sep 17 00:00:00 2001 From: Eclipse/Druss99 Date: Tue, 23 Sep 2025 19:05:38 -0400 Subject: [PATCH] add support for neutral FOBs --- game/campaignloader/mizcampaignloader.py | 19 +++++++++++++++ game/commander/objectivefinder.py | 24 +++++++++++++++++++ game/commander/theaterstate.py | 3 ++- game/game.py | 4 ++-- game/missiongenerator/luagenerator.py | 10 +++++--- game/missiongenerator/tgogenerator.py | 15 ++++++++---- .../windows/groundobject/QGroundObjectMenu.py | 4 ++-- 7 files changed, 67 insertions(+), 12 deletions(-) diff --git a/game/campaignloader/mizcampaignloader.py b/game/campaignloader/mizcampaignloader.py index d72a24f3..753dda8f 100644 --- a/game/campaignloader/mizcampaignloader.py +++ b/game/campaignloader/mizcampaignloader.py @@ -57,6 +57,7 @@ class MizCampaignLoader: FOB_UNIT_TYPE = Unarmed.SKP_11.id FARP_HELIPADS_TYPE = ["Invisible FARP", "SINGLE_HELIPAD", "FARP"] INVISIBLE_FOB_UNIT_TYPE = Unarmed.M_818.id + NEUTRAL_FOB_UNIT_TYPE = Unarmed.KrAZ6322.id OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id SHIP_UNIT_TYPE = USS_Arleigh_Burke_IIa.id @@ -180,6 +181,12 @@ class MizCampaignLoader: if group.units[0].type == self.INVISIBLE_FOB_UNIT_TYPE: yield group + @property + def neutral_fobs(self) -> Iterator[VehicleGroup]: + for group in self.red.vehicle_group: + if group.units[0].type == self.NEUTRAL_FOB_UNIT_TYPE: + yield group + @property def ships(self) -> Iterator[ShipGroup]: for group in self.red.ship_group: @@ -345,6 +352,18 @@ class MizCampaignLoader: control_point.captured_invert = fob.late_activation control_points[control_point.id] = control_point + for fob in self.neutral_fobs: + ctld_zones = self.get_ctld_zones(fob.name) + control_point = Fob( + str(fob.name), + fob.position, + self.theater, + starts_blue=Player.NEUTRAL, + ctld_zones=ctld_zones, + ) + control_point.captured_invert = fob.late_activation + control_points[control_point.id] = control_point + if self.cp_influence_zones: for cp in control_points.values(): for influence_radius in self.cp_influence_zones: diff --git a/game/commander/objectivefinder.py b/game/commander/objectivefinder.py index 674ffa75..7ab871f3 100644 --- a/game/commander/objectivefinder.py +++ b/game/commander/objectivefinder.py @@ -1,5 +1,6 @@ from __future__ import annotations +import itertools import math import operator from collections.abc import Iterable, Iterator @@ -278,6 +279,29 @@ class ObjectiveFinder: prioritized.extend(self._targets_by_range(isolated)) return prioritized + def air_assault_targets(self) -> list[ControlPoint]: + """Returns control points suitable for air assault missions, including neutral bases.""" + prioritized = [] + capturable_later = [] + isolated = [] + + combined_control_points = itertools.chain( + self.game.theater.control_points_for(self.is_player.opponent), + self.game.theater.control_points_for(Player.NEUTRAL), + ) + + for cp in combined_control_points: + if cp.is_isolated: + isolated.append(cp) + continue + if cp.has_active_frontline: + prioritized.append(cp) + else: + capturable_later.append(cp) + prioritized.extend(self._targets_by_range(capturable_later)) + prioritized.extend(self._targets_by_range(isolated)) + return prioritized + @staticmethod def closest_airfields_to(location: MissionTarget) -> ClosestAirfields: """Returns the closest airfields to the given location.""" diff --git a/game/commander/theaterstate.py b/game/commander/theaterstate.py index 4ee140f8..b3673e86 100644 --- a/game/commander/theaterstate.py +++ b/game/commander/theaterstate.py @@ -158,6 +158,7 @@ class TheaterState(WorldState["TheaterState"]): coalition = game.coalition_for(player) finder = ObjectiveFinder(game, player) ordered_capturable_points = finder.prioritized_points() + air_assault_capturable_points = finder.air_assault_targets() context = PersistentContext( game.db, @@ -177,7 +178,7 @@ class TheaterState(WorldState["TheaterState"]): battle_postitions: Dict[ControlPoint, BattlePositions] = { cp: BattlePositions.for_control_point(cp) - for cp in ordered_capturable_points + for cp in air_assault_capturable_points } vulnerable_control_points = [ diff --git a/game/game.py b/game/game.py index 877f82c3..b980f6e4 100644 --- a/game/game.py +++ b/game/game.py @@ -225,9 +225,9 @@ class Game: def neutral_country(self) -> Country: """Return the best fitting country that can be used as neutral faction in the generated mission""" countries_in_use = {self.red.faction.country, self.blue.faction.country} - if UnitedNationsPeacekeepers not in countries_in_use: + if UnitedNationsPeacekeepers() not in countries_in_use: return UnitedNationsPeacekeepers() - elif Switzerland.name not in countries_in_use: + elif Switzerland() not in countries_in_use: return Switzerland() else: return USAFAggressors() diff --git a/game/missiongenerator/luagenerator.py b/game/missiongenerator/luagenerator.py index 209a4b64..27b8cd1a 100644 --- a/game/missiongenerator/luagenerator.py +++ b/game/missiongenerator/luagenerator.py @@ -101,13 +101,15 @@ class LuaGenerator: for logistic_info in self.mission_data.logistics: if logistic_info.transport not in transports: transports.append(logistic_info.transport) - coalition_color = "blue" if logistic_info.blue else "red" + coalition_color = "blue" if logistic_info.blue.is_blue else "red" logistics_item = logistics_flights.add_item() logistics_item.add_data_array("pilot_names", logistic_info.pilot_names) logistics_item.add_key_value("pickup_zone", logistic_info.pickup_zone) logistics_item.add_key_value("drop_off_zone", logistic_info.drop_off_zone) logistics_item.add_key_value("target_zone", logistic_info.target_zone) - logistics_item.add_key_value("side", str(2 if logistic_info.blue else 1)) + logistics_item.add_key_value( + "side", str(2 if logistic_info.blue.is_blue else 1) + ) logistics_item.add_key_value("logistic_unit", logistic_info.logistic_unit) logistics_item.add_key_value( "aircraft_type", logistic_info.transport.dcs_id @@ -201,7 +203,9 @@ class LuaGenerator: # Should probably do the same with all the roles... but the script is already # tolerant of those being empty. for node in self.game.theater.iads_network.skynet_nodes(self.game): - coalition = iads_object.get_or_create_item("BLUE" if node.player else "RED") + coalition = iads_object.get_or_create_item( + "BLUE" if node.player.is_blue else "RED" + ) iads_type = coalition.get_or_create_item(node.iads_role.value) iads_element = iads_type.add_item() iads_element.add_key_value("dcsGroupName", node.dcs_name) diff --git a/game/missiongenerator/tgogenerator.py b/game/missiongenerator/tgogenerator.py index da7cb4ef..7442a64f 100644 --- a/game/missiongenerator/tgogenerator.py +++ b/game/missiongenerator/tgogenerator.py @@ -810,9 +810,12 @@ class HelipadGenerator: # capture triggers pad: BaseFARP neutral_country = self.m.country(self.game.neutral_country.name) - country = self.m.country( - self.game.coalition_for(self.cp.captured).faction.country.name - ) + if self.cp.captured is Player.NEUTRAL: + country = neutral_country + else: + country = self.m.country( + self.game.coalition_for(self.cp.captured).faction.country.name + ) name = f"{self.cp.name} {helipad_type} {i}" logging.info("Generating helipad static : " + name) @@ -1403,7 +1406,11 @@ class TgoGenerator: def generate(self) -> None: for cp in self.game.theater.controlpoints: - country = self.m.country(cp.coalition.faction.country.name) + # Use neutral country for neutral control points + if cp.captured is Player.NEUTRAL: + country = self.m.country(self.game.neutral_country.name) + else: + country = self.m.country(cp.coalition.faction.country.name) # Generate helipads helipad_gen = HelipadGenerator( diff --git a/qt_ui/windows/groundobject/QGroundObjectMenu.py b/qt_ui/windows/groundobject/QGroundObjectMenu.py index b2cdf683..5c6da3fb 100644 --- a/qt_ui/windows/groundobject/QGroundObjectMenu.py +++ b/qt_ui/windows/groundobject/QGroundObjectMenu.py @@ -305,12 +305,12 @@ class QGroundObjectMenu(QDialog): self.game.theater.iads_network.update_tgo(self.ground_object, events) if any( package.target == self.ground_object - for package in self.game.ato_for(player=False).packages + for package in self.game.ato_for(player=Player.RED).packages ): # Replan if the tgo was a target of the redfor coalition = self.ground_object.coalition self.game.initialize_turn( - events, for_red=coalition.player, for_blue=not coalition.player + events, for_red=coalition.player, for_blue=coalition.player.opponent ) EventStream.put_nowait(events) GameUpdateSignal.get_instance().updateGame(self.game)