Skip to content

Commit

Permalink
Merge branch 'dev' into pr/204
Browse files Browse the repository at this point in the history
  • Loading branch information
Raffson committed Jul 28, 2024
2 parents 8dcf25d + e0d4a03 commit 65e529a
Show file tree
Hide file tree
Showing 100 changed files with 1,343 additions and 221 deletions.
4 changes: 4 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* **[Squadrons]** Ability to define a livery-set for each squadron from which Retribution will randomly choose during mission generation
* **[Modding]** Updated support for F/A-18E/F/G mod version 2.2.5
* **[Modding]** Added VSN F-106 Delta Dart mod support (v2.9.4.101)
* **[Modding]** Added OH-6 Cayuse (v1.2) mod support, including the Vietnam Asset Pack v1.0
* **[Modding]** Added VSN EA-6B Prowler mod support (v2.9.4.102)
* **[Modding]** Added tripod3 Cold War assets mod support (v1.0)
* **[Campaign Setup]** Allow adjustments to naval TGOs (except carriers) on turn 0
Expand All @@ -25,6 +26,9 @@
* **[Modding]** Added support for Su-15 Flagon mod (v1.0)
* **[Plugins]** Support for Carsten's Arty Spotter script
* **[Modding]** Added support for SK-60 mod (v1.2.1)
* **[Mission Generation]** Introducing the Armed Recon flight plan, i.e. CAS against any Theater Ground Object
* **[Doctrine]** Ability to customize the startup time allocated to the player
* **[Mission Generation]** Ability to choose whether player flights can spawn on the sixpack or not

## Fixes
* **[UI/UX]** A-10A flights can be edited again
Expand Down
2 changes: 0 additions & 2 deletions game/ato/flight.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,8 +275,6 @@ def initialize_fuel(self) -> None:
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(
Expand Down
11 changes: 0 additions & 11 deletions game/ato/flightplans/airassault.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,24 +75,13 @@ def ingress_time(self) -> datetime:
)
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

@property
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
Expand Down
34 changes: 34 additions & 0 deletions game/ato/flightplans/armedrecon.py
Original file line number Diff line number Diff line change
@@ -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())
8 changes: 2 additions & 6 deletions game/ato/flightplans/cas.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,16 +125,12 @@ def layout(self, dump_debug_info: bool) -> 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"
Expand Down
5 changes: 2 additions & 3 deletions game/ato/flightplans/escort.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
)
from .waypointbuilder import WaypointBuilder
from .. import FlightType
from ...utils import feet


class EscortFlightPlan(FormationAttackFlightPlan):
Expand All @@ -34,7 +35,7 @@ def layout(self) -> 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
)
Expand All @@ -59,8 +60,6 @@ def layout(self) -> 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(
Expand Down
17 changes: 3 additions & 14 deletions game/ato/flightplans/flightplan.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,19 +105,6 @@ def best_speed_between_waypoints(
#
# 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
Expand Down Expand Up @@ -268,7 +255,9 @@ def startup_time(self) -> datetime:
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)
Expand Down
2 changes: 2 additions & 0 deletions game/ato/flightplans/flightplanbuildertypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
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
Expand Down Expand Up @@ -62,6 +63,7 @@ def for_flight(flight: Flight) -> Type[IBuilder[Any, Any]]:
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]
Expand Down
6 changes: 4 additions & 2 deletions game/ato/flightplans/formation.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,10 @@ def best_flight_formation_speed(self) -> Speed:
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
Expand Down
12 changes: 8 additions & 4 deletions game/ato/flightplans/formationattack.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

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
Expand Down Expand Up @@ -39,8 +39,9 @@ def speed_between_waypoints(self, a: FlightWaypoint, b: FlightWaypoint) -> Speed
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
Expand All @@ -53,7 +54,7 @@ def target_area_waypoint(self) -> FlightWaypoint:
"TARGET AREA",
FlightWaypointType.TARGET_GROUP_LOC,
self.package.target.position,
meters(0),
feet(0),
"RADIO",
)

Expand Down Expand Up @@ -192,6 +193,7 @@ def _build(
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)
Expand Down Expand Up @@ -286,6 +288,8 @@ def target_area_waypoint(
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)

Expand Down
57 changes: 37 additions & 20 deletions game/ato/flightplans/waypointbuilder.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import math
import random
from dataclasses import dataclass
from typing import (
Expand Down Expand Up @@ -252,18 +253,9 @@ def ingress(
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:
Expand Down Expand Up @@ -291,6 +283,19 @@ def ingress(
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:
Expand Down Expand Up @@ -354,6 +359,21 @@ def sead_area(self, target: MissionTarget) -> FlightWaypoint:
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)

Expand Down Expand Up @@ -398,15 +418,14 @@ def _target_area(
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(
Expand All @@ -415,7 +434,7 @@ def cas(self, position: Point) -> FlightWaypoint:
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",
Expand Down Expand Up @@ -667,14 +686,13 @@ def dropoff_zone(self, drop_off: MissionTarget) -> FlightWaypoint:
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",
Expand Down Expand Up @@ -772,8 +790,7 @@ def nav_point_prunable(self, previous: Point, current: Point, nxt: Point) -> boo
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)
7 changes: 4 additions & 3 deletions game/ato/flighttype.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -96,6 +95,7 @@ def is_air_to_ground(self) -> bool:
FlightType.SEAD_ESCORT,
FlightType.AIR_ASSAULT,
FlightType.SEAD_SWEEP,
FlightType.ARMED_RECON,
}

@property
Expand All @@ -107,6 +107,7 @@ def entity_type(self) -> AirEntity:
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,
Expand Down
1 change: 1 addition & 0 deletions game/ato/flightwaypointtype.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,4 @@ class FlightWaypointType(IntEnum):
INGRESS_AIR_ASSAULT = 31
INGRESS_ANTI_SHIP = 32
INGRESS_SEAD_SWEEP = 33
INGRESS_ARMED_RECON = 34
Loading

0 comments on commit 65e529a

Please sign in to comment.