From 24f6aff8c89d6f74b976e681293393ce228c5e45 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 13 Jul 2021 15:20:42 -0700 Subject: [PATCH] Reduce mission planning dependence on Game. --- game/coalition.py | 15 +- game/commander/objectivefinder.py | 2 +- .../tasks/compound/protectairspace.py | 2 +- game/commander/tasks/primitive/barcap.py | 15 +- game/commander/theaterstate.py | 15 +- game/theater/theatergroundobject.py | 12 +- game/transfers.py | 4 +- gen/flights/ai_flight_planner.py | 233 +++++++++--------- gen/flights/flightplan.py | 82 +++--- gen/flights/waypointbuilder.py | 16 +- qt_ui/windows/mission/QPackageDialog.py | 2 +- .../flight/settings/FlightAirfieldDisplay.py | 2 +- .../flight/waypoints/QFlightWaypointTab.py | 2 +- 13 files changed, 212 insertions(+), 190 deletions(-) diff --git a/game/coalition.py b/game/coalition.py index f7a97ecf..06432c97 100644 --- a/game/coalition.py +++ b/game/coalition.py @@ -11,7 +11,7 @@ from game.navmesh import NavMesh from game.profiling import logged_duration, MultiEventTracer from game.threatzones import ThreatZones from game.transfers import PendingTransfers -from gen.flights.ai_flight_planner import CoalitionMissionPlanner +from gen.flights.ai_flight_planner import CoalitionMissionPlanner, MissionScheduler if TYPE_CHECKING: from game import Game @@ -181,13 +181,20 @@ class Coalition: def plan_missions(self) -> None: color = "Blue" if self.player else "Red" with MultiEventTracer() as tracer: - mission_planner = CoalitionMissionPlanner(self.game, self.player) + mission_planner = CoalitionMissionPlanner( + self, + self.game.theater, + self.game.aircraft_inventory, + self.game.settings, + ) with tracer.trace(f"{color} mission planning"): with tracer.trace(f"{color} mission identification"): commander = TheaterCommander(self.game, self.player) commander.plan_missions(mission_planner, tracer) - with tracer.trace(f"{color} mission fulfillment"): - mission_planner.fulfill_missions() + with tracer.trace(f"{color} mission scheduling"): + MissionScheduler( + self, self.game.settings.desired_player_mission_duration + ).schedule_missions() def plan_procurement(self) -> None: # The first turn needs to buy a *lot* of aircraft to fill CAPs, so it gets much diff --git a/game/commander/objectivefinder.py b/game/commander/objectivefinder.py index 15e47ed0..cf5c6102 100644 --- a/game/commander/objectivefinder.py +++ b/game/commander/objectivefinder.py @@ -18,12 +18,12 @@ from game.theater.theatergroundobject import ( IadsGroundObject, NavalGroundObject, ) -from game.transfers import CargoShip, Convoy from game.utils import meters, nautical_miles from gen.flights.closestairfields import ObjectiveDistanceCache, ClosestAirfields if TYPE_CHECKING: from game import Game + from game.transfers import CargoShip, Convoy MissionTargetType = TypeVar("MissionTargetType", bound=MissionTarget) diff --git a/game/commander/tasks/compound/protectairspace.py b/game/commander/tasks/compound/protectairspace.py index 9e3c0d56..5a13e486 100644 --- a/game/commander/tasks/compound/protectairspace.py +++ b/game/commander/tasks/compound/protectairspace.py @@ -8,4 +8,4 @@ from game.htn import CompoundTask, Method class ProtectAirSpace(CompoundTask[TheaterState]): def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]: for cp in state.vulnerable_control_points: - yield [PlanBarcap(cp)] + yield [PlanBarcap(cp, state.barcap_rounds)] diff --git a/game/commander/tasks/primitive/barcap.py b/game/commander/tasks/primitive/barcap.py index 9707445c..8fd86fab 100644 --- a/game/commander/tasks/primitive/barcap.py +++ b/game/commander/tasks/primitive/barcap.py @@ -17,6 +17,7 @@ if TYPE_CHECKING: @dataclass class PlanBarcap(TheaterCommanderTask): target: ControlPoint + rounds: int def preconditions_met(self, state: TheaterState) -> bool: if state.player and not state.ato_automation_enabled: @@ -29,19 +30,7 @@ class PlanBarcap(TheaterCommanderTask): def execute( self, mission_planner: CoalitionMissionPlanner, tracer: MultiEventTracer ) -> None: - # Plan enough rounds of CAP that the target has coverage over the expected - # mission duration. - mission_duration = int( - mission_planner.game.settings.desired_player_mission_duration.total_seconds() - ) - barcap_duration = int( - mission_planner.faction.doctrine.cap_duration.total_seconds() - ) - for _ in range( - 0, - mission_duration, - barcap_duration, - ): + for _ in range(self.rounds): mission_planner.plan_mission( ProposedMission( self.target, diff --git a/game/commander/theaterstate.py b/game/commander/theaterstate.py index f737a2aa..6da5f9f2 100644 --- a/game/commander/theaterstate.py +++ b/game/commander/theaterstate.py @@ -2,6 +2,7 @@ from __future__ import annotations import dataclasses import itertools +import math from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Union, Optional @@ -18,11 +19,11 @@ from game.theater.theatergroundobject import ( VehicleGroupGroundObject, ) from game.threatzones import ThreatZones -from game.transfers import Convoy, CargoShip from gen.ground_forces.combat_stance import CombatStance if TYPE_CHECKING: from game import Game + from game.transfers import Convoy, CargoShip @dataclass @@ -30,6 +31,7 @@ class TheaterState(WorldState["TheaterState"]): player: bool stance_automation_enabled: bool ato_automation_enabled: bool + barcap_rounds: int vulnerable_control_points: list[ControlPoint] active_front_lines: list[FrontLine] front_line_stances: dict[FrontLine, Optional[CombatStance]] @@ -86,6 +88,7 @@ class TheaterState(WorldState["TheaterState"]): player=self.player, stance_automation_enabled=self.stance_automation_enabled, ato_automation_enabled=self.ato_automation_enabled, + barcap_rounds=self.barcap_rounds, vulnerable_control_points=list(self.vulnerable_control_points), active_front_lines=list(self.active_front_lines), front_line_stances=dict(self.front_line_stances), @@ -120,10 +123,20 @@ class TheaterState(WorldState["TheaterState"]): auto_stance = game.settings.automate_front_line_stance auto_ato = game.settings.auto_ato_behavior is not AutoAtoBehavior.Disabled ordered_capturable_points = finder.prioritized_unisolated_points() + + # Plan enough rounds of CAP that the target has coverage over the expected + # mission duration. + mission_duration = game.settings.desired_player_mission_duration.total_seconds() + barcap_duration = game.coalition_for( + player + ).doctrine.cap_duration.total_seconds() + barcap_rounds = math.ceil(mission_duration / barcap_duration) + return TheaterState( player=player, stance_automation_enabled=auto_stance, ato_automation_enabled=auto_ato, + barcap_rounds=barcap_rounds, vulnerable_control_points=list(finder.vulnerable_control_points()), active_front_lines=list(finder.front_lines()), front_line_stances={f: None for f in finder.front_lines()}, diff --git a/game/theater/theatergroundobject.py b/game/theater/theatergroundobject.py index fb6f015f..180dd352 100644 --- a/game/theater/theatergroundobject.py +++ b/game/theater/theatergroundobject.py @@ -4,7 +4,7 @@ import itertools import logging from abc import ABC from collections import Sequence -from typing import Iterator, List, TYPE_CHECKING, Union, Generic, TypeVar, Any +from typing import Iterator, List, TYPE_CHECKING, Union, Generic, TypeVar from dcs.mapping import Point from dcs.triggers import TriggerZone @@ -257,13 +257,17 @@ class BuildingGroundObject(TheaterGroundObject[VehicleGroup]): def kill(self) -> None: self._dead = True - def iter_building_group(self) -> Iterator[TheaterGroundObject[Any]]: + def iter_building_group(self) -> Iterator[BuildingGroundObject]: for tgo in self.control_point.ground_objects: - if tgo.obj_name == self.obj_name and not tgo.is_dead: + if ( + tgo.obj_name == self.obj_name + and not tgo.is_dead + and isinstance(tgo, BuildingGroundObject) + ): yield tgo @property - def strike_targets(self) -> List[Union[MissionTarget, Unit]]: + def strike_targets(self) -> List[BuildingGroundObject]: return list(self.iter_building_group()) @property diff --git a/game/transfers.py b/game/transfers.py index 3bdc9b3c..7401b03d 100644 --- a/game/transfers.py +++ b/game/transfers.py @@ -340,7 +340,9 @@ class AirliftPlanner: transfer.transport = transport self.package.add_flight(flight) - planner = FlightPlanBuilder(self.game, self.package, self.for_player) + planner = FlightPlanBuilder( + self.package, self.game.coalition_for(self.for_player), self.game.theater + ) planner.populate_flight_plan(flight) self.game.aircraft_inventory.claim_for_flight(flight) return flight_size diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index 7c9e3e22..e74ce0af 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -14,26 +14,26 @@ from typing import ( Tuple, ) -from game.commander import TheaterCommander from game.commander.missionproposals import ProposedFlight, ProposedMission, EscortType -from game.commander.objectivefinder import ObjectiveFinder from game.data.doctrine import Doctrine from game.dcs.aircrafttype import AircraftType -from game.factions.faction import Faction -from game.infos.information import Information from game.procurement import AircraftProcurementRequest -from game.profiling import logged_duration, MultiEventTracer +from game.profiling import MultiEventTracer +from game.settings import Settings from game.squadrons import AirWing, Squadron from game.theater import ( ControlPoint, MissionTarget, OffMapSpawn, + ConflictTheater, ) +from game.threatzones import ThreatZones from game.utils import nautical_miles -from gen.ato import Package +from gen.ato import Package, AirTaskingOrder from gen.flights.ai_flight_planner_db import aircraft_for_task from gen.flights.closestairfields import ( ClosestAirfields, + ObjectiveDistanceCache, ) from gen.flights.flight import ( Flight, @@ -44,7 +44,7 @@ from gen.flights.traveltime import TotEstimator # Avoid importing some types that cause circular imports unless type checking. if TYPE_CHECKING: - from game import Game + from game.coalition import Coalition from game.inventory import GlobalAircraftInventory @@ -201,6 +201,68 @@ class PackageBuilder: self.package.remove_flight(flight) +class MissionScheduler: + def __init__(self, coalition: Coalition, desired_mission_length: timedelta) -> None: + self.coalition = coalition + self.desired_mission_length = desired_mission_length + + def schedule_missions(self) -> None: + """Identifies and plans mission for the turn.""" + + def start_time_generator( + count: int, earliest: int, latest: int, margin: int + ) -> Iterator[timedelta]: + interval = (latest - earliest) // count + for time in range(earliest, latest, interval): + error = random.randint(-margin, margin) + yield timedelta(seconds=max(0, time + error)) + + dca_types = { + FlightType.BARCAP, + FlightType.TARCAP, + } + + previous_cap_end_time: Dict[MissionTarget, timedelta] = defaultdict(timedelta) + non_dca_packages = [ + p for p in self.coalition.ato.packages if p.primary_task not in dca_types + ] + + start_time = start_time_generator( + count=len(non_dca_packages), + earliest=5 * 60, + latest=int(self.desired_mission_length.total_seconds()), + margin=5 * 60, + ) + for package in self.coalition.ato.packages: + tot = TotEstimator(package).earliest_tot() + if package.primary_task in dca_types: + previous_end_time = previous_cap_end_time[package.target] + if tot > previous_end_time: + # Can't get there exactly on time, so get there ASAP. This + # will typically only happen for the first CAP at each + # target. + package.time_over_target = tot + else: + package.time_over_target = previous_end_time + + departure_time = package.mission_departure_time + # Should be impossible for CAPs + if departure_time is None: + logging.error(f"Could not determine mission end time for {package}") + continue + previous_cap_end_time[package.target] = departure_time + elif package.auto_asap: + package.set_tot_asap() + else: + # But other packages should be spread out a bit. Note that take + # times are delayed, but all aircraft will become active at + # mission start. This makes it more worthwhile to attack enemy + # airfields to hit grounded aircraft, since they're more likely + # to be present. Runway and air started aircraft will be + # delayed until their takeoff time by AirConflictGenerator. + package.time_over_target = next(start_time) + tot + + class CoalitionMissionPlanner: """Coalition flight planning AI. @@ -224,18 +286,46 @@ class CoalitionMissionPlanner: TODO: Stance and doctrine-specific planning behavior. """ - def __init__(self, game: Game, is_player: bool) -> None: - self.game = game - self.is_player = is_player - self.objective_finder = ObjectiveFinder(self.game, self.is_player) - self.ato = self.game.coalition_for(is_player).ato - self.threat_zones = self.game.threat_zone_for(not self.is_player) - self.procurement_requests = self.game.procurement_requests_for(self.is_player) - self.faction: Faction = self.game.faction_for(self.is_player) + def __init__( + self, + coalition: Coalition, + theater: ConflictTheater, + aircraft_inventory: GlobalAircraftInventory, + settings: Settings, + ) -> None: + self.coalition = coalition + self.theater = theater + self.aircraft_inventory = aircraft_inventory + self.player_missions_asap = settings.auto_ato_player_missions_asap + self.default_start_type = settings.default_start_type + + @property + def is_player(self) -> bool: + return self.coalition.player + + @property + def ato(self) -> AirTaskingOrder: + return self.coalition.ato + + @property + def air_wing(self) -> AirWing: + return self.coalition.air_wing @property def doctrine(self) -> Doctrine: - return self.faction.doctrine + return self.coalition.doctrine + + @property + def threat_zones(self) -> ThreatZones: + return self.coalition.opponent.threat_zone + + def add_procurement_request( + self, request: AircraftProcurementRequest, priority: bool + ) -> None: + if priority: + self.coalition.procurement_requests.insert(0, request) + else: + self.coalition.procurement_requests.append(request) def air_wing_can_plan(self, mission_type: FlightType) -> bool: """Returns True if it is possible for the air wing to plan this mission type. @@ -245,21 +335,7 @@ class CoalitionMissionPlanner: also possible for the player to exclude mission types from their squadron designs. """ - return self.game.air_wing_for(self.is_player).can_auto_plan(mission_type) - - def fulfill_missions(self) -> None: - """Identifies and plans mission for the turn.""" - player = "Blue" if self.is_player else "Red" - with logged_duration(f"{player} mission scheduling"): - self.stagger_missions() - - for cp in self.objective_finder.friendly_control_points(): - inventory = self.game.aircraft_inventory.for_control_point(cp) - for aircraft, available in inventory.all_aircraft: - self.message("Unused aircraft", f"{available} {aircraft} from {cp}") - - coalition_text = "player" if self.is_player else "opfor" - logging.debug(f"Planned {len(self.ato.packages)} {coalition_text} missions") + return self.air_wing.can_auto_plan(mission_type) def plan_flight( self, @@ -277,12 +353,9 @@ class CoalitionMissionPlanner: task_capability=flight.task, number=flight.num_aircraft, ) - if for_reserves: - # Reserves are planned for critical missions, so prioritize - # those orders over aircraft needed for non-critical missions. - self.procurement_requests.insert(0, purchase_order) - else: - self.procurement_requests.append(purchase_order) + # Reserves are planned for critical missions, so prioritize those orders + # over aircraft needed for non-critical missions. + self.add_procurement_request(purchase_order, priority=for_reserves) def scrub_mission_missing_aircraft( self, @@ -300,10 +373,9 @@ class CoalitionMissionPlanner: missing_types_str = ", ".join(sorted([t.name for t in missing_types])) builder.release_planned_aircraft() desc = "reserve aircraft" if reserves else "aircraft" - self.message( - "Insufficient aircraft", + logging.debug( f"Not enough {desc} in range for {mission.location.name} " - f"capable of: {missing_types_str}", + f"capable of: {missing_types_str}" ) def check_needed_escorts(self, builder: PackageBuilder) -> Dict[EscortType, bool]: @@ -325,12 +397,12 @@ class CoalitionMissionPlanner: """Allocates aircraft for a proposed mission and adds it to the ATO.""" builder = PackageBuilder( mission.location, - self.objective_finder.closest_airfields_to(mission.location), - self.game.aircraft_inventory, - self.game.air_wing_for(self.is_player), + ObjectiveDistanceCache.get_closest_airfields(mission.location), + self.aircraft_inventory, + self.air_wing, self.is_player, - self.game.country_for(self.is_player), - self.game.settings.default_start_type, + self.coalition.country_name, + self.default_start_type, mission.asap, ) @@ -374,7 +446,7 @@ class CoalitionMissionPlanner: # the other flights in the package. Escorts will not be able to # contribute to this. flight_plan_builder = FlightPlanBuilder( - self.game, builder.package, self.is_player + builder.package, self.coalition, self.theater ) for flight in builder.package.flights: with tracer.trace("Flight plan population"): @@ -410,75 +482,8 @@ class CoalitionMissionPlanner: with tracer.trace("Flight plan population"): flight_plan_builder.populate_flight_plan(flight) - if package.has_players and self.game.settings.auto_ato_player_missions_asap: + if package.has_players and self.player_missions_asap: package.auto_asap = True package.set_tot_asap() self.ato.add_package(package) - - def stagger_missions(self) -> None: - def start_time_generator( - count: int, earliest: int, latest: int, margin: int - ) -> Iterator[timedelta]: - interval = (latest - earliest) // count - for time in range(earliest, latest, interval): - error = random.randint(-margin, margin) - yield timedelta(seconds=max(0, time + error)) - - dca_types = { - FlightType.BARCAP, - FlightType.TARCAP, - } - - previous_cap_end_time: Dict[MissionTarget, timedelta] = defaultdict(timedelta) - non_dca_packages = [ - p for p in self.ato.packages if p.primary_task not in dca_types - ] - - start_time = start_time_generator( - count=len(non_dca_packages), - earliest=5 * 60, - latest=int( - self.game.settings.desired_player_mission_duration.total_seconds() - ), - margin=5 * 60, - ) - for package in self.ato.packages: - tot = TotEstimator(package).earliest_tot() - if package.primary_task in dca_types: - previous_end_time = previous_cap_end_time[package.target] - if tot > previous_end_time: - # Can't get there exactly on time, so get there ASAP. This - # will typically only happen for the first CAP at each - # target. - package.time_over_target = tot - else: - package.time_over_target = previous_end_time - - departure_time = package.mission_departure_time - # Should be impossible for CAPs - if departure_time is None: - logging.error(f"Could not determine mission end time for {package}") - continue - previous_cap_end_time[package.target] = departure_time - elif package.auto_asap: - package.set_tot_asap() - else: - # But other packages should be spread out a bit. Note that take - # times are delayed, but all aircraft will become active at - # mission start. This makes it more worthwhile to attack enemy - # airfields to hit grounded aircraft, since they're more likely - # to be present. Runway and air started aircraft will be - # delayed until their takeoff time by AirConflictGenerator. - package.time_over_target = next(start_time) + tot - - def message(self, title: str, text: str) -> None: - """Emits a planning message to the player. - - If the mission planner belongs to the players coalition, this emits a - message to the info panel. - """ - if self.is_player: - self.game.informations.append(Information(title, text, self.game.turn)) - else: - logging.info(f"{title}: {text}") diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index 248846d6..a93e6f39 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -28,8 +28,14 @@ from game.theater import ( SamGroundObject, TheaterGroundObject, NavalControlPoint, + ConflictTheater, ) -from game.theater.theatergroundobject import EwrGroundObject, NavalGroundObject +from game.theater.theatergroundobject import ( + EwrGroundObject, + NavalGroundObject, + BuildingGroundObject, +) +from game.threatzones import ThreatZones from game.utils import Distance, Speed, feet, meters, nautical_miles, knots from .closestairfields import ObjectiveDistanceCache from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType @@ -38,8 +44,8 @@ from .waypointbuilder import StrikeTarget, WaypointBuilder from ..conflictgen import Conflict, FRONTLINE_LENGTH if TYPE_CHECKING: - from game import Game from gen.ato import Package + from game.coalition import Coalition from game.transfers import Convoy INGRESS_TYPES = { @@ -864,7 +870,9 @@ class CustomFlightPlan(FlightPlan): class FlightPlanBuilder: """Generates flight plans for flights.""" - def __init__(self, game: Game, package: Package, is_player: bool) -> None: + def __init__( + self, package: Package, coalition: Coalition, theater: ConflictTheater + ) -> None: # TODO: Plan similar altitudes for the in-country leg of the mission. # Waypoint altitudes for a given flight *shouldn't* differ too much # between the join and split points, so we don't need speeds for each @@ -872,11 +880,21 @@ class FlightPlanBuilder: # hold too well right now since nothing is stopping each waypoint from # jumping 20k feet each time, but that's a huge waste of energy we # should be avoiding anyway. - self.game = game self.package = package - self.is_player = is_player - self.doctrine: Doctrine = self.game.faction_for(self.is_player).doctrine - self.threat_zones = self.game.threat_zone_for(not self.is_player) + self.coalition = coalition + self.theater = theater + + @property + def is_player(self) -> bool: + return self.coalition.player + + @property + def doctrine(self) -> Doctrine: + return self.coalition.doctrine + + @property + def threat_zones(self) -> ThreatZones: + return self.coalition.opponent.threat_zone def populate_flight_plan( self, @@ -1022,7 +1040,7 @@ class FlightPlanBuilder: ) def preferred_join_point(self) -> Optional[Point]: - path = self.game.navmesh_for(self.is_player).shortest_path( + path = self.coalition.nav_mesh.shortest_path( self.package_airfield().position, self.package.target.position ) for point in reversed(path): @@ -1043,26 +1061,16 @@ class FlightPlanBuilder: raise InvalidObjectiveLocation(flight.flight_type, location) targets: List[StrikeTarget] = [] - if len(location.groups) > 0 and location.dcs_identifier == "AA": + if isinstance(location, BuildingGroundObject): + # A building "group" is implemented as multiple TGOs with the same name. + for building in location.strike_targets: + targets.append(StrikeTarget(building.category, building)) + else: # TODO: Replace with DEAD? # Strike missions on SEAD targets target units. for g in location.groups: for j, u in enumerate(g.units): targets.append(StrikeTarget(f"{u.type} #{j}", u)) - else: - # TODO: Does this actually happen? - # ConflictTheater is built with the belief that multiple ground - # objects have the same name. If that's the case, - # TheaterGroundObject needs some refactoring because it behaves very - # differently for SAM sites than it does for strike targets. - buildings = self.game.theater.find_ground_objects_by_obj_name( - location.obj_name - ) - for building in buildings: - if building.is_dead: - continue - - targets.append(StrikeTarget(building.category, building)) return self.strike_flightplan( flight, location, FlightWaypointType.INGRESS_STRIKE, targets @@ -1083,7 +1091,7 @@ class FlightPlanBuilder: else: patrol_alt = feet(25000) - builder = WaypointBuilder(flight, self.game, self.is_player) + builder = WaypointBuilder(flight, self.coalition) orbit = builder.orbit(orbit_location, patrol_alt) return AwacsFlightPlan( @@ -1175,7 +1183,7 @@ class FlightPlanBuilder: ) ) - builder = WaypointBuilder(flight, self.game, self.is_player) + builder = WaypointBuilder(flight, self.coalition) start, end = builder.race_track(start_pos, end_pos, patrol_alt) return BarCapFlightPlan( @@ -1211,7 +1219,7 @@ class FlightPlanBuilder: heading, -self.doctrine.sweep_distance.meters ) - builder = WaypointBuilder(flight, self.game, self.is_player) + builder = WaypointBuilder(flight, self.coalition) start, end = builder.sweep(start_pos, target, self.doctrine.ingress_altitude) hold = builder.hold(self._hold_point(flight)) @@ -1251,7 +1259,7 @@ class FlightPlanBuilder: altitude = feet(1500) altitude_is_agl = True - builder = WaypointBuilder(flight, self.game, self.is_player) + builder = WaypointBuilder(flight, self.coalition) pickup = None nav_to_pickup = [] @@ -1373,9 +1381,7 @@ class FlightPlanBuilder: self, origin: Point, front_line: FrontLine ) -> Tuple[Point, Point]: # Find targets waypoints - ingress, heading, distance = Conflict.frontline_vector( - front_line, self.game.theater - ) + ingress, heading, distance = Conflict.frontline_vector(front_line, self.theater) center = ingress.point_from_heading(heading, distance / 2) orbit_center = center.point_from_heading( heading - 90, @@ -1414,7 +1420,7 @@ class FlightPlanBuilder: ) # Create points - builder = WaypointBuilder(flight, self.game, self.is_player) + builder = WaypointBuilder(flight, self.coalition) if isinstance(location, FrontLine): orbit0p, orbit1p = self.racetrack_for_frontline( @@ -1545,7 +1551,7 @@ class FlightPlanBuilder: def generate_escort(self, flight: Flight) -> StrikeFlightPlan: assert self.package.waypoints is not None - builder = WaypointBuilder(flight, self.game, self.is_player) + builder = WaypointBuilder(flight, self.coalition) ingress, target, egress = builder.escort( self.package.waypoints.ingress, self.package.target, @@ -1588,9 +1594,7 @@ class FlightPlanBuilder: if not isinstance(location, FrontLine): raise InvalidObjectiveLocation(flight.flight_type, location) - ingress, heading, distance = Conflict.frontline_vector( - location, self.game.theater - ) + ingress, heading, distance = Conflict.frontline_vector(location, self.theater) center = ingress.point_from_heading(heading, distance / 2) egress = ingress.point_from_heading(heading, distance) @@ -1599,7 +1603,7 @@ class FlightPlanBuilder: if egress_distance < ingress_distance: ingress, egress = egress, ingress - builder = WaypointBuilder(flight, self.game, self.is_player) + builder = WaypointBuilder(flight, self.coalition) return CasFlightPlan( package=self.package, @@ -1655,7 +1659,7 @@ class FlightPlanBuilder: orbit_heading - 90, racetrack_half_distance ) - builder = WaypointBuilder(flight, self.game, self.is_player) + builder = WaypointBuilder(flight, self.coalition) tanker_type = flight.unit_type if tanker_type.patrol_altitude is not None: @@ -1776,7 +1780,7 @@ class FlightPlanBuilder: flight: The flight to generate the landing waypoint for. arrival: Arrival airfield or carrier. """ - builder = WaypointBuilder(flight, self.game, self.is_player) + builder = WaypointBuilder(flight, self.coalition) return builder.land(arrival) def strike_flightplan( @@ -1788,7 +1792,7 @@ class FlightPlanBuilder: lead_time: timedelta = timedelta(), ) -> StrikeFlightPlan: assert self.package.waypoints is not None - builder = WaypointBuilder(flight, self.game, self.is_player, targets) + builder = WaypointBuilder(flight, self.coalition, targets) target_waypoints: List[FlightWaypoint] = [] if targets is not None: diff --git a/gen/flights/waypointbuilder.py b/gen/flights/waypointbuilder.py index e911bebd..daaea056 100644 --- a/gen/flights/waypointbuilder.py +++ b/gen/flights/waypointbuilder.py @@ -15,10 +15,10 @@ from typing import ( from dcs.mapping import Point from dcs.unit import Unit -from dcs.unitgroup import Group, VehicleGroup, ShipGroup +from dcs.unitgroup import VehicleGroup, ShipGroup if TYPE_CHECKING: - from game import Game + from game.coalition import Coalition from game.transfers import MultiGroupTransport from game.theater import ( @@ -43,17 +43,15 @@ class WaypointBuilder: def __init__( self, flight: Flight, - game: Game, - player: bool, + coalition: Coalition, targets: Optional[List[StrikeTarget]] = None, ) -> None: self.flight = flight - self.conditions = game.conditions - self.doctrine = game.faction_for(player).doctrine - self.threat_zones = game.threat_zone_for(not player) - self.navmesh = game.navmesh_for(player) + self.doctrine = coalition.doctrine + self.threat_zones = coalition.opponent.threat_zone + self.navmesh = coalition.nav_mesh self.targets = targets - self._bullseye = game.bullseye_for(player) + self._bullseye = coalition.bullseye @property def is_helo(self) -> bool: diff --git a/qt_ui/windows/mission/QPackageDialog.py b/qt_ui/windows/mission/QPackageDialog.py index 19634847..c86987ae 100644 --- a/qt_ui/windows/mission/QPackageDialog.py +++ b/qt_ui/windows/mission/QPackageDialog.py @@ -180,7 +180,7 @@ class QPackageDialog(QDialog): self.game.aircraft_inventory.claim_for_flight(flight) self.package_model.add_flight(flight) planner = FlightPlanBuilder( - self.game, self.package_model.package, is_player=True + self.package_model.package, self.game.blue, self.game.theater ) try: planner.populate_flight_plan(flight) diff --git a/qt_ui/windows/mission/flight/settings/FlightAirfieldDisplay.py b/qt_ui/windows/mission/flight/settings/FlightAirfieldDisplay.py index 282df1ce..2cca2425 100644 --- a/qt_ui/windows/mission/flight/settings/FlightAirfieldDisplay.py +++ b/qt_ui/windows/mission/flight/settings/FlightAirfieldDisplay.py @@ -100,6 +100,6 @@ class FlightAirfieldDisplay(QGroupBox): def update_flight_plan(self) -> None: planner = FlightPlanBuilder( - self.game, self.package_model.package, is_player=True + self.package_model.package, self.game.blue, self.game.theater ) planner.populate_flight_plan(self.flight) diff --git a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py index 440d3f9b..bc4a7d51 100644 --- a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py +++ b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py @@ -37,7 +37,7 @@ class QFlightWaypointTab(QFrame): self.game = game self.package = package self.flight = flight - self.planner = FlightPlanBuilder(self.game, package, is_player=True) + self.planner = FlightPlanBuilder(package, game.blue, game.theater) self.flight_waypoint_list: Optional[QFlightWaypointList] = None self.rtb_waypoint: Optional[QPushButton] = None