First pass at cargo ships.

The simple form of this works, but without the multi-mode routing it'll
only get used when the final destination is a port with a link to a port
with a factory.

These also aren't targetable or simulated yet.

https://github.com/Khopa/dcs_liberation/issues/826
This commit is contained in:
Dan Albert 2021-04-25 11:17:41 -07:00
parent 42694d2004
commit ba8fafcc95
11 changed files with 262 additions and 99 deletions

View File

@ -315,6 +315,23 @@ class ControlPoint(MissionTarget, ABC):
connected.extend(cp.transitive_connected_friendly_points(seen)) connected.extend(cp.transitive_connected_friendly_points(seen))
return connected return connected
def transitive_friendly_shipping_destinations(
self, seen: Optional[Set[ControlPoint]] = None
) -> List[ControlPoint]:
if seen is None:
seen = {self}
connected = []
for cp in self.shipping_lanes:
if cp.captured != self.captured:
continue
if cp in seen:
continue
seen.add(cp)
connected.append(cp)
connected.extend(cp.transitive_friendly_shipping_destinations(seen))
return connected
@property @property
def has_factory(self) -> bool: def has_factory(self) -> bool:
for tgo in self.connected_objectives: for tgo in self.connected_objectives:

View File

@ -4,7 +4,7 @@ import heapq
import math import math
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Dict, Iterator, List, Optional from typing import Dict, Iterable, Iterator, List, Optional
from game.theater.controlpoint import ControlPoint from game.theater.controlpoint import ControlPoint
@ -32,6 +32,25 @@ class Frontier:
return bool(self.nodes) return bool(self.nodes)
# TODO: Build a single SupplyRoute for each coalition at the start of the turn.
# Supply routes need to cover the whole network to support multi-mode links.
#
# Traverse each friendly control point and build out a network from each. Nodes create
# connections to:
#
# 1. Bases connected by road
# 2. Bases connected by rail
# 3. Bases connected by shipping lane
# 4. Airports large enough to operate cargo planes connect to each other
# 5. Airports capable of operating helicopters connect to other airports within cargo
# helicopter range, and FOBs within half of the range (since they can't be refueled
# at the drop off).
#
# The costs of each link would be set such that the above order roughly corresponds to
# the prevalence of each type of transport. Most units should move by road, rail should
# be used a little less often than road, ships a bit less often than that, cargo planes
# infrequently, and helicopters rarely. Convoys, trains, and ships make the most
# interesting targets for players (and the easiest to generate AI flight plans for).
class SupplyRoute: class SupplyRoute:
def __init__(self, control_points: List[ControlPoint]) -> None: def __init__(self, control_points: List[ControlPoint]) -> None:
self.control_points = control_points self.control_points = control_points
@ -45,20 +64,16 @@ class SupplyRoute:
def __len__(self) -> int: def __len__(self) -> int:
return len(self.control_points) return len(self.control_points)
@classmethod def connections_from(self, control_point: ControlPoint) -> Iterable:
def for_control_point(cls, control_point: ControlPoint) -> SupplyRoute: raise NotImplementedError
connected_friendly_points = control_point.transitive_connected_friendly_points()
if not connected_friendly_points:
return SupplyRoute([control_point])
return SupplyRoute([control_point] + connected_friendly_points)
def shortest_path_between( def shortest_path_between(
self, origin: ControlPoint, destination: ControlPoint self, origin: ControlPoint, destination: ControlPoint
) -> List[ControlPoint]: ) -> List[ControlPoint]:
if origin not in self: if origin not in self:
raise ValueError(f"{origin.name} is not in this supply route") raise ValueError(f"{origin} is not in supply route to {destination}")
if destination not in self: if destination not in self:
raise ValueError(f"{destination.name} is not in this supply route") raise ValueError(f"{destination} is not in supply route from {origin}")
frontier = Frontier() frontier = Frontier()
frontier.push(origin, 0) frontier.push(origin, 0)
@ -74,7 +89,7 @@ class SupplyRoute:
if cost > best_known[current]: if cost > best_known[current]:
continue continue
for neighbor in current.connected_points: for neighbor in self.connections_from(current):
if current.captured != neighbor.captured: if current.captured != neighbor.captured:
continue continue
@ -97,3 +112,29 @@ class SupplyRoute:
current = previous current = previous
path.reverse() path.reverse()
return path return path
class RoadNetwork(SupplyRoute):
@classmethod
def for_control_point(cls, control_point: ControlPoint) -> RoadNetwork:
connected_friendly_points = control_point.transitive_connected_friendly_points()
if not connected_friendly_points:
return RoadNetwork([control_point])
return RoadNetwork([control_point] + connected_friendly_points)
def connections_from(self, control_point: ControlPoint) -> Iterable:
yield from control_point.connected_points
class ShippingNetwork(SupplyRoute):
@classmethod
def for_control_point(cls, control_point: ControlPoint) -> ShippingNetwork:
connected_friendly_points = (
control_point.transitive_friendly_shipping_destinations()
)
if not connected_friendly_points:
return ShippingNetwork([control_point])
return ShippingNetwork([control_point] + connected_friendly_points)
def connections_from(self, control_point: ControlPoint) -> Iterable:
yield from control_point.shipping_lanes

