Merge branch 'develop' into helipads

This commit is contained in:
C. Perreau
2021-08-16 12:20:43 +02:00
committed by GitHub
115 changed files with 2908 additions and 2407 deletions

View File

@@ -1,13 +0,0 @@
from .aircraft import *
from .armor import *
from .airsupportgen import *
from .conflictgen import *
from .visualgen import *
from .triggergen import *
from .environmentgen import *
from .groundobjectsgen import *
from .briefinggen import *
from .forcedoptionsgen import *
from .kneeboard import *
from . import naming

View File

@@ -69,7 +69,6 @@ from game.data.weapons import Pylon, WeaponType as WeaponTypeEnum
from game.dcs.aircrafttype import AircraftType
from game.factions.faction import Faction
from game.settings import Settings
from game.squadrons import Pilot
from game.theater.controlpoint import (
Airfield,
ControlPoint,
@@ -109,6 +108,7 @@ from .naming import namegen
if TYPE_CHECKING:
from game import Game
from game.squadrons import Pilot, Squadron
WARM_START_HELI_ALT = meters(500)
WARM_START_ALTITUDE = meters(3000)
@@ -644,8 +644,7 @@ class AircraftConflictGenerator:
def spawn_unused_aircraft(
self, player_country: Country, enemy_country: Country
) -> None:
inventories = self.game.aircraft_inventory.inventories
for control_point, inventory in inventories.items():
for control_point in self.game.theater.controlpoints:
if not isinstance(control_point, Airfield):
continue
@@ -655,11 +654,9 @@ class AircraftConflictGenerator:
else:
country = enemy_country
for aircraft, available in inventory.all_aircraft:
for squadron in control_point.squadrons:
try:
self._spawn_unused_at(
control_point, country, faction, aircraft, available
)
self._spawn_unused_at(control_point, country, faction, squadron)
except NoParkingSlotError:
# If we run out of parking, stop spawning aircraft.
return
@@ -669,17 +666,16 @@ class AircraftConflictGenerator:
control_point: Airfield,
country: Country,
faction: Faction,
aircraft: AircraftType,
number: int,
squadron: Squadron,
) -> None:
for _ in range(number):
for _ in range(squadron.untasked_aircraft):
# Creating a flight even those this isn't a fragged mission lets us
# reuse the existing debriefing code.
# TODO: Special flight type?
flight = Flight(
Package(control_point),
faction.country,
self.game.air_wing_for(control_point.captured).squadron_for(aircraft),
squadron,
1,
FlightType.BARCAP,
"Cold",
@@ -691,16 +687,13 @@ class AircraftConflictGenerator:
group = self._generate_at_airport(
name=namegen.next_aircraft_name(country, control_point.id, flight),
side=country,
unit_type=aircraft.dcs_unit_type,
unit_type=squadron.aircraft.dcs_unit_type,
count=1,
start_type="Cold",
airport=control_point.airport,
)
if aircraft in faction.liveries_overrides:
livery = random.choice(faction.liveries_overrides[aircraft])
for unit in group.units:
unit.livery_id = livery
self._setup_livery(flight, group)
group.uncontrolled = True
self.unit_map.add_aircraft(group, flight)
@@ -1837,17 +1830,11 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
)
)
# 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
)
orbit = OrbitAction(
altitude=waypoint.alt,
pattern=OrbitAction.OrbitPattern.RaceTrack,
speed=int(flight_plan.patrol_speed.kph),
)
racetrack = ControlledTask(orbit)
self.set_waypoint_tot(waypoint, flight_plan.patrol_start_time)

View File

@@ -5,7 +5,8 @@ from datetime import timedelta
from typing import Optional, TYPE_CHECKING
if TYPE_CHECKING:
from gen import RadioFrequency, TacanChannel
from gen.radios import RadioFrequency
from gen.tacan import TacanChannel
@dataclass

View File

