mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
157 lines
5.6 KiB
Python
157 lines
5.6 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
from collections import defaultdict
|
|
from datetime import datetime
|
|
from typing import Optional, TYPE_CHECKING
|
|
|
|
from game.theater import ControlPoint
|
|
from .coalition import Coalition
|
|
from .dcs.groundunittype import GroundUnitType
|
|
from .theater.transitnetwork import (
|
|
NoPathError,
|
|
TransitNetwork,
|
|
)
|
|
from .transfers import TransferOrder
|
|
|
|
if TYPE_CHECKING:
|
|
from .game import Game
|
|
|
|
|
|
class GroundUnitOrders:
|
|
def __init__(self, destination: ControlPoint) -> None:
|
|
self.destination = destination
|
|
|
|
# Maps unit type to order quantity.
|
|
self.units: dict[GroundUnitType, int] = defaultdict(int)
|
|
|
|
def __str__(self) -> str:
|
|
return f"Pending ground unit delivery to {self.destination}"
|
|
|
|
def order(self, units: dict[GroundUnitType, int]) -> None:
|
|
for k, v in units.items():
|
|
self.units[k] += v
|
|
|
|
def sell(self, units: dict[GroundUnitType, int]) -> None:
|
|
for k, v in units.items():
|
|
self.units[k] -= v
|
|
if self.units[k] == 0:
|
|
del self.units[k]
|
|
|
|
def refund_all(self, coalition: Coalition) -> None:
|
|
self._refund(coalition, self.units)
|
|
self.units = defaultdict(int)
|
|
|
|
def _refund(self, coalition: Coalition, units: dict[GroundUnitType, int]) -> None:
|
|
for unit_type, count in units.items():
|
|
logging.info(f"Refunding {count} {unit_type} at {self.destination.name}")
|
|
coalition.adjust_budget(unit_type.price * count)
|
|
|
|
def pending_orders(self, unit_type: GroundUnitType) -> int:
|
|
pending_units = self.units.get(unit_type)
|
|
if pending_units is None:
|
|
pending_units = 0
|
|
return pending_units
|
|
|
|
def process(self, game: Game, now: datetime) -> None:
|
|
coalition = game.coalition_for(self.destination.captured)
|
|
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(coalition)
|
|
|
|
bought_units: dict[GroundUnitType, int] = {}
|
|
units_needing_transfer: dict[GroundUnitType, int] = {}
|
|
for unit_type, count in self.units.items():
|
|
allegiance = "Ally" if self.destination.captured else "Enemy"
|
|
d: dict[GroundUnitType, int]
|
|
if self.destination != ground_unit_source:
|
|
source = ground_unit_source
|
|
d = units_needing_transfer
|
|
else:
|
|
source = self.destination
|
|
d = bought_units
|
|
|
|
if count < 0:
|
|
logging.error(
|
|
f"Attempted sale of {unit_type} at {self.destination} but ground "
|
|
"units cannot be sold"
|
|
)
|
|
elif count > 0:
|
|
d[unit_type] = count
|
|
game.message(
|
|
f"{allegiance} reinforcements: {unit_type} x {count} at {source}"
|
|
)
|
|
|
|
self.units = defaultdict(int)
|
|
self.destination.base.commission_units(bought_units)
|
|
|
|
if units_needing_transfer:
|
|
if ground_unit_source is None:
|
|
raise RuntimeError(
|
|
f"Ground unit source could not be found for {self.destination} but "
|
|
"still tried to transfer units to there"
|
|
)
|
|
ground_unit_source.base.commission_units(units_needing_transfer)
|
|
self.create_transfer(
|
|
coalition, ground_unit_source, units_needing_transfer, now
|
|
)
|
|
|
|
def create_transfer(
|
|
self,
|
|
coalition: Coalition,
|
|
source: ControlPoint,
|
|
units: dict[GroundUnitType, int],
|
|
now: datetime,
|
|
) -> None:
|
|
coalition.transfers.new_transfer(
|
|
TransferOrder(source, self.destination, units), now
|
|
)
|
|
|
|
def find_ground_unit_source(self, game: Game) -> Optional[ControlPoint]:
|
|
# 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
|
|
|
|
# Fast path if the destination is a valid source.
|
|
if self.destination.can_recruit_ground_units(game):
|
|
return self.destination
|
|
|
|
try:
|
|
return self.find_ground_unit_source_in_network(
|
|
game.transit_network_for(self.destination.captured), game
|
|
)
|
|
except NoPathError:
|
|
return None
|
|
|
|
def find_ground_unit_source_in_network(
|
|
self, network: TransitNetwork, game: Game
|
|
) -> Optional[ControlPoint]:
|
|
sources = []
|
|
for control_point in game.theater.control_points_for(self.destination.captured):
|
|
if control_point.can_recruit_ground_units(
|
|
game
|
|
) and network.has_path_between(self.destination, control_point):
|
|
sources.append(control_point)
|
|
|
|
if not sources:
|
|
return None
|
|
|
|
# Fast path to skip the distance calculation if we have only one option.
|
|
if len(sources) == 1:
|
|
return sources[0]
|
|
|
|
closest = sources[0]
|
|
_, cost = network.shortest_path_with_cost(self.destination, closest)
|
|
for source in sources:
|
|
_, new_cost = network.shortest_path_with_cost(self.destination, source)
|
|
if new_cost < cost:
|
|
closest = source
|
|
cost = new_cost
|
|
return closest
|