View File

@ -16,7 +16,7 @@ from gen.flights.ai_flight_planner_db import TRANSPORT_CAPABLE
from gen.flights.closestairfields import ObjectiveDistanceCache from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.flights.flightplan import FlightPlanBuilder from gen.flights.flightplan import FlightPlanBuilder
from game.theater import ControlPoint, MissionTarget from game.theater import ControlPoint, MissionTarget
from game.theater.supplyroutes import SupplyRoute from game.theater.supplyroutes import RoadNetwork, ShippingNetwork, SupplyRoute
from gen.naming import namegen from gen.naming import namegen
from gen.flights.flight import Flight, FlightType from gen.flights.flight import Flight, FlightType
@ -242,20 +242,15 @@ class AirliftPlanner:
return flight_size return flight_size
class Convoy(MissionTarget, Transport): class MultiGroupTransport(MissionTarget, Transport):
def __init__(self, origin: ControlPoint, destination: ControlPoint) -> None: def __init__(
super().__init__(namegen.next_convoy_name(), origin.position) self, name: str, origin: ControlPoint, destination: ControlPoint
) -> None:
super().__init__(name, origin.position)
self.origin = origin self.origin = origin
self.destination = destination self.destination = destination
self.transfers: List[TransferOrder] = [] self.transfers: List[TransferOrder] = []
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
if self.is_friendly(for_player):
return
yield FlightType.BAI
yield from super().mission_types(for_player)
def is_friendly(self, to_player: bool) -> bool: def is_friendly(self, to_player: bool) -> bool:
return self.origin.captured return self.origin.captured
@ -298,10 +293,30 @@ class Convoy(MissionTarget, Transport):
return self.origin.captured return self.origin.captured
def find_escape_route(self) -> Optional[ControlPoint]: def find_escape_route(self) -> Optional[ControlPoint]:
return None raise NotImplementedError
def description(self) -> str: def description(self) -> str:
return f"In a convoy to {self.destination}" raise NotImplementedError
@property
def route_start(self) -> Point:
raise NotImplementedError
@property
def route_end(self) -> Point:
raise NotImplementedError
class Convoy(MultiGroupTransport):
def __init__(self, origin: ControlPoint, destination: ControlPoint) -> None:
super().__init__(namegen.next_convoy_name(), origin, destination)
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
if self.is_friendly(for_player):
return
yield FlightType.BAI
yield from super().mission_types(for_player)
@property @property
def route_start(self) -> Point: def route_start(self) -> Point:
@ -311,44 +326,85 @@ class Convoy(MissionTarget, Transport):
def route_end(self) -> Point: def route_end(self) -> Point:
return self.destination.convoy_spawns[self.origin] return self.destination.convoy_spawns[self.origin]
def description(self) -> str:
return f"In a convoy to {self.destination}"
class ConvoyMap: def find_escape_route(self) -> Optional[ControlPoint]:
return None
class CargoShip(MultiGroupTransport):
def __init__(self, origin: ControlPoint, destination: ControlPoint) -> None:
super().__init__(namegen.next_cargo_ship_name(), origin, destination)
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
if self.is_friendly(for_player):
return
yield FlightType.ANTISHIP
yield from super().mission_types(for_player)
@property
def route_start(self) -> Point:
return self.origin.shipping_lanes[self.destination][0]
@property
def route_end(self) -> Point:
return self.destination.shipping_lanes[self.origin][-1]
def description(self) -> str:
return f"On a ship to {self.destination}"
def find_escape_route(self) -> Optional[ControlPoint]:
return None
class TransportMap:
def __init__(self) -> None: def __init__(self) -> None:
# Dict of origin -> destination -> convoy. # Dict of origin -> destination -> transport.
self.convoys: Dict[ControlPoint, Dict[ControlPoint, Convoy]] = defaultdict(dict) self.transports: Dict[
ControlPoint, Dict[ControlPoint, MultiGroupTransport]
] = defaultdict(dict)
def convoy_exists(self, origin: ControlPoint, destination: ControlPoint) -> bool: def create_transport(
return destination in self.convoys[origin]
def find_convoy(
self, origin: ControlPoint, destination: ControlPoint self, origin: ControlPoint, destination: ControlPoint
) -> Optional[Convoy]: ) -> MultiGroupTransport:
return self.convoys[origin].get(destination) raise NotImplementedError
def find_or_create_convoy( def transport_exists(self, origin: ControlPoint, destination: ControlPoint) -> bool:
return destination in self.transports[origin]
def find_transport(
self, origin: ControlPoint, destination: ControlPoint self, origin: ControlPoint, destination: ControlPoint
) -> Convoy: ) -> Optional[MultiGroupTransport]:
convoy = self.find_convoy(origin, destination) return self.transports[origin].get(destination)
if convoy is None:
convoy = Convoy(origin, destination)
self.convoys[origin][destination] = convoy
return convoy
def departing_from(self, origin: ControlPoint) -> Iterator[Convoy]: def find_or_create_transport(
yield from self.convoys[origin].values() self, origin: ControlPoint, destination: ControlPoint
) -> MultiGroupTransport:
transport = self.find_transport(origin, destination)
if transport is None:
transport = self.create_transport(origin, destination)
self.transports[origin][destination] = transport
return transport
def travelling_to(self, destination: ControlPoint) -> Iterator[Convoy]: def departing_from(self, origin: ControlPoint) -> Iterator[MultiGroupTransport]:
for destination_dict in self.convoys.values(): yield from self.transports[origin].values()
def travelling_to(self, destination: ControlPoint) -> Iterator[MultiGroupTransport]:
for destination_dict in self.transports.values():
if destination in destination_dict: if destination in destination_dict:
yield destination_dict[destination] yield destination_dict[destination]
def disband_convoy(self, convoy: Convoy) -> None: def disband_transport(self, transport: MultiGroupTransport) -> None:
self.convoys[convoy.origin][convoy.destination].disband() transport.disband()
del self.convoys[convoy.origin][convoy.destination] del self.transports[transport.origin][transport.destination]
@staticmethod def network_for(self, control_point: ControlPoint) -> SupplyRoute:
def path_for(transfer: TransferOrder) -> List[ControlPoint]: raise NotImplementedError
supply_route = SupplyRoute.for_control_point(transfer.position)
def path_for(self, transfer: TransferOrder) -> List[ControlPoint]:
supply_route = self.network_for(transfer.position)
return supply_route.shortest_path_between( return supply_route.shortest_path_between(
transfer.position, transfer.destination transfer.position, transfer.destination
) )
@ -358,26 +414,47 @@ class ConvoyMap:
def add(self, transfer: TransferOrder) -> None: def add(self, transfer: TransferOrder) -> None:
next_stop = self.next_stop_for(transfer) next_stop = self.next_stop_for(transfer)
self.find_or_create_convoy(transfer.position, next_stop).add_units(transfer) self.find_or_create_transport(transfer.position, next_stop).add_units(transfer)
def remove(self, convoy: Convoy, transfer: TransferOrder) -> None: def remove(self, transport: MultiGroupTransport, transfer: TransferOrder) -> None:
convoy.remove_units(transfer) transport.remove_units(transfer)
if not convoy.transfers: if not transport.transfers:
self.disband_convoy(convoy) self.disband_transport(transport)
def disband_all(self) -> None: def disband_all(self) -> None:
for convoy in list(self): for transport in list(self):
self.disband_convoy(convoy) self.disband_transport(transport)
def __iter__(self) -> Iterator[Convoy]: def __iter__(self) -> Iterator[MultiGroupTransport]:
for destination_dict in self.convoys.values(): for destination_dict in self.transports.values():
yield from destination_dict.values() yield from destination_dict.values()
class ConvoyMap(TransportMap):
def create_transport(
self, origin: ControlPoint, destination: ControlPoint
) -> Convoy:
return Convoy(origin, destination)
def network_for(self, control_point: ControlPoint) -> RoadNetwork:
return RoadNetwork.for_control_point(control_point)
class CargoShipMap(TransportMap):
def create_transport(
self, origin: ControlPoint, destination: ControlPoint
) -> CargoShip:
return CargoShip(origin, destination)
def network_for(self, control_point: ControlPoint) -> ShippingNetwork:
return ShippingNetwork.for_control_point(control_point)
class PendingTransfers: class PendingTransfers:
def __init__(self, game: Game) -> None: def __init__(self, game: Game) -> None:
self.game = game self.game = game
self.convoys = ConvoyMap() self.convoys = ConvoyMap()
self.cargo_ships = CargoShipMap()
self.pending_transfers: List[TransferOrder] = [] self.pending_transfers: List[TransferOrder] = []
def __iter__(self) -> Iterator[TransferOrder]: def __iter__(self) -> Iterator[TransferOrder]:
@ -394,9 +471,12 @@ class PendingTransfers:
return self.pending_transfers.index(transfer) return self.pending_transfers.index(transfer)
def arrange_transport(self, transfer: TransferOrder) -> None: def arrange_transport(self, transfer: TransferOrder) -> None:
supply_route = SupplyRoute.for_control_point(transfer.position) if transfer.destination in RoadNetwork.for_control_point(transfer.position):
if transfer.destination in supply_route:
self.convoys.add(transfer) self.convoys.add(transfer)
elif transfer.destination in ShippingNetwork.for_control_point(
transfer.position
):
self.cargo_ships.add(transfer)
else: else:
AirliftPlanner(self.game, transfer).create_package_for_airlift() AirliftPlanner(self.game, transfer).create_package_for_airlift()
@ -439,6 +519,11 @@ class PendingTransfers:
) -> None: ) -> None:
self.convoys.remove(transport, transfer) self.convoys.remove(transport, transfer)
def _cancel_transport_cargo_ship(
self, transfer: TransferOrder, transport: CargoShip
) -> None:
self.cargo_ships.remove(transport, transfer)
def cancel_transfer(self, transfer: TransferOrder) -> None: def cancel_transfer(self, transfer: TransferOrder) -> None:
if transfer.transport is not None: if transfer.transport is not None:
self.cancel_transport(transfer, transfer.transport) self.cancel_transport(transfer, transfer.transport)

