def divert(self,
               divert: Optional[ControlPoint]) -> Optional[FlightWaypoint]:
        """Create divert waypoint for the given arrival airfield or carrier.

        Args:
            divert: Divert airfield or carrier.
        """
        if divert is None:
            return None

        position = divert.position
        if isinstance(divert, OffMapSpawn):
            if self.is_helo:
                altitude = meters(500)
            else:
                altitude = self.doctrine.rendezvous_altitude
            altitude_type = "BARO"
        else:
            altitude = meters(0)
            altitude_type = "RADIO"

        waypoint = FlightWaypoint(FlightWaypointType.DIVERT, position.x,
                                  position.y, altitude)
        waypoint.alt_type = altitude_type
        waypoint.name = "DIVERT"
        waypoint.description = "Divert"
        waypoint.pretty_name = "Divert"
        waypoint.only_for_player = True
        return waypoint
Beispiel #2
0
    def compatible_with_mission(
        self, unit_type: AircraftType, airfield: ControlPoint
    ) -> bool:
        if unit_type not in aircraft_for_task(FlightType.TRANSPORT):
            return False
        if not self.transfer.origin.can_operate(unit_type):
            return False
        if not self.next_stop.can_operate(unit_type):
            return False

        # Cargo planes have no maximum range.
        if not unit_type.dcs_unit_type.helicopter:
            return True

        # A helicopter that is transport capable and able to operate at both bases. Need
        # to check that no leg of the journey exceeds the maximum range. This doesn't
        # account for any routing around threats that might take place, but it's close
        # enough.

        home = airfield.position
        pickup = self.transfer.position.position
        drop_off = self.transfer.position.position
        if meters(home.distance_to_point(pickup)) > self.HELO_MAX_RANGE:
            return False

        if meters(pickup.distance_to_point(drop_off)) > self.HELO_MAX_RANGE:
            return False

        if meters(drop_off.distance_to_point(home)) > self.HELO_MAX_RANGE:
            return False

        return True
    def land(self, arrival: ControlPoint) -> FlightWaypoint:
        """Create descent waypoint for the given arrival airfield or carrier.

        Args:
            arrival: Arrival airfield or carrier.
        """
        position = arrival.position
        if isinstance(arrival, OffMapSpawn):
            waypoint = FlightWaypoint(
                FlightWaypointType.NAV,
                position.x,
                position.y,
                meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
            )
            waypoint.name = "NAV"
            waypoint.alt_type = "BARO"
            waypoint.description = "Exit theater"
            waypoint.pretty_name = "Exit theater"
        else:
            waypoint = FlightWaypoint(
                FlightWaypointType.LANDING_POINT, position.x, position.y, meters(0)
            )
            waypoint.name = "LANDING"
            waypoint.alt_type = "RADIO"
            waypoint.description = "Land"
            waypoint.pretty_name = "Land"
        return waypoint
    def land(self, arrival: ControlPoint) -> FlightWaypoint:
        """Create descent waypoint for the given arrival airfield or carrier.

        Args:
            arrival: Arrival airfield or carrier.
        """
        position = arrival.position
        if isinstance(arrival, OffMapSpawn):
            return FlightWaypoint(
                "NAV",
                FlightWaypointType.NAV,
                position,
                meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
                description="Exit theater",
                pretty_name="Exit theater",
            )

        return FlightWaypoint(
            "LANDING",
            FlightWaypointType.LANDING_POINT,
            position,
            meters(0),
            alt_type="RADIO",
            description="Land",
            pretty_name="Land",
            control_point=arrival,
        )
    def takeoff(self, departure: ControlPoint) -> FlightWaypoint:
        """Create takeoff waypoint for the given arrival airfield or carrier.

        Note that the takeoff waypoint will automatically be created by pydcs
        when we create the group, but creating our own before generation makes
        the planning code simpler.

        Args:
            departure: Departure airfield or carrier.
        """
        position = departure.position
        if isinstance(departure, OffMapSpawn):
            waypoint = FlightWaypoint(
                FlightWaypointType.NAV, position.x, position.y,
                meters(500)
                if self.is_helo else self.doctrine.rendezvous_altitude)
            waypoint.name = "NAV"
            waypoint.alt_type = "BARO"
            waypoint.description = "Enter theater"
            waypoint.pretty_name = "Enter theater"
        else:
            waypoint = FlightWaypoint(FlightWaypointType.TAKEOFF, position.x,
                                      position.y, meters(0))
            waypoint.name = "TAKEOFF"
            waypoint.alt_type = "RADIO"
            waypoint.description = "Takeoff"
            waypoint.pretty_name = "Takeoff"
        return waypoint
    def iter_iads_ranges(
        self, state: TheaterState, range_type: RangeType
    ) -> Iterator[Union[IadsGroundObject, NavalGroundObject]]:
        target_ranges: list[
            tuple[Union[IadsGroundObject, NavalGroundObject], Distance]
        ] = []
        all_iads: Iterator[
            Union[IadsGroundObject, NavalGroundObject]
        ] = itertools.chain(state.enemy_air_defenses, state.enemy_ships)
        for target in all_iads:
            distance = meters(target.distance_to(self.target))
            if range_type is RangeType.Detection:
                target_range = target.max_detection_range()
            elif range_type is RangeType.Threat:
                target_range = target.max_threat_range()
            else:
                raise ValueError(f"Unknown RangeType: {range_type}")
            if not target_range:
                continue

            # IADS out of range of our target area will have a positive
            # distance_to_threat and should be pruned. The rest have a decreasing
            # distance_to_threat as overlap increases. The most negative distance has
            # the greatest coverage of the target and should be treated as the highest
            # priority threat.
            distance_to_threat = distance - target_range
            if distance_to_threat > meters(0):
                continue
            target_ranges.append((target, distance_to_threat))

        # TODO: Prioritize IADS by vulnerability?
        target_ranges = sorted(target_ranges, key=operator.itemgetter(1))
        for target, _range in target_ranges:
            yield target
 def max_distance_from(self, cp: ControlPoint) -> Distance:
     """Returns the farthest waypoint of the flight plan from a ControlPoint.
     :arg cp The ControlPoint to measure distance from.
     """
     if not self.waypoints:
         return meters(0)
     return max(
         [meters(cp.position.distance_to_point(w.position)) for w in self.waypoints]
     )
 def cas(self, position: Point) -> FlightWaypoint:
     waypoint = FlightWaypoint(
         FlightWaypointType.CAS, position.x, position.y,
         meters(500) if self.is_helo else meters(1000))
     waypoint.alt_type = "RADIO"
     waypoint.description = "Provide CAS"
     waypoint.name = "CAS"
     waypoint.pretty_name = "CAS"
     return waypoint
 def cas(self, position: Point) -> FlightWaypoint:
     return FlightWaypoint(
         "CAS",
         FlightWaypointType.CAS,
         position,
         meters(60) if self.is_helo else meters(1000),
         "RADIO",
         description="Provide CAS",
         pretty_name="CAS",
     )
