mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Refactor flight plan generation.
This commit is contained in:
parent
8b717c4f4c
commit
cc7c2cc707
@ -5,6 +5,7 @@ from game.inventory import GlobalAircraftInventory
|
||||
from game.models.game_stats import GameStats
|
||||
from gen.ato import AirTaskingOrder
|
||||
from gen.flights.ai_flight_planner import CoalitionMissionPlanner
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from gen.ground_forces.ai_ground_planner import GroundPlanner
|
||||
from .event import *
|
||||
from .settings import Settings
|
||||
@ -204,6 +205,7 @@ class Game:
|
||||
return event and event.name and event.name == self.player_name
|
||||
|
||||
def pass_turn(self, no_action=False, ignored_cps: typing.Collection[ControlPoint] = None):
|
||||
ObjectiveDistanceCache.set_theater(self.theater)
|
||||
|
||||
logging.info("Pass turn")
|
||||
self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0))
|
||||
|
||||
@ -185,20 +185,16 @@ class Operation:
|
||||
self.airsupportgen.generate(self.is_awacs_enabled)
|
||||
|
||||
# Generate Activity on the map
|
||||
for cp in self.game.theater.controlpoints:
|
||||
side = cp.captured
|
||||
if side:
|
||||
country = self.current_mission.country(self.game.player_country)
|
||||
ato = self.game.blue_ato
|
||||
else:
|
||||
country = self.current_mission.country(self.game.enemy_country)
|
||||
ato = self.game.red_ato
|
||||
self.airgen.generate_flights(
|
||||
cp,
|
||||
country,
|
||||
ato,
|
||||
self.groundobjectgen.runways
|
||||
)
|
||||
self.airgen.generate_flights(
|
||||
self.current_mission.country(self.game.player_country),
|
||||
self.game.blue_ato,
|
||||
self.groundobjectgen.runways
|
||||
)
|
||||
self.airgen.generate_flights(
|
||||
self.current_mission.country(self.game.enemy_country),
|
||||
self.game.red_ato,
|
||||
self.groundobjectgen.runways
|
||||
)
|
||||
|
||||
# Generate ground units on frontline everywhere
|
||||
jtacs: List[JtacInfo] = []
|
||||
|
||||
@ -757,7 +757,7 @@ class AircraftConflictGenerator:
|
||||
for parking_slot in cp.airport.parking_slots:
|
||||
parking_slot.unit_id = None
|
||||
|
||||
def generate_flights(self, cp, country, ato: AirTaskingOrder,
|
||||
def generate_flights(self, country, ato: AirTaskingOrder,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> None:
|
||||
self.clear_parking_slots()
|
||||
|
||||
@ -768,7 +768,8 @@ class AircraftConflictGenerator:
|
||||
logging.info("Flight not generated: culled")
|
||||
continue
|
||||
logging.info(f"Generating flight: {flight.unit_type}")
|
||||
group = self.generate_planned_flight(cp, country, flight)
|
||||
group = self.generate_planned_flight(flight.from_cp, country,
|
||||
flight)
|
||||
self.setup_flight_group(group, flight, flight.flight_type,
|
||||
dynamic_runways)
|
||||
self.setup_group_activation_trigger(flight, group)
|
||||
|
||||
@ -106,7 +106,7 @@ class BriefingGenerator(MissionInfoGenerator):
|
||||
aircraft = flight.aircraft_type
|
||||
flight_unit_name = db.unit_type_name(aircraft)
|
||||
self.description += "-" * 50 + "\n"
|
||||
self.description += f"{flight_unit_name} x {flight.size + 2}\n\n"
|
||||
self.description += f"{flight_unit_name} x {flight.size}\n\n"
|
||||
|
||||
for i, wpt in enumerate(flight.waypoints):
|
||||
self.description += f"#{i + 1} -- {wpt.name} : {wpt.description}\n"
|
||||
|
||||
@ -19,6 +19,10 @@ from gen.flights.ai_flight_planner_db import (
|
||||
SEAD_CAPABLE,
|
||||
STRIKE_CAPABLE,
|
||||
)
|
||||
from gen.flights.closestairfields import (
|
||||
ClosestAirfields,
|
||||
ObjectiveDistanceCache,
|
||||
)
|
||||
from gen.flights.flight import (
|
||||
Flight,
|
||||
FlightType,
|
||||
@ -37,29 +41,6 @@ if TYPE_CHECKING:
|
||||
from game.inventory import GlobalAircraftInventory
|
||||
|
||||
|
||||
class ClosestAirfields:
|
||||
"""Precalculates which control points are closes to the given target."""
|
||||
|
||||
def __init__(self, target: MissionTarget,
|
||||
all_control_points: List[ControlPoint]) -> None:
|
||||
self.target = target
|
||||
self.closest_airfields: List[ControlPoint] = sorted(
|
||||
all_control_points, key=lambda c: self.target.distance_to(c)
|
||||
)
|
||||
|
||||
def airfields_within(self, meters: int) -> Iterator[ControlPoint]:
|
||||
"""Iterates over all airfields within the given range of the target.
|
||||
|
||||
Note that this iterates over *all* airfields, not just friendly
|
||||
airfields.
|
||||
"""
|
||||
for cp in self.closest_airfields:
|
||||
if cp.distance_to(self.target) < meters:
|
||||
yield cp
|
||||
else:
|
||||
break
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProposedFlight:
|
||||
"""A flight outline proposed by the mission planner.
|
||||
@ -208,11 +189,6 @@ class ObjectiveFinder:
|
||||
def __init__(self, game: Game, is_player: bool) -> None:
|
||||
self.game = game
|
||||
self.is_player = is_player
|
||||
# TODO: Cache globally at startup to avoid generating twice per turn?
|
||||
self.closest_airfields: Dict[str, ClosestAirfields] = {
|
||||
t.name: ClosestAirfields(t, self.game.theater.controlpoints)
|
||||
for t in self.all_possible_targets()
|
||||
}
|
||||
|
||||
def enemy_sams(self) -> Iterator[TheaterGroundObject]:
|
||||
"""Iterates over all enemy SAM sites."""
|
||||
@ -303,7 +279,7 @@ class ObjectiveFinder:
|
||||
CP.
|
||||
"""
|
||||
for cp in self.friendly_control_points():
|
||||
airfields_in_proximity = self.closest_airfields[cp.name]
|
||||
airfields_in_proximity = self.closest_airfields_to(cp)
|
||||
airfields_in_threat_range = airfields_in_proximity.airfields_within(
|
||||
self.AIRFIELD_THREAT_RANGE
|
||||
)
|
||||
@ -336,7 +312,7 @@ class ObjectiveFinder:
|
||||
|
||||
def closest_airfields_to(self, location: MissionTarget) -> ClosestAirfields:
|
||||
"""Returns the closest airfields to the given location."""
|
||||
return self.closest_airfields[location.name]
|
||||
return ObjectiveDistanceCache.get_closest_airfields(location)
|
||||
|
||||
|
||||
class CoalitionMissionPlanner:
|
||||
|
||||
51
gen/flights/closestairfields.py
Normal file
51
gen/flights/closestairfields.py
Normal file
@ -0,0 +1,51 @@
|
||||
"""Objective adjacency lists."""
|
||||
from typing import Dict, Iterator, List, Optional
|
||||
|
||||
from theater import ConflictTheater, ControlPoint, MissionTarget
|
||||
|
||||
|
||||
class ClosestAirfields:
|
||||
"""Precalculates which control points are closes to the given target."""
|
||||
|
||||
def __init__(self, target: MissionTarget,
|
||||
all_control_points: List[ControlPoint]) -> None:
|
||||
self.target = target
|
||||
self.closest_airfields: List[ControlPoint] = sorted(
|
||||
all_control_points, key=lambda c: self.target.distance_to(c)
|
||||
)
|
||||
|
||||
def airfields_within(self, meters: int) -> Iterator[ControlPoint]:
|
||||
"""Iterates over all airfields within the given range of the target.
|
||||
|
||||
Note that this iterates over *all* airfields, not just friendly
|
||||
airfields.
|
||||
"""
|
||||
for cp in self.closest_airfields:
|
||||
if cp.distance_to(self.target) < meters:
|
||||
yield cp
|
||||
else:
|
||||
break
|
||||
|
||||
|
||||
class ObjectiveDistanceCache:
|
||||
theater: Optional[ConflictTheater] = None
|
||||
closest_airfields: Dict[str, ClosestAirfields] = {}
|
||||
|
||||
@classmethod
|
||||
def set_theater(cls, theater: ConflictTheater) -> None:
|
||||
if cls.theater is not None:
|
||||
cls.closest_airfields = {}
|
||||
cls.theater = theater
|
||||
|
||||
@classmethod
|
||||
def get_closest_airfields(cls, location: MissionTarget) -> ClosestAirfields:
|
||||
if cls.theater is None:
|
||||
raise RuntimeError(
|
||||
"Call ObjectiveDistanceCache.set_theater before using"
|
||||
)
|
||||
|
||||
if location.name not in cls.closest_airfields:
|
||||
cls.closest_airfields[location.name] = ClosestAirfields(
|
||||
location, cls.theater.controlpoints
|
||||
)
|
||||
return cls.closest_airfields[location.name]
|
||||
@ -11,13 +11,15 @@ import logging
|
||||
import random
|
||||
from typing import List, Optional, TYPE_CHECKING
|
||||
|
||||
from game.data.doctrine import Doctrine, MODERN_DOCTRINE
|
||||
from .flight import Flight, FlightType, FlightWaypointType, FlightWaypoint
|
||||
from ..conflictgen import Conflict
|
||||
from theater import ControlPoint, FrontLine, MissionTarget, TheaterGroundObject
|
||||
from game.utils import nm_to_meter
|
||||
from dcs.unit import Unit
|
||||
|
||||
from game.data.doctrine import Doctrine, MODERN_DOCTRINE
|
||||
from game.utils import nm_to_meter
|
||||
from theater import ControlPoint, FrontLine, MissionTarget, TheaterGroundObject
|
||||
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
|
||||
from .waypointbuilder import WaypointBuilder
|
||||
from ..conflictgen import Conflict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
@ -103,108 +105,54 @@ class FlightPlanBuilder:
|
||||
|
||||
# TODO: Stop clobbering flight type.
|
||||
flight.flight_type = FlightType.STRIKE
|
||||
ascend = self.generate_ascend_point(flight.from_cp)
|
||||
flight.points.append(ascend)
|
||||
|
||||
heading = flight.from_cp.position.heading_between_point(
|
||||
location.position
|
||||
)
|
||||
ingress_heading = heading - 180 + 25
|
||||
egress_heading = heading - 180 - 25
|
||||
|
||||
ingress_pos = location.position.point_from_heading(
|
||||
ingress_heading, self.doctrine.ingress_egress_distance
|
||||
)
|
||||
ingress_point = FlightWaypoint(
|
||||
FlightWaypointType.INGRESS_STRIKE,
|
||||
ingress_pos.x,
|
||||
ingress_pos.y,
|
||||
self.doctrine.ingress_altitude
|
||||
)
|
||||
ingress_point.pretty_name = "INGRESS on " + location.name
|
||||
ingress_point.description = "INGRESS on " + location.name
|
||||
ingress_point.name = "INGRESS"
|
||||
flight.points.append(ingress_point)
|
||||
|
||||
if len(location.groups) > 0 and location.dcs_identifier == "AA":
|
||||
for g in location.groups:
|
||||
for j, u in enumerate(g.units):
|
||||
point = FlightWaypoint(
|
||||
FlightWaypointType.TARGET_POINT,
|
||||
u.position.x,
|
||||
u.position.y,
|
||||
0
|
||||
)
|
||||
point.description = (
|
||||
f"STRIKE [{location.name}] : {u.type} #{j}"
|
||||
)
|
||||
point.pretty_name = (
|
||||
f"STRIKE [{location.name}] : {u.type} #{j}"
|
||||
)
|
||||
point.name = f"{location.name} #{j}"
|
||||
point.only_for_player = True
|
||||
ingress_point.targets.append(location)
|
||||
flight.points.append(point)
|
||||
else:
|
||||
if hasattr(location, "obj_name"):
|
||||
buildings = self.game.theater.find_ground_objects_by_obj_name(
|
||||
location.obj_name
|
||||
)
|
||||
for building in buildings:
|
||||
if building.is_dead:
|
||||
continue
|
||||
|
||||
point = FlightWaypoint(
|
||||
FlightWaypointType.TARGET_POINT,
|
||||
building.position.x,
|
||||
building.position.y,
|
||||
0
|
||||
)
|
||||
point.description = (
|
||||
f"STRIKE on {building.obj_name} {building.category} "
|
||||
f"[{building.dcs_identifier}]"
|
||||
)
|
||||
point.pretty_name = (
|
||||
f"STRIKE on {building.obj_name} {building.category} "
|
||||
f"[{building.dcs_identifier}]"
|
||||
)
|
||||
point.name = building.obj_name
|
||||
point.only_for_player = True
|
||||
ingress_point.targets.append(building)
|
||||
flight.points.append(point)
|
||||
else:
|
||||
point = FlightWaypoint(
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
location.position.x,
|
||||
location.position.y,
|
||||
0
|
||||
)
|
||||
point.description = "STRIKE on " + location.name
|
||||
point.pretty_name = "STRIKE on " + location.name
|
||||
point.name = location.name
|
||||
point.only_for_player = True
|
||||
ingress_point.targets.append(location)
|
||||
flight.points.append(point)
|
||||
|
||||
egress_heading = heading - 180 - 25
|
||||
egress_pos = location.position.point_from_heading(
|
||||
egress_heading, self.doctrine.ingress_egress_distance
|
||||
)
|
||||
egress_point = FlightWaypoint(
|
||||
FlightWaypointType.EGRESS,
|
||||
egress_pos.x,
|
||||
egress_pos.y,
|
||||
self.doctrine.egress_altitude
|
||||
)
|
||||
egress_point.name = "EGRESS"
|
||||
egress_point.pretty_name = "EGRESS from " + location.name
|
||||
egress_point.description = "EGRESS from " + location.name
|
||||
flight.points.append(egress_point)
|
||||
|
||||
descend = self.generate_descend_point(flight.from_cp)
|
||||
flight.points.append(descend)
|
||||
builder = WaypointBuilder(self.doctrine)
|
||||
builder.ascent(flight.from_cp)
|
||||
builder.ingress_strike(ingress_pos, location)
|
||||
|
||||
rtb = self.generate_rtb_waypoint(flight.from_cp)
|
||||
flight.points.append(rtb)
|
||||
if len(location.groups) > 0 and location.dcs_identifier == "AA":
|
||||
# TODO: Replace with DEAD?
|
||||
# Strike missions on SEAD targets target units.
|
||||
for g in location.groups:
|
||||
for j, u in enumerate(g.units):
|
||||
builder.strike_point(u, f"{u.type} #{j}", location)
|
||||
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
|
||||
|
||||
builder.strike_point(
|
||||
building,
|
||||
f"{building.obj_name} {building.category}",
|
||||
location
|
||||
)
|
||||
|
||||
builder.egress(egress_pos, location)
|
||||
builder.rtb(flight.from_cp)
|
||||
|
||||
flight.points = builder.build()
|
||||
|
||||
def generate_barcap(self, flight: Flight, location: MissionTarget) -> None:
|
||||
"""Generate a BARCAP flight at a given location.
|
||||
@ -239,39 +187,11 @@ class FlightPlanBuilder:
|
||||
orbit0p = loc.point_from_heading(hdg - 90, radius)
|
||||
orbit1p = loc.point_from_heading(hdg + 90, radius)
|
||||
|
||||
# Create points
|
||||
ascend = self.generate_ascend_point(flight.from_cp)
|
||||
flight.points.append(ascend)
|
||||
|
||||
orbit0 = FlightWaypoint(
|
||||
FlightWaypointType.PATROL_TRACK,
|
||||
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"
|
||||
flight.points.append(orbit0)
|
||||
|
||||
orbit1 = FlightWaypoint(
|
||||
FlightWaypointType.PATROL,
|
||||
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"
|
||||
flight.points.append(orbit1)
|
||||
|
||||
orbit0.targets.append(location)
|
||||
|
||||
descend = self.generate_descend_point(flight.from_cp)
|
||||
flight.points.append(descend)
|
||||
|
||||
rtb = self.generate_rtb_waypoint(flight.from_cp)
|
||||
flight.points.append(rtb)
|
||||
builder = WaypointBuilder(self.doctrine)
|
||||
builder.ascent(flight.from_cp)
|
||||
builder.race_track(orbit0p, orbit1p, patrol_alt)
|
||||
builder.rtb(flight.from_cp)
|
||||
flight.points = builder.build()
|
||||
|
||||
def generate_frontline_cap(self, flight: Flight,
|
||||
location: MissionTarget) -> None:
|
||||
@ -309,40 +229,11 @@ class FlightPlanBuilder:
|
||||
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(
|
||||
FlightWaypointType.PATROL_TRACK,
|
||||
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"
|
||||
flight.points.append(orbit0)
|
||||
|
||||
orbit1 = FlightWaypoint(
|
||||
FlightWaypointType.PATROL,
|
||||
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"
|
||||
flight.points.append(orbit1)
|
||||
|
||||
# Note: Targets of 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)
|
||||
builder = WaypointBuilder(self.doctrine)
|
||||
builder.ascent(flight.from_cp)
|
||||
builder.race_track(orbit0p, orbit1p, patrol_alt)
|
||||
builder.rtb(flight.from_cp)
|
||||
flight.points = builder.build()
|
||||
|
||||
def generate_sead(self, flight: Flight, location: MissionTarget,
|
||||
custom_targets: Optional[List[Unit]] = None) -> None:
|
||||
@ -359,33 +250,30 @@ class FlightPlanBuilder:
|
||||
if custom_targets is None:
|
||||
custom_targets = []
|
||||
|
||||
flight.points = []
|
||||
flight.flight_type = random.choice([FlightType.SEAD, FlightType.DEAD])
|
||||
|
||||
ascend = self.generate_ascend_point(flight.from_cp)
|
||||
flight.points.append(ascend)
|
||||
|
||||
heading = flight.from_cp.position.heading_between_point(
|
||||
location.position
|
||||
)
|
||||
ingress_heading = heading - 180 + 25
|
||||
egress_heading = heading - 180 - 25
|
||||
|
||||
ingress_pos = location.position.point_from_heading(
|
||||
ingress_heading, self.doctrine.ingress_egress_distance
|
||||
)
|
||||
ingress_point = FlightWaypoint(
|
||||
FlightWaypointType.INGRESS_SEAD,
|
||||
ingress_pos.x,
|
||||
ingress_pos.y,
|
||||
self.doctrine.ingress_altitude
|
||||
)
|
||||
ingress_point.name = "INGRESS"
|
||||
ingress_point.pretty_name = "INGRESS on " + location.name
|
||||
ingress_point.description = "INGRESS on " + location.name
|
||||
flight.points.append(ingress_point)
|
||||
|
||||
if len(custom_targets) > 0:
|
||||
egress_heading = heading - 180 - 25
|
||||
egress_pos = location.position.point_from_heading(
|
||||
egress_heading, self.doctrine.ingress_egress_distance
|
||||
)
|
||||
|
||||
builder = WaypointBuilder(self.doctrine)
|
||||
builder.ascent(flight.from_cp)
|
||||
builder.ingress_sead(ingress_pos, location)
|
||||
|
||||
# TODO: Unify these.
|
||||
# There doesn't seem to be any reason to treat the UI fragged missions
|
||||
# different from the automatic missions.
|
||||
if custom_targets:
|
||||
for target in custom_targets:
|
||||
point = FlightWaypoint(
|
||||
FlightWaypointType.TARGET_POINT,
|
||||
@ -395,55 +283,19 @@ class FlightPlanBuilder:
|
||||
)
|
||||
point.alt_type = "RADIO"
|
||||
if flight.flight_type == FlightType.DEAD:
|
||||
point.description = "DEAD on " + target.type
|
||||
point.pretty_name = "DEAD on " + location.name
|
||||
point.only_for_player = True
|
||||
builder.dead_point(target, location.name, location)
|
||||
else:
|
||||
point.description = "SEAD on " + location.name
|
||||
point.pretty_name = "SEAD on " + location.name
|
||||
point.only_for_player = True
|
||||
flight.points.append(point)
|
||||
ingress_point.targets.append(location)
|
||||
ingress_point.targetGroup = location
|
||||
builder.sead_point(target, location.name, location)
|
||||
else:
|
||||
point = FlightWaypoint(
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
location.position.x,
|
||||
location.position.y,
|
||||
0
|
||||
)
|
||||
point.alt_type = "RADIO"
|
||||
if flight.flight_type == FlightType.DEAD:
|
||||
point.description = "DEAD on " + location.name
|
||||
point.pretty_name = "DEAD on " + location.name
|
||||
point.only_for_player = True
|
||||
builder.dead_area(location)
|
||||
else:
|
||||
point.description = "SEAD on " + location.name
|
||||
point.pretty_name = "SEAD on " + location.name
|
||||
point.only_for_player = True
|
||||
ingress_point.targets.append(location)
|
||||
ingress_point.targetGroup = location
|
||||
flight.points.append(point)
|
||||
builder.sead_area(location)
|
||||
|
||||
egress_pos = location.position.point_from_heading(
|
||||
egress_heading, self.doctrine.ingress_egress_distance
|
||||
)
|
||||
egress_point = FlightWaypoint(
|
||||
FlightWaypointType.EGRESS,
|
||||
egress_pos.x,
|
||||
egress_pos.y,
|
||||
self.doctrine.egress_altitude
|
||||
)
|
||||
egress_point.name = "EGRESS"
|
||||
egress_point.pretty_name = "EGRESS from " + location.name
|
||||
egress_point.description = "EGRESS from " + location.name
|
||||
flight.points.append(egress_point)
|
||||
builder.egress(egress_pos, location)
|
||||
builder.rtb(flight.from_cp)
|
||||
|
||||
descend = self.generate_descend_point(flight.from_cp)
|
||||
flight.points.append(descend)
|
||||
|
||||
rtb = self.generate_rtb_waypoint(flight.from_cp)
|
||||
flight.points.append(rtb)
|
||||
flight.points = builder.build()
|
||||
|
||||
def generate_cas(self, flight: Flight, location: MissionTarget) -> None:
|
||||
"""Generate a CAS flight plan for the given target.
|
||||
@ -455,89 +307,36 @@ class FlightPlanBuilder:
|
||||
if not isinstance(location, FrontLine):
|
||||
raise InvalidObjectiveLocation(flight.flight_type, location)
|
||||
|
||||
from_cp, location = location.control_points
|
||||
is_helo = getattr(flight.unit_type, "helicopter", False)
|
||||
cap_alt = 1000
|
||||
flight.points = []
|
||||
cap_alt = 500 if is_helo else 1000
|
||||
flight.flight_type = FlightType.CAS
|
||||
|
||||
ingress, heading, distance = Conflict.frontline_vector(
|
||||
from_cp, location, self.game.theater
|
||||
location.control_points[0], location.control_points[1],
|
||||
self.game.theater
|
||||
)
|
||||
center = ingress.point_from_heading(heading, distance / 2)
|
||||
egress = ingress.point_from_heading(heading, distance)
|
||||
|
||||
ascend = self.generate_ascend_point(flight.from_cp)
|
||||
if is_helo:
|
||||
cap_alt = 500
|
||||
ascend.alt = 500
|
||||
flight.points.append(ascend)
|
||||
builder = WaypointBuilder(self.doctrine)
|
||||
builder.ascent(flight.from_cp, is_helo)
|
||||
builder.ingress_cas(ingress, location)
|
||||
builder.cas(center, cap_alt)
|
||||
builder.egress(egress, location)
|
||||
builder.rtb(flight.from_cp, is_helo)
|
||||
|
||||
ingress_point = FlightWaypoint(
|
||||
FlightWaypointType.INGRESS_CAS,
|
||||
ingress.x,
|
||||
ingress.y,
|
||||
cap_alt
|
||||
)
|
||||
ingress_point.alt_type = "RADIO"
|
||||
ingress_point.name = "INGRESS"
|
||||
ingress_point.pretty_name = "INGRESS"
|
||||
ingress_point.description = "Ingress into CAS area"
|
||||
flight.points.append(ingress_point)
|
||||
|
||||
center_point = FlightWaypoint(
|
||||
FlightWaypointType.CAS,
|
||||
center.x,
|
||||
center.y,
|
||||
cap_alt
|
||||
)
|
||||
center_point.alt_type = "RADIO"
|
||||
center_point.description = "Provide CAS"
|
||||
center_point.name = "CAS"
|
||||
center_point.pretty_name = "CAS"
|
||||
flight.points.append(center_point)
|
||||
|
||||
egress_point = FlightWaypoint(
|
||||
FlightWaypointType.EGRESS,
|
||||
egress.x,
|
||||
egress.y,
|
||||
cap_alt
|
||||
)
|
||||
egress_point.alt_type = "RADIO"
|
||||
egress_point.description = "Egress from CAS area"
|
||||
egress_point.name = "EGRESS"
|
||||
egress_point.pretty_name = "EGRESS"
|
||||
flight.points.append(egress_point)
|
||||
|
||||
descend = self.generate_descend_point(flight.from_cp)
|
||||
if is_helo:
|
||||
descend.alt = 300
|
||||
flight.points.append(descend)
|
||||
|
||||
rtb = self.generate_rtb_waypoint(flight.from_cp)
|
||||
flight.points.append(rtb)
|
||||
flight.points = builder.build()
|
||||
|
||||
# TODO: Make a model for the waypoint builder and use that in the UI.
|
||||
def generate_ascend_point(self, departure: ControlPoint) -> FlightWaypoint:
|
||||
"""Generate ascend point.
|
||||
|
||||
Args:
|
||||
departure: Departure airfield or carrier.
|
||||
"""
|
||||
ascend_heading = departure.heading
|
||||
pos_ascend = departure.position.point_from_heading(
|
||||
ascend_heading, 10000
|
||||
)
|
||||
ascend = FlightWaypoint(
|
||||
FlightWaypointType.ASCEND_POINT,
|
||||
pos_ascend.x,
|
||||
pos_ascend.y,
|
||||
self.doctrine.pattern_altitude
|
||||
)
|
||||
ascend.name = "ASCEND"
|
||||
ascend.alt_type = "RADIO"
|
||||
ascend.description = "Ascend"
|
||||
ascend.pretty_name = "Ascend"
|
||||
return ascend
|
||||
builder = WaypointBuilder(self.doctrine)
|
||||
builder.ascent(departure)
|
||||
return builder.build()[0]
|
||||
|
||||
def generate_descend_point(self, arrival: ControlPoint) -> FlightWaypoint:
|
||||
"""Generate approach/descend point.
|
||||
@ -545,21 +344,9 @@ class FlightPlanBuilder:
|
||||
Args:
|
||||
arrival: Arrival airfield or carrier.
|
||||
"""
|
||||
ascend_heading = arrival.heading
|
||||
descend = arrival.position.point_from_heading(
|
||||
ascend_heading - 180, 10000
|
||||
)
|
||||
descend = FlightWaypoint(
|
||||
FlightWaypointType.DESCENT_POINT,
|
||||
descend.x,
|
||||
descend.y,
|
||||
self.doctrine.pattern_altitude
|
||||
)
|
||||
descend.name = "DESCEND"
|
||||
descend.alt_type = "RADIO"
|
||||
descend.description = "Descend to pattern alt"
|
||||
descend.pretty_name = "Descend to pattern alt"
|
||||
return descend
|
||||
builder = WaypointBuilder(self.doctrine)
|
||||
builder.descent(arrival)
|
||||
return builder.build()[0]
|
||||
|
||||
@staticmethod
|
||||
def generate_rtb_waypoint(arrival: ControlPoint) -> FlightWaypoint:
|
||||
@ -568,15 +355,6 @@ class FlightPlanBuilder:
|
||||
Args:
|
||||
arrival: Arrival airfield or carrier.
|
||||
"""
|
||||
rtb = arrival.position
|
||||
rtb = FlightWaypoint(
|
||||
FlightWaypointType.LANDING_POINT,
|
||||
rtb.x,
|
||||
rtb.y,
|
||||
0
|
||||
)
|
||||
rtb.name = "LANDING"
|
||||
rtb.alt_type = "RADIO"
|
||||
rtb.description = "RTB"
|
||||
rtb.pretty_name = "RTB"
|
||||
return rtb
|
||||
builder = WaypointBuilder(self.doctrine)
|
||||
builder.land(arrival)
|
||||
return builder.build()[0]
|
||||
|
||||
270
gen/flights/waypointbuilder.py
Normal file
270
gen/flights/waypointbuilder.py
Normal file
@ -0,0 +1,270 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.unit import Unit
|
||||
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.utils import nm_to_meter
|
||||
from theater import ControlPoint, MissionTarget, TheaterGroundObject
|
||||
from .flight import FlightWaypoint, FlightWaypointType
|
||||
|
||||
|
||||
class WaypointBuilder:
|
||||
def __init__(self, doctrine: Doctrine) -> None:
|
||||
self.doctrine = doctrine
|
||||
self.waypoints: List[FlightWaypoint] = []
|
||||
self.ingress_point: Optional[FlightWaypoint] = None
|
||||
|
||||
def build(self) -> List[FlightWaypoint]:
|
||||
return self.waypoints
|
||||
|
||||
def ascent(self, departure: ControlPoint, is_helo: bool = False) -> None:
|
||||
"""Create ascent waypoint for the given departure airfield or carrier.
|
||||
|
||||
Args:
|
||||
departure: Departure airfield or carrier.
|
||||
"""
|
||||
# TODO: Pick runway based on wind direction.
|
||||
heading = departure.heading
|
||||
position = departure.position.point_from_heading(
|
||||
heading, nm_to_meter(5)
|
||||
)
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.ASCEND_POINT,
|
||||
position.x,
|
||||
position.y,
|
||||
500 if is_helo else self.doctrine.pattern_altitude
|
||||
)
|
||||
waypoint.name = "ASCEND"
|
||||
waypoint.alt_type = "RADIO"
|
||||
waypoint.description = "Ascend"
|
||||
waypoint.pretty_name = "Ascend"
|
||||
self.waypoints.append(waypoint)
|
||||
|
||||
def descent(self, arrival: ControlPoint, is_helo: bool = False) -> None:
|
||||
"""Create descent waypoint for the given arrival airfield or carrier.
|
||||
|
||||
Args:
|
||||
arrival: Arrival airfield or carrier.
|
||||
"""
|
||||
# TODO: Pick runway based on wind direction.
|
||||
# ControlPoint.heading is the departure heading.
|
||||
heading = (arrival.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 is_helo else self.doctrine.pattern_altitude
|
||||
)
|
||||
waypoint.name = "DESCEND"
|
||||
waypoint.alt_type = "RADIO"
|
||||
waypoint.description = "Descend to pattern altitude"
|
||||
waypoint.pretty_name = "Ascend"
|
||||
self.waypoints.append(waypoint)
|
||||
|
||||
def land(self, arrival: ControlPoint) -> None:
|
||||
"""Create descent waypoint for the given arrival airfield or carrier.
|
||||
|
||||
Args:
|
||||
arrival: Arrival airfield or carrier.
|
||||
"""
|
||||
position = arrival.position
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.LANDING_POINT,
|
||||
position.x,
|
||||
position.y,
|
||||
0
|
||||
)
|
||||
waypoint.name = "LANDING"
|
||||
waypoint.alt_type = "RADIO"
|
||||
waypoint.description = "Land"
|
||||
waypoint.pretty_name = "Land"
|
||||
self.waypoints.append(waypoint)
|
||||
|
||||
def ingress_cas(self, position: Point, objective: MissionTarget) -> None:
|
||||
self._ingress(FlightWaypointType.INGRESS_CAS, position, objective)
|
||||
|
||||
def ingress_sead(self, position: Point, objective: MissionTarget) -> None:
|
||||
self._ingress(FlightWaypointType.INGRESS_SEAD, position, objective)
|
||||
|
||||
def ingress_strike(self, position: Point, objective: MissionTarget) -> None:
|
||||
self._ingress(FlightWaypointType.INGRESS_STRIKE, position, objective)
|
||||
|
||||
def _ingress(self, ingress_type: FlightWaypointType, position: Point,
|
||||
objective: MissionTarget) -> None:
|
||||
if self.ingress_point is not None:
|
||||
raise RuntimeError("A flight plan can have only one ingress point.")
|
||||
|
||||
waypoint = FlightWaypoint(
|
||||
ingress_type,
|
||||
position.x,
|
||||
position.y,
|
||||
self.doctrine.ingress_altitude
|
||||
)
|
||||
waypoint.pretty_name = "INGRESS on " + objective.name
|
||||
waypoint.description = "INGRESS on " + objective.name
|
||||
waypoint.name = "INGRESS"
|
||||
self.waypoints.append(waypoint)
|
||||
self.ingress_point = waypoint
|
||||
|
||||
def egress(self, position: Point, target: MissionTarget) -> None:
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.EGRESS,
|
||||
position.x,
|
||||
position.y,
|
||||
self.doctrine.ingress_altitude
|
||||
)
|
||||
waypoint.pretty_name = "EGRESS from " + target.name
|
||||
waypoint.description = "EGRESS from " + target.name
|
||||
waypoint.name = "EGRESS"
|
||||
self.waypoints.append(waypoint)
|
||||
|
||||
def dead_point(self, target: Union[TheaterGroundObject, Unit], name: str,
|
||||
location: MissionTarget) -> None:
|
||||
self._target_point(target, name, f"STRIKE [{location.name}]: {name}",
|
||||
location)
|
||||
# TODO: Seems fishy.
|
||||
self.ingress_point.targetGroup = location
|
||||
|
||||
def sead_point(self, target: Union[TheaterGroundObject, Unit], name: str,
|
||||
location: MissionTarget) -> None:
|
||||
self._target_point(target, name, f"STRIKE [{location.name}]: {name}",
|
||||
location)
|
||||
# TODO: Seems fishy.
|
||||
self.ingress_point.targetGroup = location
|
||||
|
||||
def strike_point(self, target: Union[TheaterGroundObject, Unit], name: str,
|
||||
location: MissionTarget) -> None:
|
||||
self._target_point(target, name, f"STRIKE [{location.name}]: {name}",
|
||||
location)
|
||||
|
||||
def _target_point(self, target: Union[TheaterGroundObject, Unit], name: str,
|
||||
description: str, location: MissionTarget) -> None:
|
||||
if self.ingress_point is None:
|
||||
raise RuntimeError(
|
||||
"An ingress point must be added before target points."
|
||||
)
|
||||
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.TARGET_POINT,
|
||||
target.position.x,
|
||||
target.position.y,
|
||||
0
|
||||
)
|
||||
waypoint.description = description
|
||||
waypoint.pretty_name = description
|
||||
waypoint.name = name
|
||||
waypoint.only_for_player = True
|
||||
self.waypoints.append(waypoint)
|
||||
# TODO: This seems wrong, but it's what was there before.
|
||||
self.ingress_point.targets.append(location)
|
||||
|
||||
def sead_area(self, target: MissionTarget) -> None:
|
||||
self._target_area(f"SEAD on {target.name}", target)
|
||||
# TODO: Seems fishy.
|
||||
self.ingress_point.targetGroup = target
|
||||
|
||||
def dead_area(self, target: MissionTarget) -> None:
|
||||
self._target_area(f"DEAD on {target.name}", target)
|
||||
# TODO: Seems fishy.
|
||||
self.ingress_point.targetGroup = target
|
||||
|
||||
def _target_area(self, name: str, location: MissionTarget) -> None:
|
||||
if self.ingress_point is None:
|
||||
raise RuntimeError(
|
||||
"An ingress point must be added before target points."
|
||||
)
|
||||
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
location.position.x,
|
||||
location.position.y,
|
||||
0
|
||||
)
|
||||
waypoint.description = name
|
||||
waypoint.pretty_name = name
|
||||
waypoint.name = name
|
||||
waypoint.only_for_player = True
|
||||
self.waypoints.append(waypoint)
|
||||
# TODO: This seems wrong, but it's what was there before.
|
||||
self.ingress_point.targets.append(location)
|
||||
|
||||
def cas(self, position: Point, altitude: int) -> None:
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.CAS,
|
||||
position.x,
|
||||
position.y,
|
||||
altitude
|
||||
)
|
||||
waypoint.alt_type = "RADIO"
|
||||
waypoint.description = "Provide CAS"
|
||||
waypoint.name = "CAS"
|
||||
waypoint.pretty_name = "CAS"
|
||||
self.waypoints.append(waypoint)
|
||||
|
||||
def race_track_start(self, position: Point, altitude: int) -> None:
|
||||
"""Creates a racetrack start waypoint.
|
||||
|
||||
Args:
|
||||
position: Position of the waypoint.
|
||||
altitude: Altitude of the racetrack in meters.
|
||||
"""
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.PATROL_TRACK,
|
||||
position.x,
|
||||
position.y,
|
||||
altitude
|
||||
)
|
||||
waypoint.name = "RACETRACK START"
|
||||
waypoint.description = "Orbit between this point and the next point"
|
||||
waypoint.pretty_name = "Race-track start"
|
||||
self.waypoints.append(waypoint)
|
||||
|
||||
# TODO: Does this actually do anything?
|
||||
# orbit0.targets.append(location)
|
||||
# Note: Targets of PATROL TRACK waypoints are the points to be defended.
|
||||
# orbit0.targets.append(flight.from_cp)
|
||||
# orbit0.targets.append(center)
|
||||
|
||||
def race_track_end(self, position: Point, altitude: int) -> None:
|
||||
"""Creates a racetrack end waypoint.
|
||||
|
||||
Args:
|
||||
position: Position of the waypoint.
|
||||
altitude: Altitude of the racetrack in meters.
|
||||
"""
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.PATROL,
|
||||
position.x,
|
||||
position.y,
|
||||
altitude
|
||||
)
|
||||
waypoint.name = "RACETRACK END"
|
||||
waypoint.description = "Orbit between this point and the previous point"
|
||||
waypoint.pretty_name = "Race-track end"
|
||||
self.waypoints.append(waypoint)
|
||||
|
||||
def race_track(self, start: Point, end: Point, altitude: int) -> None:
|
||||
"""Creates two waypoint for a racetrack orbit.
|
||||
|
||||
Args:
|
||||
start: The beginning racetrack waypoint.
|
||||
end: The ending racetrack waypoint.
|
||||
altitude: The racetrack altitude.
|
||||
"""
|
||||
self.race_track_start(start, altitude)
|
||||
self.race_track_end(end, altitude)
|
||||
|
||||
def rtb(self, arrival: ControlPoint, is_helo: bool = False) -> None:
|
||||
"""Creates descent ant landing waypoints for the given control point.
|
||||
|
||||
Args:
|
||||
arrival: Arrival airfield or carrier.
|
||||
"""
|
||||
self.descent(arrival, is_helo)
|
||||
self.land(arrival)
|
||||
Loading…
x
Reference in New Issue
Block a user