Merge remote-tracking branch 'khopa/develop' into helipads

This commit is contained in:
Khopa
2021-06-10 13:01:24 +02:00
96 changed files with 2054 additions and 281 deletions

View File

@@ -39,10 +39,12 @@ from dcs.planes import (
Su_33,
Tu_22M3,
)
from dcs.planes import IL_78M
from dcs.point import MovingPoint, PointAction
from dcs.task import (
AWACS,
AWACSTaskAction,
ActivateBeaconCommand,
AntishipStrike,
AttackGroup,
Bombing,
@@ -61,8 +63,10 @@ from dcs.task import (
OptReactOnThreat,
OptRestrictJettison,
OrbitAction,
Refueling,
RunwayAttack,
StartCommand,
Tanker,
Targets,
Transport,
WeaponType,
@@ -80,7 +84,7 @@ from game.data.weapons import Pylon
from game.db import GUN_RELIANT_AIRFRAMES
from game.factions.faction import Faction
from game.settings import Settings
from game.squadrons import Pilot, Squadron
from game.squadrons import Pilot
from game.theater.controlpoint import (
Airfield,
ControlPoint,
@@ -103,13 +107,15 @@ from gen.flights.flight import (
)
from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
from gen.runways import RunwayData
from .airsupportgen import AirSupport, AwacsInfo
from gen.tacan import TacanBand, TacanRegistry
from .airsupportgen import AirSupport, AwacsInfo, TankerInfo
from .callsigns import callsign_for_support_unit
from .flights.flightplan import (
AwacsFlightPlan,
CasFlightPlan,
LoiterFlightPlan,
PatrollingFlightPlan,
RefuelingFlightPlan,
SweepFlightPlan,
)
from .flights.traveltime import GroundSpeed, TotEstimator
@@ -665,10 +671,16 @@ AIRCRAFT_DATA: Dict[str, AircraftData] = {
channel_allocator=None,
channel_namer=SCR522ChannelNamer,
),
"JAS39Gripen": AircraftData(
inter_flight_radio=get_radio("R&S Series 6000"),
intra_flight_radio=get_radio("R&S Series 6000"),
channel_allocator=None,
),
}
AIRCRAFT_DATA["A-10C_2"] = AIRCRAFT_DATA["A-10C"]
AIRCRAFT_DATA["P-51D-30-NA"] = AIRCRAFT_DATA["P-51D"]
AIRCRAFT_DATA["P-47D-30"] = AIRCRAFT_DATA["P-51D"]
AIRCRAFT_DATA["JAS39Gripen_AG"] = AIRCRAFT_DATA["JAS39Gripen"]
class AircraftConflictGenerator:
@@ -678,6 +690,7 @@ class AircraftConflictGenerator:
settings: Settings,
game: Game,
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,
unit_map: UnitMap,
air_support: AirSupport,
) -> None:
@@ -685,6 +698,7 @@ class AircraftConflictGenerator:
self.game = game
self.settings = settings
self.radio_registry = radio_registry
self.tacan_registy = tacan_registry
self.unit_map = unit_map
self.flights: List[FlightData] = []
self.air_support = air_support
@@ -818,7 +832,10 @@ class AircraftConflictGenerator:
OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)
)
if flight.flight_type == FlightType.AEWC:
if (
flight.flight_type == FlightType.AEWC
or flight.flight_type == FlightType.REFUELING
):
channel = self.radio_registry.alloc_uhf()
else:
channel = self.get_intra_flight_channel(unit_type)
@@ -873,6 +890,24 @@ class AircraftConflictGenerator:
)
)
if isinstance(flight.flight_plan, RefuelingFlightPlan):
callsign = callsign_for_support_unit(group)
tacan = self.tacan_registy.alloc_for_band(TacanBand.Y)
variant = db.unit_type_name(flight.flight_plan.flight.unit_type)
self.air_support.tankers.append(
TankerInfo(
group_name=str(group.name),
callsign=callsign,
variant=variant,
freq=channel,
tacan=tacan,
start_time=flight.flight_plan.patrol_start_time,
end_time=flight.flight_plan.patrol_end_time,
blue=flight.departure.captured,
)
)
def _generate_at_airport(
self,
name: str,
@@ -1477,6 +1512,32 @@ class AircraftConflictGenerator:
group.points[0].tasks.append(AWACSTaskAction())
def configure_refueling(
self,
group: FlyingGroup,
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
) -> None:
group.task = Refueling.name
if not isinstance(flight.flight_plan, RefuelingFlightPlan):
logging.error(
f"Cannot configure racetrack refueling tasks for {flight} because it "
"does not have an racetrack refueling flight plan."
)
return
self._setup_group(group, package, flight, dynamic_runways)
self.configure_behavior(
flight,
group,
react_on_threat=OptReactOnThreat.Values.EvadeFire,
roe=OptROE.Values.WeaponHold,
restrict_jettison=True,
)
def configure_escort(
self,
group: FlyingGroup,
@@ -1555,6 +1616,8 @@ class AircraftConflictGenerator:
self.configure_sweep(group, package, flight, dynamic_runways)
elif flight_type == FlightType.AEWC:
self.configure_awacs(group, package, flight, dynamic_runways)
elif flight_type == FlightType.REFUELING:
self.configure_refueling(group, package, flight, dynamic_runways)
elif flight_type in [FlightType.CAS, FlightType.BAI]:
self.configure_cas(group, package, flight, dynamic_runways)
elif flight_type == FlightType.DEAD:
@@ -1622,7 +1685,7 @@ class AircraftConflictGenerator:
for idx, point in enumerate(filtered_points):
PydcsWaypointBuilder.for_waypoint(
point, group, package, flight, self.m
point, group, package, flight, self.m, self.air_support
).build()
# Set here rather than when the FlightData is created so they waypoints
@@ -1696,12 +1759,14 @@ class PydcsWaypointBuilder:
package: Package,
flight: Flight,
mission: Mission,
air_support: AirSupport,
) -> None:
self.waypoint = waypoint
self.group = group
self.package = package
self.flight = flight
self.mission = mission
self.air_support = air_support
def build(self) -> MovingPoint:
waypoint = self.group.add_waypoint(
@@ -1737,6 +1802,7 @@ class PydcsWaypointBuilder:
package: Package,
flight: Flight,
mission: Mission,
air_support: AirSupport,
) -> PydcsWaypointBuilder:
builders = {
FlightWaypointType.DROP_OFF: CargoStopBuilder,
@@ -1756,7 +1822,7 @@ class PydcsWaypointBuilder:
FlightWaypointType.PICKUP: CargoStopBuilder,
}
builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder)
return builder(waypoint, group, package, flight, mission)
return builder(waypoint, group, package, flight, mission, air_support)
def _viggen_client_tot(self) -> bool:
"""Viggen player aircraft consider any waypoint with a TOT set to be a target ("M") waypoint.
@@ -2139,6 +2205,8 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
# is their first priority and they will not engage any targets because
# they're fully focused on orbiting. If the STE task is first, they will
# engage targets if available and orbit if they find nothing to shoot.
if self.flight.flight_type is FlightType.REFUELING:
self.configure_refueling_actions(waypoint)
# TODO: Move the properties of this task into the flight plan?
# CAP is the only current user of this so it's not a big deal, but might
@@ -2153,17 +2221,48 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
)
)
racetrack = ControlledTask(
OrbitAction(
# TODO: Set orbit speeds for all race tracks and remove this special case.
if isinstance(flight_plan, RefuelingFlightPlan):
orbit = OrbitAction(
altitude=waypoint.alt,
pattern=OrbitAction.OrbitPattern.RaceTrack,
speed=int(flight_plan.patrol_speed.kph),
)
else:
orbit = OrbitAction(
altitude=waypoint.alt, pattern=OrbitAction.OrbitPattern.RaceTrack
)
)
racetrack = ControlledTask(orbit)
self.set_waypoint_tot(waypoint, flight_plan.patrol_start_time)
racetrack.stop_after_time(int(flight_plan.patrol_end_time.total_seconds()))
waypoint.add_task(racetrack)
return waypoint
def configure_refueling_actions(self, waypoint: MovingPoint) -> None:
waypoint.add_task(Tanker())
if self.flight.unit_type != IL_78M:
tanker_info = self.air_support.tankers[-1]
tacan = tanker_info.tacan
tacan_callsign = {
"Texaco": "TEX",
"Arco": "ARC",
"Shell": "SHL",
}.get(tanker_info.callsign)
waypoint.add_task(
ActivateBeaconCommand(
tacan.number,
tacan.band.value,
tacan_callsign,
bearing=True,
unit_id=self.group.units[0].id,
aa=True,
)
)
class RaceTrackEndBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:

View File

@@ -674,7 +674,7 @@ AIRFIELD_DATA = {
vor=("DAN", MHz(108, 400)),
atc=AtcData(MHz(3, 900), MHz(38, 700), MHz(122, 100), MHz(360, 100)),
ils={
"50": ("IDAN", MHz(109, 300)),
"05": ("IDAN", MHz(109, 300)),
"23": ("DANM", MHz(111, 700)),
},
),

View File

@@ -54,6 +54,8 @@ class TankerInfo:
variant: str
freq: RadioFrequency
tacan: TacanChannel
start_time: Optional[timedelta]
end_time: Optional[timedelta]
blue: bool
@@ -100,84 +102,86 @@ class AirSupportConflictGenerator:
else self.conflict.red_cp
)
fallback_tanker_number = 0
if not self.game.settings.disable_legacy_tanker:
for i, tanker_unit_type in enumerate(
self.game.faction_for(player=True).tankers
):
alt, airspeed = self._get_tanker_params(tanker_unit_type)
variant = db.unit_type_name(tanker_unit_type)
freq = self.radio_registry.alloc_uhf()
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y)
tanker_heading = (
self.conflict.red_cp.position.heading_between_point(
self.conflict.blue_cp.position
fallback_tanker_number = 0
for i, tanker_unit_type in enumerate(
self.game.faction_for(player=True).tankers
):
alt, airspeed = self._get_tanker_params(tanker_unit_type)
variant = db.unit_type_name(tanker_unit_type)
freq = self.radio_registry.alloc_uhf()
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y)
tanker_heading = (
self.conflict.red_cp.position.heading_between_point(
self.conflict.blue_cp.position
)
+ TANKER_HEADING_OFFSET * i
)
+ TANKER_HEADING_OFFSET * i
)
tanker_position = player_cp.position.point_from_heading(
tanker_heading, TANKER_DISTANCE
)
tanker_group = self.mission.refuel_flight(
country=self.mission.country(self.game.player_country),
name=namegen.next_tanker_name(
self.mission.country(self.game.player_country), tanker_unit_type
),
airport=None,
plane_type=tanker_unit_type,
position=tanker_position,
altitude=alt,
race_distance=58000,
frequency=freq.mhz,
start_type=StartType.Warm,
speed=airspeed,
tacanchannel=str(tacan),
)
tanker_group.set_frequency(freq.mhz)
tanker_position = player_cp.position.point_from_heading(
tanker_heading, TANKER_DISTANCE
)
tanker_group = self.mission.refuel_flight(
country=self.mission.country(self.game.player_country),
name=namegen.next_tanker_name(
self.mission.country(self.game.player_country), tanker_unit_type
),
airport=None,
plane_type=tanker_unit_type,
position=tanker_position,
altitude=alt,
race_distance=58000,
frequency=freq.mhz,
start_type=StartType.Warm,
speed=airspeed,
tacanchannel=str(tacan),
)
tanker_group.set_frequency(freq.mhz)
callsign = callsign_for_support_unit(tanker_group)
tacan_callsign = {
"Texaco": "TEX",
"Arco": "ARC",
"Shell": "SHL",
}.get(callsign)
if tacan_callsign is None:
# The dict above is all the callsigns currently in the game, but
# non-Western countries don't use the callsigns and instead just
# use numbers. It's possible that none of those nations have
# TACAN compatible refueling aircraft, but fallback just in
# case.
tacan_callsign = f"TK{fallback_tanker_number}"
fallback_tanker_number += 1
callsign = callsign_for_support_unit(tanker_group)
tacan_callsign = {
"Texaco": "TEX",
"Arco": "ARC",
"Shell": "SHL",
}.get(callsign)
if tacan_callsign is None:
# The dict above is all the callsigns currently in the game, but
# non-Western countries don't use the callsigns and instead just
# use numbers. It's possible that none of those nations have
# TACAN compatible refueling aircraft, but fallback just in
# case.
tacan_callsign = f"TK{fallback_tanker_number}"
fallback_tanker_number += 1
if tanker_unit_type != IL_78M:
# Override PyDCS tacan channel.
tanker_group.points[0].tasks.pop()
tanker_group.points[0].tasks.append(
ActivateBeaconCommand(
tacan.number,
tacan.band.value,
tacan_callsign,
True,
tanker_group.units[0].id,
True,
if tanker_unit_type != IL_78M:
# Override PyDCS tacan channel.
tanker_group.points[0].tasks.pop()
tanker_group.points[0].tasks.append(
ActivateBeaconCommand(
tacan.number,
tacan.band.value,
tacan_callsign,
True,
tanker_group.units[0].id,
True,
)
)
tanker_group.points[0].tasks.append(SetInvisibleCommand(True))
tanker_group.points[0].tasks.append(SetImmortalCommand(True))
self.air_support.tankers.append(
TankerInfo(
str(tanker_group.name),
callsign,
variant,
freq,
tacan,
blue=True,
)
)
tanker_group.points[0].tasks.append(SetInvisibleCommand(True))
tanker_group.points[0].tasks.append(SetImmortalCommand(True))
self.air_support.tankers.append(
TankerInfo(
str(tanker_group.name),
callsign,
variant,
freq,
tacan,
blue=True,
)
)
if not self.game.settings.disable_legacy_aewc:
possible_awacs = [
a

View File

@@ -168,9 +168,10 @@ class Package:
# likely to be the main task than others. For example, a package with
# only CAP flights is a CAP package, a flight with CAP and strike is a
# strike package, a flight with CAP and DEAD is a DEAD package, and a
# flight with strike and SEAD is an OCA/Strike package. The type of
# package is determined by the highest priority flight in the package.
task_priorities = [
# flight with strike and SEAD is an OCA/Strike package. This list defines the
# priority order for package task names. The package's primary task will be the
# first task in this list that matches a flight in the package.
tasks_by_priority = [
FlightType.CAS,
FlightType.STRIKE,
FlightType.ANTISHIP,
@@ -183,10 +184,11 @@ class Package:
FlightType.TARCAP,
FlightType.BARCAP,
FlightType.AEWC,
FlightType.REFUELING,
FlightType.SWEEP,
FlightType.ESCORT,
]
for task in task_priorities:
for task in tasks_by_priority:
if flight_counts[task]:
return task

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import logging
import math
import operator
import random
from collections import defaultdict
@@ -524,6 +525,24 @@ class ObjectiveFinder:
raise RuntimeError("Found no friendly control points. You probably lost.")
return farthest
def closest_friendly_control_point(self) -> ControlPoint:
"""Finds the friendly control point that is closest to any threats."""
threat_zones = self.game.threat_zone_for(not self.is_player)
closest = None
min_distance = meters(math.inf)
for cp in self.friendly_control_points():
if isinstance(cp, OffMapSpawn):
continue
distance = threat_zones.distance_to_threat(cp.position)
if distance < min_distance:
closest = cp
min_distance = distance
if closest is None:
raise RuntimeError("Found no friendly control points. You probably lost.")
return closest
def enemy_control_points(self) -> Iterator[ControlPoint]:
"""Iterates over all enemy control points."""
return (
@@ -582,6 +601,7 @@ class CoalitionMissionPlanner:
MAX_SEAD_RANGE = nautical_miles(150)
MAX_STRIKE_RANGE = nautical_miles(150)
MAX_AWEC_RANGE = nautical_miles(200)
MAX_TANKER_RANGE = nautical_miles(200)
def __init__(self, game: Game, is_player: bool) -> None:
self.game = game
@@ -628,6 +648,11 @@ class CoalitionMissionPlanner:
asap=True,
)
yield ProposedMission(
self.objective_finder.closest_friendly_control_point(),
[ProposedFlight(FlightType.REFUELING, 1, self.MAX_TANKER_RANGE)],
)
# Find friendly CPs within 100 nmi from an enemy airfield, plan CAP.
for cp in self.objective_finder.vulnerable_control_points():
# Plan CAP in such a way, that it is established during the whole desired mission length

View File

@@ -51,10 +51,13 @@ from dcs.planes import (
F_5E_3,
F_86F_Sabre,
IL_76MD,
I_16,
IL_78M,
JF_17,
J_11A,
Ju_88A4,
KC130,
KC135MPRS,
KC_135,
KJ_2000,
L_39ZA,
MQ_9_Reaper,
@@ -77,6 +80,7 @@ from dcs.planes import (
P_51D_30_NA,
RQ_1A_Predator,
S_3B,
S_3B_Tanker,
SpitfireLFMkIX,
SpitfireLFMkIXCW,
Su_17M4,
@@ -104,6 +108,7 @@ from gen.flights.flight import FlightType
from pydcs_extensions.a4ec.a4ec import A_4E_C
from pydcs_extensions.f22a.f22a import F_22A
from pydcs_extensions.hercules.hercules import Hercules
from pydcs_extensions.jas39.jas39 import JAS39Gripen, JAS39Gripen_AG
from pydcs_extensions.mb339.mb339 import MB_339PAN
from pydcs_extensions.su57.su57 import Su_57
@@ -134,6 +139,7 @@ CAP_CAPABLE = [
FA_18C_hornet,
F_16A,
F_4E,
JAS39Gripen,
JF_17,
MiG_23MLD,
MiG_21Bis,
@@ -174,6 +180,7 @@ CAS_CAPABLE = [
FA_18C_hornet,
Tornado_GR4,
Tornado_IDS,
JAS39Gripen_AG,
JF_17,
AV8BNA,
A_10A,
@@ -242,6 +249,7 @@ SEAD_CAPABLE = [
A_4E_C,
F_14B,
F_14A_135_GR,
JAS39Gripen_AG,
AV8BNA,
Su_24M,
Su_17M4,
@@ -257,6 +265,7 @@ DEAD_CAPABLE = [
AJS37,
F_14B,
F_14A_135_GR,
JAS39Gripen_AG,
B_1B,
B_52H,
Tu_160,
@@ -292,6 +301,7 @@ STRIKE_CAPABLE = [
F_16A,
F_14B,
F_14A_135_GR,
JAS39Gripen_AG,
Tornado_IDS,
Su_17M4,
Su_24MR,
@@ -342,6 +352,7 @@ ANTISHIP_CAPABLE = [
AJS37,
Tu_22M3,
FA_18C_hornet,
JAS39Gripen_AG,
Su_24M,
Su_17M4,
JF_17,
@@ -394,6 +405,15 @@ AEWC_CAPABLE = [
KJ_2000,
]
# Priority is given to the tankers that can carry the most fuel.
REFUELING_CAPABALE = [
KC_135,
KC135MPRS,
IL_78M,
KC130,
S_3B_Tanker,
]
def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
cap_missions = (FlightType.BARCAP, FlightType.TARCAP, FlightType.SWEEP)
@@ -421,6 +441,8 @@ def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
return CAP_CAPABLE
elif task == FlightType.AEWC:
return AEWC_CAPABLE
elif task == FlightType.REFUELING:
return REFUELING_CAPABALE
elif task == FlightType.TRANSPORT:
return TRANSPORT_CAPABLE
else:

View File

@@ -69,6 +69,7 @@ class FlightType(Enum):
AEWC = "AEW&C"
TRANSPORT = "Transport"
SEAD_ESCORT = "SEAD Escort"
REFUELING = "Refueling"
def __str__(self) -> str:
return self.value

View File

@@ -16,12 +16,21 @@ from functools import cached_property
from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple
from dcs.mapping import Point
from dcs.planes import E_3A, E_2C, A_50, KJ_2000
from dcs.planes import (
E_3A,
E_2C,
A_50,
IL_78M,
KC130,
KC135MPRS,
KC_135,
KJ_2000,
S_3B_Tanker,
)
from dcs.unit import Unit
from shapely.geometry import Point as ShapelyPoint
from game.data.doctrine import Doctrine
from game.squadrons import Pilot
from game.theater import (
Airfield,
ControlPoint,
@@ -31,7 +40,7 @@ from game.theater import (
TheaterGroundObject,
)
from game.theater.theatergroundobject import EwrGroundObject
from game.utils import Distance, Speed, feet, meters, nautical_miles
from game.utils import Distance, Speed, feet, meters, nautical_miles, knots
from .closestairfields import ObjectiveDistanceCache
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
from .traveltime import GroundSpeed, TravelTime
@@ -769,6 +778,28 @@ class AwacsFlightPlan(LoiterFlightPlan):
return self.push_time
@dataclass(frozen=True)
class RefuelingFlightPlan(PatrollingFlightPlan):
takeoff: FlightWaypoint
land: FlightWaypoint
divert: Optional[FlightWaypoint]
bullseye: FlightWaypoint
#: Racetrack speed.
patrol_speed: Speed
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.takeoff
yield from self.nav_to
yield self.patrol_start
yield self.patrol_end
yield from self.nav_from
yield self.land
if self.divert is not None:
yield self.divert
yield self.bullseye
@dataclass(frozen=True)
class AirliftFlightPlan(FlightPlan):
takeoff: FlightWaypoint
@@ -919,6 +950,8 @@ class FlightPlanBuilder:
return self.generate_aewc(flight)
elif task == FlightType.TRANSPORT:
return self.generate_transport(flight)
elif task == FlightType.REFUELING:
return self.generate_refueling_racetrack(flight)
raise PlanningError(f"{task} flight plan generation not implemented")
def regenerate_package_waypoints(self) -> None:
@@ -1612,6 +1645,88 @@ class FlightPlanBuilder:
bullseye=builder.bullseye(),
)
def generate_refueling_racetrack(self, flight: Flight) -> RefuelingFlightPlan:
location = self.package.target
closest_boundary = self.threat_zones.closest_boundary(location.position)
heading_to_threat_boundary = location.position.heading_between_point(
closest_boundary
)
distance_to_threat = meters(
location.position.distance_to_point(closest_boundary)
)
orbit_heading = heading_to_threat_boundary
# Station 70nm outside the threat zone.
threat_buffer = nautical_miles(70)
if self.threat_zones.threatened(location.position):
orbit_distance = distance_to_threat + threat_buffer
else:
orbit_distance = distance_to_threat - threat_buffer
racetrack_center = location.position.point_from_heading(
orbit_heading, 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
)
racetrack_end = racetrack_center.point_from_heading(
orbit_heading - 90, racetrack_half_distance
)
builder = WaypointBuilder(flight, self.game, self.is_player)
tanker_type = flight.unit_type
if tanker_type is KC_135:
# ~300 knots IAS.
speed = knots(445)
altitude = feet(24000)
elif tanker_type is KC135MPRS:
# ~300 knots IAS.
speed = knots(440)
altitude = feet(23000)
elif tanker_type is KC130:
# ~210 knots IAS, roughly the max for the KC-130 at altitude.
speed = knots(370)
altitude = feet(22000)
elif tanker_type is S_3B_Tanker:
# ~265 knots IAS.
speed = knots(320)
altitude = feet(12000)
elif tanker_type is IL_78M:
# ~280 knots IAS.
speed = knots(400)
altitude = feet(21000)
else:
# ~280 knots IAS.
speed = knots(400)
altitude = feet(21000)
racetrack = builder.race_track(racetrack_start, racetrack_end, altitude)
return RefuelingFlightPlan(
package=self.package,
flight=flight,
takeoff=builder.takeoff(flight.departure),
nav_to=builder.nav_path(
flight.departure.position, racetrack_start, altitude
),
nav_from=builder.nav_path(racetrack_end, flight.arrival.position, altitude),
patrol_start=racetrack[0],
patrol_end=racetrack[1],
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert),
bullseye=builder.bullseye(),
patrol_duration=timedelta(hours=1),
patrol_speed=speed,
# TODO: Factor out a common base of the combat and non-combat race-tracks.
# No harm in setting this, but we ought to clean up a bit.
engagement_distance=meters(0),
)
@staticmethod
def target_waypoint(
flight: Flight, builder: WaypointBuilder, target: StrikeTarget

View File

@@ -22,17 +22,19 @@ class CombatGroupRole(Enum):
LOGI = 6
INFANTRY = 7
ATGM = 8
RECON = 9
DISTANCE_FROM_FRONTLINE = {
CombatGroupRole.TANK: (2200, 3200),
CombatGroupRole.APC: (7500, 8500),
CombatGroupRole.APC: (2700, 3700),
CombatGroupRole.IFV: (2700, 3700),
CombatGroupRole.ARTILLERY: (16000, 18000),
CombatGroupRole.SHORAD: (12000, 13000),
CombatGroupRole.SHORAD: (5000, 8000),
CombatGroupRole.LOGI: (18000, 20000),
CombatGroupRole.INFANTRY: (2800, 3300),
CombatGroupRole.ATGM: (5200, 6200),
CombatGroupRole.RECON: (2000, 3000),
}
GROUP_SIZES_BY_COMBAT_STANCE = {
@@ -74,6 +76,7 @@ class GroundPlanner:
self.atgm_group: List[CombatGroup] = []
self.logi_groups: List[CombatGroup] = []
self.shorad_groups: List[CombatGroup] = []
self.recon_groups: List[CombatGroup] = []
self.units_per_cp: Dict[int, List[CombatGroup]] = {}
for cp in self.connected_enemy_cp:
@@ -115,6 +118,9 @@ class GroundPlanner:
elif unit_type in GroundUnitClass.Shorads:
collection = self.shorad_groups
role = CombatGroupRole.SHORAD
elif unit_type in GroundUnitClass.Recon:
collection = self.recon_groups
role = CombatGroupRole.RECON
else:
logging.warning(
f"Unused front line vehicle at base {unit_type}: unknown unit class"

View File

@@ -134,6 +134,7 @@ RADIOS: List[Radio] = [
Radio("AN/ARC-51BX", MHz(225), MHz(400), step=kHz(50)),
Radio("AN/ARC-131", MHz(30), MHz(76), step=kHz(50)),
Radio("AN/ARC-134", MHz(116), MHz(150), step=kHz(25)),
Radio("R&S Series 6000", MHz(100), MHz(156), step=kHz(25)),
]