View File

@ -10,6 +10,7 @@ from dcs.unittype import UnitType, VehicleType
from game.theater import ControlPoint, SupplyRoute from game.theater import ControlPoint, SupplyRoute
from gen.flights.closestairfields import ObjectiveDistanceCache from gen.flights.closestairfields import ObjectiveDistanceCache
from .db import PRICES from .db import PRICES
from .theater.supplyroutes import RoadNetwork, ShippingNetwork
from .transfers import TransferOrder from .transfers import TransferOrder
if TYPE_CHECKING: if TYPE_CHECKING:
@ -19,7 +20,6 @@ if TYPE_CHECKING:
@dataclass(frozen=True) @dataclass(frozen=True)
class GroundUnitSource: class GroundUnitSource:
control_point: ControlPoint control_point: ControlPoint
requires_airlift: bool
class PendingUnitDeliveries: class PendingUnitDeliveries:
@ -84,9 +84,9 @@ class PendingUnitDeliveries:
if ( if (
issubclass(unit_type, VehicleType) issubclass(unit_type, VehicleType)
and self.destination != ground_unit_source.control_point and self.destination != ground_unit_source
): ):
source = ground_unit_source.control_point source = ground_unit_source
d = units_needing_transfer d = units_needing_transfer
else: else:
source = self.destination source = self.destination
@ -106,41 +106,45 @@ class PendingUnitDeliveries:
self.destination.base.commit_losses(sold_units) self.destination.base.commit_losses(sold_units)
if units_needing_transfer: if units_needing_transfer:
ground_unit_source.control_point.base.commision_units( ground_unit_source.base.commision_units(units_needing_transfer)
units_needing_transfer self.create_transfer(game, ground_unit_source, units_needing_transfer)
)
self.create_transfer(
game, ground_unit_source.control_point, units_needing_transfer
)
def create_transfer( def create_transfer(
self, game: Game, source: ControlPoint, units: Dict[Type[VehicleType], int] self, game: Game, source: ControlPoint, units: Dict[Type[VehicleType], int]
) -> None: ) -> None:
game.transfers.new_transfer(TransferOrder(source, self.destination, units)) game.transfers.new_transfer(TransferOrder(source, self.destination, units))
def find_ground_unit_source(self, game: Game) -> Optional[GroundUnitSource]: def find_ground_unit_source(self, game: Game) -> Optional[ControlPoint]:
# This is running *after* the turn counter has been incremented, so this is the # 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 # 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. # delivery on turn 1 so that turn 1 always starts with units on the front line.
if game.turn == 1: if game.turn == 1:
return GroundUnitSource(self.destination, requires_airlift=False) return self.destination
# Fast path if the destination is a valid source. # Fast path if the destination is a valid source.
if self.destination.can_recruit_ground_units(game): if self.destination.can_recruit_ground_units(game):
return GroundUnitSource(self.destination, requires_airlift=False) return self.destination
by_road = self.find_ground_unit_source_by_road(game) by_road = self.find_ground_unit_source_in_supply_route(
RoadNetwork.for_control_point(self.destination), game
)
if by_road is not None: if by_road is not None:
return GroundUnitSource(by_road, requires_airlift=False) return by_road
by_ship = self.find_ground_unit_source_in_supply_route(
ShippingNetwork.for_control_point(self.destination), game
)
if by_ship is not None:
return by_ship
by_air = self.find_ground_unit_source_by_air(game) by_air = self.find_ground_unit_source_by_air(game)
if by_air is not None: if by_air is not None:
return GroundUnitSource(by_air, requires_airlift=True) return by_air
return None return None
def find_ground_unit_source_by_road(self, game: Game) -> Optional[ControlPoint]: def find_ground_unit_source_in_supply_route(
supply_route = SupplyRoute.for_control_point(self.destination) self, supply_route: SupplyRoute, game: Game
) -> Optional[ControlPoint]:
sources = [] sources = []
for control_point in supply_route: for control_point in supply_route:
if control_point.can_recruit_ground_units(game): if control_point.can_recruit_ground_units(game):

