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:
Khopa
2021-08-02 19:34:05 +02:00
408 changed files with 9630 additions and 5172 deletions

View File

@@ -1,11 +1,12 @@
from __future__ import annotations
import itertools
import logging
import random
from dataclasses import dataclass
from dataclasses import dataclass, field
from datetime import timedelta
from functools import cached_property
from typing import Dict, List, Optional, TYPE_CHECKING, Type, Union, Iterable
from typing import Dict, List, Optional, TYPE_CHECKING, Type, Union, Iterable, Any
from dcs import helicopters
from dcs.action import AITaskPush, ActivateGroup
@@ -22,7 +23,6 @@ from dcs.planes import (
C_101EB,
F_14B,
JF_17,
PlaneType,
Su_33,
Tu_22M3,
)
@@ -65,7 +65,7 @@ from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup
from dcs.unittype import FlyingType
from game import db
from game.data.weapons import Pylon
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
@@ -81,7 +81,7 @@ from game.theater.missiontarget import MissionTarget
from game.theater.theatergroundobject import TheaterGroundObject
from game.transfers import MultiGroupTransport
from game.unitmap import UnitMap
from game.utils import Distance, meters, nautical_miles
from game.utils import Distance, meters, nautical_miles, pairwise
from gen.ato import AirTaskingOrder, Package
from gen.callsigns import create_group_callsign_from_unit
from gen.flights.flight import (
@@ -90,10 +90,11 @@ from gen.flights.flight import (
FlightWaypoint,
FlightWaypointType,
)
from gen.lasercoderegistry import LaserCodeRegistry
from gen.radios import RadioFrequency, RadioRegistry
from gen.runways import RunwayData
from gen.tacan import TacanBand, TacanRegistry
from .airsupportgen import AirSupport, AwacsInfo, TankerInfo
from .airsupport import AirSupport, AwacsInfo, TankerInfo
from .callsigns import callsign_for_support_unit
from .flights.flightplan import (
AwacsFlightPlan,
@@ -138,6 +139,8 @@ class FlightData:
flight_type: FlightType
aircraft_type: AircraftType
#: All units in the flight.
units: List[FlyingUnit]
@@ -165,49 +168,24 @@ class FlightData:
#: Radio frequency for intra-flight communications.
intra_flight_channel: RadioFrequency
#: Map of radio frequencies to their assigned radio and channel, if any.
frequency_to_channel_map: Dict[RadioFrequency, ChannelAssignment]
#: Bingo fuel value in lbs.
bingo_fuel: Optional[int]
joker_fuel: Optional[int]
def __init__(
self,
package: Package,
aircraft_type: AircraftType,
flight_type: FlightType,
units: List[FlyingUnit],
size: int,
friendly: bool,
departure_delay: timedelta,
departure: RunwayData,
arrival: RunwayData,
divert: Optional[RunwayData],
waypoints: List[FlightWaypoint],
intra_flight_channel: RadioFrequency,
bingo_fuel: Optional[int],
joker_fuel: Optional[int],
custom_name: Optional[str],
) -> None:
self.package = package
self.aircraft_type = aircraft_type
self.flight_type = flight_type
self.units = units
self.size = size
self.friendly = friendly
self.departure_delay = departure_delay
self.departure = departure
self.arrival = arrival
self.divert = divert
self.waypoints = waypoints
self.intra_flight_channel = intra_flight_channel
self.frequency_to_channel_map = {}
self.bingo_fuel = bingo_fuel
self.joker_fuel = joker_fuel
laser_codes: list[Optional[int]]
custom_name: Optional[str]
callsign: str = field(init=False)
#: Map of radio frequencies to their assigned radio and channel, if any.
frequency_to_channel_map: Dict[RadioFrequency, ChannelAssignment] = field(
init=False, default_factory=dict
)
def __post_init__(self) -> None:
self.callsign = create_group_callsign_from_unit(self.units[0])
self.custom_name = custom_name
@property
def client_units(self) -> List[FlyingUnit]:
@@ -247,6 +225,7 @@ class AircraftConflictGenerator:
game: Game,
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,
laser_code_registry: LaserCodeRegistry,
unit_map: UnitMap,
air_support: AirSupport,
) -> None:
@@ -255,6 +234,7 @@ class AircraftConflictGenerator:
self.settings = settings
self.radio_registry = radio_registry
self.tacan_registy = tacan_registry
self.laser_code_registry = laser_code_registry
self.unit_map = unit_map
self.flights: List[FlightData] = []
self.air_support = air_support
@@ -262,8 +242,8 @@ class AircraftConflictGenerator:
@cached_property
def use_client(self) -> bool:
"""True if Client should be used instead of Player."""
blue_clients = self.client_slots_in_ato(self.game.blue_ato)
red_clients = self.client_slots_in_ato(self.game.red_ato)
blue_clients = self.client_slots_in_ato(self.game.blue.ato)
red_clients = self.client_slots_in_ato(self.game.red.ato)
return blue_clients + red_clients > 1
@staticmethod
@@ -321,7 +301,7 @@ class AircraftConflictGenerator:
@staticmethod
def livery_from_db(flight: Flight) -> Optional[str]:
return db.PLANE_LIVERY_OVERRIDES.get(flight.unit_type)
return db.PLANE_LIVERY_OVERRIDES.get(flight.unit_type.dcs_unit_type)
def livery_from_faction(self, flight: Flight) -> Optional[str]:
faction = self.game.faction_for(player=flight.departure.captured)
@@ -342,7 +322,7 @@ class AircraftConflictGenerator:
return livery
return None
def _setup_livery(self, flight: Flight, group: FlyingGroup) -> None:
def _setup_livery(self, flight: Flight, group: FlyingGroup[Any]) -> None:
livery = self.livery_for(flight)
if livery is None:
return
@@ -351,7 +331,7 @@ class AircraftConflictGenerator:
def _setup_group(
self,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
@@ -361,6 +341,7 @@ class AircraftConflictGenerator:
self._setup_payload(flight, group)
self._setup_livery(flight, group)
laser_codes = []
for unit, pilot in zip(group.units, flight.roster.pilots):
player = pilot is not None and pilot.player
self.set_skill(unit, pilot, blue=flight.departure.captured)
@@ -368,6 +349,11 @@ class AircraftConflictGenerator:
if player and group.late_activation:
group.late_activation = False
code: Optional[int] = None
if flight.loadout.has_weapon_of_type(WeaponTypeEnum.TGP) and player:
code = self.laser_code_registry.get_next_laser_code()
laser_codes.append(code)
# Set up F-14 Client to have pre-stored alignment
if unit_type is F_14B:
unit.set_property(F_14B.Properties.INSAlignmentStored.id, True)
@@ -383,7 +369,18 @@ class AircraftConflictGenerator:
channel = self.radio_registry.alloc_uhf()
else:
channel = flight.unit_type.alloc_flight_radio(self.radio_registry)
group.set_frequency(channel.mhz)
try:
group.set_frequency(channel.mhz)
except TypeError:
# TODO: Remote try/except when pydcs bug is fixed.
# https://github.com/pydcs/dcs/issues/175
# pydcs now emits an error when attempting to set a preset channel for an
# aircraft that doesn't support them. We're not choosing to set a preset
# here, we're just trying to set the AI's frequency. pydcs automatically
# tries to set channel 1 when it does that and doesn't suppress this new
# error.
pass
divert = None
if flight.divert is not None:
@@ -412,6 +409,7 @@ class AircraftConflictGenerator:
bingo_fuel=flight.flight_plan.bingo_fuel,
joker_fuel=flight.flight_plan.joker_fuel,
custom_name=flight.custom_name,
laser_codes=laser_codes,
)
)
@@ -458,8 +456,8 @@ class AircraftConflictGenerator:
unit_type: Type[FlyingType],
count: int,
start_type: str,
airport: Optional[Airport] = None,
) -> FlyingGroup:
airport: Airport,
) -> FlyingGroup[Any]:
assert count > 0
logging.info("airgen: {} for {} at {}".format(unit_type, side.id, airport))
@@ -476,7 +474,7 @@ class AircraftConflictGenerator:
def _generate_inflight(
self, name: str, side: Country, flight: Flight, origin: ControlPoint
) -> FlyingGroup:
) -> FlyingGroup[Any]:
assert flight.count > 0
at = origin.position
@@ -521,7 +519,7 @@ class AircraftConflictGenerator:
count: int,
start_type: str,
at: Union[ShipGroup, StaticGroup],
) -> FlyingGroup:
) -> FlyingGroup[Any]:
assert count > 0
logging.info("airgen: {} for {} at unit {}".format(unit_type, side.id, at))
@@ -536,34 +534,18 @@ class AircraftConflictGenerator:
)
def _add_radio_waypoint(
self, group: FlyingGroup, position, altitude: Distance, airspeed: int = 600
self,
group: FlyingGroup[Any],
position: Point,
altitude: Distance,
airspeed: int = 600,
) -> MovingPoint:
point = group.add_waypoint(position, altitude.meters, airspeed)
point.alt_type = "RADIO"
return point
def _rtb_for(
self,
group: FlyingGroup,
cp: ControlPoint,
at: Optional[db.StartingPosition] = None,
):
if at is None:
at = cp.at
position = at if isinstance(at, Point) else at.position
last_waypoint = group.points[-1]
if last_waypoint is not None:
heading = position.heading_between_point(last_waypoint.position)
tod_location = position.point_from_heading(heading, RTB_DISTANCE)
self._add_radio_waypoint(group, tod_location, last_waypoint.alt)
destination_waypoint = self._add_radio_waypoint(group, position, RTB_ALTITUDE)
if isinstance(at, Airport):
group.land_at(at)
return destination_waypoint
def _at_position(self, at) -> Point:
@staticmethod
def _at_position(at: Union[Point, ShipGroup, Type[Airport]]) -> Point:
if isinstance(at, Point):
return at
elif isinstance(at, ShipGroup):
@@ -573,7 +555,7 @@ class AircraftConflictGenerator:
else:
assert False
def _setup_payload(self, flight: Flight, group: FlyingGroup) -> None:
def _setup_payload(self, flight: Flight, group: FlyingGroup[Any]) -> None:
for p in group.units:
p.pylons.clear()
@@ -593,7 +575,10 @@ class AircraftConflictGenerator:
parking_slot.unit_id = None
def generate_flights(
self, country, ato: AirTaskingOrder, dynamic_runways: Dict[str, RunwayData]
self,
country: Country,
ato: AirTaskingOrder,
dynamic_runways: Dict[str, RunwayData],
) -> None:
for package in ato.packages:
@@ -614,12 +599,11 @@ class AircraftConflictGenerator:
if not isinstance(control_point, Airfield):
continue
faction = self.game.coalition_for(control_point.captured).faction
if control_point.captured:
country = player_country
faction = self.game.player_faction
else:
country = enemy_country
faction = self.game.enemy_faction
for aircraft, available in inventory.all_aircraft:
try:
@@ -672,7 +656,7 @@ class AircraftConflictGenerator:
self.unit_map.add_aircraft(group, flight)
def set_activation_time(
self, flight: Flight, group: FlyingGroup, delay: timedelta
self, flight: Flight, group: FlyingGroup[Any], delay: timedelta
) -> None:
# Note: Late activation causes the waypoint TOTs to look *weird* in the
# mission editor. Waypoint times will be relative to the group
@@ -691,7 +675,7 @@ class AircraftConflictGenerator:
self.m.triggerrules.triggers.append(activation_trigger)
def set_startup_time(
self, flight: Flight, group: FlyingGroup, delay: timedelta
self, flight: Flight, group: FlyingGroup[Any], delay: timedelta
) -> None:
# Uncontrolled causes the AI unit to spawn, but not begin startup.
group.uncontrolled = True
@@ -712,14 +696,12 @@ class AircraftConflictGenerator:
if flight.from_cp.cptype != ControlPointType.AIRBASE:
return
if flight.from_cp.captured:
coalition = self.game.get_player_coalition_id()
else:
coalition = self.game.get_enemy_coalition_id()
coalition = self.game.coalition_for(flight.departure.captured).coalition_id
trigger.add_condition(CoalitionHasAirdrome(coalition, flight.from_cp.id))
def generate_planned_flight(self, cp, country, flight: Flight):
def generate_planned_flight(
self, cp: ControlPoint, country: Country, flight: Flight
) -> FlyingGroup[Any]:
name = namegen.next_aircraft_name(country, cp.id, flight)
try:
if flight.start_type == "In Flight":
@@ -728,13 +710,19 @@ class AircraftConflictGenerator:
)
elif isinstance(cp, NavalControlPoint):
group_name = cp.get_carrier_group_name()
carrier_group = self.m.find_group(group_name)
if not isinstance(carrier_group, ShipGroup):
raise RuntimeError(
f"Carrier group {carrier_group} is a "
"{carrier_group.__class__.__name__}, expected a ShipGroup"
)
group = self._generate_at_group(
name=name,
side=country,
unit_type=flight.unit_type.dcs_unit_type,
count=flight.count,
start_type=flight.start_type,
at=self.m.find_group(group_name),
at=carrier_group,
)
else:
@@ -796,7 +784,7 @@ class AircraftConflictGenerator:
@staticmethod
def set_reduced_fuel(
flight: Flight, group: FlyingGroup, unit_type: Type[PlaneType]
flight: Flight, group: FlyingGroup[Any], unit_type: Type[FlyingType]
) -> None:
if unit_type is Su_33:
for unit in group.units:
@@ -822,9 +810,9 @@ class AircraftConflictGenerator:
def configure_behavior(
self,
flight: Flight,
group: FlyingGroup,
group: FlyingGroup[Any],
react_on_threat: Optional[OptReactOnThreat.Values] = None,
roe: Optional[OptROE.Values] = None,
roe: Optional[int] = None,
rtb_winchester: Optional[OptRTBOnOutOfAmmo.Values] = None,
restrict_jettison: Optional[bool] = None,
mission_uses_gun: bool = True,
@@ -855,13 +843,13 @@ class AircraftConflictGenerator:
# https://forums.eagle.ru/forum/english/digital-combat-simulator/dcs-world-2-5/bugs-and-problems-ai/ai-ad/7121294-ai-stuck-at-high-aoa-after-making-sharp-turn-if-afterburner-is-restricted
@staticmethod
def configure_eplrs(group: FlyingGroup, flight: Flight) -> None:
def configure_eplrs(group: FlyingGroup[Any], flight: Flight) -> None:
if flight.unit_type.eplrs_capable:
group.points[0].tasks.append(EPLRS(group.id))
def configure_cap(
self,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
@@ -878,7 +866,7 @@ class AircraftConflictGenerator:
def configure_sweep(
self,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
@@ -895,7 +883,7 @@ class AircraftConflictGenerator:
def configure_cas(
self,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
@@ -913,7 +901,7 @@ class AircraftConflictGenerator:
def configure_dead(
self,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
@@ -938,7 +926,7 @@ class AircraftConflictGenerator:
def configure_sead(
self,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
@@ -962,7 +950,7 @@ class AircraftConflictGenerator:
def configure_strike(
self,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
@@ -980,7 +968,7 @@ class AircraftConflictGenerator:
def configure_anti_ship(
self,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
@@ -998,7 +986,7 @@ class AircraftConflictGenerator:
def configure_runway_attack(
self,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
@@ -1016,7 +1004,7 @@ class AircraftConflictGenerator:
def configure_oca_strike(
self,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
@@ -1033,7 +1021,7 @@ class AircraftConflictGenerator:
def configure_awacs(
self,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
@@ -1061,7 +1049,7 @@ class AircraftConflictGenerator:
def configure_refueling(
self,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
@@ -1087,7 +1075,7 @@ class AircraftConflictGenerator:
def configure_escort(
self,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
@@ -1103,7 +1091,7 @@ class AircraftConflictGenerator:
def configure_sead_escort(
self,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
@@ -1126,7 +1114,7 @@ class AircraftConflictGenerator:
def configure_transport(
self,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
@@ -1141,13 +1129,13 @@ class AircraftConflictGenerator:
restrict_jettison=True,
)
def configure_unknown_task(self, group: FlyingGroup, flight: Flight) -> None:
def configure_unknown_task(self, group: FlyingGroup[Any], flight: Flight) -> None:
logging.error(f"Unhandled flight type: {flight.flight_type}")
self.configure_behavior(flight, group)
def setup_flight_group(
self,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
@@ -1191,7 +1179,7 @@ class AircraftConflictGenerator:
self.configure_eplrs(group, flight)
def create_waypoints(
self, group: FlyingGroup, package: Package, flight: Flight
self, group: FlyingGroup[Any], package: Package, flight: Flight
) -> None:
for waypoint in flight.points:
@@ -1236,8 +1224,57 @@ class AircraftConflictGenerator:
).build()
# Set here rather than when the FlightData is created so they waypoints
# have their TOTs set.
self.flights[-1].waypoints = [takeoff_point] + flight.points
# have their TOTs and fuel minimums set. Once we're more confident in our fuel
# estimation ability the minimum fuel amounts will be calculated during flight
# plan construction, but for now it's only used by the kneeboard so is generated
# late.
waypoints = [takeoff_point] + flight.points
self._estimate_min_fuel_for(flight, waypoints)
self.flights[-1].waypoints = waypoints
@staticmethod
def _estimate_min_fuel_for(flight: Flight, waypoints: list[FlightWaypoint]) -> None:
if flight.unit_type.fuel_consumption is None:
return
combat_speed_types = {
FlightWaypointType.INGRESS_BAI,
FlightWaypointType.INGRESS_CAS,
FlightWaypointType.INGRESS_DEAD,
FlightWaypointType.INGRESS_ESCORT,
FlightWaypointType.INGRESS_OCA_AIRCRAFT,
FlightWaypointType.INGRESS_OCA_RUNWAY,
FlightWaypointType.INGRESS_SEAD,
FlightWaypointType.INGRESS_STRIKE,
FlightWaypointType.INGRESS_SWEEP,
FlightWaypointType.SPLIT,
} | set(TARGET_WAYPOINTS)
consumption = flight.unit_type.fuel_consumption
min_fuel: float = consumption.min_safe
# The flight plan (in reverse) up to and including the arrival point.
main_flight_plan = reversed(waypoints)
try:
while waypoint := next(main_flight_plan):
if waypoint.waypoint_type is FlightWaypointType.LANDING_POINT:
waypoint.min_fuel = min_fuel
main_flight_plan = itertools.chain([waypoint], main_flight_plan)
break
except StopIteration:
# Some custom flight plan without a landing point. Skip it.
return
for b, a in pairwise(main_flight_plan):
distance = meters(a.position.distance_to_point(b.position))
if a.waypoint_type is FlightWaypointType.TAKEOFF:
ppm = consumption.climb
elif b.waypoint_type in combat_speed_types:
ppm = consumption.combat
else:
ppm = consumption.cruise
min_fuel += distance.nautical_miles * ppm
a.min_fuel = min_fuel
def should_delay_flight(self, flight: Flight, start_time: timedelta) -> bool:
if start_time.total_seconds() <= 0:
@@ -1259,7 +1296,7 @@ class AircraftConflictGenerator:
waypoint: FlightWaypoint,
package: Package,
flight: Flight,
group: FlyingGroup,
group: FlyingGroup[Any],
) -> None:
estimator = TotEstimator(package)
start_time = estimator.mission_start_time(flight)
@@ -1302,7 +1339,7 @@ class PydcsWaypointBuilder:
def __init__(
self,
waypoint: FlightWaypoint,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
mission: Mission,
@@ -1345,7 +1382,7 @@ class PydcsWaypointBuilder:
def for_waypoint(
cls,
waypoint: FlightWaypoint,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
mission: Mission,
@@ -1459,7 +1496,7 @@ class CasIngressBuilder(PydcsWaypointBuilder):
if isinstance(self.flight.flight_plan, CasFlightPlan):
waypoint.add_task(
EngageTargetsInZone(
position=self.flight.flight_plan.target,
position=self.flight.flight_plan.target.position,
radius=int(self.flight.flight_plan.engagement_distance.meters),
targets=[
Targets.All.GroundUnits.GroundVehicles,

View File

@@ -1521,4 +1521,47 @@ AIRFIELD_DATA = {
runway_length=3953,
atc=AtcData(MHz(3, 850), MHz(118, 200), MHz(38, 600), MHz(250, 200)),
),
"Antonio B. Won Pat Intl": AirfieldData(
theater="MarianaIslands",
icao="PGUM",
elevation=255,
runway_length=9359,
atc=AtcData(MHz(3, 825), MHz(118, 100), MHz(38, 550), MHz(340, 200)),
ils={
"06": ("IGUM", MHz(110, 30)),
},
),
"Andersen AFB": AirfieldData(
theater="MarianaIslands",
icao="PGUA",
elevation=545,
runway_length=10490,
tacan=TacanChannel(54, TacanBand.X),
tacan_callsign="UAM",
atc=AtcData(MHz(3, 850), MHz(126, 200), MHz(38, 600), MHz(250, 100)),
),
"Rota Intl": AirfieldData(
theater="MarianaIslands",
icao="PGRO",
elevation=568,
runway_length=6105,
atc=AtcData(MHz(3, 750), MHz(123, 600), MHz(38, 400), MHz(250, 0)),
),
"Tinian Intl": AirfieldData(
theater="MarianaIslands",
icao="PGWT",
elevation=240,
runway_length=7777,
atc=AtcData(MHz(3, 800), MHz(123, 650), MHz(38, 500), MHz(250, 50)),
),
"Saipan Intl": AirfieldData(
theater="MarianaIslands",
icao="PGSN",
elevation=213,
runway_length=7790,
atc=AtcData(MHz(3, 775), MHz(125, 700), MHz(38, 450), MHz(256, 900)),
ils={
"07": ("IGSN", MHz(109, 90)),
},
),
}

55
gen/airsupport.py Normal file
View File

@@ -0,0 +1,55 @@
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import timedelta
from typing import Optional, TYPE_CHECKING
if TYPE_CHECKING:
from gen import RadioFrequency, TacanChannel
@dataclass
class AwacsInfo:
"""AWACS information for the kneeboard."""
group_name: str
callsign: str
freq: RadioFrequency
depature_location: Optional[str]
start_time: Optional[timedelta]
end_time: Optional[timedelta]
blue: bool
@dataclass
class TankerInfo:
"""Tanker information for the kneeboard."""
group_name: str
callsign: str
variant: str
freq: RadioFrequency
tacan: TacanChannel
start_time: Optional[timedelta]
end_time: Optional[timedelta]
blue: bool
@dataclass(frozen=True)
class JtacInfo:
"""JTAC information."""
group_name: str
unit_name: str
callsign: str
region: str
code: str
blue: bool
freq: RadioFrequency
@dataclass
class AirSupport:
awacs: list[AwacsInfo] = field(default_factory=list)
tankers: list[TankerInfo] = field(default_factory=list)
jtacs: list[JtacInfo] = field(default_factory=list)

View File

@@ -1,11 +1,10 @@
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from datetime import timedelta
from typing import List, Type, Tuple, Optional
from typing import List, Type, Tuple, TYPE_CHECKING
from dcs.mission import Mission, StartType
from dcs.planes import IL_78M, KC130, KC135MPRS, KC_135
from dcs.unittype import UnitType
from dcs.planes import IL_78M, KC130, KC135MPRS, KC_135, PlaneType
from dcs.task import (
AWACS,
ActivateBeaconCommand,
@@ -14,15 +13,20 @@ from dcs.task import (
SetImmortalCommand,
SetInvisibleCommand,
)
from dcs.unittype import UnitType
from game import db
from .flights.ai_flight_planner_db import AEWC_CAPABLE
from .naming import namegen
from game.utils import Heading
from . import AirSupport
from .airsupport import TankerInfo, AwacsInfo
from .callsigns import callsign_for_support_unit
from .conflictgen import Conflict
from .radios import RadioFrequency, RadioRegistry
from .tacan import TacanBand, TacanChannel, TacanRegistry
from .flights.ai_flight_planner_db import AEWC_CAPABLE
from .naming import namegen
from .radios import RadioRegistry
from .tacan import TacanBand, TacanRegistry
if TYPE_CHECKING:
from game import Game
TANKER_DISTANCE = 15000
TANKER_ALT = 4572
@@ -32,54 +36,22 @@ AWACS_DISTANCE = 150000
AWACS_ALT = 13000
@dataclass
class AwacsInfo:
"""AWACS information for the kneeboard."""
group_name: str
callsign: str
freq: RadioFrequency
depature_location: Optional[str]
start_time: Optional[timedelta]
end_time: Optional[timedelta]
blue: bool
@dataclass
class TankerInfo:
"""Tanker information for the kneeboard."""
group_name: str
callsign: str
variant: str
freq: RadioFrequency
tacan: TacanChannel
start_time: Optional[timedelta]
end_time: Optional[timedelta]
blue: bool
@dataclass
class AirSupport:
awacs: List[AwacsInfo] = field(default_factory=list)
tankers: List[TankerInfo] = field(default_factory=list)
class AirSupportConflictGenerator:
def __init__(
self,
mission: Mission,
conflict: Conflict,
game,
game: Game,
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,
air_support: AirSupport,
) -> None:
self.mission = mission
self.conflict = conflict
self.game = game
self.air_support = AirSupport()
self.radio_registry = radio_registry
self.tacan_registry = tacan_registry
self.air_support = air_support
@classmethod
def support_tasks(cls) -> List[Type[MainTask]]:
@@ -88,46 +60,51 @@ class AirSupportConflictGenerator:
@staticmethod
def _get_tanker_params(unit_type: Type[UnitType]) -> Tuple[int, int]:
if unit_type is KC130:
return (TANKER_ALT - 500, 596)
return TANKER_ALT - 500, 596
elif unit_type is KC_135:
return (TANKER_ALT, 770)
return TANKER_ALT, 770
elif unit_type is KC135MPRS:
return (TANKER_ALT + 500, 596)
return (TANKER_ALT, 574)
return TANKER_ALT + 500, 596
return TANKER_ALT, 574
def generate(self):
def generate(self) -> None:
player_cp = (
self.conflict.blue_cp
if self.conflict.blue_cp.captured
else self.conflict.red_cp
)
country = self.mission.country(self.game.blue.country_name)
if not self.game.settings.disable_legacy_tanker:
fallback_tanker_number = 0
for i, tanker_unit_type in enumerate(
self.game.faction_for(player=True).tankers
):
unit_type = tanker_unit_type.dcs_unit_type
if not issubclass(unit_type, PlaneType):
logging.warning(f"Refueling aircraft {unit_type} must be a plane")
continue
# TODO: Make loiter altitude a property of the unit type.
alt, airspeed = self._get_tanker_params(tanker_unit_type.dcs_unit_type)
freq = self.radio_registry.alloc_uhf()
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y)
tanker_heading = (
tanker_heading = Heading.from_degrees(
self.conflict.red_cp.position.heading_between_point(
self.conflict.blue_cp.position
)
+ TANKER_HEADING_OFFSET * i
)
tanker_position = player_cp.position.point_from_heading(
tanker_heading, TANKER_DISTANCE
tanker_heading.degrees, 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
),
country=country,
name=namegen.next_tanker_name(country, tanker_unit_type),
airport=None,
plane_type=tanker_unit_type,
plane_type=unit_type,
position=tanker_position,
altitude=alt,
race_distance=58000,
@@ -177,6 +154,8 @@ class AirSupportConflictGenerator:
tanker_unit_type.name,
freq,
tacan,
start_time=None,
end_time=None,
blue=True,
)
)
@@ -195,12 +174,15 @@ class AirSupportConflictGenerator:
awacs_unit = possible_awacs[0]
freq = self.radio_registry.alloc_uhf()
unit_type = awacs_unit.dcs_unit_type
if not issubclass(unit_type, PlaneType):
logging.warning(f"AWACS aircraft {unit_type} must be a plane")
return
awacs_flight = self.mission.awacs_flight(
country=self.mission.country(self.game.player_country),
name=namegen.next_awacs_name(
self.mission.country(self.game.player_country)
),
plane_type=awacs_unit,
country=country,
name=namegen.next_awacs_name(country),
plane_type=unit_type,
altitude=AWACS_ALT,
airport=None,
position=self.conflict.position.random_point_within(

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import logging
import math
import random
from dataclasses import dataclass
from typing import TYPE_CHECKING, List, Optional, Tuple
@@ -12,9 +13,11 @@ from dcs.country import Country
from dcs.mapping import Point
from dcs.point import PointAction
from dcs.task import (
AFAC,
EPLRS,
AttackGroup,
ControlledTask,
FAC,
FireAtPoint,
GoToWaypoint,
Hold,
@@ -23,7 +26,7 @@ from dcs.task import (
SetInvisibleCommand,
)
from dcs.triggers import Event, TriggerOnce
from dcs.unit import Vehicle
from dcs.unit import Vehicle, Skill
from dcs.unitgroup import VehicleGroup
from game.data.groundunitclass import GroundUnitClass
@@ -31,16 +34,19 @@ from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
from game.theater.controlpoint import ControlPoint
from game.unitmap import UnitMap
from game.utils import heading_sum, opposite_heading
from game.utils import Heading
from gen.ground_forces.ai_ground_planner import (
DISTANCE_FROM_FRONTLINE,
CombatGroup,
CombatGroupRole,
)
from .airsupport import AirSupport, JtacInfo
from .callsigns import callsign_for_support_unit
from .conflictgen import Conflict
from .ground_forces.combat_stance import CombatStance
from .lasercoderegistry import LaserCodeRegistry
from .naming import namegen
from .radios import MHz, RadioFrequency, RadioRegistry
if TYPE_CHECKING:
from game import Game
@@ -63,19 +69,6 @@ RANDOM_OFFSET_ATTACK = 250
INFANTRY_GROUP_SIZE = 5
@dataclass(frozen=True)
class JtacInfo:
"""JTAC information."""
group_name: str
unit_name: str
callsign: str
region: str
code: str
blue: bool
# TODO: Radio info? Type?
class GroundConflictGenerator:
def __init__(
self,
@@ -85,57 +78,29 @@ class GroundConflictGenerator:
player_planned_combat_groups: List[CombatGroup],
enemy_planned_combat_groups: List[CombatGroup],
player_stance: CombatStance,
enemy_stance: CombatStance,
unit_map: UnitMap,
radio_registry: RadioRegistry,
air_support: AirSupport,
laser_code_registry: LaserCodeRegistry,
) -> None:
self.mission = mission
self.conflict = conflict
self.enemy_planned_combat_groups = enemy_planned_combat_groups
self.player_planned_combat_groups = player_planned_combat_groups
self.player_stance = CombatStance(player_stance)
self.enemy_stance = self._enemy_stance()
self.player_stance = player_stance
self.enemy_stance = enemy_stance
self.game = game
self.unit_map = unit_map
self.jtacs: List[JtacInfo] = []
self.radio_registry = radio_registry
self.air_support = air_support
self.laser_code_registry = laser_code_registry
def _enemy_stance(self):
"""Picks the enemy stance according to the number of planned groups on the frontline for each side"""
if len(self.enemy_planned_combat_groups) > len(
self.player_planned_combat_groups
):
return random.choice(
[
CombatStance.AGGRESSIVE,
CombatStance.AGGRESSIVE,
CombatStance.AGGRESSIVE,
CombatStance.ELIMINATION,
CombatStance.BREAKTHROUGH,
]
)
else:
return random.choice(
[
CombatStance.DEFENSIVE,
CombatStance.DEFENSIVE,
CombatStance.DEFENSIVE,
CombatStance.AMBUSH,
CombatStance.AGGRESSIVE,
]
)
@staticmethod
def _group_point(point: Point, base_distance) -> Point:
distance = random.randint(
int(base_distance * SPREAD_DISTANCE_FACTOR[0]),
int(base_distance * SPREAD_DISTANCE_FACTOR[1]),
)
return point.random_point_within(
distance, base_distance * SPREAD_DISTANCE_SIZE_FACTOR
)
def generate(self):
def generate(self) -> None:
position = Conflict.frontline_position(
self.conflict.front_line, self.game.theater
)
frontline_vector = Conflict.frontline_vector(
self.conflict.front_line, self.game.theater
)
@@ -150,12 +115,19 @@ class GroundConflictGenerator:
self.enemy_planned_combat_groups, frontline_vector, False
)
# TODO: Differentiate AirConflict and GroundConflict classes.
if self.conflict.heading is None:
raise RuntimeError(
"Cannot generate ground units for non-ground conflict. Ground unit "
"conflicts cannot have the heading `None`."
)
# Plan combat actions for groups
self.plan_action_for_groups(
self.player_stance,
player_groups,
enemy_groups,
self.conflict.heading + 90,
self.conflict.heading.right,
self.conflict.blue_cp,
self.conflict.red_cp,
)
@@ -163,27 +135,32 @@ class GroundConflictGenerator:
self.enemy_stance,
enemy_groups,
player_groups,
self.conflict.heading - 90,
self.conflict.heading.left,
self.conflict.red_cp,
self.conflict.blue_cp,
)
# Add JTAC
if self.game.player_faction.has_jtac:
if self.game.blue.faction.has_jtac:
n = "JTAC" + str(self.conflict.blue_cp.id) + str(self.conflict.red_cp.id)
code = 1688 - len(self.jtacs)
code: int = self.laser_code_registry.get_next_laser_code()
freq = self.radio_registry.alloc_uhf()
utype = self.game.player_faction.jtac_unit
if self.game.player_faction.jtac_unit is None:
utype = self.game.blue.faction.jtac_unit
if utype is None:
utype = AircraftType.named("MQ-9 Reaper")
jtac = self.mission.flight_group(
country=self.mission.country(self.game.player_country),
country=self.mission.country(self.game.blue.country_name),
name=n,
aircraft_type=utype.dcs_unit_type,
position=position[0],
airport=None,
altitude=5000,
maintask=AFAC,
)
jtac.points[0].tasks.append(
FAC(callsign=len(self.air_support.jtacs) + 1, frequency=int(freq.mhz))
)
jtac.points[0].tasks.append(SetInvisibleCommand(True))
jtac.points[0].tasks.append(SetImmortalCommand(True))
@@ -195,7 +172,7 @@ class GroundConflictGenerator:
)
# Note: Will need to change if we ever add ground based JTAC.
callsign = callsign_for_support_unit(jtac)
self.jtacs.append(
self.air_support.jtacs.append(
JtacInfo(
str(jtac.name),
n,
@@ -203,11 +180,16 @@ class GroundConflictGenerator:
frontline,
str(code),
blue=True,
freq=freq,
)
)
def gen_infantry_group_for_group(
self, group: VehicleGroup, is_player: bool, side: Country, forward_heading: int
self,
group: VehicleGroup,
is_player: bool,
side: Country,
forward_heading: Heading,
) -> None:
infantry_position = self.conflict.find_ground_position(
@@ -242,7 +224,7 @@ class GroundConflictGenerator:
u.dcs_unit_type,
position=infantry_position,
group_size=1,
heading=forward_heading,
heading=forward_heading.degrees,
move_formation=PointAction.OffRoad,
)
return
@@ -269,7 +251,7 @@ class GroundConflictGenerator:
units[0].dcs_unit_type,
position=infantry_position,
group_size=1,
heading=forward_heading,
heading=forward_heading.degrees,
move_formation=PointAction.OffRoad,
)
@@ -281,17 +263,19 @@ class GroundConflictGenerator:
unit.dcs_unit_type,
position=position,
group_size=1,
heading=forward_heading,
heading=forward_heading.degrees,
move_formation=PointAction.OffRoad,
)
def _set_reform_waypoint(
self, dcs_group: VehicleGroup, forward_heading: int
self, dcs_group: VehicleGroup, forward_heading: Heading
) -> None:
"""Setting a waypoint close to the spawn position allows the group to reform gracefully
rather than spin
"""
reform_point = dcs_group.position.point_from_heading(forward_heading, 50)
reform_point = dcs_group.position.point_from_heading(
forward_heading.degrees, 50
)
dcs_group.add_waypoint(reform_point)
def _plan_artillery_action(
@@ -299,7 +283,7 @@ class GroundConflictGenerator:
stance: CombatStance,
gen_group: CombatGroup,
dcs_group: VehicleGroup,
forward_heading: int,
forward_heading: Heading,
target: Point,
) -> bool:
"""
@@ -333,7 +317,7 @@ class GroundConflictGenerator:
dcs_group, forward_heading, (int)(RETREAT_DISTANCE / 3)
)
dcs_group.add_waypoint(
dcs_group.position.point_from_heading(forward_heading, 1),
dcs_group.position.point_from_heading(forward_heading.degrees, 1),
PointAction.OffRoad,
)
dcs_group.points[2].tasks.append(Hold())
@@ -361,8 +345,7 @@ class GroundConflictGenerator:
self.mission.triggerrules.triggers.append(artillery_fallback)
for u in dcs_group.units:
u.initial = True
u.heading = forward_heading + random.randint(-5, 5)
u.heading = (forward_heading + Heading.random(-5, 5)).degrees
return True
return False
@@ -371,7 +354,7 @@ class GroundConflictGenerator:
stance: CombatStance,
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]],
dcs_group: VehicleGroup,
forward_heading: int,
forward_heading: Heading,
to_cp: ControlPoint,
) -> bool:
"""
@@ -404,9 +387,7 @@ class GroundConflictGenerator:
else:
# We use an offset heading here because DCS doesn't always
# force vehicles to move if there's no heading change.
offset_heading = forward_heading - 2
if offset_heading < 0:
offset_heading = 358
offset_heading = forward_heading - Heading.from_degrees(2)
attack_point = self.find_offensive_point(
dcs_group, offset_heading, AGGRESIVE_MOVE_DISTANCE
)
@@ -424,9 +405,7 @@ class GroundConflictGenerator:
else:
# We use an offset heading here because DCS doesn't always
# force vehicles to move if there's no heading change.
offset_heading = forward_heading - 1
if offset_heading < 0:
offset_heading = 359
offset_heading = forward_heading - Heading.from_degrees(1)
attack_point = self.find_offensive_point(
dcs_group, offset_heading, BREAKTHROUGH_OFFENSIVE_DISTANCE
)
@@ -462,7 +441,7 @@ class GroundConflictGenerator:
self,
stance: CombatStance,
dcs_group: VehicleGroup,
forward_heading: int,
forward_heading: Heading,
to_cp: ControlPoint,
) -> bool:
"""
@@ -499,7 +478,7 @@ class GroundConflictGenerator:
stance: CombatStance,
ally_groups: List[Tuple[VehicleGroup, CombatGroup]],
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]],
forward_heading: int,
forward_heading: Heading,
from_cp: ControlPoint,
to_cp: ControlPoint,
) -> None:
@@ -540,12 +519,14 @@ class GroundConflictGenerator:
else:
retreat_point = self.find_retreat_point(dcs_group, forward_heading)
reposition_point = retreat_point.point_from_heading(
forward_heading, 10
forward_heading.degrees, 10
) # Another point to make the unit face the enemy
dcs_group.add_waypoint(retreat_point, PointAction.OffRoad)
dcs_group.add_waypoint(reposition_point, PointAction.OffRoad)
def add_morale_trigger(self, dcs_group: VehicleGroup, forward_heading: int) -> None:
def add_morale_trigger(
self, dcs_group: VehicleGroup, forward_heading: Heading
) -> None:
"""
This add a trigger to manage units fleeing whenever their group is hit hard, or being engaged by CAS
"""
@@ -558,7 +539,7 @@ class GroundConflictGenerator:
# Force unit heading
for unit in dcs_group.units:
unit.heading = forward_heading
unit.heading = forward_heading.degrees
dcs_group.manualHeading = True
# We add a new retreat waypoint
@@ -570,10 +551,10 @@ class GroundConflictGenerator:
)
# Fallback task
fallback = ControlledTask(GoToWaypoint(to_index=len(dcs_group.points)))
fallback.enabled = False
task = ControlledTask(GoToWaypoint(to_index=len(dcs_group.points)))
task.enabled = False
dcs_group.add_trigger_action(Hold())
dcs_group.add_trigger_action(fallback)
dcs_group.add_trigger_action(task)
# Create trigger
fallback = TriggerOnce(Event.NoEvent, "Morale manager #" + str(dcs_group.id))
@@ -589,7 +570,7 @@ class GroundConflictGenerator:
def find_retreat_point(
self,
dcs_group: VehicleGroup,
frontline_heading: int,
frontline_heading: Heading,
distance: int = RETREAT_DISTANCE,
) -> Point:
"""
@@ -599,14 +580,14 @@ class GroundConflictGenerator:
:return: dcs.mapping.Point object with the desired position
"""
desired_point = dcs_group.points[0].position.point_from_heading(
heading_sum(frontline_heading, +180), distance
frontline_heading.opposite.degrees, distance
)
if self.conflict.theater.is_on_land(desired_point):
return desired_point
return self.conflict.theater.nearest_land_pos(desired_point)
def find_offensive_point(
self, dcs_group: VehicleGroup, frontline_heading: int, distance: int
self, dcs_group: VehicleGroup, frontline_heading: Heading, distance: int
) -> Point:
"""
Find a point to attack
@@ -616,7 +597,7 @@ class GroundConflictGenerator:
:return: dcs.mapping.Point object with the desired position
"""
desired_point = dcs_group.points[0].position.point_from_heading(
frontline_heading, distance
frontline_heading.degrees, distance
)
if self.conflict.theater.is_on_land(desired_point):
return desired_point
@@ -634,7 +615,7 @@ class GroundConflictGenerator:
@param enemy_groups Potential enemy groups
@param n number of nearby groups to take
"""
targets = [] # type: List[Optional[VehicleGroup]]
targets = [] # type: List[VehicleGroup]
sorted_list = sorted(
enemy_groups,
key=lambda group: player_group.points[0].position.distance_to_point(
@@ -658,7 +639,7 @@ class GroundConflictGenerator:
@param group Group for which we should find the nearest ennemy
@param enemy_groups Potential enemy groups
"""
min_distance = 99999999
min_distance = math.inf
target = None
for dcs_group, _ in enemy_groups:
dist = player_group.points[0].position.distance_to_point(
@@ -696,7 +677,7 @@ class GroundConflictGenerator:
"""
For artilery group, decide the distance from frontline with the range of the unit
"""
rg = getattr(group.unit_type.dcs_unit_type, "threat_range", 0) - 7500
rg = group.unit_type.dcs_unit_type.threat_range - 7500
if rg > DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]:
rg = random.randint(
DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][0],
@@ -714,14 +695,14 @@ class GroundConflictGenerator:
conflict_position: Point,
combat_width: int,
distance_from_frontline: int,
heading: int,
spawn_heading: int,
):
heading: Heading,
spawn_heading: Heading,
) -> Optional[Point]:
shifted = conflict_position.point_from_heading(
heading, random.randint(0, combat_width)
heading.degrees, random.randint(0, combat_width)
)
desired_point = shifted.point_from_heading(
spawn_heading, distance_from_frontline
spawn_heading.degrees, distance_from_frontline
)
return Conflict.find_ground_position(
desired_point, combat_width, heading, self.conflict.theater
@@ -730,18 +711,14 @@ class GroundConflictGenerator:
def _generate_groups(
self,
groups: list[CombatGroup],
frontline_vector: Tuple[Point, int, int],
frontline_vector: Tuple[Point, Heading, int],
is_player: bool,
) -> List[Tuple[VehicleGroup, CombatGroup]]:
"""Finds valid positions for planned groups and generates a pydcs group for them"""
positioned_groups = []
position, heading, combat_width = frontline_vector
spawn_heading = (
int(heading_sum(heading, -90))
if is_player
else int(heading_sum(heading, 90))
)
country = self.game.player_country if is_player else self.game.enemy_country
spawn_heading = heading.left if is_player else heading.right
country = self.game.coalition_for(is_player).country_name
for group in groups:
if group.role == CombatGroupRole.ARTILLERY:
distance_from_frontline = (
@@ -763,12 +740,12 @@ class GroundConflictGenerator:
group.unit_type,
group.size,
final_position,
heading=opposite_heading(spawn_heading),
heading=spawn_heading.opposite,
)
if is_player:
g.set_skill(self.game.settings.player_skill)
g.set_skill(Skill(self.game.settings.player_skill))
else:
g.set_skill(self.game.settings.enemy_vehicle_skill)
g.set_skill(Skill(self.game.settings.enemy_vehicle_skill))
positioned_groups.append((g, group))
if group.role in [CombatGroupRole.APC, CombatGroupRole.IFV]:
@@ -776,7 +753,7 @@ class GroundConflictGenerator:
g,
is_player,
self.mission.country(country),
opposite_heading(spawn_heading),
spawn_heading.opposite,
)
else:
logging.warning(f"Unable to get valid position for {group}")
@@ -790,7 +767,7 @@ class GroundConflictGenerator:
count: int,
at: Point,
move_formation: PointAction = PointAction.OffRoad,
heading=0,
heading: Heading = Heading.from_degrees(0),
) -> VehicleGroup:
if side == self.conflict.attackers_country:
@@ -804,7 +781,7 @@ class GroundConflictGenerator:
unit_type.dcs_unit_type,
position=at,
group_size=count,
heading=heading,
heading=heading.degrees,
move_formation=move_formation,
)

View File

@@ -40,7 +40,6 @@ class Task:
class PackageWaypoints:
join: Point
ingress: Point
egress: Point
split: Point

View File

@@ -136,6 +136,16 @@ def format_waypoint_time(waypoint: FlightWaypoint, depart_prefix: str) -> str:
return ""
def format_intra_flight_channel(flight: FlightData) -> str:
frequency = flight.intra_flight_channel
channel = flight.channel_for(frequency)
if channel is None:
return str(frequency)
channel_name = flight.aircraft_type.channel_name(channel.radio_id, channel.channel)
return f"{channel_name} ({frequency})"
class BriefingGenerator(MissionInfoGenerator):
def __init__(self, mission: Mission, game: Game):
super().__init__(mission, game)
@@ -151,6 +161,7 @@ class BriefingGenerator(MissionInfoGenerator):
lstrip_blocks=True,
)
env.filters["waypoint_timing"] = format_waypoint_time
env.filters["intra_flight_channel"] = format_intra_flight_channel
self.template = env.get_template("briefingtemplate_EN.j2")
def generate(self) -> None:

View File

@@ -1,12 +1,13 @@
"""Support for working with DCS group callsigns."""
import logging
import re
from typing import Any
from dcs.unitgroup import FlyingGroup
from dcs.flyingunit import FlyingUnit
def callsign_for_support_unit(group: FlyingGroup) -> str:
def callsign_for_support_unit(group: FlyingGroup[Any]) -> str:
# Either something like Overlord11 for Western AWACS, or else just a number.
# Convert to either "Overlord" or "Flight 123".
lead = group.units[0]

View File

@@ -24,12 +24,13 @@ class CargoShipGenerator:
def generate(self) -> None:
# Reset the count to make generation deterministic.
for ship in self.game.transfers.cargo_ships:
self.generate_cargo_ship(ship)
for coalition in self.game.coalitions:
for ship in coalition.transfers.cargo_ships:
self.generate_cargo_ship(ship)
def generate_cargo_ship(self, ship: CargoShip) -> ShipGroup:
country = self.mission.country(
self.game.player_country if ship.player_owned else self.game.enemy_country
self.game.coalition_for(ship.player_owned).country_name
)
waypoints = ship.route
group = self.mission.ship_group(

View File

@@ -1,6 +1,11 @@
import logging
import random
from game import db
from typing import Optional
from dcs.unitgroup import VehicleGroup
from game import db, Game
from game.theater.theatergroundobject import CoastalSiteGroundObject
from gen.coastal.silkworm import SilkwormGenerator
COASTAL_MAP = {
@@ -8,10 +13,13 @@ COASTAL_MAP = {
}
def generate_coastal_group(game, ground_object, faction_name: str):
def generate_coastal_group(
game: Game, ground_object: CoastalSiteGroundObject, faction_name: str
) -> Optional[VehicleGroup]:
"""
This generate a coastal defenses group
:return: Nothing, but put the group reference inside the ground object
:return: The generated group, or None if this faction does not support coastal
defenses.
"""
faction = db.FACTIONS[faction_name]
if len(faction.coastal_defenses) > 0:

View File

@@ -1,14 +1,20 @@
from dcs.vehicles import MissilesSS, Unarmed, AirDefence
from gen.sam.group_generator import GroupGenerator
from game import Game
from game.factions.faction import Faction
from game.theater.theatergroundobject import CoastalSiteGroundObject
from game.utils import Heading
from gen.sam.group_generator import VehicleGroupGenerator
class SilkwormGenerator(GroupGenerator):
def __init__(self, game, ground_object, faction):
class SilkwormGenerator(VehicleGroupGenerator[CoastalSiteGroundObject]):
def __init__(
self, game: Game, ground_object: CoastalSiteGroundObject, faction: Faction
) -> None:
super(SilkwormGenerator, self).__init__(game, ground_object)
self.faction = faction
def generate(self):
def generate(self) -> None:
positions = self.get_circular_position(5, launcher_distance=120, coverage=180)
@@ -23,7 +29,7 @@ class SilkwormGenerator(GroupGenerator):
# Launchers
for i, p in enumerate(positions):
self.add_unit(
MissilesSS.Silkworm_SR,
MissilesSS.Hy_launcher,
"Missile#" + str(i),
p[0],
p[1],
@@ -54,5 +60,5 @@ class SilkwormGenerator(GroupGenerator):
"STRELA#0",
self.position.x + 200,
self.position.y + 15,
90,
Heading.from_degrees(90),
)

View File

@@ -1,3 +1,5 @@
from __future__ import annotations
import logging
from typing import Tuple, Optional
@@ -7,7 +9,7 @@ from shapely.geometry import LineString, Point as ShapelyPoint
from game.theater.conflicttheater import ConflictTheater, FrontLine
from game.theater.controlpoint import ControlPoint
from game.utils import heading_sum, opposite_heading
from game.utils import Heading
FRONTLINE_LENGTH = 80000
@@ -23,7 +25,7 @@ class Conflict:
attackers_country: Country,
defenders_country: Country,
position: Point,
heading: Optional[int] = None,
heading: Optional[Heading] = None,
size: Optional[int] = None,
):
@@ -53,26 +55,28 @@ class Conflict:
@classmethod
def frontline_position(
cls, frontline: FrontLine, theater: ConflictTheater
) -> Tuple[Point, int]:
) -> Tuple[Point, Heading]:
attack_heading = frontline.attack_heading
position = cls.find_ground_position(
frontline.position,
FRONTLINE_LENGTH,
heading_sum(attack_heading, 90),
attack_heading.right,
theater,
)
return position, opposite_heading(attack_heading)
if position is None:
raise RuntimeError("Could not find front line position")
return position, attack_heading.opposite
@classmethod
def frontline_vector(
cls, front_line: FrontLine, theater: ConflictTheater
) -> Tuple[Point, int, int]:
) -> Tuple[Point, Heading, int]:
"""
Returns a vector for a valid frontline location avoiding exclusion zones.
"""
center_position, heading = cls.frontline_position(front_line, theater)
left_heading = heading_sum(heading, -90)
right_heading = heading_sum(heading, 90)
left_heading = heading.left
right_heading = heading.right
left_position = cls.extend_ground_position(
center_position, int(FRONTLINE_LENGTH / 2), left_heading, theater
)
@@ -91,7 +95,7 @@ class Conflict:
defender: Country,
front_line: FrontLine,
theater: ConflictTheater,
):
) -> Conflict:
assert cls.has_frontline_between(front_line.blue_cp, front_line.red_cp)
position, heading, distance = cls.frontline_vector(front_line, theater)
conflict = cls(
@@ -109,10 +113,14 @@ class Conflict:
@classmethod
def extend_ground_position(
cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater
cls,
initial: Point,
max_distance: int,
heading: Heading,
theater: ConflictTheater,
) -> Point:
"""Finds the first intersection with an exclusion zone in one heading from an initial point up to max_distance"""
extended = initial.point_from_heading(heading, max_distance)
extended = initial.point_from_heading(heading.degrees, max_distance)
if theater.landmap is None:
# TODO: Why is this possible?
return extended
@@ -129,16 +137,16 @@ class Conflict:
return extended
# Otherwise extend the front line only up to the intersection.
return initial.point_from_heading(heading, p0.distance(intersection))
return initial.point_from_heading(heading.degrees, p0.distance(intersection))
@classmethod
def find_ground_position(
cls,
initial: Point,
max_distance: int,
heading: int,
heading: Heading,
theater: ConflictTheater,
coerce=True,
coerce: bool = True,
) -> Optional[Point]:
"""
Finds the nearest valid ground position along a provided heading and it's inverse up to max_distance.
@@ -149,10 +157,10 @@ class Conflict:
if theater.is_on_land(pos):
return pos
for distance in range(0, int(max_distance), 100):
pos = initial.point_from_heading(heading, distance)
pos = initial.point_from_heading(heading.degrees, distance)
if theater.is_on_land(pos):
return pos
pos = initial.point_from_heading(opposite_heading(heading), distance)
pos = initial.point_from_heading(heading.opposite.degrees, distance)
if theater.is_on_land(pos):
return pos
if coerce:

View File

@@ -27,8 +27,9 @@ class ConvoyGenerator:
def generate(self) -> None:
# Reset the count to make generation deterministic.
for convoy in self.game.transfers.convoys:
self.generate_convoy(convoy)
for coalition in self.game.coalitions:
for convoy in coalition.transfers.convoys:
self.generate_convoy(convoy)
def generate_convoy(self, convoy: Convoy) -> VehicleGroup:
group = self._create_mixed_unit_group(
@@ -53,9 +54,7 @@ class ConvoyGenerator:
units: dict[GroundUnitType, int],
for_player: bool,
) -> VehicleGroup:
country = self.mission.country(
self.game.player_country if for_player else self.game.enemy_country
)
country = self.mission.country(self.game.coalition_for(for_player).country_name)
unit_types = list(units.items())
main_unit_type, main_unit_count = unit_types[0]

View File

@@ -1,4 +1,5 @@
import random
from typing import Optional
from dcs.unitgroup import VehicleGroup
@@ -12,7 +13,9 @@ from gen.defenses.armored_group_generator import (
)
def generate_armor_group(faction: str, game, ground_object):
def generate_armor_group(
faction: str, game: Game, ground_object: VehicleGroupGroundObject
) -> Optional[VehicleGroup]:
"""
This generate a group of ground units
:return: Generated group

View File

@@ -3,10 +3,10 @@ import random
from game import Game
from game.dcs.groundunittype import GroundUnitType
from game.theater.theatergroundobject import VehicleGroupGroundObject
from gen.sam.group_generator import GroupGenerator
from gen.sam.group_generator import VehicleGroupGenerator
class ArmoredGroupGenerator(GroupGenerator):
class ArmoredGroupGenerator(VehicleGroupGenerator[VehicleGroupGroundObject]):
def __init__(
self,
game: Game,
@@ -35,7 +35,7 @@ class ArmoredGroupGenerator(GroupGenerator):
)
class FixedSizeArmorGroupGenerator(GroupGenerator):
class FixedSizeArmorGroupGenerator(VehicleGroupGenerator[VehicleGroupGroundObject]):
def __init__(
self,
game: Game,
@@ -47,7 +47,7 @@ class FixedSizeArmorGroupGenerator(GroupGenerator):
self.unit_type = unit_type
self.size = size
def generate(self):
def generate(self) -> None:
spacing = random.randint(20, 70)
index = 0

View File

@@ -2,7 +2,7 @@ from typing import Optional
from dcs.mission import Mission
from game.weather import Clouds, Fog, Conditions, WindConditions
from game.weather import Clouds, Fog, Conditions, WindConditions, AtmosphericConditions
class EnvironmentGenerator:
@@ -10,6 +10,10 @@ class EnvironmentGenerator:
self.mission = mission
self.conditions = conditions
def set_atmospheric(self, atmospheric: AtmosphericConditions) -> None:
self.mission.weather.qnh = atmospheric.qnh.mm_hg
self.mission.weather.season_temperature = atmospheric.temperature_celsius
def set_clouds(self, clouds: Optional[Clouds]) -> None:
if clouds is None:
return
@@ -22,7 +26,7 @@ class EnvironmentGenerator:
def set_fog(self, fog: Optional[Fog]) -> None:
if fog is None:
return
self.mission.weather.fog_visibility = fog.visibility.meters
self.mission.weather.fog_visibility = int(fog.visibility.meters)
self.mission.weather.fog_thickness = fog.thickness
def set_wind(self, wind: WindConditions) -> None:
@@ -30,8 +34,9 @@ class EnvironmentGenerator:
self.mission.weather.wind_at_2000 = wind.at_2000m
self.mission.weather.wind_at_8000 = wind.at_8000m
def generate(self):
def generate(self) -> None:
self.mission.start_time = self.conditions.start_time
self.set_atmospheric(self.conditions.weather.atmospheric)
self.set_clouds(self.conditions.weather.clouds)
self.set_fog(self.conditions.weather.fog)
self.set_wind(self.conditions.weather.wind)

View File

@@ -1,12 +1,13 @@
import random
from gen.sam.group_generator import ShipGroupGenerator
from game.utils import Heading
from dcs.ships import USS_Arleigh_Burke_IIa, TICONDEROG
class CarrierGroupGenerator(ShipGroupGenerator):
def generate(self):
def generate(self) -> None:
# Carrier Strike Group 8
if self.faction.carrier_names[0] == "Carrier Strike Group 8":
@@ -54,7 +55,7 @@ class CarrierGroupGenerator(ShipGroupGenerator):
)
# Add Ticonderoga escort
if self.heading >= 180:
if self.heading >= Heading.from_degrees(180):
self.add_unit(
TICONDEROG,
"USS Hué City",

View File

@@ -3,7 +3,6 @@ from __future__ import annotations
import random
from typing import TYPE_CHECKING
from dcs.ships import (
Type_052C,
Type_052B,
@@ -11,16 +10,16 @@ from dcs.ships import (
)
from game.factions.faction import Faction
from game.theater.theatergroundobject import ShipGroundObject
from gen.fleet.dd_group import DDGroupGenerator
from gen.sam.group_generator import ShipGroupGenerator
from game.theater.theatergroundobject import TheaterGroundObject
if TYPE_CHECKING:
from game.game import Game
class ChineseNavyGroupGenerator(ShipGroupGenerator):
def generate(self):
def generate(self) -> None:
include_frigate = random.choice([True, True, False])
include_dd = random.choice([True, False])
@@ -65,9 +64,7 @@ class ChineseNavyGroupGenerator(ShipGroupGenerator):
class Type54GroupGenerator(DDGroupGenerator):
def __init__(
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction):
super(Type54GroupGenerator, self).__init__(
game, ground_object, faction, Type_054A
)

View File

@@ -1,12 +1,13 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Type
from game.factions.faction import Faction
from game.theater.theatergroundobject import TheaterGroundObject
from gen.sam.group_generator import ShipGroupGenerator
from dcs.unittype import ShipType
from dcs.ships import PERRY, USS_Arleigh_Burke_IIa
from dcs.unittype import ShipType
from game.factions.faction import Faction
from game.theater.theatergroundobject import ShipGroundObject
from gen.sam.group_generator import ShipGroupGenerator
if TYPE_CHECKING:
from game.game import Game
@@ -16,14 +17,14 @@ class DDGroupGenerator(ShipGroupGenerator):
def __init__(
self,
game: Game,
ground_object: TheaterGroundObject,
ground_object: ShipGroundObject,
faction: Faction,
ddtype: Type[ShipType],
):
super(DDGroupGenerator, self).__init__(game, ground_object, faction)
self.ddtype = ddtype
def generate(self):
def generate(self) -> None:
self.add_unit(
self.ddtype,
"DD1",
@@ -42,18 +43,14 @@ class DDGroupGenerator(ShipGroupGenerator):
class OliverHazardPerryGroupGenerator(DDGroupGenerator):
def __init__(
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction):
super(OliverHazardPerryGroupGenerator, self).__init__(
game, ground_object, faction, PERRY
)
class ArleighBurkeGroupGenerator(DDGroupGenerator):
def __init__(
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction):
super(ArleighBurkeGroupGenerator, self).__init__(
game, ground_object, faction, USS_Arleigh_Burke_IIa
)

View File

@@ -1,12 +1,13 @@
from dcs.ships import La_Combattante_II
from game import Game
from game.factions.faction import Faction
from game.theater import TheaterGroundObject
from game.theater.theatergroundobject import ShipGroundObject
from gen.fleet.dd_group import DDGroupGenerator
class LaCombattanteIIGroupGenerator(DDGroupGenerator):
def __init__(self, game, ground_object: TheaterGroundObject, faction: Faction):
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction):
super(LaCombattanteIIGroupGenerator, self).__init__(
game, ground_object, faction, La_Combattante_II
)

View File

@@ -4,7 +4,7 @@ from gen.sam.group_generator import ShipGroupGenerator
class LHAGroupGenerator(ShipGroupGenerator):
def generate(self):
def generate(self) -> None:
# Add carrier
if len(self.faction.helicopter_carrier) > 0:

View File

@@ -1,4 +1,5 @@
from __future__ import annotations
import random
from typing import TYPE_CHECKING
@@ -12,18 +13,17 @@ from dcs.ships import (
SOM,
)
from game.factions.faction import Faction
from game.theater.theatergroundobject import ShipGroundObject
from gen.fleet.dd_group import DDGroupGenerator
from gen.sam.group_generator import ShipGroupGenerator
from game.factions.faction import Faction
from game.theater.theatergroundobject import TheaterGroundObject
if TYPE_CHECKING:
from game.game import Game
class RussianNavyGroupGenerator(ShipGroupGenerator):
def generate(self):
def generate(self) -> None:
include_frigate = random.choice([True, True, False])
include_dd = random.choice([True, False])
@@ -85,32 +85,24 @@ class RussianNavyGroupGenerator(ShipGroupGenerator):
class GrishaGroupGenerator(DDGroupGenerator):
def __init__(
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction):
super(GrishaGroupGenerator, self).__init__(
game, ground_object, faction, ALBATROS
)
class MolniyaGroupGenerator(DDGroupGenerator):
def __init__(
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction):
super(MolniyaGroupGenerator, self).__init__(
game, ground_object, faction, MOLNIYA
)
class KiloSubGroupGenerator(DDGroupGenerator):
def __init__(
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction):
super(KiloSubGroupGenerator, self).__init__(game, ground_object, faction, KILO)
class TangoSubGroupGenerator(DDGroupGenerator):
def __init__(
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction):
super(TangoSubGroupGenerator, self).__init__(game, ground_object, faction, SOM)

View File

@@ -6,7 +6,7 @@ from gen.sam.group_generator import ShipGroupGenerator
class SchnellbootGroupGenerator(ShipGroupGenerator):
def generate(self):
def generate(self) -> None:
for i in range(random.randint(2, 4)):
self.add_unit(

View File

@@ -1,7 +1,17 @@
from __future__ import annotations
import logging
import random
from typing import TYPE_CHECKING, Optional
from dcs.unitgroup import ShipGroup
from game import db
from game.theater.theatergroundobject import (
LhaGroundObject,
CarrierGroundObject,
ShipGroundObject,
)
from gen.fleet.carrier_group import CarrierGroupGenerator
from gen.fleet.cn_dd_group import ChineseNavyGroupGenerator, Type54GroupGenerator
from gen.fleet.dd_group import (
@@ -21,6 +31,9 @@ from gen.fleet.schnellboot import SchnellbootGroupGenerator
from gen.fleet.uboat import UBoatGroupGenerator
from gen.fleet.ww2lst import WW2LSTGroupGenerator
if TYPE_CHECKING:
from game import Game
SHIP_MAP = {
"SchnellbootGroupGenerator": SchnellbootGroupGenerator,
@@ -39,10 +52,12 @@ SHIP_MAP = {
}
def generate_ship_group(game, ground_object, faction_name: str):
def generate_ship_group(
game: Game, ground_object: ShipGroundObject, faction_name: str
) -> Optional[ShipGroup]:
"""
This generate a ship group
:return: Nothing, but put the group reference inside the ground object
:return: The generated group, or None if this faction does not support ships.
"""
faction = db.FACTIONS[faction_name]
if len(faction.navy_generators) > 0:
@@ -61,26 +76,30 @@ def generate_ship_group(game, ground_object, faction_name: str):
return None
def generate_carrier_group(faction: str, game, ground_object):
"""
This generate a carrier group
:param parentCp: The parent control point
def generate_carrier_group(
faction: str, game: Game, ground_object: CarrierGroundObject
) -> ShipGroup:
"""Generates a carrier group.
:param faction: The faction the TGO belongs to.
:param game: The Game the group is being generated for.
:param ground_object: The ground object which will own the ship group
:param country: Owner country
:return: Nothing, but put the group reference inside the ground object
:return: The generated group.
"""
generator = CarrierGroupGenerator(game, ground_object, db.FACTIONS[faction])
generator.generate()
return generator.get_generated_group()
def generate_lha_group(faction: str, game, ground_object):
"""
This generate a lha carrier group
:param parentCp: The parent control point
def generate_lha_group(
faction: str, game: Game, ground_object: LhaGroundObject
) -> ShipGroup:
"""Generate an LHA group.
:param faction: The faction the TGO belongs to.
:param game: The Game the group is being generated for.
:param ground_object: The ground object which will own the ship group
:param country: Owner country
:return: Nothing, but put the group reference inside the ground object
:return: The generated group.
"""
generator = LHAGroupGenerator(game, ground_object, db.FACTIONS[faction])
generator.generate()

View File

@@ -6,7 +6,7 @@ from gen.sam.group_generator import ShipGroupGenerator
class UBoatGroupGenerator(ShipGroupGenerator):
def generate(self):
def generate(self) -> None:
for i in range(random.randint(1, 4)):
self.add_unit(

View File

@@ -6,7 +6,7 @@ from gen.sam.group_generator import ShipGroupGenerator
class WW2LSTGroupGenerator(ShipGroupGenerator):
def generate(self):
def generate(self) -> None:
# Add LS Samuel Chase
self.add_unit(

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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}"

View File

@@ -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.

View File

@@ -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)

View File

@@ -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:

View File

@@ -38,12 +38,12 @@ class ForcedOptionsGenerator:
self.mission.forced_options.labels = ForcedOptions.Labels.None_
def _set_unrestricted_satnav(self) -> None:
blue = self.game.player_faction
red = self.game.enemy_faction
blue = self.game.blue.faction
red = self.game.red.faction
if blue.unrestricted_satnav or red.unrestricted_satnav:
self.mission.forced_options.unrestricted_satnav = True
def generate(self):
def generate(self) -> None:
self._set_options_view()
self._set_external_views()
self._set_labels()

View File

@@ -1,13 +1,18 @@
from __future__ import annotations
import logging
import random
from enum import Enum
from typing import Dict, List
from typing import Dict, List, TYPE_CHECKING
from game.data.groundunitclass import GroundUnitClass
from game.dcs.groundunittype import GroundUnitType
from game.theater import ControlPoint
from gen.ground_forces.combat_stance import CombatStance
if TYPE_CHECKING:
from game import Game
MAX_COMBAT_GROUP_PER_CP = 10
@@ -52,10 +57,9 @@ class CombatGroup:
self.unit_type = unit_type
self.size = size
self.role = role
self.assigned_enemy_cp = None
self.start_position = None
def __str__(self):
def __str__(self) -> str:
s = f"ROLE : {self.role}\n"
if self.size:
s += f"UNITS {self.unit_type} * {self.size}"
@@ -63,7 +67,7 @@ class CombatGroup:
class GroundPlanner:
def __init__(self, cp: ControlPoint, game):
def __init__(self, cp: ControlPoint, game: Game) -> None:
self.cp = cp
self.game = game
self.connected_enemy_cp = [
@@ -83,17 +87,15 @@ class GroundPlanner:
self.units_per_cp[cp.id] = []
self.reserve: List[CombatGroup] = []
def plan_groundwar(self):
def plan_groundwar(self) -> None:
ground_unit_limit = self.cp.frontline_unit_count_limit
remaining_available_frontline_units = ground_unit_limit
if hasattr(self.cp, "stance"):
group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[self.cp.stance]
else:
self.cp.stance = CombatStance.DEFENSIVE
group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[CombatStance.DEFENSIVE]
# TODO: Fix to handle the per-front stances.
# https://github.com/dcs-liberation/dcs_liberation/issues/1417
group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[CombatStance.DEFENSIVE]
# Create combat groups and assign them randomly to each enemy CP
for unit_type in self.cp.base.armor:
@@ -152,20 +154,9 @@ class GroundPlanner:
if len(self.connected_enemy_cp) > 0:
enemy_cp = random.choice(self.connected_enemy_cp).id
self.units_per_cp[enemy_cp].append(group)
group.assigned_enemy_cp = enemy_cp
else:
self.reserve.append(group)
group.assigned_enemy_cp = "__reserve__"
collection.append(group)
if remaining_available_frontline_units == 0:
break
print("------------------")
print("Ground Planner : ")
print(self.cp.name)
print("------------------")
for unit_type in self.units_per_cp.keys():
print("For : #" + str(unit_type))
for group in self.units_per_cp[unit_type]:
print(str(group))

View File

@@ -9,7 +9,18 @@ from __future__ import annotations
import logging
import random
from typing import Dict, Iterator, Optional, TYPE_CHECKING, Type, List
from typing import (
Dict,
Iterator,
Optional,
TYPE_CHECKING,
Type,
List,
TypeVar,
Any,
Generic,
Union,
)
from dcs import Mission, Point, unitgroup
from dcs.action import SceneryDestructionZone
@@ -25,13 +36,13 @@ from dcs.task import (
)
from dcs.triggers import TriggerStart, TriggerZone
from dcs.unit import Ship, Unit, Vehicle, InvisibleFARP
from dcs.unitgroup import Group, ShipGroup, StaticGroup, VehicleGroup
from dcs.unittype import StaticType, UnitType
from dcs.unitgroup import ShipGroup, StaticGroup, VehicleGroup
from dcs.unittype import StaticType, ShipType, VehicleType
from dcs.vehicles import vehicle_map
from game import db
from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID
from game.db import unit_type_from_name
from game.db import unit_type_from_name, ship_type_from_name, vehicle_type_from_name
from game.theater import ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import (
BuildingGroundObject,
@@ -44,7 +55,7 @@ from game.theater.theatergroundobject import (
SceneryGroundObject,
)
from game.unitmap import UnitMap
from game.utils import feet, knots, mps
from game.utils import Heading, feet, knots, mps
from .radios import RadioFrequency, RadioRegistry
from .runways import RunwayData
from .tacan import TacanBand, TacanChannel, TacanRegistry
@@ -56,7 +67,10 @@ FARP_FRONTLINE_DISTANCE = 10000
AA_CP_MIN_DISTANCE = 40000
class GenericGroundObjectGenerator:
TgoT = TypeVar("TgoT", bound=TheaterGroundObject[Any])
class GenericGroundObjectGenerator(Generic[TgoT]):
"""An unspecialized ground object generator.
Currently used only for SAM
@@ -64,7 +78,7 @@ class GenericGroundObjectGenerator:
def __init__(
self,
ground_object: TheaterGroundObject,
ground_object: TgoT,
country: Country,
game: Game,
mission: Mission,
@@ -89,10 +103,7 @@ class GenericGroundObjectGenerator:
logging.warning(f"Found empty group in {self.ground_object}")
continue
unit_type = unit_type_from_name(group.units[0].type)
if unit_type is None:
raise RuntimeError(f"Unrecognized unit type: {group.units[0].type}")
unit_type = vehicle_type_from_name(group.units[0].type)
vg = self.m.vehicle_group(
self.country,
group.name,
@@ -116,24 +127,27 @@ class GenericGroundObjectGenerator:
self._register_unit_group(group, vg)
@staticmethod
def enable_eplrs(group: Group, unit_type: Type[UnitType]) -> None:
if hasattr(unit_type, "eplrs"):
if unit_type.eplrs:
group.points[0].tasks.append(EPLRS(group.id))
def enable_eplrs(group: VehicleGroup, unit_type: Type[VehicleType]) -> None:
if unit_type.eplrs:
group.points[0].tasks.append(EPLRS(group.id))
def set_alarm_state(self, group: Group) -> None:
def set_alarm_state(self, group: Union[ShipGroup, VehicleGroup]) -> None:
if self.game.settings.perf_red_alert_state:
group.points[0].tasks.append(OptAlarmState(2))
else:
group.points[0].tasks.append(OptAlarmState(1))
def _register_unit_group(self, persistence_group: Group, miz_group: Group) -> None:
def _register_unit_group(
self,
persistence_group: Union[ShipGroup, VehicleGroup],
miz_group: Union[ShipGroup, VehicleGroup],
) -> None:
self.unit_map.add_ground_object_units(
self.ground_object, persistence_group, miz_group
)
class MissileSiteGenerator(GenericGroundObjectGenerator):
class MissileSiteGenerator(GenericGroundObjectGenerator[MissileSiteGroundObject]):
@property
def culled(self) -> bool:
# Don't cull missile sites - their range is long enough to make them easily
@@ -148,11 +162,11 @@ class MissileSiteGenerator(GenericGroundObjectGenerator):
for group in self.ground_object.groups:
vg = self.m.find_group(group.name)
if vg is not None:
targets = self.possible_missile_targets(vg)
targets = self.possible_missile_targets()
if targets:
target = random.choice(targets)
real_target = target.point_from_heading(
random.randint(0, 360), random.randint(0, 2500)
Heading.random().degrees, random.randint(0, 2500)
)
vg.points[0].add_task(FireAtPoint(real_target))
logging.info("Set up fire task for missile group.")
@@ -165,7 +179,7 @@ class MissileSiteGenerator(GenericGroundObjectGenerator):
"Couldn't setup missile site to fire, group was not generated."
)
def possible_missile_targets(self, vg: Group) -> List[Point]:
def possible_missile_targets(self) -> List[Point]:
"""
Find enemy control points in range
:param vg: Vehicle group we are searching a target for (There is always only oe group right now)
@@ -174,7 +188,7 @@ class MissileSiteGenerator(GenericGroundObjectGenerator):
targets: List[Point] = []
for cp in self.game.theater.controlpoints:
if cp.captured != self.ground_object.control_point.captured:
distance = cp.position.distance_to_point(vg.position)
distance = cp.position.distance_to_point(self.ground_object.position)
if distance < self.missile_site_range:
targets.append(cp.position)
return targets
@@ -196,7 +210,7 @@ class MissileSiteGenerator(GenericGroundObjectGenerator):
return site_range
class BuildingSiteGenerator(GenericGroundObjectGenerator):
class BuildingSiteGenerator(GenericGroundObjectGenerator[BuildingGroundObject]):
"""Generator for building sites.
Building sites are the primary type of non-airbase objective locations that
@@ -225,14 +239,14 @@ class BuildingSiteGenerator(GenericGroundObjectGenerator):
f"{self.ground_object.dcs_identifier} not found in static maps"
)
def generate_vehicle_group(self, unit_type: Type[UnitType]) -> None:
def generate_vehicle_group(self, unit_type: Type[VehicleType]) -> None:
if not self.ground_object.is_dead:
group = self.m.vehicle_group(
country=self.country,
name=self.ground_object.group_name,
_type=unit_type,
position=self.ground_object.position,
heading=self.ground_object.heading,
heading=self.ground_object.heading.degrees,
)
self._register_fortification(group)
@@ -242,7 +256,7 @@ class BuildingSiteGenerator(GenericGroundObjectGenerator):
name=self.ground_object.group_name,
_type=static_type,
position=self.ground_object.position,
heading=self.ground_object.heading,
heading=self.ground_object.heading.degrees,
dead=self.ground_object.is_dead,
)
self._register_building(group)
@@ -324,7 +338,7 @@ class SceneryGenerator(BuildingSiteGenerator):
self.unit_map.add_scenery(scenery)
class GenericCarrierGenerator(GenericGroundObjectGenerator):
class GenericCarrierGenerator(GenericGroundObjectGenerator[GenericCarrierGroundObject]):
"""Base type for carrier group generation.
Used by both CV(N) groups and LHA groups.
@@ -373,16 +387,17 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
# time as the recovery window.
brc = self.steam_into_wind(ship_group)
self.activate_beacons(ship_group, tacan, tacan_callsign, icls)
self.add_runway_data(brc or 0, atc, tacan, tacan_callsign, icls)
self.add_runway_data(
brc or Heading.from_degrees(0), atc, tacan, tacan_callsign, icls
)
self._register_unit_group(group, ship_group)
def get_carrier_type(self, group: Group) -> Type[UnitType]:
unit_type = unit_type_from_name(group.units[0].type)
if unit_type is None:
raise RuntimeError(f"Unrecognized carrier name: {group.units[0].type}")
return unit_type
def get_carrier_type(self, group: ShipGroup) -> Type[ShipType]:
return ship_type_from_name(group.units[0].type)
def configure_carrier(self, group: Group, atc_channel: RadioFrequency) -> ShipGroup:
def configure_carrier(
self, group: ShipGroup, atc_channel: RadioFrequency
) -> ShipGroup:
unit_type = self.get_carrier_type(group)
ship_group = self.m.ship_group(
@@ -409,14 +424,14 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
ship.set_frequency(atc_channel.hertz)
return ship
def steam_into_wind(self, group: ShipGroup) -> Optional[int]:
def steam_into_wind(self, group: ShipGroup) -> Optional[Heading]:
wind = self.game.conditions.weather.wind.at_0m
brc = wind.direction + 180
brc = Heading.from_degrees(wind.direction).opposite
# Aim for 25kts over the deck.
carrier_speed = knots(25) - mps(wind.speed)
for attempt in range(5):
point = group.points[0].position.point_from_heading(
brc, 100000 - attempt * 20000
brc.degrees, 100000 - attempt * 20000
)
if self.game.theater.is_in_sea(point):
group.points[0].speed = carrier_speed.meters_per_second
@@ -446,7 +461,7 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
def add_runway_data(
self,
brc: int,
brc: Heading,
atc: RadioFrequency,
tacan: TacanChannel,
callsign: str,
@@ -474,7 +489,7 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
class CarrierGenerator(GenericCarrierGenerator):
"""Generator for CV(N) groups."""
def get_carrier_type(self, group: Group) -> UnitType:
def get_carrier_type(self, group: ShipGroup) -> Type[ShipType]:
unit_type = super().get_carrier_type(group)
if self.game.settings.supercarrier:
unit_type = db.upgrade_to_supercarrier(unit_type, self.control_point.name)
@@ -518,7 +533,7 @@ class LhaGenerator(GenericCarrierGenerator):
)
class ShipObjectGenerator(GenericGroundObjectGenerator):
class ShipObjectGenerator(GenericGroundObjectGenerator[ShipGroundObject]):
"""Generator for non-carrier naval groups."""
def generate(self) -> None:
@@ -529,14 +544,11 @@ class ShipObjectGenerator(GenericGroundObjectGenerator):
if not group.units:
logging.warning(f"Found empty group in {self.ground_object}")
continue
self.generate_group(group, ship_type_from_name(group.units[0].type))
unit_type = unit_type_from_name(group.units[0].type)
if unit_type is None:
raise RuntimeError(f"Unrecognized unit type: {group.units[0].type}")
self.generate_group(group, unit_type)
def generate_group(self, group_def: Group, first_unit_type: Type[UnitType]) -> None:
def generate_group(
self, group_def: ShipGroup, first_unit_type: Type[ShipType]
) -> None:
group = self.m.ship_group(
self.country,
group_def.name,
@@ -578,21 +590,15 @@ class HelipadGenerator:
def generate(self) -> None:
if self.cp.captured:
country_name = self.game.player_country
else:
country_name = self.game.enemy_country
country = self.m.country(country_name)
# Note : Helipad are generated as neutral object in order not to interfer with capture triggers
neutral_country = self.m.country(self.game.neutral_country.name)
country = self.m.country(self.game.coalition_for(self.cp.captured).country_name)
for i, helipad in enumerate(self.cp.helipads):
name = self.cp.name + "_helipad_" + str(i)
logging.info("Generating helipad : " + name)
pad = InvisibleFARP(name=name)
pad.position = Point(helipad.x, helipad.y)
pad.heading = helipad.heading
pad.heading = helipad.heading.degrees
sg = unitgroup.StaticGroup(self.m.next_group_id(), name)
sg.add_unit(pad)
sp = StaticPoint()
@@ -647,19 +653,15 @@ class GroundObjectsGenerator:
self.icls_alloc = iter(range(1, 21))
self.runways: Dict[str, RunwayData] = {}
def generate(self):
def generate(self) -> None:
for cp in self.game.theater.controlpoints:
if cp.captured:
country_name = self.game.player_country
else:
country_name = self.game.enemy_country
country = self.m.country(country_name)
country = self.m.country(self.game.coalition_for(cp.captured).country_name)
HelipadGenerator(
self.m, cp, self.game, self.radio_registry, self.tacan_registry
).generate()
for ground_object in cp.ground_objects:
generator: GenericGroundObjectGenerator[Any]
if isinstance(ground_object, FactoryGroundObject):
generator = FactoryGenerator(
ground_object, country, self.game, self.m, self.unit_map

View File

@@ -23,6 +23,7 @@ only be added per airframe, so PvP missions where each side have the same
aircraft will be able to see the enemy's kneeboard for the same airframe.
"""
import datetime
import math
import textwrap
from collections import defaultdict
from dataclasses import dataclass
@@ -40,6 +41,7 @@ from game.dcs.aircrafttype import AircraftType
from game.theater import ConflictTheater, TheaterGroundObject, LatLon
from game.theater.bullseye import Bullseye
from game.utils import meters
from game.weather import Weather
from .aircraft import FlightData
from .airsupportgen import AwacsInfo, TankerInfo
from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator
@@ -76,7 +78,7 @@ class KneeboardPageWriter:
"arial.ttf", 24, layout_engine=ImageFont.LAYOUT_BASIC
)
self.content_font = ImageFont.truetype(
"arial.ttf", 20, layout_engine=ImageFont.LAYOUT_BASIC
"arial.ttf", 16, layout_engine=ImageFont.LAYOUT_BASIC
)
self.table_font = ImageFont.truetype(
"resources/fonts/Inconsolata.otf", 20, layout_engine=ImageFont.LAYOUT_BASIC
@@ -91,10 +93,15 @@ class KneeboardPageWriter:
return self.x, self.y
def text(
self, text: str, font=None, fill: Tuple[int, int, int] = (0, 0, 0)
self,
text: str,
font: Optional[ImageFont.FreeTypeFont] = None,
fill: Optional[Tuple[int, int, int]] = None,
) -> None:
if font is None:
font = self.content_font
if fill is None:
fill = self.foreground_fill
self.draw.text(self.position, text, font=font, fill=fill)
width, height = self.draw.textsize(text, font=font)
@@ -107,12 +114,17 @@ class KneeboardPageWriter:
self.text(text, font=self.heading_font, fill=self.foreground_fill)
def table(
self, cells: List[List[str]], headers: Optional[List[str]] = None
self,
cells: List[List[str]],
headers: Optional[List[str]] = None,
font: Optional[ImageFont.FreeTypeFont] = None,
) -> None:
if headers is None:
headers = []
if font is None:
font = self.table_font
table = tabulate(cells, headers=headers, numalign="right")
self.text(table, font=self.table_font, fill=self.foreground_fill)
self.text(table, font, fill=self.foreground_fill)
def write(self, path: Path) -> None:
self.image.save(path)
@@ -195,6 +207,7 @@ class FlightPlanBuilder:
self._ground_speed(self.target_points[0].waypoint),
self._format_time(self.target_points[0].waypoint.tot),
self._format_time(self.target_points[0].waypoint.departure_time),
self._format_min_fuel(self.target_points[0].waypoint.min_fuel),
]
)
self.last_waypoint = self.target_points[-1].waypoint
@@ -212,6 +225,7 @@ class FlightPlanBuilder:
self._ground_speed(waypoint.waypoint),
self._format_time(waypoint.waypoint.tot),
self._format_time(waypoint.waypoint.departure_time),
self._format_min_fuel(waypoint.waypoint.min_fuel),
]
)
@@ -250,6 +264,12 @@ class FlightPlanBuilder:
duration = (waypoint.tot - last_time).total_seconds() / 3600
return f"{int(distance.nautical_miles / duration)} kt"
@staticmethod
def _format_min_fuel(min_fuel: Optional[float]) -> str:
if min_fuel is None:
return ""
return str(math.ceil(min_fuel / 100) * 100)
def build(self) -> List[List[str]]:
return self.rows
@@ -262,14 +282,21 @@ class BriefingPage(KneeboardPage):
flight: FlightData,
bullseye: Bullseye,
theater: ConflictTheater,
weather: Weather,
start_time: datetime.datetime,
dark_kneeboard: bool,
) -> None:
self.flight = flight
self.bullseye = bullseye
self.theater = theater
self.weather = weather
self.start_time = start_time
self.dark_kneeboard = dark_kneeboard
self.flight_plan_font = ImageFont.truetype(
"resources/fonts/Inconsolata.otf",
16,
layout_engine=ImageFont.LAYOUT_BASIC,
)
def write(self, path: Path) -> None:
writer = KneeboardPageWriter(dark_theme=self.dark_kneeboard)
@@ -296,11 +323,29 @@ class BriefingPage(KneeboardPage):
flight_plan_builder.add_waypoint(num, waypoint)
writer.table(
flight_plan_builder.build(),
headers=["#", "Action", "Alt", "Dist", "GSPD", "Time", "Departure"],
headers=[
"#",
"Action",
"Alt",
"Dist",
"GSPD",
"Time",
"Departure",
"Min fuel",
],
font=self.flight_plan_font,
)
writer.text(f"Bullseye: {self.bullseye.to_lat_lon(self.theater).format_dms()}")
qnh_in_hg = f"{self.weather.atmospheric.qnh.inches_hg:.2f}"
qnh_mm_hg = f"{self.weather.atmospheric.qnh.mm_hg:.1f}"
qnh_hpa = f"{self.weather.atmospheric.qnh.hecto_pascals:.1f}"
writer.text(
f"Temperature: {round(self.weather.atmospheric.temperature_celsius)} °C at sea level"
)
writer.text(f"QNH: {qnh_in_hg} inHg / {qnh_mm_hg} mmHg / {qnh_hpa} hPa")
writer.table(
[
[
@@ -311,6 +356,12 @@ class BriefingPage(KneeboardPage):
["Bingo", "Joker"],
)
if any(self.flight.laser_codes):
codes: list[list[str]] = []
for idx, code in enumerate(self.flight.laser_codes, start=1):
codes.append([str(idx), "" if code is None else str(code)])
writer.table(codes, ["#", "Laser Code"])
writer.write(path)
def airfield_info_row(
@@ -365,6 +416,8 @@ class BriefingPage(KneeboardPage):
class SupportPage(KneeboardPage):
"""A kneeboard page containing information about support units."""
JTAC_REGION_MAX_LEN = 25
def __init__(
self,
flight: FlightData,
@@ -408,7 +461,7 @@ class SupportPage(KneeboardPage):
aewc_ladder.append(
[
str(single_aewc.callsign),
str(single_aewc.freq),
self.format_frequency(single_aewc.freq),
str(single_aewc.depature_location),
str(dep),
str(arr),
@@ -444,8 +497,18 @@ class SupportPage(KneeboardPage):
writer.heading("JTAC")
jtacs = []
for jtac in self.jtacs:
jtacs.append([jtac.callsign, jtac.region, jtac.code])
writer.table(jtacs, headers=["Callsign", "Region", "Laser Code"])
jtacs.append(
[
jtac.callsign,
KneeboardPageWriter.wrap_line(
jtac.region,
self.JTAC_REGION_MAX_LEN,
),
jtac.code,
self.format_frequency(jtac.freq),
]
)
writer.table(jtacs, headers=["Callsign", "Region", "Laser Code", "FREQ"])
writer.write(path)
@@ -554,6 +617,24 @@ class StrikeTaskPage(KneeboardPage):
]
class NotesPage(KneeboardPage):
"""A kneeboard page containing the campaign owner's notes."""
def __init__(
self,
notes: str,
dark_kneeboard: bool,
) -> None:
self.notes = notes
self.dark_kneeboard = dark_kneeboard
def write(self, path: Path) -> None:
writer = KneeboardPageWriter(dark_theme=self.dark_kneeboard)
writer.title(f"Notes")
writer.text(self.notes)
writer.write(path)
class KneeboardGenerator(MissionInfoGenerator):
"""Creates kneeboard pages for each client flight in the mission."""
@@ -609,6 +690,7 @@ class KneeboardGenerator(MissionInfoGenerator):
flight,
self.game.bullseye_for(flight.friendly),
self.game.theater,
self.game.conditions.weather,
self.mission.start_time,
self.dark_kneeboard,
),
@@ -623,6 +705,10 @@ class KneeboardGenerator(MissionInfoGenerator):
),
]
# Only create the notes page if there are notes to show.
if notes := self.game.notes:
pages.append(NotesPage(notes, self.dark_kneeboard))
if (target_page := self.generate_task_page(flight)) is not None:
pages.append(target_page)

37
gen/lasercoderegistry.py Normal file
View File

@@ -0,0 +1,37 @@
from collections import deque
from typing import Iterator
class OutOfLaserCodesError(RuntimeError):
def __init__(self) -> None:
super().__init__(
f"All JTAC laser codes have been allocated. No available codes."
)
class LaserCodeRegistry:
def __init__(self) -> None:
self.allocated_codes: set[int] = set()
self.allocator: Iterator[int] = LaserCodeRegistry.__laser_code_generator()
def get_next_laser_code(self) -> int:
try:
while (code := next(self.allocator)) in self.allocated_codes:
pass
self.allocated_codes.add(code)
return code
except StopIteration:
raise OutOfLaserCodesError
@staticmethod
def __laser_code_generator() -> Iterator[int]:
# Valid laser codes are as follows
# First digit is always 1
# Second digit is 5-7
# Third and fourth digits are 1 - 8
# We iterate backward (reversed()) so that 1687 follows 1688
q = deque(int(oct(code)[2:]) + 11 for code in reversed(range(0o1500, 0o2000)))
# We start with the default of 1688 and wrap around when we reach the end
q.rotate(-q.index(1688))
return iter(q)

View File

@@ -1,22 +0,0 @@
from dataclasses import dataclass, field
from typing import List
from gen.locations.preset_locations import PresetLocation
@dataclass
class PresetControlPointLocations:
"""A repository of preset locations for a given control point"""
# List of possible ashore locations to generate objects (Represented in miz file by an APC_AAV_7_Amphibious)
ashore_locations: List[PresetLocation] = field(default_factory=list)
# List of possible offshore locations to generate ship groups (Represented in miz file by an Oliver Hazard Perry)
offshore_locations: List[PresetLocation] = field(default_factory=list)
# Possible antiship missiles sites locations (Represented in miz file by Iranian Silkworm missiles)
antiship_locations: List[PresetLocation] = field(default_factory=list)
# List of possible powerplants locations (Represented in miz file by static Workshop A object, USA)
powerplant_locations: List[PresetLocation] = field(default_factory=list)

View File

@@ -1,21 +0,0 @@
from dataclasses import dataclass
from dcs import Point
@dataclass
class PresetLocation:
"""A preset location"""
position: Point
heading: int
id: str
def __str__(self):
return (
"-" * 10
+ "X: {}\n Y: {}\nHdg: {}°\nId: {}".format(
self.position.x, self.position.y, self.heading, self.id
)
+ "-" * 10
)

View File

@@ -1,13 +1,20 @@
import logging
import random
from game import db
from typing import Optional
from dcs.unitgroup import VehicleGroup
from game import db, Game
from game.theater.theatergroundobject import MissileSiteGroundObject
from gen.missiles.scud_site import ScudGenerator
from gen.missiles.v1_group import V1GroupGenerator
MISSILES_MAP = {"V1GroupGenerator": V1GroupGenerator, "ScudGenerator": ScudGenerator}
def generate_missile_group(game, ground_object, faction_name: str):
def generate_missile_group(
game: Game, ground_object: MissileSiteGroundObject, faction_name: str
) -> Optional[VehicleGroup]:
"""
This generate a missiles group
:return: Nothing, but put the group reference inside the ground object

View File

@@ -2,15 +2,21 @@ import random
from dcs.vehicles import Unarmed, MissilesSS, AirDefence
from gen.sam.group_generator import GroupGenerator
from game import Game
from game.factions.faction import Faction
from game.theater.theatergroundobject import MissileSiteGroundObject
from game.utils import Heading
from gen.sam.group_generator import VehicleGroupGenerator
class ScudGenerator(GroupGenerator):
def __init__(self, game, ground_object, faction):
class ScudGenerator(VehicleGroupGenerator[MissileSiteGroundObject]):
def __init__(
self, game: Game, ground_object: MissileSiteGroundObject, faction: Faction
) -> None:
super(ScudGenerator, self).__init__(game, ground_object)
self.faction = faction
def generate(self):
def generate(self) -> None:
# Scuds
self.add_unit(
@@ -58,5 +64,5 @@ class ScudGenerator(GroupGenerator):
"STRELA#0",
self.position.x + 200,
self.position.y + 15,
90,
Heading.from_degrees(90),
)

View File

@@ -2,15 +2,21 @@ import random
from dcs.vehicles import Unarmed, MissilesSS, AirDefence
from gen.sam.group_generator import GroupGenerator
from game import Game
from game.factions.faction import Faction
from game.theater.theatergroundobject import MissileSiteGroundObject
from game.utils import Heading
from gen.sam.group_generator import VehicleGroupGenerator
class V1GroupGenerator(GroupGenerator):
def __init__(self, game, ground_object, faction):
class V1GroupGenerator(VehicleGroupGenerator[MissileSiteGroundObject]):
def __init__(
self, game: Game, ground_object: MissileSiteGroundObject, faction: Faction
) -> None:
super(V1GroupGenerator, self).__init__(game, ground_object)
self.faction = faction
def generate(self):
def generate(self) -> None:
# Ramps
self.add_unit(
@@ -60,5 +66,5 @@ class V1GroupGenerator(GroupGenerator):
"Blitz#0",
self.position.x + 200,
self.position.y + 15,
90,
Heading.from_degrees(90),
)

View File

@@ -1,6 +1,6 @@
import random
import time
from typing import List
from typing import List, Any
from dcs.country import Country
@@ -256,7 +256,7 @@ class NameGenerator:
existing_alphas: List[str] = []
@classmethod
def reset(cls):
def reset(cls) -> None:
cls.number = 0
cls.infantry_number = 0
cls.convoy_number = 0
@@ -265,7 +265,7 @@ class NameGenerator:
cls.existing_alphas = []
@classmethod
def reset_numbers(cls):
def reset_numbers(cls) -> None:
cls.number = 0
cls.infantry_number = 0
cls.aircraft_number = 0
@@ -273,7 +273,9 @@ class NameGenerator:
cls.cargo_ship_number = 0
@classmethod
def next_aircraft_name(cls, country: Country, parent_base_id: int, flight: Flight):
def next_aircraft_name(
cls, country: Country, parent_base_id: int, flight: Flight
) -> str:
cls.aircraft_number += 1
try:
if flight.custom_name:
@@ -293,7 +295,9 @@ class NameGenerator:
)
@classmethod
def next_unit_name(cls, country: Country, parent_base_id: int, unit_type: UnitType):
def next_unit_name(
cls, country: Country, parent_base_id: int, unit_type: UnitType[Any]
) -> str:
cls.number += 1
return "unit|{}|{}|{}|{}|".format(
country.id, cls.number, parent_base_id, unit_type.name
@@ -301,8 +305,8 @@ class NameGenerator:
@classmethod
def next_infantry_name(
cls, country: Country, parent_base_id: int, unit_type: UnitType
):
cls, country: Country, parent_base_id: int, unit_type: UnitType[Any]
) -> str:
cls.infantry_number += 1
return "infantry|{}|{}|{}|{}|".format(
country.id,
@@ -312,17 +316,17 @@ class NameGenerator:
)
@classmethod
def next_awacs_name(cls, country: Country):
def next_awacs_name(cls, country: Country) -> str:
cls.number += 1
return "awacs|{}|{}|0|".format(country.id, cls.number)
@classmethod
def next_tanker_name(cls, country: Country, unit_type: AircraftType):
def next_tanker_name(cls, country: Country, unit_type: AircraftType) -> str:
cls.number += 1
return "tanker|{}|{}|0|{}".format(country.id, cls.number, unit_type.name)
@classmethod
def next_carrier_name(cls, country: Country):
def next_carrier_name(cls, country: Country) -> str:
cls.number += 1
return "carrier|{}|{}|0|".format(country.id, cls.number)
@@ -337,7 +341,7 @@ class NameGenerator:
return f"Cargo Ship {cls.cargo_ship_number:03}"
@classmethod
def random_objective_name(cls):
def random_objective_name(cls) -> str:
if cls.animals:
animal = random.choice(cls.animals)
cls.animals.remove(animal)

View File

@@ -15,7 +15,7 @@ class RadioFrequency:
#: The frequency in kilohertz.
hertz: int
def __str__(self):
def __str__(self) -> str:
if self.hertz >= 1000000:
return self.format("MHz", 1000000)
return self.format("kHz", 1000)

View File

@@ -8,6 +8,7 @@ from typing import Iterator, Optional
from dcs.terrain.terrain import Airport
from game.weather import Conditions
from game.utils import Heading
from .airfields import AIRFIELD_DATA
from .radios import RadioFrequency
from .tacan import TacanChannel
@@ -16,7 +17,7 @@ from .tacan import TacanChannel
@dataclass(frozen=True)
class RunwayData:
airfield_name: str
runway_heading: int
runway_heading: Heading
runway_name: str
atc: Optional[RadioFrequency] = None
tacan: Optional[TacanChannel] = None
@@ -26,7 +27,7 @@ class RunwayData:
@classmethod
def for_airfield(
cls, airport: Airport, runway_heading: int, runway_name: str
cls, airport: Airport, runway_heading: Heading, runway_name: str
) -> RunwayData:
"""Creates RunwayData for the given runway of an airfield.
@@ -66,12 +67,14 @@ class RunwayData:
runway_number = runway.heading // 10
runway_side = ["", "L", "R"][runway.leftright]
runway_name = f"{runway_number:02}{runway_side}"
yield cls.for_airfield(airport, runway.heading, runway_name)
yield cls.for_airfield(
airport, Heading.from_degrees(runway.heading), runway_name
)
# pydcs only exposes one runway per physical runway, so to expose
# both sides of the runway we need to generate the other.
heading = (runway.heading + 180) % 360
runway_number = heading // 10
heading = Heading.from_degrees(runway.heading).opposite
runway_number = heading.degrees // 10
runway_side = ["", "R", "L"][runway.leftright]
runway_name = f"{runway_number:02}{runway_side}"
yield cls.for_airfield(airport, heading, runway_name)
@@ -81,10 +84,10 @@ class RunwayAssigner:
def __init__(self, conditions: Conditions):
self.conditions = conditions
def angle_off_headwind(self, runway: RunwayData) -> int:
wind = self.conditions.weather.wind.at_0m.direction
ideal_heading = (wind + 180) % 360
return abs(runway.runway_heading - ideal_heading)
def angle_off_headwind(self, runway: RunwayData) -> Heading:
wind = Heading.from_degrees(self.conditions.weather.wind.at_0m.direction)
ideal_heading = wind.opposite
return runway.runway_heading.angle_between(ideal_heading)
def get_preferred_runway(self, airport: Airport) -> RunwayData:
"""Returns the preferred runway for the given airport.

View File

@@ -14,25 +14,21 @@ class BoforsGenerator(AirDefenseGroupGenerator):
"""
name = "Bofors AAA"
price = 75
def generate(self):
grid_x = random.randint(2, 3)
grid_y = random.randint(2, 3)
spacing = random.randint(10, 40)
def generate(self) -> None:
index = 0
for i in range(grid_x):
for j in range(grid_y):
index = index + 1
self.add_unit(
AirDefence.Bofors40,
"AAA#" + str(index),
self.position.x + spacing * i,
self.position.y + spacing * j,
self.heading,
)
for i in range(4):
spacing_x = random.randint(10, 40)
spacing_y = random.randint(10, 40)
index = index + 1
self.add_unit(
AirDefence.Bofors40,
"AAA#" + str(index),
self.position.x + spacing_x * i,
self.position.y + spacing_y * i,
self.heading,
)
@classmethod
def range(cls) -> AirDefenseRange:

View File

@@ -6,6 +6,7 @@ from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
from game.utils import Heading
GFLAK = [
AirDefence.Flak38,
@@ -23,31 +24,26 @@ class FlakGenerator(AirDefenseGroupGenerator):
"""
name = "Flak Site"
price = 135
def generate(self):
grid_x = random.randint(2, 3)
grid_y = random.randint(2, 3)
spacing = random.randint(20, 35)
def generate(self) -> None:
index = 0
mixed = random.choice([True, False])
unit_type = random.choice(GFLAK)
for i in range(grid_x):
for j in range(grid_y):
index = index + 1
self.add_unit(
unit_type,
"AAA#" + str(index),
self.position.x + spacing * i + random.randint(1, 5),
self.position.y + spacing * j + random.randint(1, 5),
self.heading,
)
for i in range(4):
index = index + 1
spacing_x = random.randint(10, 40)
spacing_y = random.randint(10, 40)
self.add_unit(
unit_type,
"AAA#" + str(index),
self.position.x + spacing_x * i + random.randint(1, 5),
self.position.y + spacing_y * i + random.randint(1, 5),
self.heading,
)
if mixed:
unit_type = random.choice(GFLAK)
if mixed:
unit_type = random.choice(GFLAK)
# Search lights
search_pos = self.get_circular_position(random.randint(2, 3), 80)
@@ -86,14 +82,14 @@ class FlakGenerator(AirDefenseGroupGenerator):
)
# Some Opel Blitz trucks
for i in range(int(max(1, grid_x / 2))):
for j in range(int(max(1, grid_x / 2))):
for i in range(int(max(1, 2))):
for j in range(int(max(1, 2))):
self.add_unit(
Unarmed.Blitz_36_6700A,
"BLITZ#" + str(index),
self.position.x + 125 + 15 * i + random.randint(1, 5),
self.position.y + 15 * j + random.randint(1, 5),
75,
Heading.from_degrees(75),
)
@classmethod

View File

@@ -14,9 +14,8 @@ class Flak18Generator(AirDefenseGroupGenerator):
"""
name = "WW2 Flak Site"
price = 40
def generate(self):
def generate(self) -> None:
spacing = random.randint(30, 60)
index = 0

View File

@@ -13,12 +13,8 @@ class KS19Generator(AirDefenseGroupGenerator):
"""
name = "KS-19 AAA Site"
price = 98
def generate(self):
spacing = random.randint(10, 40)
def generate(self) -> None:
self.add_unit(
highdigitsams.AAA_SON_9_Fire_Can,
"TR",
@@ -28,16 +24,17 @@ class KS19Generator(AirDefenseGroupGenerator):
)
index = 0
for i in range(3):
for j in range(3):
index = index + 1
self.add_unit(
highdigitsams.AAA_100mm_KS_19,
"AAA#" + str(index),
self.position.x + spacing * i,
self.position.y + spacing * j,
self.heading,
)
for i in range(4):
spacing_x = random.randint(10, 40)
spacing_y = random.randint(10, 40)
index = index + 1
self.add_unit(
highdigitsams.AAA_100mm_KS_19,
"AAA#" + str(index),
self.position.x + spacing_x * i,
self.position.y + spacing_y * i,
self.heading,
)
@classmethod
def range(cls) -> AirDefenseRange:

View File

@@ -6,6 +6,7 @@ from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
from game.utils import Heading
class AllyWW2FlakGenerator(AirDefenseGroupGenerator):
@@ -14,9 +15,8 @@ class AllyWW2FlakGenerator(AirDefenseGroupGenerator):
"""
name = "WW2 Ally Flak Site"
price = 140
def generate(self):
def generate(self) -> None:
positions = self.get_circular_position(4, launcher_distance=30, coverage=360)
for i, position in enumerate(positions):
@@ -54,28 +54,28 @@ class AllyWW2FlakGenerator(AirDefenseGroupGenerator):
"CMD#1",
self.position.x,
self.position.y - 20,
random.randint(0, 360),
Heading.random(),
)
self.add_unit(
Unarmed.M30_CC,
"LOG#1",
self.position.x,
self.position.y + 20,
random.randint(0, 360),
Heading.random(),
)
self.add_unit(
Unarmed.M4_Tractor,
"LOG#2",
self.position.x + 20,
self.position.y,
random.randint(0, 360),
Heading.random(),
)
self.add_unit(
Unarmed.Bedford_MWD,
"LOG#3",
self.position.x - 20,
self.position.y,
random.randint(0, 360),
Heading.random(),
)
@classmethod

View File

@@ -12,10 +12,9 @@ class ZSU57Generator(AirDefenseGroupGenerator):
"""
name = "ZSU-57-2 Group"
price = 60
def generate(self):
num_launchers = 5
def generate(self) -> None:
num_launchers = 4
positions = self.get_circular_position(
num_launchers, launcher_distance=110, coverage=360
)

View File

@@ -14,25 +14,20 @@ class ZU23InsurgentGenerator(AirDefenseGroupGenerator):
"""
name = "Zu-23 Site"
price = 56
def generate(self):
grid_x = random.randint(2, 3)
grid_y = random.randint(2, 3)
spacing = random.randint(10, 40)
def generate(self) -> None:
index = 0
for i in range(grid_x):
for j in range(grid_y):
index = index + 1
self.add_unit(
AirDefence.ZU_23_Closed_Insurgent,
"AAA#" + str(index),
self.position.x + spacing * i,
self.position.y + spacing * j,
self.heading,
)
for i in range(4):
index = index + 1
spacing_x = random.randint(10, 40)
spacing_y = random.randint(10, 40)
self.add_unit(
AirDefence.ZU_23_Closed_Insurgent,
"AAA#" + str(index),
self.position.x + spacing_x * i,
self.position.y + spacing_y * i,
self.heading,
)
@classmethod
def range(cls) -> AirDefenseRange:

View File

@@ -1,3 +1,5 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from enum import Enum
from typing import Iterator, List
@@ -6,36 +8,69 @@ from dcs.unitgroup import VehicleGroup
from game import Game
from game.theater.theatergroundobject import SamGroundObject
from gen.sam.group_generator import GroupGenerator
from gen.sam.group_generator import VehicleGroupGenerator
class SkynetRole(Enum):
#: A radar SAM that should be controlled by Skynet.
Sam = "Sam"
#: A radar SAM that should be controlled and used as an EWR by Skynet.
SamAsEwr = "SamAsEwr"
#: An air defense unit that should be used as point defense by Skynet.
PointDefense = "PD"
#: All other types of groups that might be present in a SAM TGO. This includes
#: SHORADS, AAA, supply trucks, etc. Anything that shouldn't be controlled by Skynet
#: should use this role.
NoSkynetBehavior = "NoSkynetBehavior"
class AirDefenseRange(Enum):
AAA = "AAA"
Short = "short"
Medium = "medium"
Long = "long"
AAA = ("AAA", SkynetRole.NoSkynetBehavior)
Short = ("short", SkynetRole.NoSkynetBehavior)
Medium = ("medium", SkynetRole.Sam)
Long = ("long", SkynetRole.SamAsEwr)
def __init__(self, description: str, default_role: SkynetRole) -> None:
self.range_name = description
self.default_role = default_role
class AirDefenseGroupGenerator(GroupGenerator, ABC):
class AirDefenseGroupGenerator(VehicleGroupGenerator[SamGroundObject], ABC):
"""
This is the base for all SAM group generators
"""
price: int
def __init__(self, game: Game, ground_object: SamGroundObject) -> None:
ground_object.skynet_capable = True
super().__init__(game, ground_object)
self.vg.name = self.group_name_for_role(self.vg.id, self.primary_group_role())
self.auxiliary_groups: List[VehicleGroup] = []
self.heading = self.heading_to_conflict()
def add_auxiliary_group(self, name_suffix: str) -> VehicleGroup:
group = VehicleGroup(
self.game.next_group_id(), "|".join([self.go.group_name, name_suffix])
)
def add_auxiliary_group(self, role: SkynetRole) -> VehicleGroup:
gid = self.game.next_group_id()
group = VehicleGroup(gid, self.group_name_for_role(gid, role))
self.auxiliary_groups.append(group)
return group
def group_name_for_role(self, gid: int, role: SkynetRole) -> str:
if role is SkynetRole.NoSkynetBehavior:
# No special naming needed for air defense groups that don't participate in
# Skynet.
return f"{self.go.group_name}|{gid}"
# For those that do, we need a prefix of `$COLOR|SAM| so our Skynet config picks
# the group up at all. To support PDs we need to append the ID of the TGO so
# that the PD will know which group it's protecting. We then append the role so
# our config knows what to do with the group, and finally the GID of *this*
# group to ensure no conflicts.
return "|".join(
[self.go.faction_color, "SAM", str(self.go.group_id), role.value, str(gid)]
)
def get_generated_group(self) -> VehicleGroup:
raise RuntimeError(
"Deprecated call to AirDefenseGroupGenerator.get_generated_group "
@@ -52,3 +87,7 @@ class AirDefenseGroupGenerator(GroupGenerator, ABC):
@abstractmethod
def range(cls) -> AirDefenseRange:
...
@classmethod
def primary_group_role(cls) -> SkynetRole:
return cls.range().default_role

View File

@@ -17,9 +17,8 @@ class EarlyColdWarFlakGenerator(AirDefenseGroupGenerator):
"""
name = "Early Cold War Flak Site"
price = 74
def generate(self):
def generate(self) -> None:
spacing = random.randint(30, 60)
index = 0
@@ -42,7 +41,7 @@ class EarlyColdWarFlakGenerator(AirDefenseGroupGenerator):
"SHO#1",
self.position.x - 40,
self.position.y - 40,
self.heading + 180,
self.heading.opposite,
),
self.add_unit(
AirDefence.S_60_Type59_Artillery,
@@ -58,7 +57,7 @@ class EarlyColdWarFlakGenerator(AirDefenseGroupGenerator):
"SHO#3",
self.position.x - 80,
self.position.y - 40,
self.heading + 180,
self.heading.opposite,
),
self.add_unit(
AirDefence.ZU_23_Emplacement_Closed,
@@ -90,9 +89,8 @@ class ColdWarFlakGenerator(AirDefenseGroupGenerator):
"""
name = "Cold War Flak Site"
price = 72
def generate(self):
def generate(self) -> None:
spacing = random.randint(30, 60)
index = 0
@@ -115,7 +113,7 @@ class ColdWarFlakGenerator(AirDefenseGroupGenerator):
"SHO#1",
self.position.x - 40,
self.position.y - 40,
self.heading + 180,
self.heading.opposite,
),
self.add_unit(
AirDefence.S_60_Type59_Artillery,
@@ -131,7 +129,7 @@ class ColdWarFlakGenerator(AirDefenseGroupGenerator):
"SHO#3",
self.position.x - 80,
self.position.y - 40,
self.heading + 180,
self.heading.opposite,
),
self.add_unit(
AirDefence.ZU_23_Emplacement_Closed,

View File

@@ -18,6 +18,7 @@ from gen.sam.ewrs import (
StraightFlushGenerator,
TallRackGenerator,
EwrGenerator,
TinShieldGenerator,
)
EWR_MAP = {
@@ -31,6 +32,7 @@ EWR_MAP = {
"SnowDriftGenerator": SnowDriftGenerator,
"StraightFlushGenerator": StraightFlushGenerator,
"HawkEwrGenerator": HawkEwrGenerator,
"TinShieldGenerator": TinShieldGenerator,
}

View File

@@ -1,23 +1,19 @@
from typing import Type
from dcs.vehicles import AirDefence
from dcs.unittype import VehicleType
from dcs.vehicles import AirDefence
from gen.sam.group_generator import GroupGenerator
from game.theater.theatergroundobject import EwrGroundObject
from gen.sam.group_generator import VehicleGroupGenerator
class EwrGenerator(GroupGenerator):
class EwrGenerator(VehicleGroupGenerator[EwrGroundObject]):
unit_type: Type[VehicleType]
@classmethod
def name(cls) -> str:
return cls.unit_type.name
@staticmethod
def price() -> int:
# TODO: Differentiate sites.
return 20
def generate(self) -> None:
self.add_unit(
self.unit_type, "EWR", self.position.x, self.position.y, self.heading
@@ -106,3 +102,9 @@ class HawkEwrGenerator(EwrGenerator):
"""
unit_type = AirDefence.Hawk_sr
class TinShieldGenerator(EwrGenerator):
"""19ZH6 "Tin Shield" EWR."""
unit_type = AirDefence.RLS_19J6

View File

@@ -4,6 +4,7 @@ from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
from game.utils import Heading
class FreyaGenerator(AirDefenseGroupGenerator):
@@ -12,9 +13,8 @@ class FreyaGenerator(AirDefenseGroupGenerator):
"""
name = "Freya EWR Site"
price = 60
def generate(self):
def generate(self) -> None:
# TODO : would be better with the Concrete structure that is supposed to protect it
self.add_unit(
@@ -102,7 +102,7 @@ class FreyaGenerator(AirDefenseGroupGenerator):
"Inf#3",
self.position.x + 20,
self.position.y - 24,
self.heading + 45,
self.heading + Heading.from_degrees(45),
)
@classmethod

View File

@@ -1,70 +1,141 @@
from __future__ import annotations
import logging
import math
import operator
import random
from typing import TYPE_CHECKING, Type
from collections import Iterable
from typing import TYPE_CHECKING, Type, TypeVar, Generic, Any
from dcs import unitgroup
from dcs.mapping import Point
from dcs.point import PointAction
from dcs.unit import Ship, Vehicle
from dcs.unittype import VehicleType
from dcs.unit import Ship, Vehicle, Unit
from dcs.unitgroup import ShipGroup, VehicleGroup
from dcs.unittype import VehicleType, UnitType, ShipType
from game.dcs.groundunittype import GroundUnitType
from game.factions.faction import Faction
from game.theater.theatergroundobject import TheaterGroundObject
from game.theater import MissionTarget
from game.theater.theatergroundobject import TheaterGroundObject, NavalGroundObject
from game.utils import Heading
if TYPE_CHECKING:
from game.game import Game
GroupT = TypeVar("GroupT", VehicleGroup, ShipGroup)
UnitT = TypeVar("UnitT", bound=Unit)
UnitTypeT = TypeVar("UnitTypeT", bound=Type[UnitType])
TgoT = TypeVar("TgoT", bound=TheaterGroundObject[Any])
# TODO: Generate a group description rather than a pydcs group.
# It appears that all of this work gets redone at miz generation time (see
# groundobjectsgen for an example). We can do less work and include the data we
# care about in the format we want if we just generate our own group description
# types rather than pydcs groups.
class GroupGenerator:
def __init__(self, game: Game, ground_object: TheaterGroundObject) -> None:
class GroupGenerator(Generic[GroupT, UnitT, UnitTypeT, TgoT]):
def __init__(self, game: Game, ground_object: TgoT, group: GroupT) -> None:
self.game = game
self.go = ground_object
self.position = ground_object.position
self.heading = random.randint(0, 359)
self.vg = unitgroup.VehicleGroup(self.game.next_group_id(), self.go.group_name)
wp = self.vg.add_waypoint(self.position, PointAction.OffRoad, 0)
wp.ETA_locked = True
self.heading: Heading = Heading.random()
self.price = 0
self.vg: GroupT = group
def generate(self):
def generate(self) -> None:
raise NotImplementedError
def get_generated_group(self) -> unitgroup.VehicleGroup:
def get_generated_group(self) -> GroupT:
return self.vg
def add_unit(
self,
unit_type: Type[VehicleType],
unit_type: UnitTypeT,
name: str,
pos_x: float,
pos_y: float,
heading: int,
) -> Vehicle:
heading: Heading,
) -> UnitT:
return self.add_unit_to_group(
self.vg, unit_type, name, Point(pos_x, pos_y), heading
)
def add_unit_to_group(
self,
group: unitgroup.VehicleGroup,
group: GroupT,
unit_type: UnitTypeT,
name: str,
position: Point,
heading: Heading,
) -> UnitT:
raise NotImplementedError
def heading_to_conflict(self) -> Heading:
# Heading for a Group to the enemy.
# Should be the point between the nearest and the most distant conflict
conflicts: dict[MissionTarget, float] = {}
for conflict in self.game.theater.conflicts():
conflicts[conflict] = conflict.distance_to(self.go)
if len(conflicts) == 0:
return self.heading
closest_conflict = min(conflicts.items(), key=operator.itemgetter(1))[0]
most_distant_conflict = max(conflicts.items(), key=operator.itemgetter(1))[0]
conflict_center = Point(
(closest_conflict.position.x + most_distant_conflict.position.x) / 2,
(closest_conflict.position.y + most_distant_conflict.position.y) / 2,
)
return Heading.from_degrees(
self.go.position.heading_between_point(conflict_center)
)
class VehicleGroupGenerator(
Generic[TgoT], GroupGenerator[VehicleGroup, Vehicle, Type[VehicleType], TgoT]
):
def __init__(self, game: Game, ground_object: TgoT) -> None:
super().__init__(
game,
ground_object,
unitgroup.VehicleGroup(game.next_group_id(), ground_object.group_name),
)
wp = self.vg.add_waypoint(self.position, PointAction.OffRoad, 0)
wp.ETA_locked = True
def generate(self) -> None:
raise NotImplementedError
def add_unit_to_group(
self,
group: VehicleGroup,
unit_type: Type[VehicleType],
name: str,
position: Point,
heading: int,
heading: Heading,
) -> Vehicle:
unit = Vehicle(self.game.next_unit_id(), f"{group.name}|{name}", unit_type.id)
unit.position = position
unit.heading = heading
unit.heading = heading.degrees
group.add_unit(unit)
# get price of unit to calculate the real price of the whole group
try:
ground_unit_type = next(GroundUnitType.for_dcs_type(unit_type))
self.price += ground_unit_type.price
except StopIteration:
logging.error(f"Cannot get price for unit {unit_type.name}")
return unit
def get_circular_position(self, num_units, launcher_distance, coverage=90):
def get_circular_position(
self, num_units: int, launcher_distance: int, coverage: int = 90
) -> Iterable[tuple[float, float, Heading]]:
"""
Given a position on the map, array a group of units in a circle a uniform distance from the unit
:param num_units:
@@ -86,43 +157,50 @@ class GroupGenerator:
positions = []
if num_units % 2 == 0:
current_offset = self.heading - ((coverage / (num_units - 1)) / 2)
current_offset = self.heading.degrees - ((coverage / (num_units - 1)) / 2)
else:
current_offset = self.heading
current_offset = self.heading.degrees
current_offset -= outer_offset * (math.ceil(num_units / 2) - 1)
for x in range(1, num_units + 1):
positions.append(
(
self.position.x
+ launcher_distance * math.cos(math.radians(current_offset)),
self.position.y
+ launcher_distance * math.sin(math.radians(current_offset)),
current_offset,
)
for _ in range(1, num_units + 1):
x: float = self.position.x + launcher_distance * math.cos(
math.radians(current_offset)
)
y: float = self.position.y + launcher_distance * math.sin(
math.radians(current_offset)
)
positions.append((x, y, Heading.from_degrees(current_offset)))
current_offset += outer_offset
return positions
class ShipGroupGenerator(GroupGenerator):
class ShipGroupGenerator(
GroupGenerator[ShipGroup, Ship, Type[ShipType], NavalGroundObject]
):
"""Abstract class for other ship generator classes"""
def __init__(
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
self.game = game
self.go = ground_object
self.position = ground_object.position
self.heading = random.randint(0, 359)
def __init__(self, game: Game, ground_object: NavalGroundObject, faction: Faction):
super().__init__(
game,
ground_object,
unitgroup.ShipGroup(game.next_group_id(), ground_object.group_name),
)
self.faction = faction
self.vg = unitgroup.ShipGroup(self.game.next_group_id(), self.go.group_name)
wp = self.vg.add_waypoint(self.position, 0)
wp.ETA_locked = True
def add_unit(self, unit_type, name, pos_x, pos_y, heading) -> Ship:
def generate(self) -> None:
raise NotImplementedError
def add_unit_to_group(
self,
group: ShipGroup,
unit_type: Type[ShipType],
name: str,
position: Point,
heading: Heading,
) -> Ship:
unit = Ship(self.game.next_unit_id(), f"{self.go.group_name}|{name}", unit_type)
unit.position.x = pos_x
unit.position.y = pos_y
unit.heading = heading
self.vg.add_unit(unit)
unit.position = position
unit.heading = heading.degrees
group.add_unit(unit)
return unit

View File

@@ -14,10 +14,9 @@ class AvengerGenerator(AirDefenseGroupGenerator):
"""
name = "Avenger Group"
price = 62
def generate(self):
num_launchers = random.randint(2, 3)
def generate(self) -> None:
num_launchers = 2
self.add_unit(
Unarmed.M_818,

View File

@@ -14,10 +14,9 @@ class ChaparralGenerator(AirDefenseGroupGenerator):
"""
name = "Chaparral Group"
price = 66
def generate(self):
num_launchers = random.randint(2, 4)
def generate(self) -> None:
num_launchers = 2
self.add_unit(
Unarmed.M_818,

View File

@@ -14,23 +14,20 @@ class GepardGenerator(AirDefenseGroupGenerator):
"""
name = "Gepard Group"
price = 50
def generate(self):
self.add_unit(
AirDefence.Gepard,
"SPAAA",
self.position.x,
self.position.y,
self.heading,
def generate(self) -> None:
num_launchers = 2
positions = self.get_circular_position(
num_launchers, launcher_distance=120, coverage=180
)
if random.randint(0, 1) == 1:
for i, position in enumerate(positions):
self.add_unit(
AirDefence.Gepard,
"SPAAA2",
self.position.x,
self.position.y,
self.heading,
"SPAA#" + str(i),
position[0],
position[1],
position[2],
)
self.add_unit(
Unarmed.M_818,

View File

@@ -28,6 +28,7 @@ from gen.sam.sam_gepard import GepardGenerator
from gen.sam.sam_hawk import HawkGenerator
from gen.sam.sam_hq7 import HQ7Generator
from gen.sam.sam_linebacker import LinebackerGenerator
from gen.sam.sam_nasams import NasamBGenerator, NasamCGenerator
from gen.sam.sam_patriot import PatriotGenerator
from gen.sam.sam_rapier import RapierGenerator
from gen.sam.sam_roland import RolandGenerator
@@ -100,6 +101,8 @@ SAM_MAP: Dict[str, Type[AirDefenseGroupGenerator]] = {
"SA20Generator": SA20Generator,
"SA20BGenerator": SA20BGenerator,
"SA23Generator": SA23Generator,
"NasamBGenerator": NasamBGenerator,
"NasamCGenerator": NasamCGenerator,
}

View File

@@ -6,6 +6,7 @@ from dcs.vehicles import AirDefence
from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
SkynetRole,
)
@@ -15,9 +16,8 @@ class HawkGenerator(AirDefenseGroupGenerator):
"""
name = "Hawk Site"
price = 115
def generate(self):
def generate(self) -> None:
self.add_unit(
AirDefence.Hawk_sr,
"SR",
@@ -41,7 +41,7 @@ class HawkGenerator(AirDefenseGroupGenerator):
)
# Triple A for close range defense
aa_group = self.add_auxiliary_group("AA")
aa_group = self.add_auxiliary_group(SkynetRole.NoSkynetBehavior)
self.add_unit_to_group(
aa_group,
AirDefence.Vulcan,
@@ -50,7 +50,7 @@ class HawkGenerator(AirDefenseGroupGenerator):
self.heading,
)
num_launchers = random.randint(3, 6)
num_launchers = 6
positions = self.get_circular_position(
num_launchers, launcher_distance=120, coverage=180
)

View File

@@ -6,6 +6,7 @@ from dcs.vehicles import AirDefence
from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
SkynetRole,
)
@@ -15,9 +16,8 @@ class HQ7Generator(AirDefenseGroupGenerator):
"""
name = "HQ-7 Site"
price = 120
def generate(self):
def generate(self) -> None:
self.add_unit(
AirDefence.HQ_7_STR_SP,
"STR",
@@ -25,16 +25,9 @@ class HQ7Generator(AirDefenseGroupGenerator):
self.position.y,
self.heading,
)
self.add_unit(
AirDefence.HQ_7_LN_SP,
"LN",
self.position.x + 20,
self.position.y,
self.heading,
)
# Triple A for close range defense
aa_group = self.add_auxiliary_group("AA")
aa_group = self.add_auxiliary_group(SkynetRole.NoSkynetBehavior)
self.add_unit_to_group(
aa_group,
AirDefence.Ural_375_ZU_23,
@@ -50,7 +43,7 @@ class HQ7Generator(AirDefenseGroupGenerator):
self.heading,
)
num_launchers = random.randint(0, 3)
num_launchers = 2
if num_launchers > 0:
positions = self.get_circular_position(
num_launchers, launcher_distance=120, coverage=360

View File

@@ -14,10 +14,9 @@ class LinebackerGenerator(AirDefenseGroupGenerator):
"""
name = "Linebacker Group"
price = 75
def generate(self):
num_launchers = random.randint(2, 4)
def generate(self) -> None:
num_launchers = 2
self.add_unit(
Unarmed.M_818,

68
gen/sam/sam_nasams.py Normal file
View File

@@ -0,0 +1,68 @@
from typing import Type
from dcs.mapping import Point
from dcs.unittype import VehicleType
from dcs.vehicles import AirDefence
from game import Game
from game.theater import SamGroundObject
from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
class NasamCGenerator(AirDefenseGroupGenerator):
"""
This generate a Nasams group with AIM-120C missiles
"""
name = "NASAMS AIM-120C"
def __init__(self, game: Game, ground_object: SamGroundObject):
super().__init__(game, ground_object)
self.launcherType: Type[VehicleType] = AirDefence.NASAMS_LN_C
def generate(self) -> None:
# Command Post
self.add_unit(
AirDefence.NASAMS_Command_Post,
"CP",
self.position.x + 30,
self.position.y + 30,
self.heading,
)
# Radar
self.add_unit(
AirDefence.NASAMS_Radar_MPQ64F1,
"RADAR",
self.position.x - 30,
self.position.y - 30,
self.heading,
)
positions = self.get_circular_position(4, launcher_distance=120, coverage=360)
for i, position in enumerate(positions):
self.add_unit(
self.launcherType,
"LN#" + str(i),
position[0],
position[1],
position[2],
)
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Medium
class NasamBGenerator(NasamCGenerator):
"""
This generate a Nasams group with AIM-120B missiles
"""
name = "NASAMS AIM-120B"
def __init__(self, game: Game, ground_object: SamGroundObject):
super().__init__(game, ground_object)
self.launcherType: Type[VehicleType] = AirDefence.NASAMS_LN_B

View File

@@ -1,11 +1,10 @@
import random
from dcs.mapping import Point
from dcs.vehicles import AirDefence
from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
SkynetRole,
)
@@ -15,9 +14,8 @@ class PatriotGenerator(AirDefenseGroupGenerator):
"""
name = "Patriot Battery"
price = 240
def generate(self):
def generate(self) -> None:
# Command Post
self.add_unit(
AirDefence.Patriot_str,
@@ -55,10 +53,7 @@ class PatriotGenerator(AirDefenseGroupGenerator):
self.heading,
)
num_launchers = random.randint(3, 4)
positions = self.get_circular_position(
num_launchers, launcher_distance=120, coverage=360
)
positions = self.get_circular_position(8, launcher_distance=120, coverage=360)
for i, position in enumerate(positions):
self.add_unit(
AirDefence.Patriot_ln,
@@ -69,11 +64,8 @@ class PatriotGenerator(AirDefenseGroupGenerator):
)
# Short range protection for high value site
aa_group = self.add_auxiliary_group("AA")
num_launchers = random.randint(3, 4)
positions = self.get_circular_position(
num_launchers, launcher_distance=200, coverage=360
)
aa_group = self.add_auxiliary_group(SkynetRole.NoSkynetBehavior)
positions = self.get_circular_position(2, launcher_distance=200, coverage=360)
for i, (x, y, heading) in enumerate(positions):
self.add_unit_to_group(
aa_group,
@@ -82,6 +74,15 @@ class PatriotGenerator(AirDefenseGroupGenerator):
Point(x, y),
heading,
)
positions = self.get_circular_position(2, launcher_distance=300, coverage=360)
for i, (x, y, heading) in enumerate(positions):
self.add_unit_to_group(
aa_group,
AirDefence.M1097_Avenger,
f"Avenger#{i}",
Point(x, y),
heading,
)
@classmethod
def range(cls) -> AirDefenseRange:

View File

@@ -5,6 +5,7 @@ from dcs.vehicles import AirDefence
from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
SkynetRole,
)
@@ -14,9 +15,8 @@ class RapierGenerator(AirDefenseGroupGenerator):
"""
name = "Rapier AA Site"
price = 50
def generate(self):
def generate(self) -> None:
self.add_unit(
AirDefence.Rapier_fsa_blindfire_radar,
"BT",
@@ -32,7 +32,7 @@ class RapierGenerator(AirDefenseGroupGenerator):
self.heading,
)
num_launchers = random.randint(3, 6)
num_launchers = 2
positions = self.get_circular_position(
num_launchers, launcher_distance=80, coverage=240
)
@@ -49,3 +49,7 @@ class RapierGenerator(AirDefenseGroupGenerator):
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Short
@classmethod
def primary_group_role(cls) -> SkynetRole:
return SkynetRole.Sam

View File

@@ -3,6 +3,7 @@ from dcs.vehicles import AirDefence, Unarmed
from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
SkynetRole,
)
@@ -12,9 +13,9 @@ class RolandGenerator(AirDefenseGroupGenerator):
"""
name = "Roland Site"
price = 40
def generate(self):
def generate(self) -> None:
num_launchers = 2
self.add_unit(
AirDefence.Roland_Radar,
"EWR",
@@ -22,13 +23,18 @@ class RolandGenerator(AirDefenseGroupGenerator):
self.position.y,
self.heading,
)
self.add_unit(
AirDefence.Roland_ADS,
"ADS",
self.position.x,
self.position.y,
self.heading,
positions = self.get_circular_position(
num_launchers, launcher_distance=80, coverage=240
)
for i, position in enumerate(positions):
self.add_unit(
AirDefence.Roland_ADS,
"ADS#" + str(i),
position[0],
position[1],
position[2],
)
self.add_unit(
Unarmed.M_818,
"TRUCK",
@@ -40,3 +46,7 @@ class RolandGenerator(AirDefenseGroupGenerator):
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Short
@classmethod
def primary_group_role(cls) -> SkynetRole:
return SkynetRole.Sam

View File

@@ -1,6 +1,7 @@
import random
from typing import Type
from dcs.mapping import Point
from dcs.unittype import VehicleType
from dcs.vehicles import AirDefence
from game import Game
@@ -8,6 +9,7 @@ from game.theater import SamGroundObject
from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
SkynetRole,
)
from pydcs_extensions.highdigitsams import highdigitsams
@@ -18,19 +20,18 @@ class SA10Generator(AirDefenseGroupGenerator):
"""
name = "SA-10/S-300PS Battery - With ZSU-23"
price = 550
def __init__(self, game: Game, ground_object: SamGroundObject):
super().__init__(game, ground_object)
self.sr1 = AirDefence.S_300PS_40B6MD_sr
self.sr2 = AirDefence.S_300PS_64H6E_sr
self.cp = AirDefence.S_300PS_54K6_cp
self.tr1 = AirDefence.S_300PS_40B6M_tr
self.tr2 = AirDefence.S_300PS_40B6M_tr
self.ln1 = AirDefence.S_300PS_5P85C_ln
self.ln2 = AirDefence.S_300PS_5P85D_ln
self.sr1: Type[VehicleType] = AirDefence.S_300PS_40B6MD_sr
self.sr2: Type[VehicleType] = AirDefence.S_300PS_64H6E_sr
self.cp: Type[VehicleType] = AirDefence.S_300PS_54K6_cp
self.tr1: Type[VehicleType] = AirDefence.S_300PS_40B6M_tr
self.tr2: Type[VehicleType] = AirDefence.S_300PS_40B6M_tr
self.ln1: Type[VehicleType] = AirDefence.S_300PS_5P85C_ln
self.ln2: Type[VehicleType] = AirDefence.S_300PS_5P85D_ln
def generate(self):
def generate(self) -> None:
# Search Radar
self.add_unit(
self.sr1, "SR1", self.position.x, self.position.y + 40, self.heading
@@ -44,17 +45,13 @@ class SA10Generator(AirDefenseGroupGenerator):
# Command Post
self.add_unit(self.cp, "CP", self.position.x, self.position.y, self.heading)
# 2 Tracking radars
# 1 Tracking radar
self.add_unit(
self.tr1, "TR1", self.position.x - 40, self.position.y - 40, self.heading
)
self.add_unit(
self.tr2, "TR2", self.position.x + 40, self.position.y - 40, self.heading
)
# 2 different launcher type (C & D)
num_launchers = random.randint(6, 8)
num_launchers = 6
positions = self.get_circular_position(
num_launchers, launcher_distance=100, coverage=360
)
@@ -76,8 +73,8 @@ class SA10Generator(AirDefenseGroupGenerator):
def generate_defensive_groups(self) -> None:
# AAA for defending against close targets.
aa_group = self.add_auxiliary_group("AA")
num_launchers = random.randint(6, 8)
aa_group = self.add_auxiliary_group(SkynetRole.NoSkynetBehavior)
num_launchers = 2
positions = self.get_circular_position(
num_launchers, launcher_distance=210, coverage=360
)
@@ -94,15 +91,14 @@ class SA10Generator(AirDefenseGroupGenerator):
class Tier2SA10Generator(SA10Generator):
name = "SA-10/S-300PS Battery - With SA-15 PD"
price = 650
def generate_defensive_groups(self) -> None:
# Create AAA the way the main group does.
super().generate_defensive_groups()
# SA-15 for both shorter range targets and point defense.
pd_group = self.add_auxiliary_group("PD")
num_launchers = random.randint(2, 4)
pd_group = self.add_auxiliary_group(SkynetRole.PointDefense)
num_launchers = 2
positions = self.get_circular_position(
num_launchers, launcher_distance=140, coverage=360
)
@@ -119,12 +115,11 @@ class Tier2SA10Generator(SA10Generator):
class Tier3SA10Generator(SA10Generator):
name = "SA-10/S-300PS Battery - With SA-15 PD & SA-19 SHORAD"
price = 750
def generate_defensive_groups(self) -> None:
# AAA for defending against close targets.
aa_group = self.add_auxiliary_group("AA")
num_launchers = random.randint(6, 8)
aa_group = self.add_auxiliary_group(SkynetRole.NoSkynetBehavior)
num_launchers = 2
positions = self.get_circular_position(
num_launchers, launcher_distance=210, coverage=360
)
@@ -138,8 +133,8 @@ class Tier3SA10Generator(SA10Generator):
)
# SA-15 for both shorter range targets and point defense.
pd_group = self.add_auxiliary_group("PD")
num_launchers = random.randint(2, 4)
pd_group = self.add_auxiliary_group(SkynetRole.PointDefense)
num_launchers = 2
positions = self.get_circular_position(
num_launchers, launcher_distance=140, coverage=360
)
@@ -155,7 +150,6 @@ class Tier3SA10Generator(SA10Generator):
class SA10BGenerator(Tier3SA10Generator):
price = 700
name = "SA-10B/S-300PS Battery"
def __init__(self, game: Game, ground_object: SamGroundObject):
@@ -171,7 +165,6 @@ class SA10BGenerator(Tier3SA10Generator):
class SA12Generator(Tier3SA10Generator):
price = 750
name = "SA-12/S-300V Battery"
def __init__(self, game: Game, ground_object: SamGroundObject):
@@ -187,7 +180,6 @@ class SA12Generator(Tier3SA10Generator):
class SA20Generator(Tier3SA10Generator):
price = 800
name = "SA-20/S-300PMU-1 Battery"
def __init__(self, game: Game, ground_object: SamGroundObject):
@@ -203,7 +195,6 @@ class SA20Generator(Tier3SA10Generator):
class SA20BGenerator(Tier3SA10Generator):
price = 850
name = "SA-20B/S-300PMU-2 Battery"
def __init__(self, game: Game, ground_object: SamGroundObject):
@@ -219,7 +210,6 @@ class SA20BGenerator(Tier3SA10Generator):
class SA23Generator(Tier3SA10Generator):
price = 950
name = "SA-23/S-300VM Battery"
def __init__(self, game: Game, ground_object: SamGroundObject):

View File

@@ -14,9 +14,8 @@ class SA11Generator(AirDefenseGroupGenerator):
"""
name = "SA-11 Buk Battery"
price = 180
def generate(self):
def generate(self) -> None:
self.add_unit(
AirDefence.SA_11_Buk_SR_9S18M1,
"SR",
@@ -32,7 +31,7 @@ class SA11Generator(AirDefenseGroupGenerator):
self.heading,
)
num_launchers = random.randint(2, 4)
num_launchers = 4
positions = self.get_circular_position(
num_launchers, launcher_distance=140, coverage=180
)

View File

@@ -14,9 +14,8 @@ class SA13Generator(AirDefenseGroupGenerator):
"""
name = "SA-13 Strela Group"
price = 50
def generate(self):
def generate(self) -> None:
self.add_unit(
Unarmed.UAZ_469,
"UAZ",
@@ -32,7 +31,7 @@ class SA13Generator(AirDefenseGroupGenerator):
self.heading,
)
num_launchers = random.randint(2, 3)
num_launchers = 2
positions = self.get_circular_position(
num_launchers, launcher_distance=120, coverage=360
)

View File

@@ -12,16 +12,20 @@ class SA15Generator(AirDefenseGroupGenerator):
"""
name = "SA-15 Tor Group"
price = 55
def generate(self):
self.add_unit(
AirDefence.Tor_9A331,
"ADS",
self.position.x,
self.position.y,
self.heading,
def generate(self) -> None:
num_launchers = 2
positions = self.get_circular_position(
num_launchers, launcher_distance=120, coverage=360
)
for i, position in enumerate(positions):
self.add_unit(
AirDefence.Tor_9A331,
"ADS#" + str(i),
position[0],
position[1],
position[2],
)
self.add_unit(
Unarmed.UAZ_469,
"EWR",

View File

@@ -13,9 +13,8 @@ class SA17Generator(AirDefenseGroupGenerator):
"""
name = "SA-17 Grizzly Battery"
price = 180
def generate(self):
def generate(self) -> None:
self.add_unit(
AirDefence.SA_11_Buk_SR_9S18M1,
"SR",

View File

@@ -14,10 +14,9 @@ class SA19Generator(AirDefenseGroupGenerator):
"""
name = "SA-19 Tunguska Group"
price = 90
def generate(self):
num_launchers = random.randint(1, 3)
def generate(self) -> None:
num_launchers = 2
if num_launchers == 1:
self.add_unit(

View File

@@ -14,9 +14,8 @@ class SA2Generator(AirDefenseGroupGenerator):
"""
name = "SA-2/S-75 Site"
price = 74
def generate(self):
def generate(self) -> None:
self.add_unit(
AirDefence.P_19_s_125_sr,
"SR",
@@ -32,7 +31,7 @@ class SA2Generator(AirDefenseGroupGenerator):
self.heading,
)
num_launchers = random.randint(3, 6)
num_launchers = 6
positions = self.get_circular_position(
num_launchers, launcher_distance=120, coverage=180
)

View File

@@ -14,9 +14,8 @@ class SA3Generator(AirDefenseGroupGenerator):
"""
name = "SA-3/S-125 Site"
price = 80
def generate(self):
def generate(self) -> None:
self.add_unit(
AirDefence.P_19_s_125_sr,
"SR",
@@ -32,7 +31,7 @@ class SA3Generator(AirDefenseGroupGenerator):
self.heading,
)
num_launchers = random.randint(3, 6)
num_launchers = 4
positions = self.get_circular_position(
num_launchers, launcher_distance=120, coverage=180
)

View File

@@ -14,9 +14,8 @@ class SA6Generator(AirDefenseGroupGenerator):
"""
name = "SA-6 Kub Site"
price = 102
def generate(self):
def generate(self) -> None:
self.add_unit(
AirDefence.Kub_1S91_str,
"STR",
@@ -25,7 +24,7 @@ class SA6Generator(AirDefenseGroupGenerator):
self.heading,
)
num_launchers = random.randint(2, 4)
num_launchers = 4
positions = self.get_circular_position(
num_launchers, launcher_distance=120, coverage=360
)

View File

@@ -12,16 +12,21 @@ class SA8Generator(AirDefenseGroupGenerator):
"""
name = "SA-8 OSA Site"
price = 55
def generate(self):
self.add_unit(
AirDefence.Osa_9A33_ln,
"OSA",
self.position.x,
self.position.y,
self.heading,
def generate(self) -> None:
num_launchers = 2
positions = self.get_circular_position(
num_launchers, launcher_distance=120, coverage=180
)
for i, position in enumerate(positions):
self.add_unit(
AirDefence.Osa_9A33_ln,
"OSA" + str(i),
position[0],
position[1],
position[2],
)
self.add_unit(
AirDefence.SA_8_Osa_LD_9T217,
"LD",

View File

@@ -14,9 +14,8 @@ class SA9Generator(AirDefenseGroupGenerator):
"""
name = "SA-9 Group"
price = 40
def generate(self):
def generate(self) -> None:
self.add_unit(
Unarmed.UAZ_469,
"UAZ",
@@ -32,7 +31,7 @@ class SA9Generator(AirDefenseGroupGenerator):
self.heading,
)
num_launchers = random.randint(2, 3)
num_launchers = 2
positions = self.get_circular_position(
num_launchers, launcher_distance=120, coverage=360
)

View File

@@ -14,23 +14,20 @@ class VulcanGenerator(AirDefenseGroupGenerator):
"""
name = "Vulcan Group"
price = 25
def generate(self):
self.add_unit(
AirDefence.Vulcan,
"SPAAA",
self.position.x,
self.position.y,
self.heading,
def generate(self) -> None:
num_launchers = 2
positions = self.get_circular_position(
num_launchers, launcher_distance=120, coverage=180
)
if random.randint(0, 1) == 1:
for i, position in enumerate(positions):
self.add_unit(
AirDefence.Vulcan,
"SPAAA2",
self.position.x,
self.position.y,
self.heading,
"SPAA#" + str(i),
position[0],
position[1],
position[2],
)
self.add_unit(
Unarmed.M_818,

View File

@@ -1,6 +1,6 @@
import random
from dcs.vehicles import AirDefence
from dcs.vehicles import AirDefence, Unarmed
from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
@@ -14,10 +14,9 @@ class ZSU23Generator(AirDefenseGroupGenerator):
"""
name = "ZSU-23 Group"
price = 50
def generate(self):
num_launchers = random.randint(4, 5)
def generate(self) -> None:
num_launchers = 4
positions = self.get_circular_position(
num_launchers, launcher_distance=120, coverage=180
@@ -30,6 +29,13 @@ class ZSU23Generator(AirDefenseGroupGenerator):
position[1],
position[2],
)
self.add_unit(
Unarmed.M_818,
"TRUCK",
self.position.x + 80,
self.position.y,
self.heading,
)
@classmethod
def range(cls) -> AirDefenseRange:

View File

@@ -1,6 +1,6 @@
import random
from dcs.vehicles import AirDefence
from dcs.vehicles import AirDefence, Unarmed
from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
@@ -14,25 +14,27 @@ class ZU23Generator(AirDefenseGroupGenerator):
"""
name = "ZU-23 Group"
price = 54
def generate(self):
grid_x = random.randint(2, 3)
grid_y = random.randint(2, 3)
spacing = random.randint(10, 40)
def generate(self) -> None:
index = 0
for i in range(grid_x):
for j in range(grid_y):
index = index + 1
self.add_unit(
AirDefence.ZU_23_Emplacement_Closed,
"AAA#" + str(index),
self.position.x + spacing * i,
self.position.y + spacing * j,
self.heading,
)
for i in range(4):
index = index + 1
spacing_x = random.randint(10, 40)
spacing_y = random.randint(10, 40)
self.add_unit(
AirDefence.ZU_23_Emplacement_Closed,
"AAA#" + str(index),
self.position.x + spacing_x * i,
self.position.y + spacing_y * i,
self.heading,
)
self.add_unit(
Unarmed.M_818,
"TRUCK",
self.position.x + 80,
self.position.y,
self.heading,
)
@classmethod
def range(cls) -> AirDefenseRange:

View File

@@ -14,10 +14,9 @@ class ZU23UralGenerator(AirDefenseGroupGenerator):
"""
name = "ZU-23 Ural Group"
price = 64
def generate(self):
num_launchers = random.randint(2, 8)
def generate(self) -> None:
num_launchers = 4
positions = self.get_circular_position(
num_launchers, launcher_distance=80, coverage=360

View File

@@ -14,10 +14,13 @@ class ZU23UralInsurgentGenerator(AirDefenseGroupGenerator):
"""
name = "ZU-23 Ural Insurgent Group"
price = 64
def generate(self):
num_launchers = random.randint(2, 8)
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.AAA
def generate(self) -> None:
num_launchers = 4
positions = self.get_circular_position(
num_launchers, launcher_distance=80, coverage=360
@@ -30,7 +33,3 @@ class ZU23UralInsurgentGenerator(AirDefenseGroupGenerator):
position[1],
position[2],
)
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.AAA

View File

@@ -51,11 +51,11 @@ class TriggersGenerator:
capture_zone_types = (Fob,)
capture_zone_flag = 600
def __init__(self, mission: Mission, game: Game):
def __init__(self, mission: Mission, game: Game) -> None:
self.mission = mission
self.game = game
def _set_allegiances(self, player_coalition: str, enemy_coalition: str):
def _set_allegiances(self, player_coalition: str, enemy_coalition: str) -> None:
"""
Set airbase initial coalition
"""
@@ -83,11 +83,16 @@ class TriggersGenerator:
for cp in self.game.theater.controlpoints:
if isinstance(cp, Airfield):
self.mission.terrain.airport_by_id(cp.at.id).set_coalition(
cp_airport = self.mission.terrain.airport_by_id(cp.airport.id)
if cp_airport is None:
raise RuntimeError(
f"Could not find {cp.airport.name} in the mission"
)
cp_airport.set_coalition(
cp.captured and player_coalition or enemy_coalition
)
def _set_skill(self, player_coalition: str, enemy_coalition: str):
def _set_skill(self, player_coalition: str, enemy_coalition: str) -> None:
"""
Set skill level for all aircraft in the mission
"""
@@ -103,7 +108,7 @@ class TriggersGenerator:
for vehicle_group in country.vehicle_group:
vehicle_group.set_skill(skill_level)
def _gen_markers(self):
def _gen_markers(self) -> None:
"""
Generate markers on F10 map for each existing objective
"""
@@ -188,7 +193,7 @@ class TriggersGenerator:
recapture_trigger.add_action(ClearFlag(flag=flag))
self.mission.triggerrules.triggers.append(recapture_trigger)
def generate(self):
def generate(self) -> None:
player_coalition = "blue"
enemy_coalition = "red"
@@ -198,7 +203,7 @@ class TriggersGenerator:
self._generate_capture_triggers(player_coalition, enemy_coalition)
@classmethod
def get_capture_zone_flag(cls):
def get_capture_zone_flag(cls) -> int:
flag = cls.capture_zone_flag
cls.capture_zone_flag += 1
return flag

View File

@@ -1,6 +0,0 @@
"""Unit conversions."""
def meters_to_feet(meters: float) -> float:
"""Convers meters to feet."""
return meters * 3.28084

View File

@@ -1,9 +1,8 @@
from __future__ import annotations
import random
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any
from dcs.mapping import Point
from dcs.mission import Mission
from dcs.unit import Static
from dcs.unittype import StaticType
@@ -11,22 +10,22 @@ from dcs.unittype import StaticType
if TYPE_CHECKING:
from game import Game
from .conflictgen import Conflict, FRONTLINE_LENGTH
from .conflictgen import Conflict
class MarkerSmoke(StaticType):
id = "big_smoke"
category = "Effects"
name = "big_smoke"
shape_name = 5
rate = 0.1
shape_name = 5 # type: ignore
rate = 0.1 # type: ignore
class Smoke(StaticType):
id = "big_smoke"
category = "Effects"
name = "big_smoke"
shape_name = 2
shape_name = 2 # type: ignore
rate = 1
@@ -34,7 +33,7 @@ class BigSmoke(StaticType):
id = "big_smoke"
category = "Effects"
name = "big_smoke"
shape_name = 3
shape_name = 3 # type: ignore
rate = 1
@@ -42,17 +41,11 @@ class MassiveSmoke(StaticType):
id = "big_smoke"
category = "Effects"
name = "big_smoke"
shape_name = 4
shape_name = 4 # type: ignore
rate = 1
class Outpost(StaticType):
id = "outpost"
name = "outpost"
category = "Fortifications"
def __monkey_static_dict(self: Static):
def __monkey_static_dict(self: Static) -> dict[str, Any]:
global __original_static_dict
d = __original_static_dict(self)
@@ -63,9 +56,8 @@ def __monkey_static_dict(self: Static):
__original_static_dict = Static.dict
Static.dict = __monkey_static_dict
Static.dict = __monkey_static_dict # type: ignore
FRONT_SMOKE_SPACING = 800
FRONT_SMOKE_RANDOM_SPREAD = 4000
FRONT_SMOKE_TYPE_CHANCES = {
2: MassiveSmoke,
@@ -74,29 +66,13 @@ FRONT_SMOKE_TYPE_CHANCES = {
100: Smoke,
}
DESTINATION_SMOKE_AMOUNT_FACTOR = 0.03
DESTINATION_SMOKE_DISTANCE_FACTOR = 1
DESTINATION_SMOKE_TYPE_CHANCES = {
5: BigSmoke,
100: Smoke,
}
def turn_heading(heading, fac):
heading += fac
if heading > 359:
heading = heading - 359
if heading < 0:
heading = 359 + heading
return heading
class VisualGenerator:
def __init__(self, mission: Mission, game: Game):
def __init__(self, mission: Mission, game: Game) -> None:
self.mission = mission
self.game = game
def _generate_frontline_smokes(self):
def _generate_frontline_smokes(self) -> None:
for front_line in self.game.theater.conflicts():
from_cp = front_line.blue_cp
to_cp = front_line.red_cp
@@ -110,7 +86,7 @@ class VisualGenerator:
continue
for offset in range(0, distance, self.game.settings.perf_smoke_spacing):
position = plane_start.point_from_heading(heading, offset)
position = plane_start.point_from_heading(heading.degrees, offset)
for k, v in FRONT_SMOKE_TYPE_CHANCES.items():
if random.randint(0, 100) <= k:
@@ -121,68 +97,12 @@ class VisualGenerator:
break
self.mission.static_group(
self.mission.country(self.game.enemy_country),
self.mission.country(self.game.red.country_name),
"",
_type=v,
position=pos,
)
break
def _generate_stub_planes(self):
pass
"""
mission_units = set()
for coalition_name, coalition in self.mission.coalition.items():
for country in coalition.countries.values():
for group in country.plane_group + country.helicopter_group + country.vehicle_group:
for unit in group.units:
mission_units.add(db.unit_type_of(unit))
for unit_type in mission_units:
self.mission.static_group(self.mission.country(self.game.player_country), "a", unit_type, Point(0, 300000), hidden=True)"""
def generate_target_smokes(self, target):
spread = target.size * DESTINATION_SMOKE_DISTANCE_FACTOR
for _ in range(
0,
int(
target.size
* DESTINATION_SMOKE_AMOUNT_FACTOR
* (1.1 - target.base.strength)
),
):
for k, v in DESTINATION_SMOKE_TYPE_CHANCES.items():
if random.randint(0, 100) <= k:
position = target.position.random_point_within(0, spread)
if not self.game.theater.is_on_land(position):
break
self.mission.static_group(
self.mission.country(self.game.enemy_country),
"",
_type=v,
position=position,
hidden=True,
)
break
def generate_transportation_marker(self, at: Point):
self.mission.static_group(
self.mission.country(self.game.player_country),
"",
_type=MarkerSmoke,
position=at,
)
def generate_transportation_destination(self, at: Point):
self.generate_transportation_marker(at.point_from_heading(0, 20))
self.mission.static_group(
self.mission.country(self.game.player_country),
"",
_type=Outpost,
position=at,
)
def generate(self):
def generate(self) -> None:
self._generate_frontline_smokes()
self._generate_stub_planes()