Merge branch 'dev' into pr/204

This commit is contained in:
Raffson
2024-07-28 15:57:38 +02:00
100 changed files with 1342 additions and 220 deletions

View File

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

View File

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

View 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())

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -51,3 +51,4 @@ class FlightWaypointType(IntEnum):
INGRESS_AIR_ASSAULT = 31
INGRESS_ANTI_SHIP = 32
INGRESS_SEAD_SWEEP = 33
INGRESS_ARMED_RECON = 34

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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