@@ -16,8 +16,7 @@ from dcs.task import (
from dcs.unittype import UnitType
from game.utils import Heading
from . import AirSupport
from .airsupport import TankerInfo, AwacsInfo
from .airsupport import AirSupport, TankerInfo, AwacsInfo
from .callsigns import callsign_for_support_unit
from .conflictgen import Conflict
from .flights.ai_flight_planner_db import AEWC_CAPABLE

View File

@@ -129,7 +129,6 @@ CAP_CAPABLE = [
F_14B,
F_14A_135_GR,
Su_33,
Su_34,
J_11A,
Su_30,
Su_27,

View File

@@ -2,20 +2,19 @@ from __future__ import annotations
from datetime import timedelta
from enum import Enum
from typing import List, Optional, TYPE_CHECKING, Union, Sequence, Any
from typing import List, Optional, TYPE_CHECKING, Union, Sequence
from dcs.mapping import Point
from dcs.point import MovingPoint, PointAction
from dcs.unit import Unit
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
from gen.flights.loadouts import Loadout
if TYPE_CHECKING:
from game.squadrons import Pilot, Squadron
from game.transfers import TransferOrder
from gen.ato import Package
from gen.flights.flightplan import FlightPlan
@@ -50,6 +49,8 @@ class FlightType(Enum):
strike-like missions will need more specialized control.
* ai_flight_planner.py: Use the new mission type in propose_missions so the AI will
plan the new mission type.
* FlightType.is_air_to_air and FlightType.is_air_to_ground: If the new mission type
fits either of these categories, update those methods accordingly.
"""
TARCAP = "TARCAP"
@@ -80,6 +81,30 @@ class FlightType(Enum):
return entry
raise KeyError(f"No FlightType with name {name}")
@property
def is_air_to_air(self) -> bool:
return self in {
FlightType.TARCAP,
FlightType.BARCAP,
FlightType.INTERCEPTION,
FlightType.ESCORT,
FlightType.SWEEP,
}
@property
def is_air_to_ground(self) -> bool:
return self in {
FlightType.CAS,
FlightType.STRIKE,
FlightType.ANTISHIP,
FlightType.SEAD,
FlightType.DEAD,
FlightType.BAI,
FlightType.OCA_RUNWAY,
FlightType.OCA_AIRCRAFT,
FlightType.SEAD_ESCORT,
}
class FlightWaypointType(Enum):
"""Enumeration of waypoint types.
@@ -141,8 +166,8 @@ class FlightWaypoint:
waypoint_type: The waypoint type.
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".
alt: Altitude of the waypoint. By default this is MSL, but it can be
changed to AGL by setting alt_type to "RADIO"
"""
self.waypoint_type = waypoint_type
self.x = x
@@ -169,12 +194,6 @@ 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)
@@ -273,6 +292,7 @@ class Flight:
self.package = package
self.country = country
self.squadron = squadron
self.squadron.claim_inventory(count)
if roster is None:
self.roster = FlightRoster(self.squadron, initial_size=count)
else:
@@ -321,6 +341,7 @@ class Flight:
return self.flight_plan.waypoints[1:]
def resize(self, new_size: int) -> None:
self.squadron.claim_inventory(new_size - self.count)
self.roster.resize(new_size)
def set_pilot(self, index: int, pilot: Optional[Pilot]) -> None:
@@ -330,8 +351,9 @@ class Flight:
def missing_pilots(self) -> int:
return self.roster.missing_pilots
def clear_roster(self) -> None:
def return_pilots_and_aircraft(self) -> None:
self.roster.clear()
self.squadron.claim_inventory(-self.count)
def __repr__(self) -> str:
if self.custom_name:

View File

@@ -411,6 +411,9 @@ class PatrollingFlightPlan(FlightPlan):
#: Maximum time to remain on station.
patrol_duration: timedelta
#: Racetrack speed TAS.
patrol_speed: Speed
#: The engagement range of any Search Then Engage task, or the radius of a
#: Search Then Engage in Zone task. Any enemies of the appropriate type for
#: this mission within this range of the flight's current position (or the
@@ -779,9 +782,6 @@ class RefuelingFlightPlan(PatrollingFlightPlan):
divert: Optional[FlightWaypoint]
bullseye: FlightWaypoint
#: Racetrack speed.
patrol_speed: Speed
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.takeoff
yield from self.nav_to
@@ -1115,7 +1115,7 @@ class FlightPlanBuilder:
if isinstance(location, FrontLine):
raise InvalidObjectiveLocation(flight.flight_type, location)
start_pos, end_pos = self.racetrack_for_objective(location, barcap=True)
start_pos, end_pos = self.cap_racetrack_for_objective(location, barcap=True)
preferred_alt = flight.unit_type.preferred_patrol_altitude
randomized_alt = preferred_alt + feet(random.randint(-2, 1) * 1000)
@@ -1124,6 +1124,11 @@ class FlightPlanBuilder:
min(self.doctrine.max_patrol_altitude, randomized_alt),
)
patrol_speed = flight.unit_type.preferred_patrol_speed(patrol_alt)
logging.debug(
f"BARCAP patrol speed for {flight.unit_type.name} at {patrol_alt.feet}ft: {patrol_speed.knots} KTAS"
)
builder = WaypointBuilder(flight, self.coalition)
start, end = builder.race_track(start_pos, end_pos, patrol_alt)
@@ -1131,6 +1136,7 @@ class FlightPlanBuilder:
package=self.package,
flight=flight,
patrol_duration=self.doctrine.cap_duration,
patrol_speed=patrol_speed,
engagement_distance=self.doctrine.cap_engagement_range,
takeoff=builder.takeoff(flight.departure),
nav_to=builder.nav_path(
@@ -1238,7 +1244,7 @@ class FlightPlanBuilder:
bullseye=builder.bullseye(),
)
def racetrack_for_objective(
def cap_racetrack_for_objective(
self, location: MissionTarget, barcap: bool
) -> Tuple[Point, Point]:
closest_cache = ObjectiveDistanceCache.get_closest_airfields(location)
@@ -1270,6 +1276,7 @@ class FlightPlanBuilder:
- self.doctrine.cap_engagement_range
- nautical_miles(5)
)
max_track_length = self.doctrine.cap_max_track_length
else:
# Other race tracks (TARCAPs, currently) just try to keep some
# distance from the nearest enemy airbase, but since they are by
@@ -1283,6 +1290,11 @@ class FlightPlanBuilder:
)
distance_to_no_fly = distance_to_airfield - min_distance_from_enemy
# TARCAPs fly short racetracks because they need to react faster.
max_track_length = self.doctrine.cap_min_track_length + 0.3 * (
self.doctrine.cap_max_track_length - self.doctrine.cap_min_track_length
)
min_cap_distance = min(
self.doctrine.cap_min_distance_from_cp, distance_to_no_fly
)
@@ -1294,11 +1306,12 @@ class FlightPlanBuilder:
heading.degrees,
random.randint(int(min_cap_distance.meters), int(max_cap_distance.meters)),
)
diameter = random.randint(
track_length = random.randint(
int(self.doctrine.cap_min_track_length.meters),
int(self.doctrine.cap_max_track_length.meters),
int(max_track_length.meters),
)
start = end.point_from_heading(heading.opposite.degrees, diameter)
start = end.point_from_heading(heading.opposite.degrees, track_length)
return start, end
def aewc_orbit(self, location: MissionTarget) -> Point:
@@ -1321,33 +1334,6 @@ class FlightPlanBuilder:
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.theater)
center = ingress.point_from_heading(heading.degrees, distance / 2)
orbit_center = center.point_from_heading(
heading.left.degrees,
random.randint(
int(nautical_miles(6).meters), int(nautical_miles(15).meters)
),
)
combat_width = distance / 2
if combat_width > 500000:
combat_width = 500000
if combat_width < 35000:
combat_width = 35000
radius = combat_width * 1.25
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
return start, end
def generate_tarcap(self, flight: Flight) -> TarCapFlightPlan:
"""Generate a CAP flight plan for the given front line.
@@ -1362,16 +1348,14 @@ class FlightPlanBuilder:
self.doctrine.min_patrol_altitude,
min(self.doctrine.max_patrol_altitude, randomized_alt),
)
patrol_speed = flight.unit_type.preferred_patrol_speed(patrol_alt)
logging.debug(
f"TARCAP patrol speed for {flight.unit_type.name} at {patrol_alt.feet}ft: {patrol_speed.knots} KTAS"
)
# Create points
builder = WaypointBuilder(flight, self.coalition)
if isinstance(location, FrontLine):
orbit0p, orbit1p = self.racetrack_for_frontline(
flight.departure.position, location
)
else:
orbit0p, orbit1p = self.racetrack_for_objective(location, barcap=False)
orbit0p, orbit1p = self.cap_racetrack_for_objective(location, barcap=False)
start, end = builder.race_track(orbit0p, orbit1p, patrol_alt)
return TarCapFlightPlan(
@@ -1383,6 +1367,7 @@ class FlightPlanBuilder:
# requests an escort the CAP flight will remain on station for the
# duration of the escorted mission, or until it is winchester/bingo.
patrol_duration=self.doctrine.cap_duration,
patrol_speed=patrol_speed,
engagement_distance=self.doctrine.cap_engagement_range,
takeoff=builder.takeoff(flight.departure),
nav_to=builder.nav_path(flight.departure.position, orbit0p, patrol_alt),
@@ -1546,16 +1531,33 @@ class FlightPlanBuilder:
builder = WaypointBuilder(flight, self.coalition)
# 2021-08-02: patrol_speed will currently have no effect because
# CAS doesn't use OrbitAction. But all PatrollingFlightPlan are expected
# to have patrol_speed
is_helo = flight.unit_type.dcs_unit_type.helicopter
ingress_egress_altitude = (
self.doctrine.ingress_altitude if not is_helo else meters(50)
)
patrol_speed = flight.unit_type.preferred_patrol_speed(ingress_egress_altitude)
use_agl_ingress_egress = is_helo
return CasFlightPlan(
package=self.package,
flight=flight,
patrol_duration=self.doctrine.cas_duration,
patrol_speed=patrol_speed,
takeoff=builder.takeoff(flight.departure),
nav_to=builder.nav_path(
flight.departure.position, ingress, self.doctrine.ingress_altitude
flight.departure.position,
ingress,
ingress_egress_altitude,
use_agl_ingress_egress,
),
nav_from=builder.nav_path(
egress, flight.arrival.position, self.doctrine.ingress_altitude
egress,
flight.arrival.position,
ingress_egress_altitude,
use_agl_ingress_egress,
),
patrol_start=builder.ingress(
FlightWaypointType.INGRESS_CAS, ingress, location
@@ -1608,6 +1610,7 @@ class FlightPlanBuilder:
else:
altitude = feet(21000)
# TODO: Could use flight.unit_type.preferred_patrol_speed(altitude) instead.
if tanker_type.patrol_speed is not None:
speed = tanker_type.patrol_speed
else:

View File

@@ -1,4 +1,5 @@
from __future__ import annotations
import logging
import random
from dataclasses import dataclass
@@ -55,7 +56,7 @@ class WaypointBuilder:
@property
def is_helo(self) -> bool:
return getattr(self.flight.unit_type, "helicopter", False)
return self.flight.unit_type.dcs_unit_type.helicopter
def takeoff(self, departure: ControlPoint) -> FlightWaypoint:
"""Create takeoff waypoint for the given arrival airfield or carrier.
@@ -167,6 +168,8 @@ class WaypointBuilder:
position.y,
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
)
if self.is_helo:
waypoint.alt_type = "RADIO"
waypoint.pretty_name = "Hold"
waypoint.description = "Wait until push time"
waypoint.name = "HOLD"
@@ -210,7 +213,7 @@ class WaypointBuilder:
ingress_type,
position.x,
position.y,
meters(50) if self.is_helo else self.doctrine.ingress_altitude,
meters(60) if self.is_helo else self.doctrine.ingress_altitude,
)
if self.is_helo:
waypoint.alt_type = "RADIO"
@@ -225,7 +228,7 @@ class WaypointBuilder:
FlightWaypointType.EGRESS,
position.x,
position.y,
meters(50) if self.is_helo else self.doctrine.ingress_altitude,
meters(60) if self.is_helo else self.doctrine.ingress_altitude,
)
if self.is_helo:
waypoint.alt_type = "RADIO"
@@ -309,7 +312,7 @@ class WaypointBuilder:
FlightWaypointType.CAS,
position.x,
position.y,
meters(50) if self.is_helo else meters(1000),
meters(60) if self.is_helo else meters(1000),
)
waypoint.alt_type = "RADIO"
waypoint.description = "Provide CAS"
@@ -445,7 +448,7 @@ class WaypointBuilder:
FlightWaypointType.TARGET_GROUP_LOC,
target.position.x,
target.position.y,
meters(50) if self.is_helo else self.doctrine.ingress_altitude,
meters(60) if self.is_helo else self.doctrine.ingress_altitude,
)
if self.is_helo:
waypoint.alt_type = "RADIO"

View File

@@ -43,8 +43,14 @@ class ForcedOptionsGenerator:
if blue.unrestricted_satnav or red.unrestricted_satnav:
self.mission.forced_options.unrestricted_satnav = True
def _set_battle_damage_assessment(self) -> None:
self.mission.forced_options.battle_damage_assessment = (
self.game.settings.battle_damage_assessment
)
def generate(self) -> None:
self._set_options_view()
self._set_external_views()
self._set_labels()
self._set_unrestricted_satnav()
self._set_battle_damage_assessment()

View File

@@ -65,7 +65,8 @@ class KneeboardPageWriter:
else:
self.foreground_fill = (15, 15, 15)
self.background_fill = (255, 252, 252)
self.image = Image.new("RGB", (768, 1024), self.background_fill)
self.image_size = (768, 1024)
self.image = Image.new("RGB", self.image_size, self.background_fill)
# These font sizes create a relatively full page for current sorties. If
# we start generating more complicated flight plans, or start including
# more information in the comm ladder (the latter of which we should
@@ -84,6 +85,7 @@ class KneeboardPageWriter:
"resources/fonts/Inconsolata.otf", 20, layout_engine=ImageFont.LAYOUT_BASIC
)
self.draw = ImageDraw.Draw(self.image)
self.page_margin = page_margin
self.x = page_margin
self.y = page_margin
self.line_spacing = line_spacing
@@ -97,12 +99,21 @@ class KneeboardPageWriter:
text: str,
font: Optional[ImageFont.FreeTypeFont] = None,
fill: Optional[Tuple[int, int, int]] = None,
wrap: bool = False,
) -> None:
if font is None:
font = self.content_font
if fill is None:
fill = self.foreground_fill
if wrap:
text = "\n".join(
self.wrap_line_with_font(
line, self.image_size[0] - self.page_margin - self.x, font
)
for line in text.splitlines()
)
self.draw.text(self.position, text, font=font, fill=fill)
width, height = self.draw.textsize(text, font=font)
self.y += height + self.line_spacing
@@ -146,6 +157,24 @@ class KneeboardPageWriter:
output = combo
return "".join(segments + [output]).strip()
@staticmethod
def wrap_line_with_font(
inputstr: str, max_width: int, font: ImageFont.FreeTypeFont
) -> str:
if font.getsize(inputstr)[0] <= max_width:
return inputstr
tokens = inputstr.split(" ")
output = ""
segments = []
for token in tokens:
combo = output + " " + token
if font.getsize(combo)[0] > max_width:
segments.append(output + "\n")
output = token
else:
output = combo
return "".join(segments + [output]).strip()
class KneeboardPage:
"""Base class for all kneeboard pages."""
@@ -631,7 +660,7 @@ class NotesPage(KneeboardPage):
def write(self, path: Path) -> None:
writer = KneeboardPageWriter(dark_theme=self.dark_kneeboard)
writer.title(f"Notes")
writer.text(self.notes)
writer.text(self.notes, wrap=True)
writer.write(path)

View File

@@ -1,12 +1,16 @@
from __future__ import annotations
import random
import time
from typing import List, Any
from typing import List, Any, TYPE_CHECKING
from dcs.country import Country
from game.dcs.aircrafttype import AircraftType
from game.dcs.unittype import UnitType
from gen.flights.flight import Flight
if TYPE_CHECKING:
from gen.flights.flight import Flight
ALPHA_MILITARY = [
"Alpha",

View File

@@ -2,7 +2,7 @@
import itertools
import logging
from dataclasses import dataclass
from typing import Dict, Iterator, List, Set
from typing import Dict, FrozenSet, Iterator, List, Reversible, Set, Tuple
@dataclass(frozen=True)
@@ -45,14 +45,8 @@ def kHz(num: int) -> RadioFrequency:
@dataclass(frozen=True)
class Radio:
"""A radio.
Defines the minimum (inclusive) and maximum (exclusive) range of the radio.
"""
#: The name of the radio.
name: str
class RadioRange:
"""Defines the minimum (inclusive) and maximum (exclusive) range of the radio."""
#: The minimum (inclusive) frequency tunable by this radio.
minimum: RadioFrequency
@@ -63,19 +57,51 @@ class Radio:
#: The spacing between adjacent frequencies.
step: RadioFrequency
def __str__(self) -> str:
return self.name
#: Specific frequencies to exclude. (e.g. Guard channels)
excludes: FrozenSet[RadioFrequency] = frozenset()
def range(self) -> Iterator[RadioFrequency]:
"""Returns an iterator over the usable frequencies of this radio."""
return (
RadioFrequency(x)
for x in range(self.minimum.hertz, self.maximum.hertz, self.step.hertz)
if RadioFrequency(x) not in self.excludes
)
@property
def last_channel(self) -> RadioFrequency:
return RadioFrequency(self.maximum.hertz - self.step.hertz)
return next(
RadioFrequency(x)
for x in reversed(
range(self.minimum.hertz, self.maximum.hertz, self.step.hertz)
)
if RadioFrequency(x) not in self.excludes
)
@dataclass(frozen=True)
class Radio:
"""A radio.
Defines ranges of usable frequencies of the radio.
"""
#: The name of the radio.
name: str
#: List of usable frequency range of this radio.
ranges: Tuple[RadioRange, ...]
def __str__(self) -> str:
return self.name
def range(self) -> Iterator[RadioFrequency]:
"""Returns an iterator over the usable frequencies of this radio."""
return itertools.chain.from_iterable(rng.range() for rng in self.ranges)
@property
def last_channel(self) -> RadioFrequency:
return self.ranges[-1].last_channel
class ChannelInUseError(RuntimeError):
@@ -88,53 +114,58 @@ class ChannelInUseError(RuntimeError):
# TODO: Figure out appropriate steps for each radio. These are just guesses.
#: List of all known radios used by aircraft in the game.
RADIOS: List[Radio] = [
Radio("AN/ARC-164", MHz(225), MHz(400), step=MHz(1)),
Radio("AN/ARC-186(V) AM", MHz(116), MHz(152), step=MHz(1)),
Radio("AN/ARC-186(V) FM", MHz(30), MHz(76), step=MHz(1)),
# The AN/ARC-210 can also use [30, 88) and [108, 118), but the current
# implementation can't implement the gap and the radio can't transmit on the
# latter. There's still plenty of channels between 118 MHz and 400 MHz, so
# not worth worrying about.
Radio("AN/ARC-210", MHz(118), MHz(400), step=MHz(1)),
Radio("AN/ARC-222", MHz(116), MHz(174), step=MHz(1)),
Radio("SCR-522", MHz(100), MHz(156), step=MHz(1)),
Radio("A.R.I. 1063", MHz(100), MHz(156), step=MHz(1)),
Radio("BC-1206", kHz(200), kHz(400), step=kHz(10)),
Radio("AN/ARC-164", (RadioRange(MHz(225), MHz(400), step=MHz(1)),)),
Radio("AN/ARC-186(V) AM", (RadioRange(MHz(116), MHz(152), step=MHz(1)),)),
Radio("AN/ARC-186(V) FM", (RadioRange(MHz(30), MHz(76), step=MHz(1)),)),
Radio(
"AN/ARC-210",
(
RadioRange(MHz(225), MHz(400), MHz(1), frozenset((MHz(243),))),
RadioRange(MHz(136), MHz(155), MHz(1)),
RadioRange(MHz(156), MHz(174), MHz(1)),
RadioRange(MHz(118), MHz(136), MHz(1)),
RadioRange(MHz(30), MHz(88), MHz(1)),
),
),
Radio("AN/ARC-222", (RadioRange(MHz(116), MHz(174), step=MHz(1)),)),
Radio("SCR-522", (RadioRange(MHz(100), MHz(156), step=MHz(1)),)),
Radio("A.R.I. 1063", (RadioRange(MHz(100), MHz(156), step=MHz(1)),)),
Radio("BC-1206", (RadioRange(kHz(200), kHz(400), step=kHz(10)),)),
# Note: The M2000C V/UHF can operate in both ranges, but has a gap between
# 150 MHz and 225 MHz. We can't allocate in that gap, and the current
# system doesn't model gaps, so just pretend it ends at 150 MHz for now. We
# can model gaps later if needed.
Radio("TRT ERA 7000 V/UHF", MHz(118), MHz(150), step=MHz(1)),
Radio("TRT ERA 7200 UHF", MHz(225), MHz(400), step=MHz(1)),
Radio("TRT ERA 7000 V/UHF", (RadioRange(MHz(118), MHz(150), step=MHz(1)),)),
Radio("TRT ERA 7200 UHF", (RadioRange(MHz(225), MHz(400), step=MHz(1)),)),
# Tomcat radios
# # https://www.heatblur.se/F-14Manual/general.html#an-arc-159-uhf-1-radio
Radio("AN/ARC-159", MHz(225), MHz(400), step=MHz(1)),
Radio("AN/ARC-159", (RadioRange(MHz(225), MHz(400), step=MHz(1)),)),
# AN/ARC-182 can also operate from 30 MHz to 88 MHz, as well as from 225 MHz
# to 400 MHz range, but we can't model gaps with the current implementation.
# https://www.heatblur.se/F-14Manual/general.html#an-arc-182-v-uhf-2-radio
Radio("AN/ARC-182", MHz(108), MHz(174), step=MHz(1)),
Radio("AN/ARC-182", (RadioRange(MHz(108), MHz(174), step=MHz(1)),)),
# Also capable of [103, 156) at 25 kHz intervals, but we can't do gaps.
Radio("FR 22", MHz(225), MHz(400), step=kHz(50)),
Radio("FR 22", (RadioRange(MHz(225), MHz(400), step=kHz(50)),)),
# P-51 / P-47 Radio
# 4 preset channels (A/B/C/D)
Radio("SCR522", MHz(100), MHz(156), step=kHz(25)),
Radio("R&S M3AR VHF", MHz(120), MHz(174), step=MHz(1)),
Radio("R&S M3AR UHF", MHz(225), MHz(400), step=MHz(1)),
Radio("SCR522", (RadioRange(MHz(100), MHz(156), step=kHz(25)),)),
Radio("R&S M3AR VHF", (RadioRange(MHz(120), MHz(174), step=MHz(1)),)),
Radio("R&S M3AR UHF", (RadioRange(MHz(225), MHz(400), step=MHz(1)),)),
# MiG-15bis
Radio("RSI-6K HF", MHz(3, 750), MHz(5), step=kHz(25)),
Radio("RSI-6K HF", (RadioRange(MHz(3, 750), MHz(5), step=kHz(25)),)),
# MiG-19P
Radio("RSIU-4V", MHz(100), MHz(150), step=MHz(1)),
Radio("RSIU-4V", (RadioRange(MHz(100), MHz(150), step=MHz(1)),)),
# MiG-21bis
Radio("RSIU-5V", MHz(118), MHz(140), step=MHz(1)),
Radio("RSIU-5V", (RadioRange(MHz(118), MHz(140), step=MHz(1)),)),
# Ka-50
# Note: Also capable of 100MHz-150MHz, but we can't model gaps.
Radio("R-800L1", MHz(220), MHz(400), step=kHz(25)),
Radio("R-828", MHz(20), MHz(60), step=kHz(25)),
Radio("R-800L1", (RadioRange(MHz(220), MHz(400), step=kHz(25)),)),
Radio("R-828", (RadioRange(MHz(20), MHz(60), step=kHz(25)),)),
# UH-1H
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)),
Radio("AN/ARC-51BX", (RadioRange(MHz(225), MHz(400), step=kHz(50)),)),
Radio("AN/ARC-131", (RadioRange(MHz(30), MHz(76), step=kHz(50)),)),
Radio("AN/ARC-134", (RadioRange(MHz(116), MHz(150), step=kHz(25)),)),
Radio("R&S Series 6000", (RadioRange(MHz(100), MHz(156), step=kHz(25)),)),
]
@@ -175,7 +206,7 @@ class RadioRegistry:
# Not a real radio, but useful for allocating a channel usable for
# inter-flight communications.
BLUFOR_UHF = Radio("BLUFOR UHF", MHz(225), MHz(400), step=MHz(1))
BLUFOR_UHF = Radio("BLUFOR UHF", (RadioRange(MHz(225), MHz(400), step=MHz(1)),))
def __init__(self) -> None:
self.allocated_channels: Set[RadioFrequency] = set()

View File

@@ -82,8 +82,10 @@ class FlakGenerator(AirDefenseGroupGenerator):
)
# Some Opel Blitz trucks
index = 0
for i in range(int(max(1, 2))):
for j in range(int(max(1, 2))):
index += 1
self.add_unit(
Unarmed.Blitz_36_6700A,
"BLITZ#" + str(index),

View File

@@ -16,7 +16,11 @@ class EwrGenerator(VehicleGroupGenerator[EwrGroundObject]):
def generate(self) -> None:
self.add_unit(
self.unit_type, "EWR", self.position.x, self.position.y, self.heading
self.unit_type,
"EWR",
self.position.x,
self.position.y,
self.heading_to_conflict(),
)