Beispiel #10
0
 def from_pydcs(cls, point: MovingPoint,
                from_cp: ControlPoint) -> "FlightWaypoint":
     waypoint = FlightWaypoint(
         FlightWaypointType.NAV,
         point.position.x,
         point.position.y,
         meters(point.alt),
     )
     waypoint.alt_type = point.alt_type
     # Other actions exist... but none of them *should* be the first
     # waypoint for a flight.
     waypoint.waypoint_type = {
         PointAction.TurningPoint: FlightWaypointType.NAV,
         PointAction.FlyOverPoint: FlightWaypointType.NAV,
         PointAction.FromParkingArea: FlightWaypointType.TAKEOFF,
         PointAction.FromParkingAreaHot: FlightWaypointType.TAKEOFF,
         PointAction.FromRunway: FlightWaypointType.TAKEOFF,
     }[point.action]
     if waypoint.waypoint_type == FlightWaypointType.NAV:
         waypoint.name = "NAV"
         waypoint.pretty_name = "Nav"
         waypoint.description = "Nav"
     else:
         waypoint.name = "TAKEOFF"
         waypoint.pretty_name = "Takeoff"
         waypoint.description = "Takeoff"
         waypoint.description = f"Takeoff from {from_cp.name}"
     return waypoint
Beispiel #11
0
    def _waypoint_distance(self, waypoint: FlightWaypoint) -> str:
        if self.last_waypoint is None:
            return "-"

        distance = meters(
            self.last_waypoint.position.distance_to_point(waypoint.position))
        return f"{distance.nautical_miles:.1f} NM"
    def _target_area(
        name: str, location: MissionTarget, flyover: bool = False
    ) -> FlightWaypoint:
        waypoint = FlightWaypoint(
            FlightWaypointType.TARGET_GROUP_LOC,
            location.position.x,
            location.position.y,
            meters(0),
        )
        waypoint.description = name
        waypoint.pretty_name = name
        waypoint.name = name
        waypoint.alt_type = "RADIO"

        # Most target waypoints are only for the player's benefit. AI tasks for
        # the target are set on the ingress point so they begin their attack
        # *before* reaching the target.
        #
        # The exception is for flight plans that require passing over the
        # target. For example, OCA strikes need to get close enough to detect
        # the targets in their engagement zone or they will RTB immediately.
        if flyover:
            waypoint.flyover = True
        else:
            waypoint.only_for_player = True
        return waypoint
