mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Merge branch 'develop' into helipads
# Conflicts: # game/theater/conflicttheater.py # gen/flights/flightplan.py
This commit is contained in:
@@ -162,7 +162,7 @@ class AircraftAllocator:
|
||||
self, flight: ProposedFlight, task: FlightType
|
||||
) -> Optional[Tuple[ControlPoint, Squadron]]:
|
||||
types = aircraft_for_task(task)
|
||||
airfields_in_range = self.closest_airfields.airfields_within(
|
||||
airfields_in_range = self.closest_airfields.operational_airfields_within(
|
||||
flight.max_distance
|
||||
)
|
||||
|
||||
@@ -180,7 +180,7 @@ class AircraftAllocator:
|
||||
# Valid location with enough aircraft available. Find a squadron to fit
|
||||
# the role.
|
||||
for squadron in self.air_wing.squadrons_for(aircraft):
|
||||
if task not in squadron.mission_types:
|
||||
if task not in squadron.auto_assignable_mission_types:
|
||||
continue
|
||||
if len(squadron.available_pilots) >= flight.num_aircraft:
|
||||
inventory.remove_aircraft(aircraft, flight.num_aircraft)
|
||||
@@ -258,7 +258,9 @@ class PackageBuilder:
|
||||
self, aircraft: Type[FlyingType], arrival: ControlPoint
|
||||
) -> Optional[ControlPoint]:
|
||||
divert_limit = nautical_miles(150)
|
||||
for airfield in self.closest_airfields.airfields_within(divert_limit):
|
||||
for airfield in self.closest_airfields.operational_airfields_within(
|
||||
divert_limit
|
||||
):
|
||||
if airfield.captured != self.is_player:
|
||||
continue
|
||||
if airfield == arrival:
|
||||
@@ -433,7 +435,7 @@ class ObjectiveFinder:
|
||||
|
||||
is_building = isinstance(ground_object, BuildingGroundObject)
|
||||
is_fob = isinstance(enemy_cp, Fob)
|
||||
if is_building and is_fob and ground_object.airbase_group:
|
||||
if is_building and is_fob and ground_object.is_control_point:
|
||||
# This is the FOB structure itself. Can't be repaired or
|
||||
# targeted by the player, so shouldn't be targetable by the
|
||||
# AI.
|
||||
@@ -467,8 +469,10 @@ class ObjectiveFinder:
|
||||
# Off-map spawn locations don't need protection.
|
||||
continue
|
||||
airfields_in_proximity = self.closest_airfields_to(cp)
|
||||
airfields_in_threat_range = airfields_in_proximity.airfields_within(
|
||||
self.AIRFIELD_THREAT_RANGE
|
||||
airfields_in_threat_range = (
|
||||
airfields_in_proximity.operational_airfields_within(
|
||||
self.AIRFIELD_THREAT_RANGE
|
||||
)
|
||||
)
|
||||
for airfield in airfields_in_threat_range:
|
||||
if not airfield.is_friendly(self.is_player):
|
||||
@@ -502,31 +506,23 @@ class ObjectiveFinder:
|
||||
c for c in self.game.theater.controlpoints if c.is_friendly(self.is_player)
|
||||
)
|
||||
|
||||
def farthest_friendly_control_point(self) -> Optional[ControlPoint]:
|
||||
"""
|
||||
Iterates over all friendly control points and find the one farthest away from the frontline
|
||||
BUT! prefer Cvs. Everybody likes CVs!
|
||||
"""
|
||||
from_frontline = 0
|
||||
cp = None
|
||||
first_friendly_cp = None
|
||||
def farthest_friendly_control_point(self) -> ControlPoint:
|
||||
"""Finds the friendly control point that is farthest from any threats."""
|
||||
threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||
|
||||
for c in self.game.theater.controlpoints:
|
||||
if c.is_friendly(self.is_player):
|
||||
if first_friendly_cp is None:
|
||||
first_friendly_cp = c
|
||||
if c.is_carrier:
|
||||
return c
|
||||
if c.has_active_frontline:
|
||||
if c.distance_to(self.front_lines().__next__()) > from_frontline:
|
||||
from_frontline = c.distance_to(self.front_lines().__next__())
|
||||
cp = c
|
||||
farthest = None
|
||||
max_distance = meters(0)
|
||||
for cp in self.friendly_control_points():
|
||||
if isinstance(cp, OffMapSpawn):
|
||||
continue
|
||||
distance = threat_zones.distance_to_threat(cp.position)
|
||||
if distance > max_distance:
|
||||
farthest = cp
|
||||
max_distance = distance
|
||||
|
||||
# If no frontlines on the map, return the first friendly cp
|
||||
if cp is None:
|
||||
return first_friendly_cp
|
||||
else:
|
||||
return cp
|
||||
if farthest is None:
|
||||
raise RuntimeError("Found no friendly control points. You probably lost.")
|
||||
return farthest
|
||||
|
||||
def enemy_control_points(self) -> Iterator[ControlPoint]:
|
||||
"""Iterates over all enemy control points."""
|
||||
@@ -608,7 +604,7 @@ class CoalitionMissionPlanner:
|
||||
for squadron in self.game.air_wing_for(self.is_player).iter_squadrons():
|
||||
if (
|
||||
squadron.aircraft in all_compatible
|
||||
and mission_type in squadron.mission_types
|
||||
and mission_type in squadron.auto_assignable_mission_types
|
||||
):
|
||||
return True
|
||||
return False
|
||||
@@ -624,15 +620,13 @@ class CoalitionMissionPlanner:
|
||||
eliminated this turn.
|
||||
"""
|
||||
|
||||
# Find farthest, friendly CP for AEWC
|
||||
cp = self.objective_finder.farthest_friendly_control_point()
|
||||
if cp is not None:
|
||||
yield ProposedMission(
|
||||
cp,
|
||||
[ProposedFlight(FlightType.AEWC, 1, self.MAX_AWEC_RANGE)],
|
||||
# Supports all the early CAP flights, so should be in the air ASAP.
|
||||
asap=True,
|
||||
)
|
||||
# Find farthest, friendly CP for AEWC.
|
||||
yield ProposedMission(
|
||||
self.objective_finder.farthest_friendly_control_point(),
|
||||
[ProposedFlight(FlightType.AEWC, 1, self.MAX_AWEC_RANGE)],
|
||||
# Supports all the early CAP flights, so should be in the air ASAP.
|
||||
asap=True,
|
||||
)
|
||||
|
||||
# Find friendly CPs within 100 nmi from an enemy airfield, plan CAP.
|
||||
for cp in self.objective_finder.vulnerable_control_points():
|
||||
@@ -1012,7 +1006,7 @@ class CoalitionMissionPlanner:
|
||||
interval = (latest - earliest) // count
|
||||
for time in range(earliest, latest, interval):
|
||||
error = random.randint(-margin, margin)
|
||||
yield timedelta(minutes=max(0, time + error))
|
||||
yield timedelta(seconds=max(0, time + error))
|
||||
|
||||
dca_types = {
|
||||
FlightType.BARCAP,
|
||||
@@ -1026,11 +1020,11 @@ class CoalitionMissionPlanner:
|
||||
|
||||
start_time = start_time_generator(
|
||||
count=len(non_dca_packages),
|
||||
earliest=5,
|
||||
earliest=5 * 60,
|
||||
latest=int(
|
||||
self.game.settings.desired_player_mission_duration.total_seconds() / 60
|
||||
self.game.settings.desired_player_mission_duration.total_seconds()
|
||||
),
|
||||
margin=5,
|
||||
margin=5 * 60,
|
||||
)
|
||||
for package in self.ato.packages:
|
||||
tot = TotEstimator(package).earliest_tot()
|
||||
|
||||
@@ -31,17 +31,35 @@ class ClosestAirfields:
|
||||
if c.runway_is_operational() or c.has_helipads
|
||||
)
|
||||
|
||||
def airfields_within(self, distance: Distance) -> Iterator[ControlPoint]:
|
||||
def _airfields_within(
|
||||
self, distance: Distance, operational: bool
|
||||
) -> Iterator[ControlPoint]:
|
||||
airfields = (
|
||||
self.operational_airfields if operational else self.closest_airfields
|
||||
)
|
||||
for cp in airfields:
|
||||
if cp.distance_to(self.target) < distance.meters:
|
||||
yield cp
|
||||
else:
|
||||
break
|
||||
|
||||
def operational_airfields_within(
|
||||
self, distance: Distance
|
||||
) -> Iterator[ControlPoint]:
|
||||
"""Iterates over all airfields within the given range of the target.
|
||||
|
||||
Note that this iterates over *all* airfields, not just friendly
|
||||
airfields.
|
||||
"""
|
||||
for cp in self.closest_airfields:
|
||||
if cp.distance_to(self.target) < distance.meters:
|
||||
yield cp
|
||||
else:
|
||||
break
|
||||
return self._airfields_within(distance, operational=True)
|
||||
|
||||
def all_airfields_within(self, distance: Distance) -> Iterator[ControlPoint]:
|
||||
"""Iterates over all airfields within the given range of the target.
|
||||
|
||||
Note that this iterates over *all* airfields, not just friendly
|
||||
airfields.
|
||||
"""
|
||||
return self._airfields_within(distance, operational=False)
|
||||
|
||||
|
||||
class ObjectiveDistanceCache:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from enum import Enum
|
||||
from typing import List, Optional, TYPE_CHECKING, Type, Union
|
||||
@@ -202,6 +203,49 @@ class FlightWaypoint:
|
||||
return waypoint
|
||||
|
||||
|
||||
class FlightRoster:
|
||||
def __init__(self, squadron: Squadron, initial_size: int = 0) -> None:
|
||||
self.squadron = squadron
|
||||
self.pilots: list[Optional[Pilot]] = []
|
||||
self.resize(initial_size)
|
||||
|
||||
@property
|
||||
def max_size(self) -> int:
|
||||
return len(self.pilots)
|
||||
|
||||
@property
|
||||
def player_count(self) -> int:
|
||||
return len([p for p in self.pilots if p is not None and p.player])
|
||||
|
||||
@property
|
||||
def missing_pilots(self) -> int:
|
||||
return len([p for p in self.pilots if p is None])
|
||||
|
||||
def resize(self, new_size: int) -> None:
|
||||
if self.max_size > new_size:
|
||||
self.squadron.return_pilots(
|
||||
[p for p in self.pilots[new_size:] if p is not None]
|
||||
)
|
||||
self.pilots = self.pilots[:new_size]
|
||||
return
|
||||
self.pilots.extend(
|
||||
[
|
||||
self.squadron.claim_available_pilot()
|
||||
for _ in range(new_size - self.max_size)
|
||||
]
|
||||
)
|
||||
|
||||
def set_pilot(self, index: int, pilot: Optional[Pilot]) -> None:
|
||||
if pilot is not None:
|
||||
self.squadron.claim_pilot(pilot)
|
||||
if (current_pilot := self.pilots[index]) is not None:
|
||||
self.squadron.return_pilot(current_pilot)
|
||||
self.pilots[index] = pilot
|
||||
|
||||
def clear(self) -> None:
|
||||
self.squadron.return_pilots([p for p in self.pilots if p is not None])
|
||||
|
||||
|
||||
class Flight:
|
||||
def __init__(
|
||||
self,
|
||||
@@ -216,11 +260,15 @@ class Flight:
|
||||
divert: Optional[ControlPoint],
|
||||
custom_name: Optional[str] = None,
|
||||
cargo: Optional[TransferOrder] = None,
|
||||
roster: Optional[FlightRoster] = None,
|
||||
) -> None:
|
||||
self.package = package
|
||||
self.country = country
|
||||
self.squadron = squadron
|
||||
self.pilots = [squadron.claim_available_pilot() for _ in range(count)]
|
||||
if roster is None:
|
||||
self.roster = FlightRoster(self.squadron, initial_size=count)
|
||||
else:
|
||||
self.roster = roster
|
||||
self.departure = departure
|
||||
self.arrival = arrival
|
||||
self.divert = divert
|
||||
@@ -246,11 +294,11 @@ class Flight:
|
||||
|
||||
@property
|
||||
def count(self) -> int:
|
||||
return len(self.pilots)
|
||||
return self.roster.max_size
|
||||
|
||||
@property
|
||||
def client_count(self) -> int:
|
||||
return len([p for p in self.pilots if p is not None and p.player])
|
||||
return self.roster.player_count
|
||||
|
||||
@property
|
||||
def unit_type(self) -> Type[FlyingType]:
|
||||
@@ -265,32 +313,17 @@ class Flight:
|
||||
return self.flight_plan.waypoints[1:]
|
||||
|
||||
def resize(self, new_size: int) -> None:
|
||||
if self.count > new_size:
|
||||
self.squadron.return_pilots(
|
||||
p for p in self.pilots[new_size:] if p is not None
|
||||
)
|
||||
self.pilots = self.pilots[:new_size]
|
||||
return
|
||||
self.pilots.extend(
|
||||
[
|
||||
self.squadron.claim_available_pilot()
|
||||
for _ in range(new_size - self.count)
|
||||
]
|
||||
)
|
||||
self.roster.resize(new_size)
|
||||
|
||||
def set_pilot(self, index: int, pilot: Optional[Pilot]) -> None:
|
||||
if pilot is not None:
|
||||
self.squadron.claim_pilot(pilot)
|
||||
if (current_pilot := self.pilots[index]) is not None:
|
||||
self.squadron.return_pilot(current_pilot)
|
||||
self.pilots[index] = pilot
|
||||
self.roster.set_pilot(index, pilot)
|
||||
|
||||
@property
|
||||
def missing_pilots(self) -> int:
|
||||
return len([p for p in self.pilots if p is None])
|
||||
return self.roster.missing_pilots
|
||||
|
||||
def clear_roster(self) -> None:
|
||||
self.squadron.return_pilots([p for p in self.pilots if p is not None])
|
||||
self.roster.clear()
|
||||
|
||||
def __repr__(self):
|
||||
name = db.unit_type_name(self.unit_type)
|
||||
|
||||
@@ -1057,7 +1057,7 @@ class FlightPlanBuilder:
|
||||
"""
|
||||
location = self.package.target
|
||||
|
||||
start = self.aewc_orbit(location)
|
||||
orbit_location = self.aewc_orbit(location)
|
||||
|
||||
# As high as possible to maximize detection and on-station time.
|
||||
if flight.unit_type == E_2C:
|
||||
@@ -1072,22 +1072,22 @@ class FlightPlanBuilder:
|
||||
patrol_alt = feet(25000)
|
||||
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
start = builder.orbit(start, patrol_alt)
|
||||
orbit_location = builder.orbit(orbit_location, patrol_alt)
|
||||
|
||||
return AwacsFlightPlan(
|
||||
package=self.package,
|
||||
flight=flight,
|
||||
takeoff=builder.takeoff(flight.departure),
|
||||
nav_to=builder.nav_path(
|
||||
flight.departure.position, start.position, patrol_alt
|
||||
flight.departure.position, orbit_location.position, patrol_alt
|
||||
),
|
||||
nav_from=builder.nav_path(
|
||||
start.position, flight.arrival.position, patrol_alt
|
||||
orbit_location.position, flight.arrival.position, patrol_alt
|
||||
),
|
||||
land=builder.land(flight.arrival),
|
||||
divert=builder.divert(flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
hold=start,
|
||||
hold=orbit_location,
|
||||
hold_duration=timedelta(hours=4),
|
||||
)
|
||||
|
||||
@@ -1339,20 +1339,24 @@ class FlightPlanBuilder:
|
||||
return start, end
|
||||
|
||||
def aewc_orbit(self, location: MissionTarget) -> Point:
|
||||
# in threat zone
|
||||
closest_boundary = self.threat_zones.closest_boundary(location.position)
|
||||
heading_to_threat_boundary = location.position.heading_between_point(
|
||||
closest_boundary
|
||||
)
|
||||
distance_to_threat = meters(
|
||||
location.position.distance_to_point(closest_boundary)
|
||||
)
|
||||
orbit_heading = heading_to_threat_boundary
|
||||
# Station 100nm outside the threat zone.
|
||||
threat_buffer = nautical_miles(100)
|
||||
if self.threat_zones.threatened(location.position):
|
||||
# Borderpoint
|
||||
closest_boundary = self.threat_zones.closest_boundary(location.position)
|
||||
|
||||
# Heading + Distance to border point
|
||||
heading = location.position.heading_between_point(closest_boundary)
|
||||
distance = location.position.distance_to_point(closest_boundary)
|
||||
|
||||
return location.position.point_from_heading(heading, distance)
|
||||
|
||||
# this Part is fine. No threat zone, just use our point
|
||||
orbit_distance = distance_to_threat + threat_buffer
|
||||
else:
|
||||
return location.position
|
||||
orbit_distance = distance_to_threat - threat_buffer
|
||||
|
||||
return location.position.point_from_heading(
|
||||
orbit_heading, orbit_distance.meters
|
||||
)
|
||||
|
||||
def racetrack_for_frontline(
|
||||
self, origin: Point, front_line: FrontLine
|
||||
@@ -1807,7 +1811,7 @@ class FlightPlanBuilder:
|
||||
# We'll always have a package, but if this is being planned via the UI
|
||||
# it could be the first flight in the package.
|
||||
if not self.package.flights:
|
||||
raise RuntimeError(
|
||||
raise PlanningError(
|
||||
"Cannot determine source airfield for package with no flights"
|
||||
)
|
||||
|
||||
@@ -1819,5 +1823,4 @@ class FlightPlanBuilder:
|
||||
for flight in self.package.flights:
|
||||
if flight.departure == airfield:
|
||||
return airfield
|
||||
|
||||
raise RuntimeError("Could not find any airfield assigned to this package")
|
||||
raise PlanningError("Could not find any airfield assigned to this package")
|
||||
|
||||
Reference in New Issue
Block a user