diff --git a/changelog.md b/changelog.md index 5d5e763e..bc6c15fa 100644 --- a/changelog.md +++ b/changelog.md @@ -46,6 +46,7 @@ Saves from 2.5 are not compatible with 3.0. * **[Campaign]** Fixed bug where offshore strike locations were being used to spawn ship objectives. * **[Flight Planner]** AI strike flight plans now include the correct target actions for building groups. * **[Flight Planner]** AI BAI/DEAD/SEAD flights now have tasks to attack all groups at the target location, not just the primary group (for multi-group SAM sites). +* **[Flight Planner]** Fixed some contexts where damaged runways would be used. Destroying a carrier will no longer break the game. # 2.5.1 diff --git a/game/procurement.py b/game/procurement.py index 846b517f..95b00b38 100644 --- a/game/procurement.py +++ b/game/procurement.py @@ -258,11 +258,9 @@ class ProcurementAi: ) -> Iterator[ControlPoint]: distance_cache = ObjectiveDistanceCache.get_closest_airfields(request.near) threatened = [] - for cp in distance_cache.airfields_within(request.range): + for cp in distance_cache.operational_airfields_within(request.range): if not cp.is_friendly(self.is_player): continue - if not cp.runway_is_operational(): - continue if cp.unclaimed_parking(self.game) < request.number: continue if self.threat_zones.threatened(cp.position): diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index 05f35d72..ad316afc 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -517,7 +517,7 @@ class ControlPoint(MissionTarget, ABC): max_retreat_distance = nautical_miles(200) # Skip the first airbase because that's the airbase we're retreating # from. - airfields = list(closest.airfields_within(max_retreat_distance))[1:] + airfields = list(closest.operational_airfields_within(max_retreat_distance))[1:] for airbase in airfields: if not airbase.can_operate(airframe): continue diff --git a/game/threatzones.py b/game/threatzones.py index c7207a74..6118d4d5 100644 --- a/game/threatzones.py +++ b/game/threatzones.py @@ -124,7 +124,7 @@ class ThreatZones: cls, location: ControlPoint, max_distance: Distance ) -> Optional[ControlPoint]: airfields = ObjectiveDistanceCache.get_closest_airfields(location) - for airfield in airfields.airfields_within(max_distance): + for airfield in airfields.all_airfields_within(max_distance): if airfield.captured != location.captured: return airfield return None diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index 64126a33..fa7e9d3c 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -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 ) @@ -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: @@ -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): diff --git a/gen/flights/closestairfields.py b/gen/flights/closestairfields.py index 4d6bc4fb..4dd0032e 100644 --- a/gen/flights/closestairfields.py +++ b/gen/flights/closestairfields.py @@ -27,17 +27,35 @@ class ClosestAirfields: def operational_airfields(self) -> Iterator[ControlPoint]: return (c for c in self.closest_airfields if c.runway_is_operational()) - 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: diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index 94f7fe24..1d6fc982 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -1807,7 +1807,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,4 +1819,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")