From b5278550e7379106f686b4b79091953163d89d1a Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Fri, 1 Jan 2021 15:19:16 -0800 Subject: [PATCH] Retreat air units from captured bases. If there are not airbases withing ferry range with available parking for the aircraft then the aircraft will be captured and sold. Otherwise the aircraft will retreat to the closest available airbase. Fixes https://github.com/Khopa/dcs_liberation/issues/693 --- changelog.md | 2 +- game/theater/controlpoint.py | 57 +++++++++++++++++++++++++++++++-- gen/flights/closestairfields.py | 8 +++-- 3 files changed, 62 insertions(+), 5 deletions(-) diff --git a/changelog.md b/changelog.md index 8fa9a4c1..6b27fd95 100644 --- a/changelog.md +++ b/changelog.md @@ -14,7 +14,7 @@ Saves from 2.3 are not compatible with 2.4. * **[Mission Generator]** Multiple groups are created for complex SAM sites (SAMs with additional point defense or SHORADS), improving Skynet behavior. * **[Skynet]** Point defenses are now configured to remain on to protect the site they accompany. * **[Balance]** Opfor now gains income using the same rules as the player, significantly increasing their income relative to the player for most campaigns. -* **[Balance]** Ground units now retreat from captured bases when they are connected to a friendly base. Units with no retreat path will be captured and sold. +* **[Balance]** Units now retreat from captured bases when able. Units with no retreat path will be captured and sold. * **[Economy]** FOBs generate only $10M per turn (previously $20M like airbases). * **[Economy]** Carriers and off-map spawns generate no income (previously $20M like airbases). * **[UI]** Multi-SAM objectives now show threat and detection rings per group. diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index 57a0de52..12745c6c 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -22,6 +22,7 @@ from dcs.terrain.terrain import Airport, ParkingSlot from dcs.unittype import FlyingType from game import db +from gen.flights.closestairfields import ObjectiveDistanceCache from gen.ground_forces.ai_ground_planner_db import TYPE_SHORAD from gen.ground_forces.combat_stance import CombatStance from gen.runways import RunwayAssigner, RunwayData @@ -35,6 +36,8 @@ from .theatergroundobject import ( TheaterGroundObject, VehicleGroupGroundObject, ) +from ..db import PRICES +from ..utils import nautical_miles from ..weather import Conditions if TYPE_CHECKING: @@ -416,10 +419,62 @@ class ControlPoint(MissionTarget, ABC): destination.control_point.base.commision_units({unit_type: 1}) destination = heapq.heappushpop(destinations, destination) + def capture_aircraft(self, game: Game, airframe: Type[FlyingType], + count: int) -> None: + try: + value = PRICES[airframe] * count + except KeyError: + logging.exception(f"Unknown price for {airframe.id}") + return + + game.adjust_budget(value, player=not self.captured) + game.message( + f"No valid retreat destination in range of {self.name} for " + f"{airframe.id}. {count} aircraft have been captured and sold for " + f"${value}M.") + + def aircraft_retreat_destination( + self, game: Game, + airframe: Type[FlyingType]) -> Optional[ControlPoint]: + closest = ObjectiveDistanceCache.get_closest_airfields(self) + # TODO: Should be airframe dependent. + 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:] + for airbase in airfields: + if not airbase.can_operate(airframe): + continue + if airbase.captured != self.captured: + continue + if airbase.unclaimed_parking(game) > 0: + return airbase + return None + + def _retreat_air_units(self, game: Game, airframe: Type[FlyingType], + count: int) -> None: + while count: + logging.debug(f"Retreating {count} {airframe.id} from {self.name}") + destination = self.aircraft_retreat_destination(game, airframe) + if destination is None: + self.capture_aircraft(game, airframe, count) + return + parking = destination.unclaimed_parking(game) + transfer_amount = min([parking, count]) + destination.base.commision_units({airframe: transfer_amount}) + count -= transfer_amount + + def retreat_air_units(self, game: Game) -> None: + # TODO: Capture in order of price to retain maximum value? + while self.base.aircraft: + airframe, count = self.base.aircraft.popitem() + self._retreat_air_units(game, airframe, count) + # TODO: Should be Airbase specific. def capture(self, game: Game, for_player: bool) -> None: self.pending_unit_deliveries.refund_all(game) self.retreat_ground_units(game) + self.retreat_air_units(game) if for_player: self.captured = True @@ -428,8 +483,6 @@ class ControlPoint(MissionTarget, ABC): self.base.set_strength_to_minimum() - self.base.aircraft = {} - self.clear_base_defenses() from .start_generator import BaseDefenseGenerator BaseDefenseGenerator(game, self).generate() diff --git a/gen/flights/closestairfields.py b/gen/flights/closestairfields.py index 0708d5c2..fb3e84dd 100644 --- a/gen/flights/closestairfields.py +++ b/gen/flights/closestairfields.py @@ -1,9 +1,13 @@ """Objective adjacency lists.""" -from typing import Dict, Iterator, List, Optional +from __future__ import annotations + +from typing import Dict, Iterator, List, Optional, TYPE_CHECKING -from game.theater import ConflictTheater, ControlPoint, MissionTarget from game.utils import Distance +if TYPE_CHECKING: + from game.theater import ConflictTheater, ControlPoint, MissionTarget + class ClosestAirfields: """Precalculates which control points are closes to the given target."""