def generate_frontline_cap(self, flight, ally_cp, enemy_cp): """ Generate a cap flight for the frontline between ally_cp and enemy cp in order to ensure air superiority and protect friendly CAP airbase :param flight: Flight to setup :param ally_cp: CP to protect :param enemy_cp: Enemy connected cp """ flight.flight_type = FlightType.CAP patrol_alt = random.randint(self.doctrine["PATROL_ALT_RANGE"][0], self.doctrine["PATROL_ALT_RANGE"][1]) # Find targets waypoints ingress, heading, distance = Conflict.frontline_vector( ally_cp, enemy_cp, self.game.theater) center = ingress.point_from_heading(heading, distance / 2) orbit_center = center.point_from_heading( heading - 90, random.randint(nm_to_meter(6), nm_to_meter(15))) combat_width = distance / 2 if combat_width > 500000: combat_width = 500000 if combat_width < 35000: combat_width = 35000 radius = combat_width * 1.25 orbit0p = orbit_center.point_from_heading(heading, radius) orbit1p = orbit_center.point_from_heading(heading + 180, radius) # Create points ascend = self.generate_ascend_point(flight.from_cp) flight.points.append(ascend) orbit0 = FlightWaypoint(orbit0p.x, orbit0p.y, patrol_alt) orbit0.name = "ORBIT 0" orbit0.description = "Standby between this point and the next one" orbit0.pretty_name = "Race-track start" orbit0.waypoint_type = FlightWaypointType.PATROL_TRACK flight.points.append(orbit0) orbit1 = FlightWaypoint(orbit1p.x, orbit1p.y, patrol_alt) orbit1.name = "ORBIT 1" orbit1.description = "Standby between this point and the previous one" orbit1.pretty_name = "Race-track end" orbit1.waypoint_type = FlightWaypointType.PATROL flight.points.append(orbit1) # Note : Targets of a PATROL TRACK waypoints are the points to be defended orbit0.targets.append(flight.from_cp) orbit0.targets.append(center) descend = self.generate_descend_point(flight.from_cp) flight.points.append(descend) rtb = self.generate_rtb_waypoint(flight.from_cp) flight.points.append(rtb)
def generate_frontline_cap(self, flight: Flight) -> FrontLineCapFlightPlan: """Generate a CAP flight plan for the given front line. Args: flight: The flight to generate the flight plan for. """ location = self.package.target if not isinstance(location, FrontLine): raise InvalidObjectiveLocation(flight.flight_type, location) ally_cp, enemy_cp = location.control_points patrol_alt = random.randint(self.doctrine.min_patrol_altitude, self.doctrine.max_patrol_altitude) # Find targets waypoints ingress, heading, distance = Conflict.frontline_vector( ally_cp, enemy_cp, self.game.theater) center = ingress.point_from_heading(heading, distance / 2) orbit_center = center.point_from_heading( heading - 90, random.randint(nm_to_meter(6), nm_to_meter(15))) combat_width = distance / 2 if combat_width > 500000: combat_width = 500000 if combat_width < 35000: combat_width = 35000 radius = combat_width * 1.25 orbit0p = orbit_center.point_from_heading(heading, radius) orbit1p = orbit_center.point_from_heading(heading + 180, radius) # Create points builder = WaypointBuilder(self.game.conditions, flight, self.doctrine) start, end = builder.race_track(orbit0p, orbit1p, patrol_alt) descent, land = builder.rtb(flight.from_cp) return FrontLineCapFlightPlan( package=self.package, flight=flight, # Note that this duration only has an effect if there are no # flights in the package that have requested escort. If the package # requests an escort the CAP flight will remain on station for the # duration of the escorted mission, or until it is winchester/bingo. patrol_duration=self.doctrine.cap_duration, takeoff=builder.takeoff(flight.from_cp), ascent=builder.ascent(flight.from_cp), patrol_start=start, patrol_end=end, descent=descent, land=land)
def setGame(self, game: Optional[Game]): self.game = game if self.game is not None: logging.debug("Reloading Map Canvas") self.nm_to_pixel_ratio = self.km_to_pixel( float(nm_to_meter(1)) / 1000.0) self.reload_scene()
def generate_barcap(self, flight: Flight) -> BarCapFlightPlan: """Generate a BARCAP flight at a given location. Args: flight: The flight to generate the flight plan for. """ location = self.package.target if isinstance(location, FrontLine): raise InvalidObjectiveLocation(flight.flight_type, location) patrol_alt = random.randint(self.doctrine.min_patrol_altitude, self.doctrine.max_patrol_altitude) closest_cache = ObjectiveDistanceCache.get_closest_airfields(location) for airfield in closest_cache.closest_airfields: # If the mission is a BARCAP of an enemy airfield, find the *next* # closest enemy airfield. if airfield == self.package.target: continue if airfield.captured != self.is_player: closest_airfield = airfield break else: raise PlanningError("Could not find any enemy airfields") heading = location.position.heading_between_point( closest_airfield.position) min_distance_from_enemy = nm_to_meter(20) distance_to_airfield = int( closest_airfield.position.distance_to_point( self.package.target.position)) distance_to_no_fly = distance_to_airfield - min_distance_from_enemy min_cap_distance = min(self.doctrine.cap_min_distance_from_cp, distance_to_no_fly) max_cap_distance = min(self.doctrine.cap_max_distance_from_cp, distance_to_no_fly) end = location.position.point_from_heading( heading, random.randint(min_cap_distance, max_cap_distance)) diameter = random.randint(self.doctrine.cap_min_track_length, self.doctrine.cap_max_track_length) start = end.point_from_heading(heading - 180, diameter) builder = WaypointBuilder(self.game.conditions, flight, self.doctrine) start, end = builder.race_track(start, end, patrol_alt) descent, land = builder.rtb(flight.from_cp) return BarCapFlightPlan(package=self.package, flight=flight, patrol_duration=self.doctrine.cap_duration, takeoff=builder.takeoff(flight.from_cp), ascent=builder.ascent(flight.from_cp), patrol_start=start, patrol_end=end, descent=descent, land=land)
def find_divert_field(self, aircraft: FlyingType, arrival: ControlPoint) -> Optional[ControlPoint]: divert_limit = nm_to_meter(150) for airfield in self.closest_airfields.airfields_within(divert_limit): if airfield.captured != self.is_player: continue if airfield == arrival: continue if not airfield.can_operate(aircraft): continue if isinstance(airfield, OffMapSpawn): continue return airfield return None
def draw_scale(self, scale_distance_nm=20, number_of_points=4): PADDING = 14 POS_X = 0 POS_Y = 10 BIG_LINE = 5 SMALL_LINE = 2 dist = self.km_to_pixel(nm_to_meter(scale_distance_nm) / 1000.0) self.scene().addRect(POS_X, POS_Y - PADDING, PADDING * 2 + dist, BIG_LINE * 2 + 3 * PADDING, pen=CONST.COLORS["black"], brush=CONST.COLORS["black"]) l = self.scene().addLine(POS_X + PADDING, POS_Y + BIG_LINE * 2, POS_X + PADDING + dist, POS_Y + BIG_LINE * 2) text = self.scene().addText("0nm", font=QFont("Trebuchet MS", 6, weight=5, italic=False)) text.setPos(POS_X, POS_Y + BIG_LINE * 2) text.setDefaultTextColor(Qt.white) text2 = self.scene().addText(str(scale_distance_nm) + "nm", font=QFont("Trebuchet MS", 6, weight=5, italic=False)) text2.setPos(POS_X + dist, POS_Y + BIG_LINE * 2) text2.setDefaultTextColor(Qt.white) l.setPen(CONST.COLORS["white"]) for i in range(number_of_points + 1): d = float(i) / float(number_of_points) if i == 0 or i == number_of_points: h = BIG_LINE else: h = SMALL_LINE l = self.scene().addLine(POS_X + PADDING + d * dist, POS_Y + BIG_LINE * 2, POS_X + PADDING + d * dist, POS_Y + BIG_LINE - h) l.setPen(CONST.COLORS["white"])
def ascent(self, departure: ControlPoint) -> FlightWaypoint: """Create ascent waypoint for the given departure airfield or carrier. Args: departure: Departure airfield or carrier. """ heading = RunwayAssigner(self.conditions).takeoff_heading(departure) position = departure.position.point_from_heading( heading, nm_to_meter(5)) waypoint = FlightWaypoint( FlightWaypointType.ASCEND_POINT, position.x, position.y, 500 if self.is_helo else self.doctrine.pattern_altitude) waypoint.name = "ASCEND" waypoint.alt_type = "RADIO" waypoint.description = "Ascend" waypoint.pretty_name = "Ascend" return waypoint
def descent(self, arrival: ControlPoint) -> FlightWaypoint: """Create descent waypoint for the given arrival airfield or carrier. Args: arrival: Arrival airfield or carrier. """ landing_heading = RunwayAssigner( self.conditions).landing_heading(arrival) heading = (landing_heading + 180) % 360 position = arrival.position.point_from_heading(heading, nm_to_meter(5)) waypoint = FlightWaypoint( FlightWaypointType.DESCENT_POINT, position.x, position.y, 300 if self.is_helo else self.doctrine.pattern_altitude) waypoint.name = "DESCEND" waypoint.alt_type = "RADIO" waypoint.description = "Descend to pattern altitude" waypoint.pretty_name = "Descend" return waypoint
def setup_flight_group(self, group, flight, flight_type, dynamic_runways: Dict[str, RunwayData]): if flight_type in [ FlightType.CAP, FlightType.BARCAP, FlightType.TARCAP, FlightType.INTERCEPTION ]: group.task = CAP.name self._setup_group(group, CAP, flight, dynamic_runways) # group.points[0].tasks.clear() group.points[0].tasks.clear() group.points[0].tasks.append( EngageTargets(max_distance=nm_to_meter(50), targets=[Targets.All.Air])) # group.tasks.append(EngageTargets(max_distance=nm_to_meter(120), targets=[Targets.All.Air])) if flight.unit_type not in GUNFIGHTERS: group.points[0].tasks.append( OptRTBOnOutOfAmmo(OptRTBOnOutOfAmmo.Values.AAM)) else: group.points[0].tasks.append( OptRTBOnOutOfAmmo(OptRTBOnOutOfAmmo.Values.Cannon)) elif flight_type in [FlightType.CAS, FlightType.BAI]: group.task = CAS.name self._setup_group(group, CAS, flight, dynamic_runways) group.points[0].tasks.clear() group.points[0].tasks.append( EngageTargets(max_distance=nm_to_meter(10), targets=[Targets.All.GroundUnits.GroundVehicles ])) group.points[0].tasks.append( OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)) group.points[0].tasks.append( OptROE(OptROE.Values.OpenFireWeaponFree)) group.points[0].tasks.append( OptRTBOnOutOfAmmo(OptRTBOnOutOfAmmo.Values.Unguided)) group.points[0].tasks.append(OptRestrictJettison(True)) elif flight_type in [FlightType.SEAD, FlightType.DEAD]: group.task = SEAD.name self._setup_group(group, SEAD, flight, dynamic_runways) group.points[0].tasks.clear() group.points[0].tasks.append(NoTask()) group.points[0].tasks.append( OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)) group.points[0].tasks.append(OptROE(OptROE.Values.OpenFire)) group.points[0].tasks.append(OptRestrictJettison(True)) group.points[0].tasks.append( OptRTBOnOutOfAmmo(OptRTBOnOutOfAmmo.Values.ASM)) elif flight_type in [FlightType.STRIKE]: group.task = PinpointStrike.name self._setup_group(group, GroundAttack, flight, dynamic_runways) group.points[0].tasks.clear() group.points[0].tasks.append( OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)) group.points[0].tasks.append(OptROE(OptROE.Values.OpenFire)) group.points[0].tasks.append(OptRestrictJettison(True)) elif flight_type in [FlightType.ANTISHIP]: group.task = AntishipStrike.name self._setup_group(group, AntishipStrike, flight, dynamic_runways) group.points[0].tasks.clear() group.points[0].tasks.append( OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)) group.points[0].tasks.append(OptROE(OptROE.Values.OpenFire)) group.points[0].tasks.append(OptRestrictJettison(True)) group.points[0].tasks.append(OptRTBOnBingoFuel(True)) group.points[0].tasks.append(OptRestrictAfterburner(True)) if hasattr(flight.unit_type, 'eplrs'): if flight.unit_type.eplrs: group.points[0].tasks.append(EPLRS(group.id)) for i, point in enumerate(flight.points): if not point.only_for_player or (point.only_for_player and flight.client_count > 0): pt = group.add_waypoint(Point(point.x, point.y), point.alt) if point.waypoint_type == FlightWaypointType.PATROL_TRACK: action = ControlledTask( OrbitAction( altitude=pt.alt, pattern=OrbitAction.OrbitPattern.RaceTrack)) action.stop_after_duration(CAP_DURATION * 60) #for tgt in point.targets: # if hasattr(tgt, "position"): # engagetgt = EngageTargetsInZone(tgt.position, radius=CAP_DEFAULT_ENGAGE_DISTANCE, targets=[Targets.All.Air]) # pt.tasks.append(engagetgt) elif point.waypoint_type == FlightWaypointType.LANDING_POINT: pt.type = "Land" pt.action = PointAction.Landing elif point.waypoint_type == FlightWaypointType.INGRESS_STRIKE: if group.units[0].unit_type == B_17G: if len(point.targets) > 0: bcenter = Point(0, 0) for j, t in enumerate(point.targets): bcenter.x += t.position.x bcenter.y += t.position.y bcenter.x = bcenter.x / len(point.targets) bcenter.y = bcenter.y / len(point.targets) bombing = Bombing(bcenter) bombing.params["expend"] = "All" bombing.params["attackQtyLimit"] = False bombing.params["directionEnabled"] = False bombing.params["altitudeEnabled"] = False bombing.params["weaponType"] = 2032 bombing.params["groupAttack"] = True pt.tasks.append(bombing) else: for j, t in enumerate(point.targets): print(t.position) pt.tasks.append(Bombing(t.position)) if group.units[0].unit_type == JF_17 and j < 4: group.add_nav_target_point( t.position, "PP" + str(j + 1)) if group.units[0].unit_type == F_14B and j == 0: group.add_nav_target_point(t.position, "ST") if group.units[0].unit_type == AJS37 and j < 9: group.add_nav_target_point( t.position, "M" + str(j + 1)) elif point.waypoint_type == FlightWaypointType.INGRESS_SEAD: tgroup = self.m.find_group( point.targetGroup.group_identifier) if tgroup is not None: task = AttackGroup(tgroup.id) task.params["expend"] = "All" task.params["attackQtyLimit"] = False task.params["directionEnabled"] = False task.params["altitudeEnabled"] = False task.params["weaponType"] = 268402702 # Guided Weapons task.params["groupAttack"] = True pt.tasks.append(task) for j, t in enumerate(point.targets): if group.units[0].unit_type == JF_17 and j < 4: group.add_nav_target_point(t.position, "PP" + str(j + 1)) if group.units[0].unit_type == F_14B and j == 0: group.add_nav_target_point(t.position, "ST") if group.units[0].unit_type == AJS37 and j < 9: group.add_nav_target_point(t.position, "M" + str(j + 1)) if pt is not None: pt.alt_type = point.alt_type pt.name = String(point.name) self._setup_custom_payload(flight, group)
cap_min_distance_from_cp: int cap_max_distance_from_cp: int cas_duration: timedelta MODERN_DOCTRINE = Doctrine( cap=True, cas=True, sead=True, strike=True, antiship=True, strike_max_range=1500000, sead_max_range=1500000, rendezvous_altitude=feet_to_meter(25000), hold_distance=nm_to_meter(15), push_distance=nm_to_meter(20), join_distance=nm_to_meter(20), split_distance=nm_to_meter(20), ingress_egress_distance=nm_to_meter(45), ingress_altitude=feet_to_meter(20000), egress_altitude=feet_to_meter(20000), min_patrol_altitude=feet_to_meter(15000), max_patrol_altitude=feet_to_meter(33000), pattern_altitude=feet_to_meter(5000), cap_duration=timedelta(minutes=30), cap_min_track_length=nm_to_meter(15), cap_max_track_length=nm_to_meter(40), cap_min_distance_from_cp=nm_to_meter(10), cap_max_distance_from_cp=nm_to_meter(40), cas_duration=timedelta(minutes=30),
MODERN_DOCTRINE = { "GENERATORS": { "CAS": True, "CAP": True, "SEAD": True, "STRIKE": True, "ANTISHIP": True, }, "STRIKE_MAX_RANGE": 1500000, "SEAD_MAX_RANGE": 1500000, "CAP_EVERY_X_MINUTES": 20, "CAS_EVERY_X_MINUTES": 30, "SEAD_EVERY_X_MINUTES": 40, "STRIKE_EVERY_X_MINUTES": 40, "INGRESS_EGRESS_DISTANCE": nm_to_meter(45), "INGRESS_ALT": feet_to_meter(20000), "EGRESS_ALT": feet_to_meter(20000), "PATROL_ALT_RANGE": (feet_to_meter(15000), feet_to_meter(33000)), "PATTERN_ALTITUDE": feet_to_meter(5000), "CAP_PATTERN_LENGTH": (nm_to_meter(15), nm_to_meter(40)), "FRONTLINE_CAP_DISTANCE_FROM_FRONTLINE": (nm_to_meter(6), nm_to_meter(15)), "CAP_DISTANCE_FROM_CP": (nm_to_meter(10), nm_to_meter(40)), "MAX_NUMBER_OF_INTERCEPTION_GROUP": 3, } COLDWAR_DOCTRINE = { "GENERATORS": { "CAS": True, "CAP": True, "SEAD": True,
class CoalitionMissionPlanner: """Coalition flight planning AI. This class is responsible for automatically planning missions for the coalition at the start of the turn. The primary goal of the mission planner is to protect existing friendly assets. Missions will be planned with the following priorities: 1. CAP for airfields/fleets in close proximity to the enemy to prevent heavy losses of friendly aircraft. 2. CAP for front line areas to protect ground and CAS units. 3. DEAD to reduce necessity of SEAD for future missions. 4. CAS to protect friendly ground units. 5. Strike missions to reduce the enemy's resources. TODO: Anti-ship and airfield strikes to reduce enemy sortie rates. TODO: BAI to prevent enemy forces from reaching the front line. TODO: Should fleets always have a CAP? TODO: Stance and doctrine-specific planning behavior. """ # TODO: Merge into doctrine, also limit by aircraft. MAX_CAP_RANGE = nm_to_meter(100) MAX_CAS_RANGE = nm_to_meter(50) MAX_SEAD_RANGE = nm_to_meter(150) MAX_STRIKE_RANGE = nm_to_meter(150) 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.blue_ato if is_player else self.game.red_ato def propose_missions(self) -> Iterator[ProposedMission]: """Identifies and iterates over potential mission in priority order.""" # Find friendly CPs within 100 nmi from an enemy airfield, plan CAP. for cp in self.objective_finder.vulnerable_control_points(): yield ProposedMission(cp, [ ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE), ]) # Find front lines, plan CAP. for front_line in self.objective_finder.front_lines(): yield ProposedMission(front_line, [ ProposedFlight(FlightType.TARCAP, 2, self.MAX_CAP_RANGE), ProposedFlight(FlightType.CAS, 2, self.MAX_CAS_RANGE), ]) # Find enemy SAM sites with ranges that cover friendly CPs, front lines, # or objects, plan DEAD. # Find enemy SAM sites with ranges that extend to within 50 nmi of # friendly CPs, front, lines, or objects, plan DEAD. for sam in self.objective_finder.threatening_sams(): yield ProposedMission( sam, [ ProposedFlight(FlightType.DEAD, 2, self.MAX_SEAD_RANGE), # TODO: Max escort range. ProposedFlight(FlightType.ESCORT, 2, self.MAX_SEAD_RANGE), ]) # Plan strike missions. for target in self.objective_finder.strike_targets(): yield ProposedMission( target, [ ProposedFlight(FlightType.STRIKE, 2, self.MAX_STRIKE_RANGE), # TODO: Max escort range. ProposedFlight(FlightType.SEAD, 2, self.MAX_STRIKE_RANGE), ProposedFlight(FlightType.ESCORT, 2, self.MAX_STRIKE_RANGE), ]) def plan_missions(self) -> None: """Identifies and plans mission for the turn.""" for proposed_mission in self.propose_missions(): self.plan_mission(proposed_mission) 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.id} from {cp}") def plan_mission(self, mission: ProposedMission) -> None: """Allocates aircraft for a proposed mission and adds it to the ATO.""" if self.game.settings.perf_ai_parking_start: start_type = "Cold" else: start_type = "Warm" builder = PackageBuilder( mission.location, self.objective_finder.closest_airfields_to(mission.location), self.game.aircraft_inventory, self.is_player, start_type) missing_types: Set[FlightType] = set() for proposed_flight in mission.flights: if not builder.plan_flight(proposed_flight): missing_types.add(proposed_flight.task) if missing_types: missing_types_str = ", ".join( sorted([t.name for t in missing_types])) builder.release_planned_aircraft() self.message( "Insufficient aircraft", f"Not enough aircraft in range for {mission.location.name} " f"capable of: {missing_types_str}") return package = builder.build() flight_plan_builder = FlightPlanBuilder(self.game, package, self.is_player) for flight in package.flights: flight_plan_builder.populate_flight_plan(flight) 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(minutes=max(0, time + error)) dca_types = (FlightType.BARCAP, FlightType.INTERCEPTION) 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, latest=90, margin=5) for package in self.ato.packages: tot = TotEstimator(package).earliest_tot() if package.primary_task in dca_types: # All CAP missions should be on station ASAP. package.time_over_target = tot 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, text) -> 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}")
class ObjectiveFinder: """Identifies potential objectives for the mission planner.""" # TODO: Merge into doctrine. AIRFIELD_THREAT_RANGE = nm_to_meter(150) SAM_THREAT_RANGE = nm_to_meter(100) def __init__(self, game: Game, is_player: bool) -> None: self.game = game self.is_player = is_player def enemy_sams(self) -> Iterator[TheaterGroundObject]: """Iterates over all enemy SAM sites.""" # Control points might have the same ground object several times, for # some reason. found_targets: Set[str] = set() for cp in self.enemy_control_points(): for ground_object in cp.ground_objects: if not isinstance(ground_object, SamGroundObject): continue if ground_object.is_dead: continue if ground_object.name in found_targets: continue if not self.object_has_radar(ground_object): continue # TODO: Yield in order of most threatening. # Need to sort in order of how close their defensive range comes # to friendly assets. To do that we need to add effective range # information to the database. yield ground_object found_targets.add(ground_object.name) def threatening_sams(self) -> Iterator[TheaterGroundObject]: """Iterates over enemy SAMs in threat range of friendly control points. SAM sites are sorted by their closest proximity to any friendly control point (airfield or fleet). """ sams: List[Tuple[TheaterGroundObject, int]] = [] for sam in self.enemy_sams(): ranges: List[int] = [] for cp in self.friendly_control_points(): ranges.append(sam.distance_to(cp)) sams.append((sam, min(ranges))) sams = sorted(sams, key=operator.itemgetter(1)) for sam, _range in sams: yield sam def strike_targets(self) -> Iterator[TheaterGroundObject]: """Iterates over enemy strike targets. Targets are sorted by their closest proximity to any friendly control point (airfield or fleet). """ targets: List[Tuple[TheaterGroundObject, int]] = [] # Control points might have the same ground object several times, for # some reason. found_targets: Set[str] = set() for enemy_cp in self.enemy_control_points(): for ground_object in enemy_cp.ground_objects: if ground_object.is_dead: continue if ground_object.name in found_targets: continue ranges: List[int] = [] for friendly_cp in self.friendly_control_points(): ranges.append(ground_object.distance_to(friendly_cp)) targets.append((ground_object, min(ranges))) found_targets.add(ground_object.name) targets = sorted(targets, key=operator.itemgetter(1)) for target, _range in targets: yield target @staticmethod def object_has_radar(ground_object: TheaterGroundObject) -> bool: """Returns True if the ground object contains a unit with radar.""" for group in ground_object.groups: for unit in group.units: if db.unit_type_from_name(unit.type) in UNITS_WITH_RADAR: return True return False def front_lines(self) -> Iterator[FrontLine]: """Iterates over all active front lines in the theater.""" for cp in self.friendly_control_points(): for connected in cp.connected_points: if connected.is_friendly(self.is_player): continue if Conflict.has_frontline_between(cp, connected): yield FrontLine(cp, connected) def vulnerable_control_points(self) -> Iterator[ControlPoint]: """Iterates over friendly CPs that are vulnerable to enemy CPs. Vulnerability is defined as any enemy CP within threat range of of the CP. """ for cp in self.friendly_control_points(): airfields_in_proximity = self.closest_airfields_to(cp) airfields_in_threat_range = airfields_in_proximity.airfields_within( self.AIRFIELD_THREAT_RANGE) for airfield in airfields_in_threat_range: if not airfield.is_friendly(self.is_player): yield cp break def friendly_control_points(self) -> Iterator[ControlPoint]: """Iterates over all friendly control points.""" return (c for c in self.game.theater.controlpoints if c.is_friendly(self.is_player)) def enemy_control_points(self) -> Iterator[ControlPoint]: """Iterates over all enemy control points.""" return (c for c in self.game.theater.controlpoints if not c.is_friendly(self.is_player)) def all_possible_targets(self) -> Iterator[MissionTarget]: """Iterates over all possible mission targets in the theater. Valid mission targets are control points (airfields and carriers), front lines, and ground objects (SAM sites, factories, resource extraction sites, etc). """ for cp in self.game.theater.controlpoints: yield cp yield from cp.ground_objects yield from self.front_lines() @staticmethod def closest_airfields_to(location: MissionTarget) -> ClosestAirfields: """Returns the closest airfields to the given location.""" return ObjectiveDistanceCache.get_closest_airfields(location)
def _hold_point(self, flight: Flight) -> Point: heading = flight.from_cp.position.heading_between_point( self.package.target.position) return flight.from_cp.position.point_from_heading( heading, nm_to_meter(15))