Merge branch 'develop' into helipads

# Conflicts:
#	game/theater/conflicttheater.py
#	gen/flights/flightplan.py
This commit is contained in:
Khopa
2021-06-06 15:46:30 +02:00
103 changed files with 1992 additions and 3914 deletions

View File

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

View File

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

View File

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

View File

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