View File

@ -9,7 +9,7 @@ from dcs.unittype import VehicleType
from game import db from game import db
from game.theater import Airfield, ControlPoint, TheaterGroundObject from game.theater import Airfield, ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import BuildingGroundObject from game.theater.theatergroundobject import BuildingGroundObject
from game.transfers import Convoy, TransferOrder from game.transfers import MultiGroupTransport, TransferOrder
from gen.flights.flight import Flight from gen.flights.flight import Flight
@ -29,7 +29,7 @@ class GroundObjectUnit:
@dataclass(frozen=True) @dataclass(frozen=True)
class ConvoyUnit: class ConvoyUnit:
unit_type: Type[VehicleType] unit_type: Type[VehicleType]
convoy: Convoy convoy: MultiGroupTransport
@dataclass(frozen=True) @dataclass(frozen=True)
@ -130,7 +130,7 @@ class UnitMap:
def ground_object_unit(self, name: str) -> Optional[GroundObjectUnit]: def ground_object_unit(self, name: str) -> Optional[GroundObjectUnit]:
return self.ground_object_units.get(name, None) return self.ground_object_units.get(name, None)
def add_convoy_units(self, group: Group, convoy: Convoy) -> None: def add_convoy_units(self, group: Group, convoy: MultiGroupTransport) -> None:
for unit in group.units: for unit in group.units:
# The actual name is a String (the pydcs translatable string), which # The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__. # doesn't define __eq__.

