diff --git a/changelog.md b/changelog.md index 797cf2f4..cd883fb5 100644 --- a/changelog.md +++ b/changelog.md @@ -19,6 +19,7 @@ * **[Campaign Management]** Improve squadron retreat logic to account for parking-slot sizes * **[Autoplanner]** Support for auto-planning Air Assaults * **[UI]** Improved frequency selector to support all modeled bands for every aircraft's intra-flight radio +* **[Options]** New options in Settings: Helicopter waypoint altitude (feet AGL) for combat & cruise waypoints ## Fixes * **[Mission Generation]** Anti-ship strikes should use "group attack" in their attack-task diff --git a/game/ato/flightplans/aewc.py b/game/ato/flightplans/aewc.py index fef75af0..d2f3e495 100644 --- a/game/ato/flightplans/aewc.py +++ b/game/ato/flightplans/aewc.py @@ -49,7 +49,7 @@ class Builder(IBuilder[AewcFlightPlan, PatrollingLayout]): # Station 80nm outside the threat zone. threat_buffer = nautical_miles( - self.flight.coalition.game.settings.aewc_threat_buffer_min_distance + self.coalition.game.settings.aewc_threat_buffer_min_distance ) if self.threat_zones.threatened(location.position): orbit_distance = distance_to_threat + threat_buffer @@ -68,7 +68,7 @@ class Builder(IBuilder[AewcFlightPlan, PatrollingLayout]): orbit_heading.left.degrees, racetrack_half_distance ) - builder = WaypointBuilder(self.flight, self.coalition) + builder = WaypointBuilder(self.flight) if self.flight.unit_type.patrol_altitude is not None: altitude = self.flight.unit_type.patrol_altitude diff --git a/game/ato/flightplans/airassault.py b/game/ato/flightplans/airassault.py index 19b5b23c..ace2850e 100644 --- a/game/ato/flightplans/airassault.py +++ b/game/ato/flightplans/airassault.py @@ -105,10 +105,11 @@ class Builder(FormationAttackBuilder[AirAssaultFlightPlan, AirAssaultLayout]): ) assert self.package.waypoints is not None - altitude = feet(1500) if self.flight.is_helo else self.doctrine.ingress_altitude + heli_alt = feet(self.coalition.game.settings.heli_cruise_alt_agl) + altitude = heli_alt if self.flight.is_helo else self.doctrine.ingress_altitude altitude_is_agl = self.flight.is_helo - builder = WaypointBuilder(self.flight, self.coalition) + builder = WaypointBuilder(self.flight) if self.flight.is_hercules or self.flight.departure.cptype in [ ControlPointType.AIRCRAFT_CARRIER_GROUP, diff --git a/game/ato/flightplans/airlift.py b/game/ato/flightplans/airlift.py index 73e5ccbb..1ec88736 100644 --- a/game/ato/flightplans/airlift.py +++ b/game/ato/flightplans/airlift.py @@ -110,10 +110,11 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]): "Cannot plan transport mission for flight with no cargo." ) - altitude = feet(1500) - altitude_is_agl = True + heli_alt = feet(self.coalition.game.settings.heli_cruise_alt_agl) + altitude = heli_alt if self.flight.is_helo else self.doctrine.ingress_altitude + altitude_is_agl = self.flight.is_helo - builder = WaypointBuilder(self.flight, self.coalition) + builder = WaypointBuilder(self.flight) pickup = None drop_off = None diff --git a/game/ato/flightplans/barcap.py b/game/ato/flightplans/barcap.py index 615dcf48..9e59fb65 100644 --- a/game/ato/flightplans/barcap.py +++ b/game/ato/flightplans/barcap.py @@ -48,7 +48,7 @@ class Builder(CapBuilder[BarCapFlightPlan, PatrollingLayout]): min(self.doctrine.max_patrol_altitude, randomized_alt), ) - builder = WaypointBuilder(self.flight, self.coalition) + builder = WaypointBuilder(self.flight) start, end = builder.race_track(start_pos, end_pos, patrol_alt) return PatrollingLayout( diff --git a/game/ato/flightplans/cas.py b/game/ato/flightplans/cas.py index 275d1b09..973ba23e 100644 --- a/game/ato/flightplans/cas.py +++ b/game/ato/flightplans/cas.py @@ -6,7 +6,7 @@ from datetime import timedelta from typing import TYPE_CHECKING, Type from game.theater import FrontLine -from game.utils import Distance, Speed, kph, meters, nautical_miles +from game.utils import Distance, Speed, kph, feet, nautical_miles from .ibuilder import IBuilder from .invalidobjectivelocation import InvalidObjectiveLocation from .patrolling import PatrollingFlightPlan, PatrollingLayout @@ -97,11 +97,13 @@ class Builder(IBuilder[CasFlightPlan, CasLayout]): if egress_distance < ingress_distance: ingress, egress = egress, ingress - builder = WaypointBuilder(self.flight, self.coalition) + builder = WaypointBuilder(self.flight) is_helo = self.flight.unit_type.dcs_unit_type.helicopter ingress_egress_altitude = ( - self.doctrine.ingress_altitude if not is_helo else meters(50) + self.doctrine.ingress_altitude + if not is_helo + else feet(self.coalition.game.settings.heli_combat_alt_agl) ) use_agl_ingress_egress = is_helo diff --git a/game/ato/flightplans/custom.py b/game/ato/flightplans/custom.py index 54c5a2bd..36b5a336 100644 --- a/game/ato/flightplans/custom.py +++ b/game/ato/flightplans/custom.py @@ -69,7 +69,7 @@ class Builder(IBuilder[CustomFlightPlan, CustomLayout]): self.waypoints = waypoints def layout(self) -> CustomLayout: - builder = WaypointBuilder(self.flight, self.coalition) + builder = WaypointBuilder(self.flight) return CustomLayout(builder.takeoff(self.flight.departure), self.waypoints) def build(self) -> CustomFlightPlan: diff --git a/game/ato/flightplans/escort.py b/game/ato/flightplans/escort.py index b6961a85..f2a8824b 100644 --- a/game/ato/flightplans/escort.py +++ b/game/ato/flightplans/escort.py @@ -39,7 +39,7 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]): def layout(self) -> FormationAttackLayout: assert self.package.waypoints is not None - builder = WaypointBuilder(self.flight, self.coalition) + builder = WaypointBuilder(self.flight) ingress, target = builder.escort( self.package.waypoints.ingress, self.package.target ) @@ -58,11 +58,11 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]): split = builder.split(self.package.waypoints.split) ingress_alt = self.doctrine.ingress_altitude + is_helo = builder.flight.is_helo + heli_alt = feet(self.coalition.game.settings.heli_combat_alt_agl) initial = builder.escort_hold( - target.position - if builder.flight.is_helo - else self.package.waypoints.initial, - min(feet(500), ingress_alt) if builder.flight.is_helo else ingress_alt, + target.position if is_helo else self.package.waypoints.initial, + min(heli_alt, ingress_alt) if is_helo else ingress_alt, ) pf = self.package.primary_flight diff --git a/game/ato/flightplans/ferry.py b/game/ato/flightplans/ferry.py index 93579fba..b694def7 100644 --- a/game/ato/flightplans/ferry.py +++ b/game/ato/flightplans/ferry.py @@ -56,14 +56,14 @@ class Builder(IBuilder[FerryFlightPlan, FerryLayout]): f"{self.flight.departure}" ) - altitude_is_agl = self.flight.unit_type.dcs_unit_type.helicopter + altitude_is_agl = self.flight.is_helo altitude = ( - feet(1500) + feet(self.coalition.game.settings.heli_cruise_alt_agl) if altitude_is_agl else self.flight.unit_type.preferred_patrol_altitude ) - builder = WaypointBuilder(self.flight, self.coalition) + builder = WaypointBuilder(self.flight) return FerryLayout( departure=builder.takeoff(self.flight.departure), nav_to=builder.nav_path( diff --git a/game/ato/flightplans/formationattack.py b/game/ato/flightplans/formationattack.py index f1fb47e6..6d4c871d 100644 --- a/game/ato/flightplans/formationattack.py +++ b/game/ato/flightplans/formationattack.py @@ -10,7 +10,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 Speed, meters, nautical_miles, feet from .flightplan import FlightPlan from .formation import FormationFlightPlan, FormationLayout from .ibuilder import IBuilder @@ -170,7 +170,7 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC): targets: list[StrikeTarget] | None = None, ) -> FormationAttackLayout: assert self.package.waypoints is not None - builder = WaypointBuilder(self.flight, self.coalition, targets) + builder = WaypointBuilder(self.flight, targets) target_waypoints: list[FlightWaypoint] = [] if targets is not None: @@ -209,13 +209,22 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC): pos = ingress.position.point_from_heading(hdg, nautical_miles(10).meters) lineup = builder.nav(pos, self.flight.coalition.doctrine.ingress_altitude) + is_helo = self.flight.is_helo + ingress_egress_altitude = ( + self.doctrine.ingress_altitude + if not is_helo + else feet(self.coalition.game.settings.heli_combat_alt_agl) + ) + use_agl_ingress_egress = is_helo + return FormationAttackLayout( departure=builder.takeoff(self.flight.departure), hold=hold, nav_to=builder.nav_path( hold.position if hold else self.flight.departure.position, join.position if join else ingress.position, - self.doctrine.ingress_altitude, + ingress_egress_altitude, + use_agl_ingress_egress, ), join=join, lineup=lineup, @@ -227,7 +236,8 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC): nav_from=builder.nav_path( refuel.position if refuel else split.position, self.flight.arrival.position, - self.doctrine.ingress_altitude, + ingress_egress_altitude, + use_agl_ingress_egress, ), arrival=builder.land(self.flight.arrival), divert=builder.divert(self.flight.divert), diff --git a/game/ato/flightplans/packagerefueling.py b/game/ato/flightplans/packagerefueling.py index e59e130c..34317959 100644 --- a/game/ato/flightplans/packagerefueling.py +++ b/game/ato/flightplans/packagerefueling.py @@ -96,7 +96,7 @@ class Builder(IBuilder[PackageRefuelingFlightPlan, PatrollingLayout]): home_heading.degrees, racetrack_half_distance ) - builder = WaypointBuilder(self.flight, self.coalition) + builder = WaypointBuilder(self.flight) tanker_type = self.flight.unit_type if tanker_type.patrol_altitude is not None: diff --git a/game/ato/flightplans/rtb.py b/game/ato/flightplans/rtb.py index 291d575a..663c3624 100644 --- a/game/ato/flightplans/rtb.py +++ b/game/ato/flightplans/rtb.py @@ -61,13 +61,13 @@ class Builder(IBuilder[RtbFlightPlan, RtbLayout]): current_position = self.flight.state.estimate_position() current_altitude, altitude_reference = self.flight.state.estimate_altitude() - altitude_is_agl = self.flight.unit_type.dcs_unit_type.helicopter + altitude_is_agl = self.flight.is_helo altitude = ( - feet(1500) + feet(self.coalition.game.settings.heli_cruise_alt_agl) if altitude_is_agl else self.flight.unit_type.preferred_patrol_altitude ) - builder = WaypointBuilder(self.flight, self.flight.coalition) + builder = WaypointBuilder(self.flight) abort_point = builder.nav( current_position, current_altitude, altitude_reference == "RADIO" ) diff --git a/game/ato/flightplans/sweep.py b/game/ato/flightplans/sweep.py index d178cf7f..e039d0da 100644 --- a/game/ato/flightplans/sweep.py +++ b/game/ato/flightplans/sweep.py @@ -101,7 +101,7 @@ class Builder(IBuilder[SweepFlightPlan, SweepLayout]): heading.degrees, -self.doctrine.sweep_distance.meters ) - builder = WaypointBuilder(self.flight, self.coalition) + builder = WaypointBuilder(self.flight) start, end = builder.sweep(start_pos, target, self.doctrine.ingress_altitude) hold = builder.hold(self._hold_point()) diff --git a/game/ato/flightplans/tarcap.py b/game/ato/flightplans/tarcap.py index 880c7e29..9f8689f8 100644 --- a/game/ato/flightplans/tarcap.py +++ b/game/ato/flightplans/tarcap.py @@ -102,7 +102,7 @@ class Builder(CapBuilder[TarCapFlightPlan, TarCapLayout]): min(self.doctrine.max_patrol_altitude, randomized_alt), ) - builder = WaypointBuilder(self.flight, self.coalition) + builder = WaypointBuilder(self.flight) orbit0p, orbit1p = self.cap_racetrack_for_objective(location, barcap=False) start, end = builder.race_track(orbit0p, orbit1p, patrol_alt) diff --git a/game/ato/flightplans/theaterrefueling.py b/game/ato/flightplans/theaterrefueling.py index d9d151bc..bdc120b5 100644 --- a/game/ato/flightplans/theaterrefueling.py +++ b/game/ato/flightplans/theaterrefueling.py @@ -37,7 +37,7 @@ class Builder(IBuilder[TheaterRefuelingFlightPlan, PatrollingLayout]): # Station 70nm outside the threat zone. threat_buffer = nautical_miles( - self.flight.coalition.game.settings.tanker_threat_buffer_min_distance + self.coalition.game.settings.tanker_threat_buffer_min_distance ) if self.threat_zones.threatened(location.position): orbit_distance = distance_to_threat + threat_buffer @@ -56,7 +56,7 @@ class Builder(IBuilder[TheaterRefuelingFlightPlan, PatrollingLayout]): orbit_heading.left.degrees, racetrack_half_distance ) - builder = WaypointBuilder(self.flight, self.coalition) + builder = WaypointBuilder(self.flight) tanker_type = self.flight.unit_type if tanker_type.patrol_altitude is not None: diff --git a/game/ato/flightplans/waypointbuilder.py b/game/ato/flightplans/waypointbuilder.py index 9cd843b2..f60962e9 100644 --- a/game/ato/flightplans/waypointbuilder.py +++ b/game/ato/flightplans/waypointbuilder.py @@ -29,7 +29,6 @@ from game.theater.interfaces.CTLD import CTLD from game.utils import Distance, meters, nautical_miles, feet if TYPE_CHECKING: - from game.coalition import Coalition from game.transfers import MultiGroupTransport from game.theater.theatergroup import TheaterGroup from game.ato.flight import Flight @@ -45,9 +44,9 @@ class WaypointBuilder: def __init__( self, flight: Flight, - coalition: Coalition, targets: Optional[List[StrikeTarget]] = None, ) -> None: + coalition = flight.coalition self.flight = flight self.doctrine = coalition.doctrine self.threat_zones = coalition.opponent.threat_zone @@ -75,7 +74,9 @@ class WaypointBuilder: "NAV", FlightWaypointType.NAV, position, - meters(500) if self.is_helo else self.doctrine.rendezvous_altitude, + feet(self.flight.coalition.game.settings.heli_cruise_alt_agl) + if self.is_helo + else self.doctrine.rendezvous_altitude, description="Enter theater", pretty_name="Enter theater", ) @@ -102,7 +103,9 @@ class WaypointBuilder: "NAV", FlightWaypointType.NAV, position, - meters(500) if self.is_helo else self.doctrine.rendezvous_altitude, + feet(self.flight.coalition.game.settings.heli_cruise_alt_agl) + if self.is_helo + else self.doctrine.rendezvous_altitude, description="Exit theater", pretty_name="Exit theater", ) @@ -131,7 +134,9 @@ class WaypointBuilder: altitude_type: AltitudeReference if isinstance(divert, OffMapSpawn): altitude = ( - meters(500) if self.is_helo else self.doctrine.rendezvous_altitude + feet(self.flight.coalition.game.settings.heli_cruise_alt_agl) + if self.is_helo + else self.doctrine.rendezvous_altitude ) altitude_type = "BARO" else: @@ -170,7 +175,9 @@ class WaypointBuilder: "HOLD", FlightWaypointType.LOITER, position, - feet(1000) if self.is_helo else self.doctrine.rendezvous_altitude, + feet(self.flight.coalition.game.settings.heli_cruise_alt_agl) + if self.is_helo + else self.doctrine.ingress_altitude, alt_type, description="Wait until push time", pretty_name="Hold", @@ -185,7 +192,9 @@ class WaypointBuilder: "JOIN", FlightWaypointType.JOIN, position, - meters(80) if self.is_helo else self.doctrine.ingress_altitude, + feet(self.flight.coalition.game.settings.heli_cruise_alt_agl) + if self.is_helo + else self.doctrine.ingress_altitude, alt_type, description="Rendezvous with package", pretty_name="Join", @@ -200,7 +209,9 @@ class WaypointBuilder: "REFUEL", FlightWaypointType.REFUEL, position, - meters(80) if self.is_helo else self.doctrine.ingress_altitude, + feet(self.flight.coalition.game.settings.heli_cruise_alt_agl) + if self.is_helo + else self.doctrine.ingress_altitude, alt_type, description="Refuel from tanker", pretty_name="Refuel", @@ -215,7 +226,9 @@ class WaypointBuilder: "SPLIT", FlightWaypointType.SPLIT, position, - meters(80) if self.is_helo else self.doctrine.ingress_altitude, + feet(self.flight.coalition.game.settings.heli_combat_alt_agl) + if self.is_helo + else self.doctrine.ingress_altitude, alt_type, description="Depart from package", pretty_name="Split", @@ -231,7 +244,11 @@ class WaypointBuilder: alt_type: AltitudeReference = "BARO" if self.is_helo or self.flight.is_hercules: alt_type = "RADIO" - alt = meters(60) if self.is_helo else feet(1000) + alt = ( + feet(self.flight.coalition.game.settings.heli_combat_alt_agl) + if self.is_helo + else feet(1000) + ) return FlightWaypoint( "INGRESS", @@ -253,7 +270,9 @@ class WaypointBuilder: "EGRESS", FlightWaypointType.EGRESS, position, - meters(60) if self.is_helo else self.doctrine.ingress_altitude, + feet(self.flight.coalition.game.settings.heli_combat_alt_agl) + if self.is_helo + else self.doctrine.ingress_altitude, alt_type, description=f"EGRESS from {target.name}", pretty_name=f"EGRESS from {target.name}", @@ -350,7 +369,9 @@ class WaypointBuilder: "CAS", FlightWaypointType.CAS, position, - meters(60) if self.is_helo else meters(1000), + feet(self.flight.coalition.game.settings.heli_combat_alt_agl) + if self.is_helo + else meters(1000), "RADIO", description="Provide CAS", pretty_name="CAS", @@ -430,7 +451,9 @@ class WaypointBuilder: "SEAD Search", FlightWaypointType.NAV, hold, - self.doctrine.ingress_altitude, + feet(self.flight.coalition.game.settings.heli_combat_alt_agl) + if self.is_helo + else self.doctrine.ingress_altitude, alt_type="BARO", description="Anchor and search from this point", pretty_name="SEAD Search", @@ -443,7 +466,9 @@ class WaypointBuilder: "SEAD Sweep", FlightWaypointType.NAV, hold, - self.doctrine.ingress_altitude, + feet(self.flight.coalition.game.settings.heli_combat_alt_agl) + if self.is_helo + else self.doctrine.ingress_altitude, alt_type="BARO", description="Anchor and search from this point", pretty_name="SEAD Sweep", @@ -565,7 +590,9 @@ class WaypointBuilder: "TARGET", FlightWaypointType.TARGET_GROUP_LOC, target.position, - meters(60) if self.is_helo else self.doctrine.ingress_altitude, + feet(self.flight.coalition.game.settings.heli_combat_alt_agl) + if self.is_helo + else self.doctrine.ingress_altitude, alt_type, description="Escort the package", pretty_name="Target area", diff --git a/game/settings/settings.py b/game/settings/settings.py index 52cf9dd8..6d0ba745 100644 --- a/game/settings/settings.py +++ b/game/settings/settings.py @@ -255,6 +255,33 @@ class Settings: "within threatened airspace." ), ) + heli_combat_alt_agl: int = bounded_int_option( + "Helicopter combat altitude (feet AGL)", + page=CAMPAIGN_DOCTRINE_PAGE, + section=GENERAL_SECTION, + default=200, + min=1, + max=10000, + detail=( + "Altitude for helicopters in feet AGL while flying between combat waypoints." + " Combat waypoints are considered INGRESS, CAS, TGT, EGRESS & SPLIT." + " In campaigns in more mountainous areas, you might want to increase this " + "setting to avoid the AI flying into the terrain." + ), + ) + heli_cruise_alt_agl: int = bounded_int_option( + "Helicopter cruise altitude (feet AGL)", + page=CAMPAIGN_DOCTRINE_PAGE, + section=GENERAL_SECTION, + default=500, + min=1, + max=10000, + detail=( + "Altitude for helicopters in feet AGL while flying between non-combat waypoints." + " In campaigns in more mountainous areas, you might want to increase this " + "setting to avoid the AI flying into the terrain." + ), + ) airbase_threat_range: int = bounded_int_option( "Airbase threat range (nmi)", page=CAMPAIGN_DOCTRINE_PAGE, diff --git a/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py b/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py index a9a3b103..a2473e39 100644 --- a/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py +++ b/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py @@ -84,7 +84,9 @@ class PilotSelector(QComboBox): self.roster.set_pilot(self.pilot_index, pilot) self.available_pilots_changed.emit() - def replace(self, squadron: Optional[Squadron], new_roster: Optional[FlightRoster]) -> None: + def replace( + self, squadron: Optional[Squadron], new_roster: Optional[FlightRoster] + ) -> None: self.squadron = squadron self.roster = new_roster self.rebuild() @@ -159,7 +161,9 @@ class PilotControls(QHBoxLayout): finally: self.player_checkbox.blockSignals(False) - def replace(self, squadron: Optional[Squadron], new_roster: Optional[FlightRoster]) -> None: + def replace( + self, squadron: Optional[Squadron], new_roster: Optional[FlightRoster] + ) -> None: self.roster = new_roster if self.roster is None or self.pilot_index >= self.roster.max_size: self.disable_and_clear() diff --git a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py index a288a457..3004d6e6 100644 --- a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py +++ b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py @@ -194,7 +194,7 @@ class QFlightWaypointTab(QFrame): self.on_change() def on_rtb_waypoint(self): - rtb = WaypointBuilder(self.flight, self.coalition).land(self.flight.arrival) + rtb = WaypointBuilder(self.flight).land(self.flight.arrival) self.degrade_to_custom_flight_plan() assert isinstance(self.flight.flight_plan, CustomFlightPlan) self.flight.flight_plan.layout.custom_waypoints.append(rtb)