Beispiel #13
0
    def __init__(self, waypoint_type: FlightWaypointType, x: float, y: float,
                 alt: Distance = meters(0)) -> None:
        """Creates a flight waypoint.

        Args:
            waypoint_type: The waypoint type.
            x: X cooidinate of the waypoint.
            y: Y coordinate of the waypoint.
            alt: Altitude of the waypoint. By default this is AGL, but it can be
            changed to MSL by setting alt_type to "RADIO".
        """
        self.waypoint_type = waypoint_type
        self.x = x
        self.y = y
        self.alt = alt
        self.alt_type = "BARO"
        self.name = ""
        # TODO: Merge with pretty_name.
        # Only used in the waypoint list in the flight edit page. No sense
        # having three names. A short and long form is enough.
        self.description = ""
        self.targets: List[MissionTarget] = []
        self.obj_name = ""
        self.pretty_name = ""
        self.only_for_player = False
        self.flyover = False

        # These are set very late by the air conflict generator (part of mission
        # generation). We do it late so that we don't need to propagate changes
        # to waypoint times whenever the player alters the package TOT or the
        # flight's offset in the UI.
        self.tot: Optional[timedelta] = None
        self.departure_time: Optional[timedelta] = None
Beispiel #14
0
 def generate_fog(self) -> Optional[Fog]:
     if random.randrange(5) != 0:
         return None
     return Fog(
         visibility=meters(random.randint(2500, 5000)),
         thickness=random.randint(100, 500)
     )