View File

@ -10,7 +10,7 @@ from dcs.unit import Vehicle
from dcs.unitgroup import VehicleGroup from dcs.unitgroup import VehicleGroup
from dcs.unittype import VehicleType from dcs.unittype import VehicleType
from game.transfers import Convoy from game.transfers import MultiGroupTransport
from game.unitmap import UnitMap from game.unitmap import UnitMap
from game.utils import kph from game.utils import kph
@ -30,7 +30,7 @@ class ConvoyGenerator:
for convoy in self.game.transfers.convoys: for convoy in self.game.transfers.convoys:
self.generate_convoy(convoy) self.generate_convoy(convoy)
def generate_convoy(self, convoy: Convoy) -> VehicleGroup: def generate_convoy(self, convoy: MultiGroupTransport) -> VehicleGroup:
group = self._create_mixed_unit_group( group = self._create_mixed_unit_group(
convoy.name, convoy.name,
convoy.route_start, convoy.route_start,

View File

@ -39,7 +39,7 @@ from game.theater.theatergroundobject import (
NavalGroundObject, NavalGroundObject,
VehicleGroupGroundObject, VehicleGroupGroundObject,
) )
from game.transfers import Convoy, TransferOrder from game.transfers import Convoy, MultiGroupTransport, TransferOrder
from game.utils import Distance, nautical_miles from game.utils import Distance, nautical_miles
from gen import Conflict from gen import Conflict
from gen.ato import Package from gen.ato import Package
@ -445,7 +445,7 @@ class ObjectiveFinder:
airfields.append(control_point) airfields.append(control_point)
return self._targets_by_range(airfields) return self._targets_by_range(airfields)
def convoys(self) -> Iterator[Convoy]: def convoys(self) -> Iterator[MultiGroupTransport]:
for front_line in self.front_lines(): for front_line in self.front_lines():
if front_line.control_point_a.is_friendly(self.is_player): if front_line.control_point_a.is_friendly(self.is_player):
enemy_cp = front_line.control_point_a enemy_cp = front_line.control_point_a

View File

@ -251,6 +251,7 @@ class NameGenerator:
infantry_number = 0 infantry_number = 0
aircraft_number = 0 aircraft_number = 0
convoy_number = 0 convoy_number = 0
cargo_ship_number = 0
ANIMALS = ANIMALS ANIMALS = ANIMALS
existing_alphas: List[str] = [] existing_alphas: List[str] = []
@ -260,6 +261,7 @@ class NameGenerator:
cls.number = 0 cls.number = 0
cls.infantry_number = 0 cls.infantry_number = 0
cls.convoy_number = 0 cls.convoy_number = 0
cls.cargo_ship_number = 0
cls.ANIMALS = ANIMALS cls.ANIMALS = ANIMALS
cls.existing_alphas = [] cls.existing_alphas = []
@ -269,6 +271,7 @@ class NameGenerator:
cls.infantry_number = 0 cls.infantry_number = 0
cls.aircraft_number = 0 cls.aircraft_number = 0
cls.convoy_number = 0 cls.convoy_number = 0
cls.cargo_ship_number = 0
@classmethod @classmethod
def next_aircraft_name(cls, country: Country, parent_base_id: int, flight: Flight): def next_aircraft_name(cls, country: Country, parent_base_id: int, flight: Flight):
@ -335,6 +338,11 @@ class NameGenerator:
cls.convoy_number += 1 cls.convoy_number += 1
return f"Convoy {cls.convoy_number:03}" return f"Convoy {cls.convoy_number:03}"
@classmethod
def next_cargo_ship_name(cls) -> str:
cls.cargo_ship_number += 1
return f"Cargo Ship {cls.cargo_ship_number:03}"
@classmethod @classmethod
def random_objective_name(cls): def random_objective_name(cls):
if len(cls.ANIMALS) == 0: if len(cls.ANIMALS) == 0:

View File

@ -4,7 +4,7 @@ import datetime
import logging import logging
import math import math
from functools import singledispatchmethod from functools import singledispatchmethod
from typing import Iterable, Iterator, List, Optional, Tuple from typing import Iterable, Iterator, List, Optional, Sequence, Tuple
from PySide2 import QtCore, QtWidgets from PySide2 import QtCore, QtWidgets
from PySide2.QtCore import QLineF, QPointF, QRectF, Qt from PySide2.QtCore import QLineF, QPointF, QRectF, Qt
@ -87,7 +87,7 @@ def bernstein(t: float, i: int, n: int) -> float:
return binomial(i, n) * (t ** i) * ((1 - t) ** (n - i)) return binomial(i, n) * (t ** i) * ((1 - t) ** (n - i))
def bezier(t: float, points: Iterable[Tuple[float, float]]) -> Tuple[float, float]: def bezier(t: float, points: Sequence[Tuple[float, float]]) -> Tuple[float, float]:
"""Calculate coordinate of a point in the bezier curve""" """Calculate coordinate of a point in the bezier curve"""
n = len(points) - 1 n = len(points) - 1
x = y = 0 x = y = 0
@ -99,7 +99,7 @@ def bezier(t: float, points: Iterable[Tuple[float, float]]) -> Tuple[float, floa
def bezier_curve_range( def bezier_curve_range(
n: int, points: Iterable[Tuple[float, float]] n: int, points: Sequence[Tuple[float, float]]
) -> Iterator[Tuple[float, float]]: ) -> Iterator[Tuple[float, float]]:
"""Range of points in a curve bezier""" """Range of points in a curve bezier"""
for i in range(n): for i in range(n):
@ -145,7 +145,7 @@ class QLiberationMap(QGraphicsView):
QtCore.QLineF(QPointF(0, 0), QPointF(0, 0)) QtCore.QLineF(QPointF(0, 0), QPointF(0, 0))
) )
self.movement_line.setPen(QPen(CONST.COLORS["orange"], width=10.0)) self.movement_line.setPen(QPen(CONST.COLORS["orange"], width=10.0))
self.selected_cp: QMapControlPoint = None self.selected_cp: Optional[QMapControlPoint] = None
GameUpdateSignal.get_instance().flight_paths_changed.connect( GameUpdateSignal.get_instance().flight_paths_changed.connect(
lambda: self.draw_flight_plans(self.scene()) lambda: self.draw_flight_plans(self.scene())
@ -767,7 +767,7 @@ class QLiberationMap(QGraphicsView):
scene: QGraphicsScene, scene: QGraphicsScene,
number: int, number: int,
waypoint: FlightWaypoint, waypoint: FlightWaypoint,
position: Tuple[int, int], position: Tuple[float, float],
flight_plan: FlightPlan, flight_plan: FlightPlan,
) -> None: ) -> None:
@ -880,19 +880,28 @@ class QLiberationMap(QGraphicsView):
self.draw_shipping_lane_between(cp, destination) self.draw_shipping_lane_between(cp, destination)
def draw_shipping_lane_between(self, a: ControlPoint, b: ControlPoint) -> None: def draw_shipping_lane_between(self, a: ControlPoint, b: ControlPoint) -> None:
ship_map = self.game.transfers.cargo_ships
ships = []
ship = ship_map.find_transport(a, b)
if ship is not None:
ships.append(ship)
ship = ship_map.find_transport(b, a)
if ship is not None:
ships.append(ship)
scene = self.scene() scene = self.scene()
for pa, pb in self.bezier_points(a.shipping_lanes[b]): for pa, pb in self.bezier_points(a.shipping_lanes[b]):
scene.addItem(ShippingLaneSegment(pa[0], pa[1], pb[0], pb[1], a, b)) scene.addItem(ShippingLaneSegment(pa[0], pa[1], pb[0], pb[1], a, b, ships))
def draw_supply_route_between(self, a: ControlPoint, b: ControlPoint) -> None: def draw_supply_route_between(self, a: ControlPoint, b: ControlPoint) -> None:
scene = self.scene() scene = self.scene()
convoy_map = self.game.transfers.convoys convoy_map = self.game.transfers.convoys
convoys = [] convoys = []
convoy = convoy_map.find_convoy(a, b) convoy = convoy_map.find_transport(a, b)
if convoy is not None: if convoy is not None:
convoys.append(convoy) convoys.append(convoy)
convoy = convoy_map.find_convoy(b, a) convoy = convoy_map.find_transport(b, a)
if convoy is not None: if convoy is not None:
convoys.append(convoy) convoys.append(convoy)

