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."""