Beispiel #15
0
    def setGame(self, game: Optional[Game]):
        if game is None:
            return

        self.air_wing.setEnabled(True)
        self.transfers.setEnabled(True)

        self.conditionsWidget.setCurrentTurn(game.turn, game.conditions)

        if game.conditions.weather.clouds:
            base_m = game.conditions.weather.clouds.base
            base_ft = int(meters(base_m).feet)
            self.conditionsWidget.setToolTip(f"Cloud Base: {base_m}m / {base_ft}ft")
        else:
            self.conditionsWidget.setToolTip("")

        self.intel_box.set_game(game)
        self.budgetBox.setGame(game)
        self.factionsInfos.setGame(game)

        self.passTurnButton.setEnabled(True)
        if game and game.turn > 0:
            self.passTurnButton.setText("Pass Turn")

        if game and game.turn == 0:
            self.passTurnButton.setText("Begin Campaign")
            self.proceedButton.setEnabled(False)
        else:
            self.proceedButton.setEnabled(True)
    def escort(self, ingress: Point, target: MissionTarget, egress: Point) -> \
            Tuple[FlightWaypoint, FlightWaypoint, FlightWaypoint]:
        """Creates the waypoints needed to escort the package.

        Args:
            ingress: The package ingress point.
            target: The mission target.
            egress: The package egress point.
        """
        # This would preferably be no points at all, and instead the Escort task
        # would begin on the join point and end on the split point, however the
        # escort task does not appear to work properly (see the longer
        # description in gen.aircraft.JoinPointBuilder), so instead we give
        # the escort flights a flight plan including the ingress point, target
        # area, and egress point.
        ingress = self.ingress(FlightWaypointType.INGRESS_ESCORT, ingress,
                               target)

        waypoint = FlightWaypoint(
            FlightWaypointType.TARGET_GROUP_LOC, target.position.x,
            target.position.y,
            meters(500) if self.is_helo else self.doctrine.ingress_altitude)
        waypoint.name = "TARGET"
        waypoint.description = "Escort the package"
        waypoint.pretty_name = "Target area"

        egress = self.egress(egress, target)
        return ingress, waypoint, egress
    def escort(
        self,
        ingress: Point,
        target: MissionTarget,
    ) -> Tuple[FlightWaypoint, FlightWaypoint]:
        """Creates the waypoints needed to escort the package.

        Args:
            ingress: The package ingress point.
            target: The mission target.
        """
        alt_type: AltitudeReference = "BARO"
        if self.is_helo:
            alt_type = "RADIO"

        # This would preferably be no points at all, and instead the Escort task
        # would begin on the join point and end on the split point, however the
        # escort task does not appear to work properly (see the longer
        # description in gen.aircraft.JoinPointBuilder), so instead we give
        # the escort flights a flight plan including the ingress point and target area.
        ingress_wp = self.ingress(FlightWaypointType.INGRESS_ESCORT, ingress, target)

        return ingress_wp, FlightWaypoint(
            "TARGET",
            FlightWaypointType.TARGET_GROUP_LOC,
            target.position,
            meters(60) if self.is_helo else self.doctrine.ingress_altitude,
            alt_type,
            description="Escort the package",
            pretty_name="Target area",
        )
 def is_valid_ship_pos(self, scene_position: Point) -> bool:
     world_destination = self._scene_to_dcs_coords(scene_position)
     distance = self.selected_cp.control_point.position.distance_to_point(
         world_destination)
     if meters(distance) > MAX_SHIP_DISTANCE:
         return False
     return self.game.theater.is_in_sea(world_destination)
Beispiel #19
0
 def fuel_consumption_between_points(
     self, a: FlightWaypoint, b: FlightWaypoint
 ) -> float | None:
     ppm = self.fuel_rate_to_between_points(a, b)
     if ppm is None:
         return None
     distance = meters(a.position.distance_to_point(b.position))
     return distance.nautical_miles * ppm
 def egress(self, position: Point, target: MissionTarget) -> FlightWaypoint:
     waypoint = FlightWaypoint(
         FlightWaypointType.EGRESS, position.x, position.y,
         meters(500) if self.is_helo else self.doctrine.ingress_altitude)
     waypoint.pretty_name = "EGRESS from " + target.name
     waypoint.description = "EGRESS from " + target.name
     waypoint.name = "EGRESS"
     return waypoint
 def split(self, position: Point) -> FlightWaypoint:
     waypoint = FlightWaypoint(
         FlightWaypointType.SPLIT, position.x, position.y,
         meters(500) if self.is_helo else self.doctrine.ingress_altitude)
     waypoint.pretty_name = "Split"
     waypoint.description = "Depart from package"
     waypoint.name = "SPLIT"
     return waypoint
 def join(self, position: Point) -> FlightWaypoint:
     waypoint = FlightWaypoint(
         FlightWaypointType.JOIN, position.x, position.y,
         meters(500) if self.is_helo else self.doctrine.ingress_altitude)
     waypoint.pretty_name = "Join"
     waypoint.description = "Rendezvous with package"
     waypoint.name = "JOIN"
     return waypoint