View File

@ -1,4 +1,4 @@
from typing import Optional from typing import List, Optional
from PySide2.QtCore import Qt from PySide2.QtCore import Qt
from PySide2.QtGui import QColor, QPen from PySide2.QtGui import QColor, QPen
@ -8,6 +8,7 @@ from PySide2.QtWidgets import (
) )
from game.theater import ControlPoint from game.theater import ControlPoint
from game.transfers import CargoShip
from qt_ui.uiconstants import COLORS from qt_ui.uiconstants import COLORS
@ -20,12 +21,13 @@ class ShippingLaneSegment(QGraphicsLineItem):
y1: float, y1: float,
control_point_a: ControlPoint, control_point_a: ControlPoint,
control_point_b: ControlPoint, control_point_b: ControlPoint,
ships: List[CargoShip],
parent: Optional[QGraphicsItem] = None, parent: Optional[QGraphicsItem] = None,
) -> None: ) -> None:
super().__init__(x0, y0, x1, y1, parent) super().__init__(x0, y0, x1, y1, parent)
self.control_point_a = control_point_a self.control_point_a = control_point_a
self.control_point_b = control_point_b self.control_point_b = control_point_b
self.ships = [] self.ships = ships
self.setPen(self.make_pen()) self.setPen(self.make_pen())
self.setToolTip(self.make_tooltip()) self.setToolTip(self.make_tooltip())
self.setAcceptHoverEvents(True) self.setAcceptHoverEvents(True)

