mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Merge branch 'dev' into pr/204
This commit is contained in:
@@ -275,8 +275,6 @@ class Flight(
|
||||
self.fuel = unit_type.fuel_max * 0.5
|
||||
elif unit_type == Hercules:
|
||||
self.fuel = unit_type.fuel_max * 0.75
|
||||
elif self.departure.cptype.name in ["FARP", "FOB"] and not self.is_helo:
|
||||
self.fuel = unit_type.fuel_max * 0.75
|
||||
|
||||
def any_member_has_weapon_of_type(self, weapon_type: WeaponType) -> bool:
|
||||
return any(
|
||||
|
||||
@@ -75,13 +75,6 @@ class AirAssaultFlightPlan(FormationAttackFlightPlan, UiZoneDisplay):
|
||||
)
|
||||
return tot - travel_time
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
if waypoint is self.tot_waypoint:
|
||||
return self.tot
|
||||
elif waypoint is self.layout.ingress:
|
||||
return self.ingress_time
|
||||
return None
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
return None
|
||||
|
||||
@@ -89,10 +82,6 @@ class AirAssaultFlightPlan(FormationAttackFlightPlan, UiZoneDisplay):
|
||||
def ctld_target_zone_radius(self) -> Distance:
|
||||
return meters(2500)
|
||||
|
||||
@property
|
||||
def mission_begin_on_station_time(self) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> datetime:
|
||||
return self.package.time_over_target
|
||||
|
||||
34
game/ato/flightplans/armedrecon.py
Normal file
34
game/ato/flightplans/armedrecon.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Type
|
||||
|
||||
from .formationattack import (
|
||||
FormationAttackBuilder,
|
||||
FormationAttackFlightPlan,
|
||||
FormationAttackLayout,
|
||||
)
|
||||
from .uizonedisplay import UiZone, UiZoneDisplay
|
||||
from ..flightwaypointtype import FlightWaypointType
|
||||
from ...utils import nautical_miles
|
||||
|
||||
|
||||
class ArmedReconFlightPlan(FormationAttackFlightPlan, UiZoneDisplay):
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
def ui_zone(self) -> UiZone:
|
||||
return UiZone(
|
||||
[self.tot_waypoint.position],
|
||||
nautical_miles(
|
||||
self.flight.coalition.game.settings.armed_recon_engagement_range_distance
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class Builder(FormationAttackBuilder[ArmedReconFlightPlan, FormationAttackLayout]):
|
||||
def layout(self) -> FormationAttackLayout:
|
||||
return self._build(FlightWaypointType.INGRESS_ARMED_RECON)
|
||||
|
||||
def build(self, dump_debug_info: bool = False) -> ArmedReconFlightPlan:
|
||||
return ArmedReconFlightPlan(self.flight, self.layout())
|
||||
@@ -125,16 +125,12 @@ class Builder(IBuilder[CasFlightPlan, CasLayout]):
|
||||
ingress_point_shapely.x, ingress_point_shapely.y
|
||||
)
|
||||
|
||||
patrol_start_waypoint = builder.nav(
|
||||
patrol_start, ingress_egress_altitude, use_agl_patrol_altitude
|
||||
)
|
||||
patrol_start_waypoint = builder.cas(patrol_start, ingress_egress_altitude)
|
||||
patrol_start_waypoint.name = "FLOT START"
|
||||
patrol_start_waypoint.pretty_name = "FLOT start"
|
||||
patrol_start_waypoint.description = "FLOT boundary"
|
||||
|
||||
patrol_end_waypoint = builder.nav(
|
||||
patrol_end, ingress_egress_altitude, use_agl_patrol_altitude
|
||||
)
|
||||
patrol_end_waypoint = builder.cas(patrol_end, ingress_egress_altitude)
|
||||
patrol_end_waypoint.name = "FLOT END"
|
||||
patrol_end_waypoint.pretty_name = "FLOT end"
|
||||
patrol_end_waypoint.description = "FLOT boundary"
|
||||
|
||||
@@ -11,6 +11,7 @@ from .formationattack import (
|
||||
)
|
||||
from .waypointbuilder import WaypointBuilder
|
||||
from .. import FlightType
|
||||
from ...utils import feet
|
||||
|
||||
|
||||
class EscortFlightPlan(FormationAttackFlightPlan):
|
||||
@@ -34,7 +35,7 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
|
||||
hold = builder.hold(self._hold_point())
|
||||
|
||||
join_pos = (
|
||||
self.package.waypoints.ingress
|
||||
WaypointBuilder.perturb(self.package.waypoints.ingress, feet(500))
|
||||
if self.flight.is_helo
|
||||
else self.package.waypoints.join
|
||||
)
|
||||
@@ -59,8 +60,6 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
|
||||
join = builder.join(ascent.position)
|
||||
if layout.pickup and layout.drop_off_ascent:
|
||||
join = builder.join(layout.drop_off_ascent.position)
|
||||
elif layout.pickup:
|
||||
join = builder.join(layout.pickup.position)
|
||||
split = builder.split(layout.arrival.position)
|
||||
if layout.drop_off:
|
||||
initial = builder.escort_hold(
|
||||
|
||||
@@ -105,19 +105,6 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
#
|
||||
# Plus, it's a loiter point so there's no reason to hurry.
|
||||
factor = 0.75
|
||||
elif (
|
||||
self.flight.is_helo
|
||||
and (
|
||||
a.waypoint_type == FlightWaypointType.JOIN
|
||||
or "INGRESS" in a.waypoint_type.name
|
||||
or a.waypoint_type == FlightWaypointType.CUSTOM
|
||||
)
|
||||
and self.package.primary_flight
|
||||
and not self.package.primary_flight.flight_plan.is_airassault
|
||||
):
|
||||
# Helicopter flights should be slowed down between JOIN & INGRESS
|
||||
# to allow the escort to keep up while engaging targets along the way.
|
||||
factor = 0.50
|
||||
# TODO: Adjust if AGL.
|
||||
# We don't have an exact heightmap, but we should probably be performing
|
||||
# *some* adjustment for NTTR since the minimum altitude of the map is
|
||||
@@ -268,7 +255,9 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
def estimate_startup(self) -> timedelta:
|
||||
if self.flight.start_type is StartType.COLD:
|
||||
if self.flight.client_count:
|
||||
return timedelta(minutes=10)
|
||||
return timedelta(
|
||||
minutes=self.flight.coalition.game.settings.player_startup_time
|
||||
)
|
||||
else:
|
||||
# The AI doesn't seem to have a real startup procedure.
|
||||
return timedelta(minutes=2)
|
||||
|
||||
@@ -7,6 +7,7 @@ from .aewc import AewcFlightPlan
|
||||
from .airassault import AirAssaultFlightPlan
|
||||
from .airlift import AirliftFlightPlan
|
||||
from .antiship import AntiShipFlightPlan
|
||||
from .armedrecon import ArmedReconFlightPlan
|
||||
from .bai import BaiFlightPlan
|
||||
from .barcap import BarCapFlightPlan
|
||||
from .cas import CasFlightPlan
|
||||
@@ -62,6 +63,7 @@ class FlightPlanBuilderTypes:
|
||||
FlightType.FERRY: FerryFlightPlan.builder_type(),
|
||||
FlightType.AIR_ASSAULT: AirAssaultFlightPlan.builder_type(),
|
||||
FlightType.PRETENSE_CARGO: PretenseCargoFlightPlan.builder_type(),
|
||||
FlightType.ARMED_RECON: ArmedReconFlightPlan.builder_type(),
|
||||
}
|
||||
try:
|
||||
return builder_dict[flight.flight_type]
|
||||
|
||||
@@ -64,8 +64,10 @@ class FormationFlightPlan(LoiterFlightPlan, ABC):
|
||||
return min(speeds)
|
||||
|
||||
def speed_between_waypoints(self, a: FlightWaypoint, b: FlightWaypoint) -> Speed:
|
||||
if self.package.formation_speed and b in self.package_speed_waypoints:
|
||||
return self.package.formation_speed
|
||||
if (
|
||||
speed := self.package.formation_speed(self.flight.is_helo)
|
||||
) and b in self.package_speed_waypoints:
|
||||
return speed
|
||||
return super().speed_between_waypoints(a, b)
|
||||
|
||||
@property
|
||||
|
||||
@@ -11,7 +11,7 @@ from dcs import Point
|
||||
|
||||
from game.flightplan import HoldZoneGeometry
|
||||
from game.theater import MissionTarget
|
||||
from game.utils import Speed, meters, nautical_miles
|
||||
from game.utils import nautical_miles, Speed, feet
|
||||
from .flightplan import FlightPlan
|
||||
from .formation import FormationFlightPlan, FormationLayout
|
||||
from .ibuilder import IBuilder
|
||||
@@ -39,8 +39,9 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
|
||||
if b.waypoint_type == FlightWaypointType.TARGET_GROUP_LOC:
|
||||
# Should be impossible, as any package with at least one
|
||||
# FormationFlightPlan flight needs a formation speed.
|
||||
assert self.package.formation_speed is not None
|
||||
return self.package.formation_speed
|
||||
speed = self.package.formation_speed(self.flight.is_helo)
|
||||
assert speed is not None
|
||||
return speed
|
||||
return super().speed_between_waypoints(a, b)
|
||||
|
||||
@property
|
||||
@@ -53,7 +54,7 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
|
||||
"TARGET AREA",
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
self.package.target.position,
|
||||
meters(0),
|
||||
feet(0),
|
||||
"RADIO",
|
||||
)
|
||||
|
||||
@@ -192,6 +193,7 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
||||
join_pos = self.package.waypoints.join
|
||||
if self.flight.is_helo:
|
||||
join_pos = self.package.waypoints.ingress
|
||||
join_pos = WaypointBuilder.perturb(join_pos, feet(500))
|
||||
join = builder.join(join_pos)
|
||||
split = builder.split(self._get_split())
|
||||
refuel = self._build_refuel(builder)
|
||||
@@ -286,6 +288,8 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
||||
return builder.sead_area(location)
|
||||
elif flight.flight_type == FlightType.OCA_AIRCRAFT:
|
||||
return builder.oca_strike_area(location)
|
||||
elif flight.flight_type == FlightType.ARMED_RECON:
|
||||
return builder.armed_recon_area(location)
|
||||
else:
|
||||
return builder.strike_area(location)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from typing import (
|
||||
@@ -252,18 +253,9 @@ class WaypointBuilder:
|
||||
if ingress_type in [
|
||||
FlightWaypointType.INGRESS_CAS,
|
||||
FlightWaypointType.INGRESS_OCA_AIRCRAFT,
|
||||
FlightWaypointType.INGRESS_ARMED_RECON,
|
||||
]:
|
||||
weather = self.flight.coalition.game.conditions.weather
|
||||
max_alt = feet(30000)
|
||||
if weather.clouds and (
|
||||
weather.clouds.preset
|
||||
and "overcast" in weather.clouds.preset.description.lower()
|
||||
or weather.clouds.density > 5
|
||||
):
|
||||
max_alt = meters(
|
||||
max(feet(500).meters, weather.clouds.base - feet(500).meters)
|
||||
)
|
||||
alt = min(alt, max_alt)
|
||||
alt = self._adjust_altitude_for_clouds(alt)
|
||||
|
||||
alt_type: AltitudeReference = "BARO"
|
||||
if self.is_helo or self.flight.is_hercules:
|
||||
@@ -291,6 +283,19 @@ class WaypointBuilder:
|
||||
targets=objective.strike_targets,
|
||||
)
|
||||
|
||||
def _adjust_altitude_for_clouds(self, alt: Distance) -> Distance:
|
||||
weather = self.flight.coalition.game.conditions.weather
|
||||
max_alt = feet(math.inf)
|
||||
if weather.clouds and (
|
||||
weather.clouds.preset
|
||||
and "overcast" in weather.clouds.preset.description.lower()
|
||||
or weather.clouds.density > 5
|
||||
):
|
||||
max_alt = meters(
|
||||
max(feet(500).meters, weather.clouds.base - feet(500).meters)
|
||||
)
|
||||
return min(alt, max_alt)
|
||||
|
||||
def egress(self, position: Point, target: MissionTarget) -> FlightWaypoint:
|
||||
alt_type: AltitudeReference = "BARO"
|
||||
if self.is_helo or self.get_combat_altitude.feet <= AGL_TRANSITION_ALT:
|
||||
@@ -354,6 +359,21 @@ class WaypointBuilder:
|
||||
def dead_area(self, target: MissionTarget) -> FlightWaypoint:
|
||||
return self._target_area(f"DEAD on {target.name}", target)
|
||||
|
||||
def armed_recon_area(self, target: MissionTarget) -> FlightWaypoint:
|
||||
# Force AI aircraft to fly towards target area
|
||||
alt = self.get_combat_altitude
|
||||
alt = self._adjust_altitude_for_clouds(alt)
|
||||
alt_type: AltitudeReference = "BARO"
|
||||
if self.is_helo or alt.feet <= AGL_TRANSITION_ALT:
|
||||
alt_type = "RADIO"
|
||||
return self._target_area(
|
||||
f"ARMED RECON {target.name}",
|
||||
target,
|
||||
altitude=alt,
|
||||
alt_type=alt_type,
|
||||
flyover=True,
|
||||
)
|
||||
|
||||
def oca_strike_area(self, target: MissionTarget) -> FlightWaypoint:
|
||||
return self._target_area(f"ATTACK {target.name}", target, flyover=True)
|
||||
|
||||
@@ -398,15 +418,14 @@ class WaypointBuilder:
|
||||
waypoint.only_for_player = True
|
||||
return waypoint
|
||||
|
||||
def cas(self, position: Point) -> FlightWaypoint:
|
||||
def cas(self, position: Point, altitude: Distance) -> FlightWaypoint:
|
||||
weather = self.flight.coalition.game.conditions.weather
|
||||
max_alt = feet(30000)
|
||||
if weather.clouds and (
|
||||
weather.clouds.preset
|
||||
and "overcast" in weather.clouds.preset.description.lower()
|
||||
or weather.clouds.density > 5
|
||||
):
|
||||
max_alt = meters(
|
||||
altitude = meters(
|
||||
max(feet(500).meters, weather.clouds.base - feet(500).meters)
|
||||
)
|
||||
return FlightWaypoint(
|
||||
@@ -415,7 +434,7 @@ class WaypointBuilder:
|
||||
position,
|
||||
feet(self.flight.coalition.game.settings.heli_combat_alt_agl)
|
||||
if self.is_helo
|
||||
else min(meters(1000), max_alt),
|
||||
else max(meters(1000), altitude),
|
||||
"RADIO",
|
||||
description="Provide CAS",
|
||||
pretty_name="CAS",
|
||||
@@ -667,14 +686,13 @@ class WaypointBuilder:
|
||||
This waypoint is used to generate the Trigger Zone used for AirAssault and
|
||||
AirLift using the CTLD plugin (see LogisticsGenerator)
|
||||
"""
|
||||
heli_alt = feet(self.flight.coalition.game.settings.heli_cruise_alt_agl)
|
||||
altitude = heli_alt if self.flight.is_helo else meters(0)
|
||||
alt = self.get_combat_altitude if self.flight.is_helo else meters(0)
|
||||
|
||||
return FlightWaypoint(
|
||||
"DROPOFFZONE",
|
||||
FlightWaypointType.DROPOFF_ZONE,
|
||||
drop_off.position,
|
||||
altitude,
|
||||
alt,
|
||||
"RADIO",
|
||||
description=f"Drop off cargo at {drop_off.name}",
|
||||
pretty_name="Drop-off zone",
|
||||
@@ -772,8 +790,7 @@ class WaypointBuilder:
|
||||
return previous_threatened and next_threatened
|
||||
|
||||
@staticmethod
|
||||
def perturb(point: Point) -> Point:
|
||||
deviation = nautical_miles(1)
|
||||
def perturb(point: Point, deviation: Distance = nautical_miles(1)) -> Point:
|
||||
x_adj = random.randint(int(-deviation.meters), int(deviation.meters))
|
||||
y_adj = random.randint(int(-deviation.meters), int(deviation.meters))
|
||||
return point + Vector2(x_adj, y_adj)
|
||||
|
||||
@@ -58,9 +58,8 @@ class FlightType(Enum):
|
||||
FERRY = "Ferry"
|
||||
AIR_ASSAULT = "Air Assault"
|
||||
SEAD_SWEEP = "SEAD Sweep" # Reintroduce legacy "engage-whatever-you-can-find" SEAD
|
||||
PRETENSE_CARGO = (
|
||||
"Cargo Transport" # Flight type for Pretense campaign AI cargo planes
|
||||
)
|
||||
PRETENSE_CARGO = "Cargo Transport" # For Pretense campaign AI cargo planes
|
||||
ARMED_RECON = "Armed Recon"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
@@ -96,6 +95,7 @@ class FlightType(Enum):
|
||||
FlightType.SEAD_ESCORT,
|
||||
FlightType.AIR_ASSAULT,
|
||||
FlightType.SEAD_SWEEP,
|
||||
FlightType.ARMED_RECON,
|
||||
}
|
||||
|
||||
@property
|
||||
@@ -107,6 +107,7 @@ class FlightType(Enum):
|
||||
return {
|
||||
FlightType.AEWC: AirEntity.AIRBORNE_EARLY_WARNING,
|
||||
FlightType.ANTISHIP: AirEntity.ANTISURFACE_WARFARE,
|
||||
FlightType.ARMED_RECON: AirEntity.ATTACK_STRIKE,
|
||||
FlightType.BAI: AirEntity.ATTACK_STRIKE,
|
||||
FlightType.BARCAP: AirEntity.FIGHTER,
|
||||
FlightType.CAS: AirEntity.ATTACK_STRIKE,
|
||||
|
||||
@@ -51,3 +51,4 @@ class FlightWaypointType(IntEnum):
|
||||
INGRESS_AIR_ASSAULT = 31
|
||||
INGRESS_ANTI_SHIP = 32
|
||||
INGRESS_SEAD_SWEEP = 33
|
||||
INGRESS_ARMED_RECON = 34
|
||||
|
||||
@@ -208,6 +208,7 @@ class Loadout:
|
||||
loadout_names[FlightType.INTERCEPTION].extend(loadout_names[FlightType.BARCAP])
|
||||
# OCA/Aircraft falls back to BAI, which falls back to CAS.
|
||||
loadout_names[FlightType.BAI].extend(loadout_names[FlightType.CAS])
|
||||
loadout_names[FlightType.ARMED_RECON].extend(loadout_names[FlightType.CAS])
|
||||
loadout_names[FlightType.OCA_AIRCRAFT].extend(loadout_names[FlightType.BAI])
|
||||
# DEAD also falls back to BAI.
|
||||
loadout_names[FlightType.DEAD].extend(loadout_names[FlightType.BAI])
|
||||
|
||||
@@ -54,8 +54,7 @@ class Package(RadioFrequencyContainer):
|
||||
def has_players(self) -> bool:
|
||||
return any(flight.client_count for flight in self.flights)
|
||||
|
||||
@property
|
||||
def formation_speed(self) -> Optional[Speed]:
|
||||
def formation_speed(self, is_helo: bool) -> Optional[Speed]:
|
||||
"""The speed of the package when in formation.
|
||||
|
||||
If none of the flights in the package will join a formation, this
|
||||
@@ -66,7 +65,10 @@ class Package(RadioFrequencyContainer):
|
||||
"""
|
||||
speeds = []
|
||||
for flight in self.flights:
|
||||
if isinstance(flight.flight_plan, FormationFlightPlan):
|
||||
if (
|
||||
isinstance(flight.flight_plan, FormationFlightPlan)
|
||||
and flight.is_helo == is_helo
|
||||
):
|
||||
speeds.append(flight.flight_plan.best_flight_formation_speed)
|
||||
if not speeds:
|
||||
return None
|
||||
@@ -183,6 +185,7 @@ class Package(RadioFrequencyContainer):
|
||||
FlightType.SEAD_SWEEP,
|
||||
FlightType.TARCAP,
|
||||
FlightType.BARCAP,
|
||||
FlightType.ARMED_RECON,
|
||||
FlightType.AEWC,
|
||||
FlightType.FERRY,
|
||||
FlightType.REFUELING,
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from game.utils import Distance, SPEED_OF_SOUND_AT_SEA_LEVEL, Speed, mach, meters
|
||||
from game.utils import Distance, SPEED_OF_SOUND_AT_SEA_LEVEL, Speed, mach
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .flight import Flight
|
||||
@@ -30,8 +30,8 @@ class GroundSpeed:
|
||||
# as it can at sea level. This probably isn't great assumption, but
|
||||
# might. be sufficient given the wiggle room. We can come up with
|
||||
# another heuristic if needed.
|
||||
cruise_mach = max_speed.mach() * (0.60 if flight.is_helo else 0.85)
|
||||
return mach(cruise_mach, altitude if not flight.is_helo else meters(0))
|
||||
cruise_mach = max_speed.mach() * (0.7 if flight.is_helo else 0.85)
|
||||
return mach(cruise_mach, altitude)
|
||||
|
||||
|
||||
# TODO: Most if not all of this should move into FlightPlan.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import random
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from game.squadrons import Squadron
|
||||
@@ -101,9 +102,10 @@ class DefaultSquadronAssigner:
|
||||
if aircraft not in self.coalition.faction.all_aircrafts:
|
||||
return None
|
||||
|
||||
lo = self.coalition.faction.liveries_overrides
|
||||
squadron_def = self.find_squadron_for_airframe(aircraft, task, control_point)
|
||||
if squadron_def is not None and lo.get(aircraft) is None:
|
||||
if squadron_def is not None and (
|
||||
squadron_def.livery is not None or squadron_def.livery_set is not None
|
||||
):
|
||||
return squadron_def
|
||||
|
||||
# No premade squadron available for this aircraft that meets the requirements,
|
||||
@@ -124,11 +126,14 @@ class DefaultSquadronAssigner:
|
||||
def find_squadron_for_airframe(
|
||||
self, aircraft: AircraftType, task: FlightType, control_point: ControlPoint
|
||||
) -> Optional[SquadronDef]:
|
||||
choices = []
|
||||
for squadron in self.air_wing.squadron_defs[aircraft]:
|
||||
if not squadron.claimed and self.squadron_compatible_with(
|
||||
squadron, task, control_point
|
||||
):
|
||||
return squadron
|
||||
choices.append(squadron)
|
||||
if choices:
|
||||
return random.choice(choices)
|
||||
return None
|
||||
|
||||
def find_squadron_by_name(
|
||||
|
||||
@@ -167,24 +167,6 @@ class ObjectiveFinder:
|
||||
yield cp
|
||||
break
|
||||
|
||||
def vulnerable_enemy_control_points(self) -> Iterator[ControlPoint]:
|
||||
"""Iterates over enemy CPs that are vulnerable to Air Assault.
|
||||
Vulnerability is defined as any unit being alive in the CP's "blocking_capture" groups.
|
||||
"""
|
||||
for cp in self.enemy_control_points():
|
||||
include = True
|
||||
for tgo in cp.connected_objectives:
|
||||
if tgo.distance_to(cp) > cp.CAPTURE_DISTANCE.meters:
|
||||
continue
|
||||
for u in tgo.units:
|
||||
if u.is_vehicle and u.alive:
|
||||
include = False
|
||||
break
|
||||
if not include:
|
||||
break
|
||||
if include:
|
||||
yield cp
|
||||
|
||||
def oca_targets(self, min_aircraft: int) -> Iterator[ControlPoint]:
|
||||
parking_type = ParkingType()
|
||||
parking_type.include_rotary_wing = True
|
||||
|
||||
@@ -6,7 +6,7 @@ import math
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Optional, TYPE_CHECKING, Union
|
||||
from typing import Optional, TYPE_CHECKING, Union, Dict
|
||||
|
||||
from game.commander.battlepositions import BattlePositions
|
||||
from game.commander.objectivefinder import ObjectiveFinder
|
||||
@@ -163,6 +163,15 @@ class TheaterState(WorldState["TheaterState"]):
|
||||
barcap_duration = coalition.doctrine.cap_duration.total_seconds()
|
||||
barcap_rounds = math.ceil(mission_duration / barcap_duration)
|
||||
|
||||
battle_postitions: Dict[ControlPoint, BattlePositions] = {
|
||||
cp: BattlePositions.for_control_point(cp)
|
||||
for cp in ordered_capturable_points
|
||||
}
|
||||
|
||||
vulnerable_control_points = [
|
||||
cp for cp, bp in battle_postitions.items() if not bp.blocking_capture
|
||||
]
|
||||
|
||||
return TheaterState(
|
||||
context=context,
|
||||
barcaps_needed={
|
||||
@@ -179,10 +188,7 @@ class TheaterState(WorldState["TheaterState"]):
|
||||
enemy_convoys=list(finder.convoys()),
|
||||
enemy_shipping=list(finder.cargo_ships()),
|
||||
enemy_ships=list(finder.enemy_ships()),
|
||||
enemy_battle_positions={
|
||||
cp: BattlePositions.for_control_point(cp)
|
||||
for cp in ordered_capturable_points
|
||||
},
|
||||
enemy_battle_positions=battle_postitions,
|
||||
oca_targets=list(
|
||||
finder.oca_targets(
|
||||
min_aircraft=game.settings.oca_target_autoplanner_min_aircraft_count
|
||||
@@ -191,5 +197,5 @@ class TheaterState(WorldState["TheaterState"]):
|
||||
strike_targets=list(finder.strike_targets()),
|
||||
enemy_barcaps=list(game.theater.control_points_for(not player)),
|
||||
threat_zones=game.threat_zone_for(not player),
|
||||
vulnerable_control_points=list(finder.vulnerable_enemy_control_points()),
|
||||
vulnerable_control_points=vulnerable_control_points,
|
||||
)
|
||||
|
||||
@@ -241,9 +241,18 @@ class AircraftType(UnitType[Type[FlyingType]]):
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
enrich = {}
|
||||
for t in self.task_priorities:
|
||||
if t == FlightType.SEAD:
|
||||
enrich[FlightType.SEAD_SWEEP] = self.task_priorities[t]
|
||||
if FlightType.SEAD_SWEEP not in self.task_priorities:
|
||||
if (value := self.task_priorities.get(FlightType.SEAD)) or (
|
||||
value := self.task_priorities.get(FlightType.SEAD_ESCORT)
|
||||
):
|
||||
enrich[FlightType.SEAD_SWEEP] = value
|
||||
|
||||
if FlightType.ARMED_RECON not in self.task_priorities:
|
||||
if (value := self.task_priorities.get(FlightType.CAS)) or (
|
||||
value := self.task_priorities.get(FlightType.BAI)
|
||||
):
|
||||
enrich[FlightType.ARMED_RECON] = value
|
||||
|
||||
self.task_priorities.update(enrich)
|
||||
|
||||
@classmethod
|
||||
@@ -299,17 +308,24 @@ class AircraftType(UnitType[Type[FlyingType]]):
|
||||
elif max_speed > SPEED_OF_SOUND_AT_SEA_LEVEL * 0.7:
|
||||
# Semi-fast like airliners or similar
|
||||
return (
|
||||
Speed.from_mach(0.5, altitude)
|
||||
Speed.from_mach(0.6, altitude)
|
||||
if altitude.feet > 20000
|
||||
else Speed.from_mach(0.4, altitude)
|
||||
else Speed.from_mach(0.5, altitude)
|
||||
)
|
||||
elif self.helicopter:
|
||||
return max_speed * 0.4
|
||||
else:
|
||||
# Slow like warbirds or helicopters
|
||||
# Use whichever is slowest - mach 0.35 or 50% of max speed
|
||||
logging.debug(
|
||||
f"{self.display_name} max_speed * 0.5 is {max_speed * 0.5}"
|
||||
# Slow like warbirds or attack planes
|
||||
# return 50% of max speed + 5% per 2k above 10k to maintain momentum
|
||||
return max_speed * min(
|
||||
1.0,
|
||||
0.5
|
||||
+ (
|
||||
(((altitude.feet - 10000) / 2000) * 0.05)
|
||||
if altitude.feet > 10000
|
||||
else 0
|
||||
),
|
||||
)
|
||||
return min(Speed.from_mach(0.35, altitude), max_speed * 0.5)
|
||||
|
||||
@cached_property
|
||||
def preferred_cruise_altitude(self) -> Distance:
|
||||
@@ -526,17 +542,7 @@ class AircraftType(UnitType[Type[FlyingType]]):
|
||||
if prop_overrides is not None:
|
||||
cls._set_props_overrides(prop_overrides, aircraft)
|
||||
|
||||
from game.ato.flighttype import FlightType
|
||||
|
||||
task_priorities: dict[FlightType, int] = {}
|
||||
for task_name, priority in data.get("tasks", {}).items():
|
||||
task_priorities[FlightType(task_name)] = priority
|
||||
|
||||
if (
|
||||
FlightType.SEAD_SWEEP not in task_priorities
|
||||
and FlightType.SEAD in task_priorities
|
||||
):
|
||||
task_priorities[FlightType.SEAD_SWEEP] = task_priorities[FlightType.SEAD]
|
||||
task_priorities = cls.get_task_priorities(data)
|
||||
|
||||
cls._custom_weapon_injections(aircraft, data)
|
||||
cls._user_weapon_injections(aircraft)
|
||||
@@ -583,6 +589,27 @@ class AircraftType(UnitType[Type[FlyingType]]):
|
||||
use_f15e_waypoint_names=data.get("use_f15e_waypoint_names", False),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_task_priorities(cls, data: dict[str, Any]) -> dict[FlightType, int]:
|
||||
task_priorities: dict[FlightType, int] = {}
|
||||
for task_name, priority in data.get("tasks", {}).items():
|
||||
task_priorities[FlightType(task_name)] = priority
|
||||
if (
|
||||
FlightType.SEAD_SWEEP not in task_priorities
|
||||
and FlightType.SEAD in task_priorities
|
||||
):
|
||||
task_priorities[FlightType.SEAD_SWEEP] = task_priorities[FlightType.SEAD]
|
||||
if FlightType.ARMED_RECON not in task_priorities:
|
||||
if FlightType.CAS in task_priorities:
|
||||
task_priorities[FlightType.ARMED_RECON] = task_priorities[
|
||||
FlightType.CAS
|
||||
]
|
||||
elif FlightType.BAI in task_priorities:
|
||||
task_priorities[FlightType.ARMED_RECON] = task_priorities[
|
||||
FlightType.BAI
|
||||
]
|
||||
return task_priorities
|
||||
|
||||
@staticmethod
|
||||
def _custom_weapon_injections(
|
||||
aircraft: Type[FlyingType], data: Dict[str, Any]
|
||||
|
||||
@@ -352,6 +352,45 @@ class Faction:
|
||||
self.remove_aircraft("A-4E-C")
|
||||
if not mod_settings.hercules:
|
||||
self.remove_aircraft("Hercules")
|
||||
if not mod_settings.oh_6:
|
||||
self.remove_aircraft("OH-6A")
|
||||
if not mod_settings.oh_6_vietnamassetpack:
|
||||
self.remove_vehicle("vap_mutt_gun")
|
||||
self.remove_vehicle("vap_type63_mlrs")
|
||||
self.remove_vehicle("vap_vc_bicycle_mortar")
|
||||
self.remove_vehicle("vap_zis_150_aa")
|
||||
self.remove_vehicle("vap_us_hooch_LP")
|
||||
self.remove_vehicle("vap_ammo_50cal_line")
|
||||
self.remove_vehicle("vap_ammo_50cal_pack")
|
||||
self.remove_vehicle("vap_barrels_line")
|
||||
self.remove_vehicle("vap_barrels")
|
||||
self.remove_vehicle("vap_ammo_box_pile")
|
||||
self.remove_vehicle("vap_ammo_box_wood_long")
|
||||
self.remove_vehicle("vap_ammo_box_wood_small")
|
||||
self.remove_vehicle("vap_barrel_red")
|
||||
self.remove_vehicle("vap_barrel_green")
|
||||
self.remove_vehicle("vap_mre_boxes")
|
||||
self.remove_vehicle("vap_mixed_cargo_1")
|
||||
self.remove_vehicle("vap_mixed_cargo_2")
|
||||
self.remove_vehicle("vap_watchtower")
|
||||
self.remove_vehicle("vap_house_high")
|
||||
self.remove_vehicle("vap_house_long")
|
||||
self.remove_vehicle("vap_house_small")
|
||||
self.remove_vehicle("vap_house_T")
|
||||
self.remove_vehicle("vap_house_tiny")
|
||||
self.remove_vehicle("vap_house1")
|
||||
self.remove_vehicle("vap_us_hooch_radio")
|
||||
self.remove_vehicle("vap_us_hooch_closed")
|
||||
self.remove_vehicle("vap_vc_bunker_single")
|
||||
self.remove_vehicle("vap_vc_mg_nest")
|
||||
self.remove_vehicle("vap_mule")
|
||||
self.remove_vehicle("vap_mutt")
|
||||
self.remove_vehicle("vap_m35_truck")
|
||||
self.remove_vehicle("vap_vc_zis")
|
||||
self.remove_vehicle("vap_vc_bicycle")
|
||||
self.remove_vehicle("vap_vc_zil")
|
||||
self.remove_vehicle("vap_vc_bicycle_ak")
|
||||
self.remove_ship("vap_us_seafloat")
|
||||
if not mod_settings.uh_60l:
|
||||
self.remove_aircraft("UH-60L")
|
||||
self.remove_aircraft("KC130J")
|
||||
|
||||
@@ -11,6 +11,7 @@ from game.ato.flightplans.formation import FormationLayout
|
||||
from game.ato.flightplans.waypointbuilder import WaypointBuilder
|
||||
from game.ato.packagewaypoints import PackageWaypoints
|
||||
from game.data.doctrine import MODERN_DOCTRINE, COLDWAR_DOCTRINE, WWII_DOCTRINE
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.theater import ParkingType, SeasonalConditions
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -164,6 +165,7 @@ class Migrator:
|
||||
try_set_attr(s, "max_size", 12)
|
||||
try_set_attr(s, "radio_presets", {})
|
||||
try_set_attr(s, "livery_set", [])
|
||||
s.aircraft = AircraftType.named(s.aircraft.variant_id)
|
||||
if isinstance(s.country, str):
|
||||
c = country_dict.get(s.country, s.country)
|
||||
s.country = countries_by_name[c]()
|
||||
|
||||
@@ -57,6 +57,8 @@ class AircraftBehavior:
|
||||
self.configure_refueling(group, flight)
|
||||
elif self.task in [FlightType.CAS, FlightType.BAI]:
|
||||
self.configure_cas(group, flight)
|
||||
elif self.task == FlightType.ARMED_RECON:
|
||||
self.configure_armed_recon(group, flight)
|
||||
elif self.task == FlightType.DEAD:
|
||||
self.configure_dead(group, flight)
|
||||
elif self.task in [FlightType.SEAD, FlightType.SEAD_SWEEP]:
|
||||
@@ -183,6 +185,17 @@ class AircraftBehavior:
|
||||
restrict_jettison=True,
|
||||
)
|
||||
|
||||
def configure_armed_recon(self, group: FlyingGroup[Any], flight: Flight) -> None:
|
||||
self.configure_task(flight, group, CAS, [AFAC, AntishipStrike])
|
||||
self.configure_behavior(
|
||||
flight,
|
||||
group,
|
||||
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||
roe=OptROE.Values.OpenFire,
|
||||
rtb_winchester=OptRTBOnOutOfAmmo.Values.All,
|
||||
restrict_jettison=True,
|
||||
)
|
||||
|
||||
def configure_dead(self, group: FlyingGroup[Any], flight: Flight) -> None:
|
||||
# Only CAS and SEAD are capable of the Attack Group task. SEAD is arguably more
|
||||
# appropriate but it has an extremely limited list of capable aircraft, whereas
|
||||
@@ -379,23 +392,35 @@ class AircraftBehavior:
|
||||
|
||||
if preferred_task in flight.unit_type.dcs_unit_type.tasks:
|
||||
group.task = preferred_task.name
|
||||
elif fallback_tasks:
|
||||
return
|
||||
if fallback_tasks:
|
||||
for task in fallback_tasks:
|
||||
if task in flight.unit_type.dcs_unit_type.tasks:
|
||||
group.task = task.name
|
||||
return
|
||||
elif flight.unit_type.dcs_unit_type.task_default and preferred_task == Nothing:
|
||||
if flight.unit_type.dcs_unit_type.task_default and preferred_task == Nothing:
|
||||
group.task = flight.unit_type.dcs_unit_type.task_default.name
|
||||
logging.warning(
|
||||
f"{ac_type} is not capable of 'Nothing', using default task '{group.task}'"
|
||||
)
|
||||
else:
|
||||
fallback_part = (
|
||||
f" nor any of the following fall-back tasks: {[task.name for task in fallback_tasks]}"
|
||||
if fallback_tasks
|
||||
else ""
|
||||
return
|
||||
if flight.roster.members and flight.roster.members[0].is_player:
|
||||
group.task = (
|
||||
flight.unit_type.dcs_unit_type.task_default.name
|
||||
if flight.unit_type.dcs_unit_type.task_default
|
||||
else group.task # even if this is incompatible, if it's a client we don't really care...
|
||||
)
|
||||
raise RuntimeError(
|
||||
f"{ac_type} is neither capable of {preferred_task.name}"
|
||||
f"{fallback_part}. Can't generate {flight.flight_type} flight."
|
||||
logging.warning(
|
||||
f"Client override: {ac_type} is not capable of '{preferred_task}', using default task '{group.task}'"
|
||||
)
|
||||
return
|
||||
|
||||
fallback_part = (
|
||||
f" nor any of the following fall-back tasks: {[task.name for task in fallback_tasks]}"
|
||||
if fallback_tasks
|
||||
else ""
|
||||
)
|
||||
raise RuntimeError(
|
||||
f"{ac_type} is neither capable of {preferred_task.name}"
|
||||
f"{fallback_part}. Can't generate {flight.flight_type} flight."
|
||||
)
|
||||
|
||||
@@ -497,21 +497,20 @@ class FlightGroupSpawner:
|
||||
) -> Optional[FlyingGroup[Any]]:
|
||||
is_airbase = False
|
||||
is_roadbase = False
|
||||
ground_spawn = None
|
||||
|
||||
try:
|
||||
if is_large:
|
||||
if len(self.ground_spawns_large[cp]) > 0:
|
||||
ground_spawn = self.ground_spawns_large[cp].pop()
|
||||
is_airbase = True
|
||||
else:
|
||||
if len(self.ground_spawns_roadbase[cp]) > 0:
|
||||
ground_spawn = self.ground_spawns_roadbase[cp].pop()
|
||||
is_roadbase = True
|
||||
if len(self.ground_spawns[cp]) > 0:
|
||||
ground_spawn = self.ground_spawns[cp].pop()
|
||||
is_airbase = True
|
||||
except IndexError as ex:
|
||||
logging.warning("Not enough ground spawn slots available at " + str(ex))
|
||||
if not is_large and len(self.ground_spawns_roadbase[cp]) > 0:
|
||||
ground_spawn = self.ground_spawns_roadbase[cp].pop()
|
||||
is_roadbase = True
|
||||
elif not is_large and len(self.ground_spawns[cp]) > 0:
|
||||
ground_spawn = self.ground_spawns[cp].pop()
|
||||
is_airbase = True
|
||||
elif len(self.ground_spawns_large[cp]) > 0:
|
||||
ground_spawn = self.ground_spawns_large[cp].pop()
|
||||
is_airbase = True
|
||||
|
||||
if ground_spawn is None:
|
||||
logging.warning("Not enough ground spawn slots available at " + cp.name)
|
||||
return None
|
||||
|
||||
group = self._generate_at_group(name, ground_spawn[0])
|
||||
@@ -581,14 +580,12 @@ class FlightGroupSpawner:
|
||||
for i in range(self.flight.count - 1):
|
||||
try:
|
||||
terrain = cp.coalition.game.theater.terrain
|
||||
if is_large:
|
||||
if len(self.ground_spawns_large[cp]) > 0:
|
||||
ground_spawn = self.ground_spawns_large[cp].pop()
|
||||
else:
|
||||
if len(self.ground_spawns_roadbase[cp]) > 0:
|
||||
ground_spawn = self.ground_spawns_roadbase[cp].pop()
|
||||
else:
|
||||
ground_spawn = self.ground_spawns[cp].pop()
|
||||
if not is_large and len(self.ground_spawns_roadbase[cp]) > 0:
|
||||
ground_spawn = self.ground_spawns_roadbase[cp].pop()
|
||||
elif not is_large and len(self.ground_spawns[cp]) > 0:
|
||||
ground_spawn = self.ground_spawns[cp].pop()
|
||||
elif len(self.ground_spawns_large[cp]) > 0:
|
||||
ground_spawn = self.ground_spawns_large[cp].pop()
|
||||
group.units[1 + i].position = Point(
|
||||
ground_spawn[0].x, ground_spawn[0].y, terrain=terrain
|
||||
)
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
from dcs.point import MovingPoint
|
||||
from dcs.task import (
|
||||
OptECMUsing,
|
||||
ControlledTask,
|
||||
Targets,
|
||||
EngageTargetsInZone,
|
||||
)
|
||||
|
||||
from game.utils import nautical_miles
|
||||
from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
|
||||
class ArmedReconIngressBuilder(PydcsWaypointBuilder):
|
||||
def add_tasks(self, waypoint: MovingPoint) -> None:
|
||||
self.register_special_ingress_points()
|
||||
# Preemptively use ECM to better avoid getting swatted.
|
||||
ecm_option = OptECMUsing(value=OptECMUsing.Values.UseIfDetectedLockByRadar)
|
||||
waypoint.tasks.append(ecm_option)
|
||||
|
||||
waypoint.add_task(
|
||||
ControlledTask(
|
||||
EngageTargetsInZone(
|
||||
position=self.flight.flight_plan.tot_waypoint.position,
|
||||
radius=int(
|
||||
nautical_miles(
|
||||
self.flight.coalition.game.settings.armed_recon_engagement_range_distance
|
||||
).meters
|
||||
),
|
||||
targets=[
|
||||
Targets.All.GroundUnits,
|
||||
Targets.All.Air.Helicopters,
|
||||
],
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -17,7 +17,7 @@ class HoldPointBuilder(PydcsWaypointBuilder):
|
||||
loiter = ControlledTask(
|
||||
OrbitAction(
|
||||
altitude=waypoint.alt,
|
||||
speed=speed.meters_per_second,
|
||||
speed=speed.kph,
|
||||
pattern=OrbitAction.OrbitPattern.Circle,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -8,7 +8,6 @@ from dcs.task import (
|
||||
OptECMUsing,
|
||||
OptFormation,
|
||||
Targets,
|
||||
OptROE,
|
||||
SetUnlimitedFuelCommand,
|
||||
)
|
||||
|
||||
@@ -94,12 +93,6 @@ class JoinPointBuilder(PydcsWaypointBuilder):
|
||||
max_dist: float = 30.0,
|
||||
vertical_spacing: float = 2000.0,
|
||||
) -> None:
|
||||
if self.flight.is_helo:
|
||||
# Make helicopters a bit more aggressive
|
||||
waypoint.tasks.append(OptROE(value=OptROE.Values.OpenFireWeaponFree))
|
||||
else:
|
||||
waypoint.tasks.append(OptROE(value=OptROE.Values.OpenFire))
|
||||
|
||||
rx = (random.random() + 0.1) * 333
|
||||
ry = feet(vertical_spacing).meters
|
||||
rz = (random.random() + 0.1) * 166 * random.choice([-1, 1])
|
||||
|
||||
@@ -3,7 +3,7 @@ import logging
|
||||
from dcs.point import MovingPoint
|
||||
from dcs.task import EngageTargetsInZone, Targets
|
||||
|
||||
from game.theater import Airfield
|
||||
from game.theater import Airfield, Fob
|
||||
from game.utils import nautical_miles
|
||||
from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
@@ -12,7 +12,7 @@ class OcaAircraftIngressBuilder(PydcsWaypointBuilder):
|
||||
def add_tasks(self, waypoint: MovingPoint) -> None:
|
||||
target = self.package.target
|
||||
self.register_special_ingress_points()
|
||||
if not isinstance(target, Airfield):
|
||||
if not (isinstance(target, Airfield) or isinstance(target, Fob)):
|
||||
logging.error(
|
||||
"Unexpected target type for OCA Strike mission: %s",
|
||||
target.__class__.__name__,
|
||||
|
||||
@@ -2,9 +2,8 @@ from dcs.point import MovingPoint
|
||||
from dcs.task import (
|
||||
OptECMUsing,
|
||||
ControlledTask,
|
||||
EngageTargets,
|
||||
Targets,
|
||||
OptROE,
|
||||
EngageTargetsInZone,
|
||||
)
|
||||
|
||||
from game.utils import nautical_miles
|
||||
@@ -14,16 +13,15 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
class SeadSweepIngressBuilder(PydcsWaypointBuilder):
|
||||
def add_tasks(self, waypoint: MovingPoint) -> None:
|
||||
self.register_special_ingress_points()
|
||||
waypoint.tasks.append(OptROE(value=OptROE.Values.OpenFireWeaponFree))
|
||||
# Preemptively use ECM to better avoid getting swatted.
|
||||
ecm_option = OptECMUsing(value=OptECMUsing.Values.UseIfDetectedLockByRadar)
|
||||
waypoint.tasks.append(ecm_option)
|
||||
|
||||
waypoint.add_task(
|
||||
ControlledTask(
|
||||
EngageTargets(
|
||||
# TODO: From doctrine.
|
||||
max_distance=int(
|
||||
EngageTargetsInZone(
|
||||
position=self.flight.flight_plan.tot_waypoint.position,
|
||||
radius=int(
|
||||
nautical_miles(
|
||||
self.flight.coalition.game.settings.sead_sweep_engagement_range_distance
|
||||
).meters
|
||||
|
||||
@@ -22,6 +22,7 @@ from game.settings import Settings
|
||||
from game.utils import pairwise
|
||||
from .airassaultingress import AirAssaultIngressBuilder
|
||||
from .antishipingress import AntiShipIngressBuilder
|
||||
from .armedreconingress import ArmedReconIngressBuilder
|
||||
from .baiingress import BaiIngressBuilder
|
||||
from .casingress import CasIngressBuilder
|
||||
from .deadingress import DeadIngressBuilder
|
||||
@@ -136,6 +137,7 @@ class WaypointGenerator:
|
||||
FlightWaypointType.DROPOFF_ZONE: LandingZoneBuilder,
|
||||
FlightWaypointType.INGRESS_AIR_ASSAULT: AirAssaultIngressBuilder,
|
||||
FlightWaypointType.INGRESS_ANTI_SHIP: AntiShipIngressBuilder,
|
||||
FlightWaypointType.INGRESS_ARMED_RECON: ArmedReconIngressBuilder,
|
||||
FlightWaypointType.INGRESS_BAI: BaiIngressBuilder,
|
||||
FlightWaypointType.INGRESS_CAS: CasIngressBuilder,
|
||||
FlightWaypointType.INGRESS_DEAD: DeadIngressBuilder,
|
||||
@@ -203,7 +205,10 @@ class WaypointGenerator:
|
||||
not self.flight.state.in_flight
|
||||
and self.flight.state.spawn_type is not StartType.RUNWAY
|
||||
and self.flight.departure.is_fleet
|
||||
and not self.flight.client_count
|
||||
and not (
|
||||
self.flight.client_count
|
||||
and self.flight.coalition.game.settings.player_flights_sixpack
|
||||
)
|
||||
):
|
||||
# https://github.com/dcs-liberation/dcs_liberation/issues/1309
|
||||
# Without a delay, AI aircraft will be spawned on the sixpack, which other
|
||||
|
||||
@@ -325,6 +325,20 @@ class Settings:
|
||||
default=2,
|
||||
detail="Creates a randomized altitude offset for airplanes.",
|
||||
)
|
||||
|
||||
player_startup_time: int = bounded_int_option(
|
||||
"Player startup time",
|
||||
page=CAMPAIGN_DOCTRINE_PAGE,
|
||||
section=GENERAL_SECTION,
|
||||
default=10,
|
||||
min=0,
|
||||
max=100,
|
||||
detail=(
|
||||
"The startup time allocated to player flights (default : 10 minutes, AI is 2 minutes). "
|
||||
"Packages have to be planned again for this to take effect. "
|
||||
),
|
||||
)
|
||||
|
||||
# Doctrine Distances Section
|
||||
airbase_threat_range: int = bounded_int_option(
|
||||
"Airbase threat range (NM)",
|
||||
@@ -346,6 +360,14 @@ class Settings:
|
||||
min=0,
|
||||
max=100,
|
||||
)
|
||||
armed_recon_engagement_range_distance: int = bounded_int_option(
|
||||
"Armed Recon engagement range (NM)",
|
||||
page=CAMPAIGN_DOCTRINE_PAGE,
|
||||
section=DOCTRINE_DISTANCES_SECTION,
|
||||
default=5,
|
||||
min=0,
|
||||
max=25,
|
||||
)
|
||||
sead_sweep_engagement_range_distance: int = bounded_int_option(
|
||||
"SEAD Sweep engagement range (NM)",
|
||||
page=CAMPAIGN_DOCTRINE_PAGE,
|
||||
@@ -424,6 +446,7 @@ class Settings:
|
||||
"range is defined in the helicopter's yaml specification."
|
||||
),
|
||||
)
|
||||
|
||||
# Pilots and Squadrons
|
||||
ai_pilot_levelling: bool = boolean_option(
|
||||
"Allow AI pilot leveling",
|
||||
@@ -944,6 +967,12 @@ class Settings:
|
||||
default=True,
|
||||
detail=("Enables dynamic cargo for airfields, ships, FARPs & warehouses."),
|
||||
)
|
||||
player_flights_sixpack: bool = boolean_option(
|
||||
"Player flights can spawn on the sixpack",
|
||||
MISSION_GENERATOR_PAGE,
|
||||
GAMEPLAY_SECTION,
|
||||
default=True,
|
||||
)
|
||||
|
||||
# Performance
|
||||
perf_smoke_gen: bool = boolean_option(
|
||||
|
||||
@@ -1388,6 +1388,11 @@ class NavalControlPoint(
|
||||
FlightType.SEAD_ESCORT,
|
||||
]
|
||||
yield from super().mission_types(for_player)
|
||||
if self.is_friendly(for_player):
|
||||
yield from [
|
||||
FlightType.AEWC,
|
||||
FlightType.REFUELING,
|
||||
]
|
||||
|
||||
@property
|
||||
def heading(self) -> Heading:
|
||||
@@ -1486,16 +1491,6 @@ class Carrier(NavalControlPoint):
|
||||
def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]:
|
||||
return SymbolSet.SEA_SURFACE, SeaSurfaceEntity.CARRIER
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
from game.ato.flighttype import FlightType
|
||||
|
||||
yield from super().mission_types(for_player)
|
||||
if self.is_friendly(for_player):
|
||||
yield from [
|
||||
FlightType.AEWC,
|
||||
FlightType.REFUELING,
|
||||
]
|
||||
|
||||
def capture(self, game: Game, events: GameUpdateEvents, for_player: bool) -> None:
|
||||
raise RuntimeError("Carriers cannot be captured")
|
||||
|
||||
@@ -1661,7 +1656,6 @@ class Fob(ControlPoint, RadioFrequencyContainer, CTLD):
|
||||
from game.ato import FlightType
|
||||
|
||||
if not self.is_friendly(for_player):
|
||||
yield FlightType.STRIKE
|
||||
yield FlightType.AIR_ASSAULT
|
||||
if self.total_aircraft_parking(ParkingType(True, True, True)):
|
||||
yield FlightType.OCA_AIRCRAFT
|
||||
|
||||
@@ -39,6 +39,7 @@ class MissionTarget:
|
||||
FlightType.TARCAP,
|
||||
FlightType.SEAD_ESCORT,
|
||||
FlightType.SEAD_SWEEP,
|
||||
FlightType.ARMED_RECON,
|
||||
FlightType.SWEEP,
|
||||
# TODO: FlightType.ELINT,
|
||||
# TODO: FlightType.EWAR,
|
||||
|
||||
@@ -84,6 +84,8 @@ class ModSettings:
|
||||
f106_deltadart: bool = False
|
||||
hercules: bool = False
|
||||
irondome: bool = False
|
||||
oh_6: bool = False
|
||||
oh_6_vietnamassetpack: bool = False
|
||||
uh_60l: bool = False
|
||||
jas39_gripen: bool = False
|
||||
sk_60: bool = False
|
||||
|
||||
Reference in New Issue
Block a user