Beispiel #23
0
 def target_area_waypoint(self) -> FlightWaypoint:
     return FlightWaypoint(
         "TARGET AREA",
         FlightWaypointType.TARGET_GROUP_LOC,
         self.package.target.position,
         meters(0),
         "RADIO",
     )
Beispiel #24
0
    def _waypoint_distance(self, waypoint: FlightWaypoint) -> str:
        if self.last_waypoint is None:
            return "-"

        distance = meters(
            self.last_waypoint.position.distance_to_point(waypoint.position))

        return f"{self.units.distance_long(distance):.1f}"
 def hold(self, position: Point) -> FlightWaypoint:
     waypoint = FlightWaypoint(
         FlightWaypointType.LOITER, position.x, position.y,
         meters(500) if self.is_helo else self.doctrine.rendezvous_altitude)
     waypoint.pretty_name = "Hold"
     waypoint.description = "Wait until push time"
     waypoint.name = "HOLD"
     return waypoint
 def bullseye(self) -> FlightWaypoint:
     return FlightWaypoint(
         "BULLSEYE",
         FlightWaypointType.BULLSEYE,
         self._bullseye.position,
         meters(0),
         description="Bullseye",
         pretty_name="Bullseye",
         only_for_player=True,
     )
Beispiel #27
0
 def estimate_altitude(self) -> tuple[Distance, str]:
     return (
         meters(
             lerp(
                 self.current_waypoint.alt.meters,
                 self.next_waypoint.alt.meters,
                 self.progress(),
             )),
         self.current_waypoint.alt_type,
     )
    def nav_point_prunable(self, previous: Point, current: Point, nxt: Point) -> bool:
        previous_threatened = self.threat_zones.path_threatened(previous, current)
        next_threatened = self.threat_zones.path_threatened(current, nxt)
        pruned_threatened = self.threat_zones.path_threatened(previous, nxt)
        previous_distance = meters(previous.distance_to_point(current))
        distance = meters(current.distance_to_point(nxt))
        distance_without = previous_distance + distance
        if distance > distance_without:
            # Don't prune paths to make them longer.
            return False

        # We could shorten the path by removing the intermediate
        # waypoint. Do so if the new path isn't higher threat.
        if not pruned_threatened:
            # The new path is not threatened, so safe to prune.
            return True

        # The new path is threatened. Only allow if both paths were
        # threatened anyway.
        return previous_threatened and next_threatened
Beispiel #29
0
 def estimate_fuel(self) -> float:
     initial_fuel = self.estimate_fuel_at_current_waypoint()
     ppm = self.flight.flight_plan.fuel_rate_to_between_points(
         self.current_waypoint, self.next_waypoint)
     if ppm is None:
         return initial_fuel
     position = self.estimate_position()
     distance = meters(
         self.current_waypoint.position.distance_to_point(position))
     consumption = distance.nautical_miles * ppm * LBS_TO_KG
     return initial_fuel - consumption
 def ingress(self, ingress_type: FlightWaypointType, position: Point,
             objective: MissionTarget) -> FlightWaypoint:
     waypoint = FlightWaypoint(
         ingress_type, position.x, position.y,
         meters(500) if self.is_helo else self.doctrine.ingress_altitude)
     waypoint.pretty_name = "INGRESS on " + objective.name
     waypoint.description = "INGRESS on " + objective.name
     waypoint.name = "INGRESS"
     # TODO: This seems wrong, but it's what was there before.
     waypoint.targets.append(objective)
     return waypoint