mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Merge branch 'develop' into helipads
# Conflicts: # game/game.py # game/operation/operation.py # game/theater/conflicttheater.py # game/theater/controlpoint.py # gen/groundobjectsgen.py # resources/campaigns/golan_heights_lite.miz
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
from typing import List, Type
|
||||
from collections import Sequence
|
||||
from typing import Type
|
||||
|
||||
from dcs.helicopters import (
|
||||
AH_1W,
|
||||
@@ -124,29 +125,30 @@ from pydcs_extensions.su57.su57 import Su_57
|
||||
CAP_CAPABLE = [
|
||||
Su_57,
|
||||
F_22A,
|
||||
MiG_31,
|
||||
F_15C,
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
MiG_25PD,
|
||||
Su_33,
|
||||
Su_34,
|
||||
J_11A,
|
||||
Su_30,
|
||||
Su_27,
|
||||
J_11A,
|
||||
F_15C,
|
||||
MiG_29S,
|
||||
MiG_29G,
|
||||
MiG_29A,
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
JF_17,
|
||||
JAS39Gripen,
|
||||
F_16A,
|
||||
F_4E,
|
||||
JAS39Gripen,
|
||||
JF_17,
|
||||
MiG_31,
|
||||
MiG_25PD,
|
||||
MiG_29G,
|
||||
MiG_29A,
|
||||
MiG_23MLD,
|
||||
MiG_21Bis,
|
||||
Mirage_2000_5,
|
||||
M_2000C,
|
||||
F_15E,
|
||||
M_2000C,
|
||||
F_5E_3,
|
||||
MiG_19P,
|
||||
A_4E_C,
|
||||
@@ -173,6 +175,7 @@ CAS_CAPABLE = [
|
||||
A_10C_2,
|
||||
A_10C,
|
||||
Hercules,
|
||||
Su_34,
|
||||
Su_25TM,
|
||||
Su_25T,
|
||||
Su_25,
|
||||
@@ -190,17 +193,16 @@ CAS_CAPABLE = [
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
AJS37,
|
||||
Su_24MR,
|
||||
Su_24M,
|
||||
Su_17M4,
|
||||
Su_33,
|
||||
F_4E,
|
||||
S_3B,
|
||||
Su_34,
|
||||
Su_30,
|
||||
MiG_19P,
|
||||
MiG_29S,
|
||||
MiG_27K,
|
||||
MiG_29A,
|
||||
MiG_21Bis,
|
||||
AH_64D,
|
||||
AH_64A,
|
||||
AH_1W,
|
||||
@@ -212,13 +214,14 @@ CAS_CAPABLE = [
|
||||
Mi_24P,
|
||||
Mi_24V,
|
||||
Mi_8MT,
|
||||
UH_1H,
|
||||
MiG_19P,
|
||||
MiG_15bis,
|
||||
M_2000C,
|
||||
F_5E_3,
|
||||
F_86F_Sabre,
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
UH_1H,
|
||||
A_20G,
|
||||
Ju_88A4,
|
||||
P_47D_40,
|
||||
@@ -299,13 +302,14 @@ STRIKE_CAPABLE = [
|
||||
Tornado_GR4,
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
AV8BNA,
|
||||
JF_17,
|
||||
F_16A,
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
JAS39Gripen_AG,
|
||||
Tornado_IDS,
|
||||
Su_17M4,
|
||||
Su_24MR,
|
||||
Su_24M,
|
||||
Su_25TM,
|
||||
Su_25T,
|
||||
@@ -317,11 +321,9 @@ STRIKE_CAPABLE = [
|
||||
MiG_29S,
|
||||
MiG_29G,
|
||||
MiG_29A,
|
||||
JF_17,
|
||||
F_4E,
|
||||
A_10C_2,
|
||||
A_10C,
|
||||
AV8BNA,
|
||||
S_3B,
|
||||
A_4E_C,
|
||||
M_2000C,
|
||||
@@ -375,6 +377,7 @@ RUNWAY_ATTACK_CAPABLE = [
|
||||
Su_34,
|
||||
Su_30,
|
||||
Tornado_IDS,
|
||||
M_2000C,
|
||||
] + STRIKE_CAPABLE
|
||||
|
||||
# For any aircraft that isn't necessarily directly involved in strike
|
||||
@@ -415,7 +418,7 @@ REFUELING_CAPABALE = [
|
||||
]
|
||||
|
||||
|
||||
def dcs_types_for_task(task: FlightType) -> list[Type[FlyingType]]:
|
||||
def dcs_types_for_task(task: FlightType) -> Sequence[Type[FlyingType]]:
|
||||
cap_missions = (FlightType.BARCAP, FlightType.TARCAP, FlightType.SWEEP)
|
||||
if task in cap_missions:
|
||||
return CAP_CAPABLE
|
||||
|
||||
@@ -2,14 +2,14 @@ from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from enum import Enum
|
||||
from typing import List, Optional, TYPE_CHECKING, Union
|
||||
from typing import List, Optional, TYPE_CHECKING, Union, Sequence, Any
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.point import MovingPoint, PointAction
|
||||
from dcs.unit import Unit
|
||||
|
||||
from game import db
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.savecompat import has_save_compat_for
|
||||
from game.squadrons import Pilot, Squadron
|
||||
from game.theater.controlpoint import ControlPoint, MissionTarget
|
||||
from game.utils import Distance, meters
|
||||
@@ -139,7 +139,7 @@ class FlightWaypoint:
|
||||
|
||||
Args:
|
||||
waypoint_type: The waypoint type.
|
||||
x: X cooidinate of the waypoint.
|
||||
x: X coordinate 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".
|
||||
@@ -154,11 +154,13 @@ class FlightWaypoint:
|
||||
# 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[Union[MissionTarget, Unit]] = []
|
||||
self.targets: Sequence[Union[MissionTarget, Unit]] = []
|
||||
self.obj_name = ""
|
||||
self.pretty_name = ""
|
||||
self.only_for_player = False
|
||||
self.flyover = False
|
||||
# The minimum amount of fuel remaining at this waypoint in pounds.
|
||||
self.min_fuel: Optional[float] = None
|
||||
|
||||
# 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
|
||||
@@ -167,6 +169,12 @@ class FlightWaypoint:
|
||||
self.tot: Optional[timedelta] = None
|
||||
self.departure_time: Optional[timedelta] = None
|
||||
|
||||
@has_save_compat_for(5)
|
||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||
if "min_fuel" not in state:
|
||||
state["min_fuel"] = None
|
||||
self.__dict__.update(state)
|
||||
|
||||
@property
|
||||
def position(self) -> Point:
|
||||
return Point(self.x, self.y)
|
||||
@@ -325,12 +333,12 @@ class Flight:
|
||||
def clear_roster(self) -> None:
|
||||
self.roster.clear()
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
if self.custom_name:
|
||||
return f"{self.custom_name} {self.count} x {self.unit_type}"
|
||||
return f"[{self.flight_type}] {self.count} x {self.unit_type}"
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
if self.custom_name:
|
||||
return f"{self.custom_name} {self.count} x {self.unit_type}"
|
||||
return f"[{self.flight_type}] {self.count} x {self.unit_type}"
|
||||
|
||||
@@ -20,6 +20,8 @@ from dcs.unit import Unit
|
||||
from shapely.geometry import Point as ShapelyPoint
|
||||
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.dcs.aircrafttype import FuelConsumption
|
||||
from game.flightplan import IpZoneGeometry, JoinZoneGeometry, HoldZoneGeometry
|
||||
from game.theater import (
|
||||
Airfield,
|
||||
ControlPoint,
|
||||
@@ -28,9 +30,17 @@ from game.theater import (
|
||||
SamGroundObject,
|
||||
TheaterGroundObject,
|
||||
NavalControlPoint,
|
||||
ConflictTheater,
|
||||
)
|
||||
from game.theater.theatergroundobject import EwrGroundObject, NavalGroundObject
|
||||
from game.utils import Distance, Speed, feet, meters, nautical_miles, knots
|
||||
from game.theater.theatergroundobject import (
|
||||
EwrGroundObject,
|
||||
NavalGroundObject,
|
||||
BuildingGroundObject,
|
||||
)
|
||||
|
||||
from game.threatzones import ThreatZones
|
||||
from game.utils import Distance, Heading, Speed, feet, meters, nautical_miles, knots
|
||||
|
||||
from .closestairfields import ObjectiveDistanceCache
|
||||
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
|
||||
from .traveltime import GroundSpeed, TravelTime
|
||||
@@ -38,8 +48,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 = {
|
||||
@@ -131,6 +141,17 @@ class FlightPlan:
|
||||
@cached_property
|
||||
def bingo_fuel(self) -> int:
|
||||
"""Bingo fuel value for the FlightPlan"""
|
||||
if (fuel := self.flight.unit_type.fuel_consumption) is not None:
|
||||
return self._bingo_estimate(fuel)
|
||||
return self._legacy_bingo_estimate()
|
||||
|
||||
def _bingo_estimate(self, fuel: FuelConsumption) -> int:
|
||||
distance_to_arrival = self.max_distance_from(self.flight.arrival)
|
||||
fuel_consumed = fuel.cruise * distance_to_arrival.nautical_miles
|
||||
bingo = fuel_consumed + fuel.min_safe
|
||||
return math.ceil(bingo / 100) * 100
|
||||
|
||||
def _legacy_bingo_estimate(self) -> int:
|
||||
distance_to_arrival = self.max_distance_from(self.flight.arrival)
|
||||
|
||||
bingo = 1000.0 # Minimum Emergency Fuel
|
||||
@@ -219,11 +240,7 @@ class FlightPlan:
|
||||
tot_waypoint = self.tot_waypoint
|
||||
if tot_waypoint is None:
|
||||
return None
|
||||
|
||||
time = self.tot
|
||||
if time is None:
|
||||
return None
|
||||
return time - self._travel_time_to_waypoint(tot_waypoint)
|
||||
return self.tot - self._travel_time_to_waypoint(tot_waypoint)
|
||||
|
||||
def startup_time(self) -> Optional[timedelta]:
|
||||
takeoff_time = self.takeoff_time()
|
||||
@@ -540,7 +557,6 @@ class StrikeFlightPlan(FormationFlightPlan):
|
||||
join: FlightWaypoint
|
||||
ingress: FlightWaypoint
|
||||
targets: List[FlightWaypoint]
|
||||
egress: FlightWaypoint
|
||||
split: FlightWaypoint
|
||||
nav_from: List[FlightWaypoint]
|
||||
land: FlightWaypoint
|
||||
@@ -555,7 +571,6 @@ class StrikeFlightPlan(FormationFlightPlan):
|
||||
yield self.join
|
||||
yield self.ingress
|
||||
yield from self.targets
|
||||
yield self.egress
|
||||
yield self.split
|
||||
yield from self.nav_from
|
||||
yield self.land
|
||||
@@ -567,7 +582,6 @@ class StrikeFlightPlan(FormationFlightPlan):
|
||||
def package_speed_waypoints(self) -> Set[FlightWaypoint]:
|
||||
return {
|
||||
self.ingress,
|
||||
self.egress,
|
||||
self.split,
|
||||
} | set(self.targets)
|
||||
|
||||
@@ -631,8 +645,8 @@ class StrikeFlightPlan(FormationFlightPlan):
|
||||
|
||||
@property
|
||||
def split_time(self) -> timedelta:
|
||||
travel_time = self.travel_time_between_waypoints(self.egress, self.split)
|
||||
return self.egress_time + travel_time
|
||||
travel_time = self.travel_time_between_waypoints(self.ingress, self.split)
|
||||
return self.ingress_time + travel_time
|
||||
|
||||
@property
|
||||
def ingress_time(self) -> timedelta:
|
||||
@@ -642,19 +656,9 @@ class StrikeFlightPlan(FormationFlightPlan):
|
||||
)
|
||||
return tot - travel_time
|
||||
|
||||
@property
|
||||
def egress_time(self) -> timedelta:
|
||||
tot = self.tot
|
||||
travel_time = self.travel_time_between_waypoints(
|
||||
self.target_area_waypoint, self.egress
|
||||
)
|
||||
return tot + travel_time
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
|
||||
if waypoint == self.ingress:
|
||||
return self.ingress_time
|
||||
elif waypoint == self.egress:
|
||||
return self.egress_time
|
||||
elif waypoint in self.targets:
|
||||
return self.tot
|
||||
return super().tot_for_waypoint(waypoint)
|
||||
@@ -868,7 +872,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
|
||||
@@ -876,11 +882,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,
|
||||
@@ -945,95 +961,33 @@ class FlightPlanBuilder:
|
||||
raise PlanningError(f"{task} flight plan generation not implemented")
|
||||
|
||||
def regenerate_package_waypoints(self) -> None:
|
||||
# The simple case is where the target is greater than the ingress
|
||||
# distance into the threat zone and the target is not near the departure
|
||||
# airfield. In this case, we can plan the shortest route from the
|
||||
# departure airfield to the target, use the last non-threatened point as
|
||||
# the join point, and plan the IP inside the threatened area.
|
||||
#
|
||||
# When the target is near the edge of the threat zone the IP may need to
|
||||
# be placed outside the zone.
|
||||
#
|
||||
# +--------------+ +---------------+
|
||||
# | | | |
|
||||
# | | IP---+-T |
|
||||
# | | | |
|
||||
# | | | |
|
||||
# +--------------+ +---------------+
|
||||
#
|
||||
# Here we want to place the IP first and route the flight to the IP
|
||||
# rather than routing to the target and placing the IP based on the join
|
||||
# point.
|
||||
#
|
||||
# The other case that we need to handle is when the target is close to
|
||||
# the origin airfield. In this case we also need to set up the IP first,
|
||||
# but depending on the placement of the IP we may need to place the join
|
||||
# point in a retreating position.
|
||||
#
|
||||
# A messy (and very unlikely) case that we can't do much about:
|
||||
#
|
||||
# +--------------+ +---------------+
|
||||
# | | | |
|
||||
# | IP-+---+-T |
|
||||
# | | | |
|
||||
# | | | |
|
||||
# +--------------+ +---------------+
|
||||
from gen.ato import PackageWaypoints
|
||||
|
||||
target = self.package.target.position
|
||||
package_airfield = self.package_airfield()
|
||||
|
||||
join_point = self.preferred_join_point()
|
||||
if join_point is None:
|
||||
# The whole path from the origin airfield to the target is
|
||||
# threatened. Need to retreat out of the threat area.
|
||||
join_point = self.retreat_point(self.package_airfield().position)
|
||||
# Start by picking the best IP for the attack.
|
||||
ingress_point = IpZoneGeometry(
|
||||
self.package.target.position,
|
||||
package_airfield.position,
|
||||
self.coalition,
|
||||
).find_best_ip()
|
||||
|
||||
attack_heading = join_point.heading_between_point(target)
|
||||
ingress_point = self._ingress_point(attack_heading)
|
||||
join_distance = meters(join_point.distance_to_point(target))
|
||||
ingress_distance = meters(ingress_point.distance_to_point(target))
|
||||
if join_distance < ingress_distance:
|
||||
# The second case described above. The ingress point is farther from
|
||||
# the target than the join point. Use the fallback behavior for now.
|
||||
self.legacy_package_waypoints_impl()
|
||||
return
|
||||
join_point = JoinZoneGeometry(
|
||||
self.package.target.position,
|
||||
package_airfield.position,
|
||||
ingress_point,
|
||||
self.coalition,
|
||||
).find_best_join_point()
|
||||
|
||||
# The first case described above. The ingress and join points are placed
|
||||
# reasonably relative to each other.
|
||||
egress_point = self._egress_point(attack_heading)
|
||||
# And the split point based on the best route from the IP. Since that's no
|
||||
# different than the best route *to* the IP, this is the same as the join point.
|
||||
# TODO: Estimate attack completion point based on the IP and split from there?
|
||||
self.package.waypoints = PackageWaypoints(
|
||||
WaypointBuilder.perturb(join_point),
|
||||
ingress_point,
|
||||
egress_point,
|
||||
WaypointBuilder.perturb(join_point),
|
||||
)
|
||||
|
||||
def retreat_point(self, origin: Point) -> Point:
|
||||
return self.threat_zones.closest_boundary(origin)
|
||||
|
||||
def legacy_package_waypoints_impl(self) -> None:
|
||||
from gen.ato import PackageWaypoints
|
||||
|
||||
ingress_point = self._ingress_point(self._target_heading_to_package_airfield())
|
||||
egress_point = self._egress_point(self._target_heading_to_package_airfield())
|
||||
join_point = self._rendezvous_point(ingress_point)
|
||||
split_point = self._rendezvous_point(egress_point)
|
||||
self.package.waypoints = PackageWaypoints(
|
||||
join_point,
|
||||
ingress_point,
|
||||
egress_point,
|
||||
split_point,
|
||||
)
|
||||
|
||||
def preferred_join_point(self) -> Optional[Point]:
|
||||
path = self.game.navmesh_for(self.is_player).shortest_path(
|
||||
self.package_airfield().position, self.package.target.position
|
||||
)
|
||||
for point in reversed(path):
|
||||
if not self.threat_zones.threatened(point):
|
||||
return point
|
||||
return None
|
||||
|
||||
def generate_strike(self, flight: Flight) -> StrikeFlightPlan:
|
||||
"""Generates a strike flight plan.
|
||||
|
||||
@@ -1047,26 +1001,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
|
||||
@@ -1087,23 +1031,23 @@ class FlightPlanBuilder:
|
||||
else:
|
||||
patrol_alt = feet(25000)
|
||||
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
orbit_location = builder.orbit(orbit_location, patrol_alt)
|
||||
builder = WaypointBuilder(flight, self.coalition)
|
||||
orbit = builder.orbit(orbit_location, patrol_alt)
|
||||
|
||||
return AwacsFlightPlan(
|
||||
package=self.package,
|
||||
flight=flight,
|
||||
takeoff=builder.takeoff(flight.departure),
|
||||
nav_to=builder.nav_path(
|
||||
flight.departure.position, orbit_location.position, patrol_alt
|
||||
flight.departure.position, orbit.position, patrol_alt
|
||||
),
|
||||
nav_from=builder.nav_path(
|
||||
orbit_location.position, flight.arrival.position, patrol_alt
|
||||
orbit.position, flight.arrival.position, patrol_alt
|
||||
),
|
||||
land=builder.land(flight.arrival),
|
||||
divert=builder.divert(flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
hold=orbit_location,
|
||||
hold=orbit,
|
||||
hold_duration=timedelta(hours=4),
|
||||
)
|
||||
|
||||
@@ -1134,7 +1078,7 @@ class FlightPlanBuilder:
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def anti_ship_targets_for_tgo(tgo: TheaterGroundObject) -> List[StrikeTarget]:
|
||||
def anti_ship_targets_for_tgo(tgo: NavalGroundObject) -> List[StrikeTarget]:
|
||||
return [StrikeTarget(f"{g.name} at {tgo.name}", g) for g in tgo.groups]
|
||||
|
||||
def generate_anti_ship(self, flight: Flight) -> StrikeFlightPlan:
|
||||
@@ -1171,16 +1115,17 @@ class FlightPlanBuilder:
|
||||
if isinstance(location, FrontLine):
|
||||
raise InvalidObjectiveLocation(flight.flight_type, location)
|
||||
|
||||
start, end = self.racetrack_for_objective(location, barcap=True)
|
||||
patrol_alt = meters(
|
||||
random.randint(
|
||||
int(self.doctrine.min_patrol_altitude.meters),
|
||||
int(self.doctrine.max_patrol_altitude.meters),
|
||||
)
|
||||
start_pos, end_pos = self.racetrack_for_objective(location, barcap=True)
|
||||
|
||||
preferred_alt = flight.unit_type.preferred_patrol_altitude
|
||||
randomized_alt = preferred_alt + feet(random.randint(-2, 1) * 1000)
|
||||
patrol_alt = max(
|
||||
self.doctrine.min_patrol_altitude,
|
||||
min(self.doctrine.max_patrol_altitude, randomized_alt),
|
||||
)
|
||||
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
start, end = builder.race_track(start, end, patrol_alt)
|
||||
builder = WaypointBuilder(flight, self.coalition)
|
||||
start, end = builder.race_track(start_pos, end_pos, patrol_alt)
|
||||
|
||||
return BarCapFlightPlan(
|
||||
package=self.package,
|
||||
@@ -1209,12 +1154,15 @@ class FlightPlanBuilder:
|
||||
"""
|
||||
assert self.package.waypoints is not None
|
||||
target = self.package.target.position
|
||||
heading = Heading.from_degrees(
|
||||
self.package.waypoints.join.heading_between_point(target)
|
||||
)
|
||||
start_pos = target.point_from_heading(
|
||||
heading.degrees, -self.doctrine.sweep_distance.meters
|
||||
)
|
||||
|
||||
heading = self.package.waypoints.join.heading_between_point(target)
|
||||
start = target.point_from_heading(heading, -self.doctrine.sweep_distance.meters)
|
||||
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
start, end = builder.sweep(start, target, self.doctrine.ingress_altitude)
|
||||
builder = WaypointBuilder(flight, self.coalition)
|
||||
start, end = builder.sweep(start_pos, target, self.doctrine.ingress_altitude)
|
||||
|
||||
hold = builder.hold(self._hold_point(flight))
|
||||
|
||||
@@ -1253,7 +1201,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 = []
|
||||
@@ -1305,7 +1253,9 @@ class FlightPlanBuilder:
|
||||
else:
|
||||
raise PlanningError("Could not find any enemy airfields")
|
||||
|
||||
heading = location.position.heading_between_point(closest_airfield.position)
|
||||
heading = Heading.from_degrees(
|
||||
location.position.heading_between_point(closest_airfield.position)
|
||||
)
|
||||
|
||||
position = ShapelyPoint(
|
||||
self.package.target.position.x, self.package.target.position.y
|
||||
@@ -1341,20 +1291,20 @@ class FlightPlanBuilder:
|
||||
)
|
||||
|
||||
end = location.position.point_from_heading(
|
||||
heading,
|
||||
heading.degrees,
|
||||
random.randint(int(min_cap_distance.meters), int(max_cap_distance.meters)),
|
||||
)
|
||||
diameter = random.randint(
|
||||
int(self.doctrine.cap_min_track_length.meters),
|
||||
int(self.doctrine.cap_max_track_length.meters),
|
||||
)
|
||||
start = end.point_from_heading(heading - 180, diameter)
|
||||
start = end.point_from_heading(heading.opposite.degrees, diameter)
|
||||
return start, end
|
||||
|
||||
def aewc_orbit(self, location: MissionTarget) -> Point:
|
||||
closest_boundary = self.threat_zones.closest_boundary(location.position)
|
||||
heading_to_threat_boundary = location.position.heading_between_point(
|
||||
closest_boundary
|
||||
heading_to_threat_boundary = Heading.from_degrees(
|
||||
location.position.heading_between_point(closest_boundary)
|
||||
)
|
||||
distance_to_threat = meters(
|
||||
location.position.distance_to_point(closest_boundary)
|
||||
@@ -1368,19 +1318,17 @@ class FlightPlanBuilder:
|
||||
orbit_distance = distance_to_threat - threat_buffer
|
||||
|
||||
return location.position.point_from_heading(
|
||||
orbit_heading, orbit_distance.meters
|
||||
orbit_heading.degrees, orbit_distance.meters
|
||||
)
|
||||
|
||||
def racetrack_for_frontline(
|
||||
self, origin: Point, front_line: FrontLine
|
||||
) -> Tuple[Point, Point]:
|
||||
# Find targets waypoints
|
||||
ingress, heading, distance = Conflict.frontline_vector(
|
||||
front_line, self.game.theater
|
||||
)
|
||||
center = ingress.point_from_heading(heading, distance / 2)
|
||||
ingress, heading, distance = Conflict.frontline_vector(front_line, self.theater)
|
||||
center = ingress.point_from_heading(heading.degrees, distance / 2)
|
||||
orbit_center = center.point_from_heading(
|
||||
heading - 90,
|
||||
heading.left.degrees,
|
||||
random.randint(
|
||||
int(nautical_miles(6).meters), int(nautical_miles(15).meters)
|
||||
),
|
||||
@@ -1393,8 +1341,8 @@ class FlightPlanBuilder:
|
||||
combat_width = 35000
|
||||
|
||||
radius = combat_width * 1.25
|
||||
start = orbit_center.point_from_heading(heading, radius)
|
||||
end = orbit_center.point_from_heading(heading + 180, radius)
|
||||
start = orbit_center.point_from_heading(heading.degrees, radius)
|
||||
end = orbit_center.point_from_heading(heading.opposite.degrees, radius)
|
||||
|
||||
if end.distance_to_point(origin) < start.distance_to_point(origin):
|
||||
start, end = end, start
|
||||
@@ -1408,15 +1356,15 @@ class FlightPlanBuilder:
|
||||
"""
|
||||
location = self.package.target
|
||||
|
||||
patrol_alt = meters(
|
||||
random.randint(
|
||||
int(self.doctrine.min_patrol_altitude.meters),
|
||||
int(self.doctrine.max_patrol_altitude.meters),
|
||||
)
|
||||
preferred_alt = flight.unit_type.preferred_patrol_altitude
|
||||
randomized_alt = preferred_alt + feet(random.randint(-2, 1) * 1000)
|
||||
patrol_alt = max(
|
||||
self.doctrine.min_patrol_altitude,
|
||||
min(self.doctrine.max_patrol_altitude, randomized_alt),
|
||||
)
|
||||
|
||||
# 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(
|
||||
@@ -1547,11 +1495,9 @@ class FlightPlanBuilder:
|
||||
def generate_escort(self, flight: Flight) -> StrikeFlightPlan:
|
||||
assert self.package.waypoints is not None
|
||||
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
ingress, target, egress = builder.escort(
|
||||
self.package.waypoints.ingress,
|
||||
self.package.target,
|
||||
self.package.waypoints.egress,
|
||||
builder = WaypointBuilder(flight, self.coalition)
|
||||
ingress, target = builder.escort(
|
||||
self.package.waypoints.ingress, self.package.target
|
||||
)
|
||||
hold = builder.hold(self._hold_point(flight))
|
||||
join = builder.join(self.package.waypoints.join)
|
||||
@@ -1569,7 +1515,6 @@ class FlightPlanBuilder:
|
||||
join=join,
|
||||
ingress=ingress,
|
||||
targets=[target],
|
||||
egress=egress,
|
||||
split=split,
|
||||
nav_from=builder.nav_path(
|
||||
split.position, flight.arrival.position, self.doctrine.ingress_altitude
|
||||
@@ -1590,18 +1535,16 @@ class FlightPlanBuilder:
|
||||
if not isinstance(location, FrontLine):
|
||||
raise InvalidObjectiveLocation(flight.flight_type, location)
|
||||
|
||||
ingress, heading, distance = Conflict.frontline_vector(
|
||||
location, self.game.theater
|
||||
)
|
||||
center = ingress.point_from_heading(heading, distance / 2)
|
||||
egress = ingress.point_from_heading(heading, distance)
|
||||
ingress, heading, distance = Conflict.frontline_vector(location, self.theater)
|
||||
center = ingress.point_from_heading(heading.degrees, distance / 2)
|
||||
egress = ingress.point_from_heading(heading.degrees, distance)
|
||||
|
||||
ingress_distance = ingress.distance_to_point(flight.departure.position)
|
||||
egress_distance = egress.distance_to_point(flight.departure.position)
|
||||
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,
|
||||
@@ -1629,8 +1572,8 @@ class FlightPlanBuilder:
|
||||
location = self.package.target
|
||||
|
||||
closest_boundary = self.threat_zones.closest_boundary(location.position)
|
||||
heading_to_threat_boundary = location.position.heading_between_point(
|
||||
closest_boundary
|
||||
heading_to_threat_boundary = Heading.from_degrees(
|
||||
location.position.heading_between_point(closest_boundary)
|
||||
)
|
||||
distance_to_threat = meters(
|
||||
location.position.distance_to_point(closest_boundary)
|
||||
@@ -1645,19 +1588,19 @@ class FlightPlanBuilder:
|
||||
orbit_distance = distance_to_threat - threat_buffer
|
||||
|
||||
racetrack_center = location.position.point_from_heading(
|
||||
orbit_heading, orbit_distance.meters
|
||||
orbit_heading.degrees, orbit_distance.meters
|
||||
)
|
||||
|
||||
racetrack_half_distance = Distance.from_nautical_miles(20).meters
|
||||
|
||||
racetrack_start = racetrack_center.point_from_heading(
|
||||
orbit_heading + 90, racetrack_half_distance
|
||||
orbit_heading.right.degrees, racetrack_half_distance
|
||||
)
|
||||
racetrack_end = racetrack_center.point_from_heading(
|
||||
orbit_heading - 90, racetrack_half_distance
|
||||
orbit_heading.left.degrees, 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:
|
||||
@@ -1724,49 +1667,10 @@ class FlightPlanBuilder:
|
||||
origin = flight.departure.position
|
||||
target = self.package.target.position
|
||||
join = self.package.waypoints.join
|
||||
origin_to_target = origin.distance_to_point(target)
|
||||
join_to_target = join.distance_to_point(target)
|
||||
if origin_to_target < join_to_target:
|
||||
# If the origin airfield is closer to the target than the join
|
||||
# point, plan the hold point such that it retreats from the origin
|
||||
# airfield.
|
||||
return join.point_from_heading(
|
||||
target.heading_between_point(origin), self.doctrine.push_distance.meters
|
||||
)
|
||||
|
||||
heading_to_join = origin.heading_between_point(join)
|
||||
hold_point = origin.point_from_heading(
|
||||
heading_to_join, self.doctrine.push_distance.meters
|
||||
)
|
||||
hold_distance = meters(hold_point.distance_to_point(join))
|
||||
if hold_distance >= self.doctrine.push_distance:
|
||||
# Hold point is between the origin airfield and the join point and
|
||||
# spaced sufficiently.
|
||||
return hold_point
|
||||
|
||||
# The hold point is between the origin airfield and the join point, but
|
||||
# the distance between the hold point and the join point is too short.
|
||||
# Bend the hold point out to extend the distance while maintaining the
|
||||
# minimum distance from the origin airfield to keep the AI flying
|
||||
# properly.
|
||||
origin_to_join = origin.distance_to_point(join)
|
||||
cos_theta = (
|
||||
self.doctrine.hold_distance.meters ** 2
|
||||
+ origin_to_join ** 2
|
||||
- self.doctrine.join_distance.meters ** 2
|
||||
) / (2 * self.doctrine.hold_distance.meters * origin_to_join)
|
||||
try:
|
||||
theta = math.acos(cos_theta)
|
||||
except ValueError:
|
||||
# No solution that maintains hold and join distances. Extend the
|
||||
# hold point away from the target.
|
||||
return origin.point_from_heading(
|
||||
target.heading_between_point(origin), self.doctrine.hold_distance.meters
|
||||
)
|
||||
|
||||
return origin.point_from_heading(
|
||||
heading_to_join - theta, self.doctrine.hold_distance.meters
|
||||
)
|
||||
ip = self.package.waypoints.ingress
|
||||
return HoldZoneGeometry(
|
||||
target, origin, ip, join, self.coalition, self.theater
|
||||
).find_best_hold_point()
|
||||
|
||||
# TODO: Make a model for the waypoint builder and use that in the UI.
|
||||
def generate_rtb_waypoint(
|
||||
@@ -1778,7 +1682,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(
|
||||
@@ -1790,7 +1694,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:
|
||||
@@ -1819,7 +1723,6 @@ class FlightPlanBuilder:
|
||||
ingress_type, self.package.waypoints.ingress, location
|
||||
),
|
||||
targets=target_waypoints,
|
||||
egress=builder.egress(self.package.waypoints.egress, location),
|
||||
split=split,
|
||||
nav_from=builder.nav_path(
|
||||
split.position, flight.arrival.position, self.doctrine.ingress_altitude
|
||||
@@ -1830,64 +1733,6 @@ class FlightPlanBuilder:
|
||||
lead_time=lead_time,
|
||||
)
|
||||
|
||||
def _retreating_rendezvous_point(self, attack_transition: Point) -> Point:
|
||||
"""Creates a rendezvous point that retreats from the origin airfield."""
|
||||
return attack_transition.point_from_heading(
|
||||
self.package.target.position.heading_between_point(
|
||||
self.package_airfield().position
|
||||
),
|
||||
self.doctrine.join_distance.meters,
|
||||
)
|
||||
|
||||
def _advancing_rendezvous_point(self, attack_transition: Point) -> Point:
|
||||
"""Creates a rendezvous point that advances toward the target."""
|
||||
heading = self._heading_to_package_airfield(attack_transition)
|
||||
return attack_transition.point_from_heading(
|
||||
heading, -self.doctrine.join_distance.meters
|
||||
)
|
||||
|
||||
def _rendezvous_should_retreat(self, attack_transition: Point) -> bool:
|
||||
transition_target_distance = attack_transition.distance_to_point(
|
||||
self.package.target.position
|
||||
)
|
||||
origin_target_distance = self._distance_to_package_airfield(
|
||||
self.package.target.position
|
||||
)
|
||||
|
||||
# If the origin point is closer to the target than the ingress point,
|
||||
# the rendezvous point should be positioned in a position that retreats
|
||||
# from the origin airfield.
|
||||
return origin_target_distance < transition_target_distance
|
||||
|
||||
def _rendezvous_point(self, attack_transition: Point) -> Point:
|
||||
"""Returns the position of the rendezvous point.
|
||||
|
||||
Args:
|
||||
attack_transition: The ingress or egress point for this rendezvous.
|
||||
"""
|
||||
if self._rendezvous_should_retreat(attack_transition):
|
||||
return self._retreating_rendezvous_point(attack_transition)
|
||||
return self._advancing_rendezvous_point(attack_transition)
|
||||
|
||||
def _ingress_point(self, heading: int) -> Point:
|
||||
return self.package.target.position.point_from_heading(
|
||||
heading - 180 + 15, self.doctrine.ingress_egress_distance.meters
|
||||
)
|
||||
|
||||
def _egress_point(self, heading: int) -> Point:
|
||||
return self.package.target.position.point_from_heading(
|
||||
heading - 180 - 15, self.doctrine.ingress_egress_distance.meters
|
||||
)
|
||||
|
||||
def _target_heading_to_package_airfield(self) -> int:
|
||||
return self._heading_to_package_airfield(self.package.target.position)
|
||||
|
||||
def _heading_to_package_airfield(self, point: Point) -> int:
|
||||
return self.package_airfield().position.heading_between_point(point)
|
||||
|
||||
def _distance_to_package_airfield(self, point: Point) -> int:
|
||||
return self.package_airfield().position.distance_to_point(point)
|
||||
|
||||
def package_airfield(self) -> ControlPoint:
|
||||
# We'll always have a package, but if this is being planned via the UI
|
||||
# it could be the first flight in the package.
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from typing import Optional, List, Iterator, TYPE_CHECKING, Mapping
|
||||
from collections import Iterable
|
||||
from typing import Optional, Iterator, TYPE_CHECKING, Mapping
|
||||
|
||||
from game.data.weapons import Weapon, Pylon
|
||||
from game.data.weapons import Weapon, Pylon, WeaponType
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -19,16 +20,45 @@ class Loadout:
|
||||
is_custom: bool = False,
|
||||
) -> None:
|
||||
self.name = name
|
||||
self.pylons = {k: v for k, v in pylons.items() if v is not None}
|
||||
# We clear unused pylon entries on initialization, but UI actions can still
|
||||
# cause a pylon to be emptied, so make the optional type explicit.
|
||||
self.pylons: Mapping[int, Optional[Weapon]] = {
|
||||
k: v for k, v in pylons.items() if v is not None
|
||||
}
|
||||
self.date = date
|
||||
self.is_custom = is_custom
|
||||
|
||||
def derive_custom(self, name: str) -> Loadout:
|
||||
return Loadout(name, self.pylons, self.date, is_custom=True)
|
||||
|
||||
def has_weapon_of_type(self, weapon_type: WeaponType) -> bool:
|
||||
for weapon in self.pylons.values():
|
||||
if weapon is not None and weapon.weapon_group.type is weapon_type:
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _fallback_for(
|
||||
weapon: Weapon,
|
||||
pylon: Pylon,
|
||||
date: datetime.date,
|
||||
skip_types: Optional[Iterable[WeaponType]] = None,
|
||||
) -> Optional[Weapon]:
|
||||
if skip_types is None:
|
||||
skip_types = set()
|
||||
for fallback in weapon.fallbacks:
|
||||
if not pylon.can_equip(fallback):
|
||||
continue
|
||||
if not fallback.available_on(date):
|
||||
continue
|
||||
if fallback.weapon_group.type in skip_types:
|
||||
continue
|
||||
return fallback
|
||||
return None
|
||||
|
||||
def degrade_for_date(self, unit_type: AircraftType, date: datetime.date) -> Loadout:
|
||||
if self.date is not None and self.date <= date:
|
||||
return Loadout(self.name, self.pylons, self.date)
|
||||
return Loadout(self.name, self.pylons, self.date, self.is_custom)
|
||||
|
||||
new_pylons = dict(self.pylons)
|
||||
for pylon_number, weapon in self.pylons.items():
|
||||
@@ -37,16 +67,39 @@ class Loadout:
|
||||
continue
|
||||
if not weapon.available_on(date):
|
||||
pylon = Pylon.for_aircraft(unit_type, pylon_number)
|
||||
for fallback in weapon.fallbacks:
|
||||
if not pylon.can_equip(fallback):
|
||||
continue
|
||||
if not fallback.available_on(date):
|
||||
continue
|
||||
new_pylons[pylon_number] = fallback
|
||||
break
|
||||
else:
|
||||
fallback = self._fallback_for(weapon, pylon, date)
|
||||
if fallback is None:
|
||||
del new_pylons[pylon_number]
|
||||
return Loadout(f"{self.name} ({date.year})", new_pylons, date)
|
||||
else:
|
||||
new_pylons[pylon_number] = fallback
|
||||
loadout = Loadout(self.name, new_pylons, date, self.is_custom)
|
||||
# If this is not a custom loadout, we should replace any LGBs with iron bombs if
|
||||
# the loadout lost its TGP.
|
||||
#
|
||||
# If the loadout was chosen explicitly by the user, assume they know what
|
||||
# they're doing. They may be coordinating buddy-lase.
|
||||
if not loadout.is_custom:
|
||||
loadout.replace_lgbs_if_no_tgp(unit_type, date)
|
||||
return loadout
|
||||
|
||||
def replace_lgbs_if_no_tgp(
|
||||
self, unit_type: AircraftType, date: datetime.date
|
||||
) -> None:
|
||||
if self.has_weapon_of_type(WeaponType.TGP):
|
||||
return
|
||||
|
||||
new_pylons = dict(self.pylons)
|
||||
for pylon_number, weapon in self.pylons.items():
|
||||
if weapon is not None and weapon.weapon_group.type is WeaponType.LGB:
|
||||
pylon = Pylon.for_aircraft(unit_type, pylon_number)
|
||||
fallback = self._fallback_for(
|
||||
weapon, pylon, date, skip_types={WeaponType.LGB}
|
||||
)
|
||||
if fallback is None:
|
||||
del new_pylons[pylon_number]
|
||||
else:
|
||||
new_pylons[pylon_number] = fallback
|
||||
self.pylons = new_pylons
|
||||
|
||||
@classmethod
|
||||
def iter_for(cls, flight: Flight) -> Iterator[Loadout]:
|
||||
@@ -64,14 +117,10 @@ class Loadout:
|
||||
pylons = payload["pylons"]
|
||||
yield Loadout(
|
||||
name,
|
||||
{p["num"]: Weapon.from_clsid(p["CLSID"]) for p in pylons.values()},
|
||||
{p["num"]: Weapon.with_clsid(p["CLSID"]) for p in pylons.values()},
|
||||
date=None,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def all_for(cls, flight: Flight) -> List[Loadout]:
|
||||
return list(cls.iter_for(flight))
|
||||
|
||||
@classmethod
|
||||
def default_loadout_names_for(cls, flight: Flight) -> Iterator[str]:
|
||||
from gen.flights.flight import FlightType
|
||||
@@ -92,6 +141,7 @@ class Loadout:
|
||||
FlightType.CAS: ("CAS MAVERICK F", "CAS"),
|
||||
FlightType.STRIKE: ("STRIKE",),
|
||||
FlightType.ANTISHIP: ("ANTISHIP",),
|
||||
FlightType.DEAD: ("DEAD",),
|
||||
FlightType.SEAD: ("SEAD",),
|
||||
FlightType.BAI: ("BAI",),
|
||||
FlightType.OCA_RUNWAY: ("RUNWAY_ATTACK", "RUNWAY_STRIKE"),
|
||||
@@ -128,9 +178,13 @@ class Loadout:
|
||||
if payload is not None:
|
||||
return Loadout(
|
||||
name,
|
||||
{i: Weapon.from_clsid(d["clsid"]) for i, d in payload},
|
||||
{i: Weapon.with_clsid(d["clsid"]) for i, d in payload},
|
||||
date=None,
|
||||
)
|
||||
|
||||
# TODO: Try group.load_task_default_loadout(loadout_for_task)
|
||||
return cls.empty_loadout()
|
||||
|
||||
@classmethod
|
||||
def empty_loadout(cls) -> Loadout:
|
||||
return Loadout("Empty", {}, date=None)
|
||||
|
||||
@@ -10,14 +10,15 @@ from typing import (
|
||||
TYPE_CHECKING,
|
||||
Tuple,
|
||||
Union,
|
||||
Any,
|
||||
)
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.unit import Unit
|
||||
from dcs.unitgroup import Group, VehicleGroup
|
||||
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 (
|
||||
@@ -33,24 +34,24 @@ from .flight import Flight, FlightWaypoint, FlightWaypointType
|
||||
@dataclass(frozen=True)
|
||||
class StrikeTarget:
|
||||
name: str
|
||||
target: Union[VehicleGroup, TheaterGroundObject, Unit, Group, MultiGroupTransport]
|
||||
target: Union[
|
||||
VehicleGroup, TheaterGroundObject[Any], Unit, ShipGroup, MultiGroupTransport
|
||||
]
|
||||
|
||||
|
||||
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:
|
||||
@@ -426,22 +427,19 @@ class WaypointBuilder:
|
||||
self,
|
||||
ingress: Point,
|
||||
target: MissionTarget,
|
||||
egress: Point,
|
||||
) -> Tuple[FlightWaypoint, FlightWaypoint, FlightWaypoint]:
|
||||
) -> Tuple[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)
|
||||
# the escort flights a flight plan including the ingress point and target area.
|
||||
ingress_wp = self.ingress(FlightWaypointType.INGRESS_ESCORT, ingress, target)
|
||||
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
@@ -454,9 +452,7 @@ class WaypointBuilder:
|
||||
waypoint.name = "TARGET"
|
||||
waypoint.description = "Escort the package"
|
||||
waypoint.pretty_name = "Target area"
|
||||
|
||||
egress = self.egress(egress, target)
|
||||
return ingress, waypoint, egress
|
||||
return ingress_wp, waypoint
|
||||
|
||||
@staticmethod
|
||||
def pickup(control_point: ControlPoint) -> FlightWaypoint:
|
||||
|
||||
Reference in New Issue
Block a user