mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Add AI planning for airlifts.
Downside to the current implementation is that whether or not transports that were purchased last turn will be available for airlift this turn is arbitrary. This is because transfers are created at the same time as units are delivered, and units are delivered in an arbitrary order per CP. If the helicopters are delivered before the ground units they'll have access to the transports, otherwise they'll be refunded. This will be fixed later when I rework the transfer requests to not require immediate fulfillment. https://github.com/Khopa/dcs_liberation/issues/825
This commit is contained in:
@@ -2,18 +2,28 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Optional, TYPE_CHECKING, Type
|
||||
|
||||
from dcs.unittype import UnitType, VehicleType
|
||||
|
||||
from game.theater import ControlPoint, SupplyRoute
|
||||
from gen.ato import Package
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from gen.flights.flight import Flight
|
||||
from .db import PRICES
|
||||
from .transfers import RoadTransferOrder
|
||||
from .transfers import AirliftOrder, AirliftPlanner, RoadTransferOrder
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .game import Game
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GroundUnitSource:
|
||||
control_point: ControlPoint
|
||||
requires_airlift: bool
|
||||
|
||||
|
||||
class PendingUnitDeliveries:
|
||||
def __init__(self, destination: ControlPoint) -> None:
|
||||
self.destination = destination
|
||||
@@ -59,6 +69,14 @@ class PendingUnitDeliveries:
|
||||
|
||||
def process(self, game: Game) -> None:
|
||||
ground_unit_source = self.find_ground_unit_source(game)
|
||||
if ground_unit_source is None:
|
||||
game.message(
|
||||
f"{self.destination.name} lost its source for ground unit "
|
||||
"reinforcements. Refunding purchase price."
|
||||
)
|
||||
self.refund_all(game)
|
||||
return
|
||||
|
||||
bought_units: Dict[Type[UnitType], int] = {}
|
||||
units_needing_transfer: Dict[Type[VehicleType], int] = {}
|
||||
sold_units: Dict[Type[UnitType], int] = {}
|
||||
@@ -68,25 +86,19 @@ class PendingUnitDeliveries:
|
||||
|
||||
if (
|
||||
issubclass(unit_type, VehicleType)
|
||||
and self.destination != ground_unit_source
|
||||
and self.destination != ground_unit_source.control_point
|
||||
):
|
||||
source = ground_unit_source
|
||||
source = ground_unit_source.control_point
|
||||
d = units_needing_transfer
|
||||
ground = True
|
||||
else:
|
||||
source = self.destination
|
||||
d = bought_units
|
||||
ground = False
|
||||
|
||||
if count >= 0:
|
||||
# The destination dict will be set appropriately even if we have no
|
||||
# source, and we'll refund later, buto nly emit the message when we're
|
||||
# actually completing the purchase.
|
||||
d[unit_type] = count
|
||||
if ground or ground_unit_source is not None:
|
||||
game.message(
|
||||
f"{coalition} reinforcements: {name} x {count} at {source}"
|
||||
)
|
||||
game.message(
|
||||
f"{coalition} reinforcements: {name} x {count} at {source}"
|
||||
)
|
||||
else:
|
||||
sold_units[unit_type] = -count
|
||||
game.message(f"{coalition} sold: {name} x {-count} at {source}")
|
||||
@@ -95,36 +107,70 @@ class PendingUnitDeliveries:
|
||||
self.destination.base.commision_units(bought_units)
|
||||
self.destination.base.commit_losses(sold_units)
|
||||
|
||||
if ground_unit_source is None:
|
||||
game.message(
|
||||
f"{self.destination.name} lost its source for ground unit "
|
||||
"reinforcements. Refunding purchase price."
|
||||
)
|
||||
self.refund(game, units_needing_transfer)
|
||||
return
|
||||
|
||||
if units_needing_transfer:
|
||||
ground_unit_source.base.commision_units(units_needing_transfer)
|
||||
game.transfers.new_transfer(
|
||||
RoadTransferOrder(
|
||||
ground_unit_source,
|
||||
self.destination,
|
||||
self.destination.captured,
|
||||
units_needing_transfer,
|
||||
)
|
||||
ground_unit_source.control_point.base.commision_units(
|
||||
units_needing_transfer
|
||||
)
|
||||
if ground_unit_source.requires_airlift:
|
||||
self.create_air_transfer(
|
||||
game, ground_unit_source.control_point, units_needing_transfer
|
||||
)
|
||||
else:
|
||||
self.create_road_transfer(
|
||||
game, ground_unit_source.control_point, units_needing_transfer
|
||||
)
|
||||
|
||||
def find_ground_unit_source(self, game: Game) -> Optional[ControlPoint]:
|
||||
def create_air_transfer(
|
||||
self, game: Game, source: ControlPoint, units: Dict[Type[VehicleType], int]
|
||||
) -> None:
|
||||
planner = AirliftPlanner(game, source, self.destination, units)
|
||||
leftovers = planner.create_package_for_airlift()
|
||||
if leftovers:
|
||||
game.message(
|
||||
f"No airlift capacity remaining for {self.destination}. "
|
||||
"Remaining unit orders were refunded."
|
||||
)
|
||||
self.refund(game, leftovers)
|
||||
source.base.commit_losses(leftovers)
|
||||
|
||||
def find_transport_for(
|
||||
self,
|
||||
origin: ControlPoint,
|
||||
destination: ControlPoint,
|
||||
units: Dict[Type[VehicleType], int],
|
||||
) -> Optional[Flight]:
|
||||
pass
|
||||
|
||||
def create_road_transfer(
|
||||
self, game: Game, source: ControlPoint, units: Dict[Type[VehicleType], int]
|
||||
) -> None:
|
||||
game.transfers.new_transfer(
|
||||
RoadTransferOrder(
|
||||
source, self.destination, self.destination.captured, units
|
||||
)
|
||||
)
|
||||
|
||||
def find_ground_unit_source(self, game: Game) -> Optional[GroundUnitSource]:
|
||||
# This is running *after* the turn counter has been incremented, so this is the
|
||||
# reaction to turn 0. On turn zero we allow units to be recruited anywhere for
|
||||
# delivery on turn 1 so that turn 1 always starts with units on the front line.
|
||||
if game.turn == 1:
|
||||
return self.destination
|
||||
return GroundUnitSource(self.destination, requires_airlift=False)
|
||||
|
||||
# Fast path if the destination is a valid source.
|
||||
if self.destination.can_recruit_ground_units(game):
|
||||
return self.destination
|
||||
return GroundUnitSource(self.destination, requires_airlift=False)
|
||||
|
||||
by_road = self.find_ground_unit_source_by_road(game)
|
||||
if by_road is not None:
|
||||
return GroundUnitSource(by_road, requires_airlift=False)
|
||||
|
||||
by_air = self.find_ground_unit_source_by_air(game)
|
||||
if by_air is not None:
|
||||
return GroundUnitSource(by_air, requires_airlift=True)
|
||||
return None
|
||||
|
||||
def find_ground_unit_source_by_road(self, game: Game) -> Optional[ControlPoint]:
|
||||
supply_route = SupplyRoute.for_control_point(self.destination)
|
||||
|
||||
sources = []
|
||||
@@ -149,3 +195,14 @@ class PendingUnitDeliveries:
|
||||
closest = source
|
||||
distance = new_distance
|
||||
return closest
|
||||
|
||||
def find_ground_unit_source_by_air(self, game: Game) -> Optional[ControlPoint]:
|
||||
closest_airfields = ObjectiveDistanceCache.get_closest_airfields(
|
||||
self.destination
|
||||
)
|
||||
for airfield in closest_airfields.operational_airfields:
|
||||
if airfield.is_friendly(
|
||||
self.destination.captured
|
||||
) and airfield.can_recruit_ground_units(game):
|
||||
return airfield
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user