View File

@ -2,12 +2,10 @@ from __future__ import annotations
import logging import logging
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass
from typing import Callable, Dict, Type from typing import Callable, Dict, Type
from PySide2.QtCore import Qt from PySide2.QtCore import Qt
from PySide2.QtWidgets import ( from PySide2.QtWidgets import (
QCheckBox,
QComboBox, QComboBox,
QDialog, QDialog,
QFrame, QFrame,
@ -15,7 +13,6 @@ from PySide2.QtWidgets import (
QGroupBox, QGroupBox,
QHBoxLayout, QHBoxLayout,
QLabel, QLabel,
QMessageBox,
QPushButton, QPushButton,
QScrollArea, QScrollArea,
QSizePolicy, QSizePolicy,
@ -27,7 +24,7 @@ from dcs.task import PinpointStrike
from dcs.unittype import UnitType from dcs.unittype import UnitType
from game import Game, db from game import Game, db
from game.theater import ControlPoint, SupplyRoute from game.theater import ControlPoint
from game.transfers import TransferOrder from game.transfers import TransferOrder
from qt_ui.models import GameModel from qt_ui.models import GameModel
from qt_ui.widgets.QLabeledWidget import QLabeledWidget from qt_ui.widgets.QLabeledWidget import QLabeledWidget