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
This commit is contained in:
Dan Albert 2021-01-01 15:19:16 -08:00
parent 461635c001
commit b5278550e7
3 changed files with 62 additions and 5 deletions

View File

@ -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. * **[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. * **[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]** 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]** FOBs generate only $10M per turn (previously $20M like airbases).
* **[Economy]** Carriers and off-map spawns generate no income (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. * **[UI]** Multi-SAM objectives now show threat and detection rings per group.

View File

@ -22,6 +22,7 @@ from dcs.terrain.terrain import Airport, ParkingSlot
from dcs.unittype import FlyingType from dcs.unittype import FlyingType
from game import db 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.ai_ground_planner_db import TYPE_SHORAD
from gen.ground_forces.combat_stance import CombatStance from gen.ground_forces.combat_stance import CombatStance
from gen.runways import RunwayAssigner, RunwayData from gen.runways import RunwayAssigner, RunwayData
@ -35,6 +36,8 @@ from .theatergroundobject import (
TheaterGroundObject, TheaterGroundObject,
VehicleGroupGroundObject, VehicleGroupGroundObject,
) )
from ..db import PRICES
from ..utils import nautical_miles
from ..weather import Conditions from ..weather import Conditions
if TYPE_CHECKING: if TYPE_CHECKING:
@ -416,10 +419,62 @@ class ControlPoint(MissionTarget, ABC):
destination.control_point.base.commision_units({unit_type: 1}) destination.control_point.base.commision_units({unit_type: 1})
destination = heapq.heappushpop(destinations, destination) 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. # TODO: Should be Airbase specific.
def capture(self, game: Game, for_player: bool) -> None: def capture(self, game: Game, for_player: bool) -> None:
self.pending_unit_deliveries.refund_all(game) self.pending_unit_deliveries.refund_all(game)
self.retreat_ground_units(game) self.retreat_ground_units(game)
self.retreat_air_units(game)
if for_player: if for_player:
self.captured = True self.captured = True
@ -428,8 +483,6 @@ class ControlPoint(MissionTarget, ABC):
self.base.set_strength_to_minimum() self.base.set_strength_to_minimum()
self.base.aircraft = {}
self.clear_base_defenses() self.clear_base_defenses()
from .start_generator import BaseDefenseGenerator from .start_generator import BaseDefenseGenerator
BaseDefenseGenerator(game, self).generate() BaseDefenseGenerator(game, self).generate()

View File

@ -1,9 +1,13 @@
"""Objective adjacency lists.""" """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 from game.utils import Distance
if TYPE_CHECKING:
from game.theater import ConflictTheater, ControlPoint, MissionTarget
class ClosestAirfields: class ClosestAirfields:
"""Precalculates which control points are closes to the given target.""" """Precalculates which control points are closes to the given target."""