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