diff --git a/game/operation/operation.py b/game/operation/operation.py
index 5bf7c4e7..1376be88 100644
--- a/game/operation/operation.py
+++ b/game/operation/operation.py
@@ -1,10 +1,9 @@
from __future__ import annotations
-from game.theater.theatergroundobject import TheaterGroundObject
import logging
import os
from pathlib import Path
-from typing import TYPE_CHECKING, Iterable, List, Optional, Set
+from typing import Iterable, List, Optional, Set, TYPE_CHECKING
from dcs import Mission
from dcs.action import DoScript, DoScriptFile
@@ -14,7 +13,9 @@ from dcs.lua.parse import loads
from dcs.mapping import Point
from dcs.translation import String
from dcs.triggers import TriggerStart
+
from game.plugins import LuaPluginManager
+from game.theater.theatergroundobject import TheaterGroundObject
from gen import Conflict, FlightType, VisualGenerator
from gen.aircraft import AIRCRAFT_DATA, AircraftConflictGenerator, FlightData
from gen.airfields import AIRFIELD_DATA
@@ -31,7 +32,6 @@ from gen.naming import namegen
from gen.radios import RadioFrequency, RadioRegistry
from gen.tacan import TacanRegistry
from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator
-
from .. import db
from ..theater import Airfield
from ..unitmap import UnitMap
@@ -43,18 +43,13 @@ if TYPE_CHECKING:
class Operation:
"""Static class for managing the final Mission generation"""
- current_mission = None # type: Mission
- airgen = None # type: AircraftConflictGenerator
- triggersgen = None # type: TriggersGenerator
- airsupportgen = None # type: AirSupportConflictGenerator
- visualgen = None # type: VisualGenerator
- groundobjectgen = None # type: GroundObjectsGenerator
- briefinggen = None # type: BriefingGenerator
- forcedoptionsgen = None # type: ForcedOptionsGenerator
- radio_registry: Optional[RadioRegistry] = None
- tacan_registry: Optional[TacanRegistry] = None
- game = None # type: Game
- environment_settings = None
+ current_mission: Mission
+ airgen: AircraftConflictGenerator
+ airsupportgen: AirSupportConflictGenerator
+ groundobjectgen: GroundObjectsGenerator
+ radio_registry: RadioRegistry
+ tacan_registry: TacanRegistry
+ game: Game
trigger_radius = TRIGGER_RADIUS_MEDIUM
is_quick = None
player_awacs_enabled = True
@@ -309,13 +304,13 @@ class Operation:
# Set mission time and weather conditions.
EnvironmentGenerator(cls.current_mission, cls.game.conditions).generate()
cls._generate_ground_units()
+ cls._generate_convoys()
cls._generate_destroyed_units()
cls._generate_air_units()
cls.assign_channels_to_flights(
cls.airgen.flights, cls.airsupportgen.air_support
)
cls._generate_ground_conflicts()
- cls._generate_convoys()
# Triggers
triggersgen = TriggersGenerator(cls.current_mission, cls.game)
diff --git a/game/theater/supplyroutes.py b/game/theater/supplyroutes.py
index 790f95a8..eb02663d 100644
--- a/game/theater/supplyroutes.py
+++ b/game/theater/supplyroutes.py
@@ -6,8 +6,6 @@ from collections import defaultdict
from dataclasses import dataclass, field
from typing import Dict, Iterator, List, Optional
-from dcs import Point
-from game.theater import FlightType, MissionTarget
from game.theater.controlpoint import ControlPoint
@@ -99,25 +97,3 @@ class SupplyRoute:
current = previous
path.reverse()
return path
-
-
-class SupplyRouteLink(MissionTarget):
- def __init__(self, a: ControlPoint, b: ControlPoint) -> None:
- self.control_point_a = a
- self.control_point_b = b
- super().__init__(
- f"Supply route between {a} and {b}",
- Point((a.position.x + b.position.x) / 2, (a.position.y + b.position.y) / 2),
- )
-
- def mission_types(self, for_player: bool) -> Iterator[FlightType]:
- yield from [
- FlightType.BAI,
- # TODO: Escort
- # TODO: SEAD
- # TODO: Recon
- # TODO: TARCAP
- ]
-
- def is_friendly(self, to_player: bool) -> bool:
- return self.control_point_a.captured
diff --git a/game/transfers.py b/game/transfers.py
index e9a5fdc3..dce97546 100644
--- a/game/transfers.py
+++ b/game/transfers.py
@@ -3,8 +3,10 @@ from dataclasses import dataclass, field
from typing import Dict, Iterator, List, Type
from dcs.unittype import VehicleType
-from game.theater import ControlPoint
+from game.theater import ControlPoint, MissionTarget
from game.theater.supplyroutes import SupplyRoute
+from gen.naming import namegen
+from gen.flights.flight import FlightType
@dataclass
@@ -35,6 +37,8 @@ class RoadTransferOrder(TransferOrder):
#: point a turn through the supply line.
position: ControlPoint = field(init=False)
+ name: str = field(init=False, default_factory=namegen.next_convoy_name)
+
def __post_init__(self) -> None:
self.position = self.origin
@@ -46,6 +50,27 @@ class RoadTransferOrder(TransferOrder):
return self.path()[0]
+class Convoy(MissionTarget):
+ def __init__(self, transfer: RoadTransferOrder) -> None:
+ self.transfer = transfer
+ count = sum(c for c in transfer.units.values())
+ super().__init__(
+ f"{transfer.name} of {count} units moving from {transfer.position} to "
+ f"{transfer.destination}",
+ transfer.position.position,
+ )
+
+ 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:
+ return self.transfer.position.captured
+
+
class PendingTransfers:
def __init__(self) -> None:
self.pending_transfers: List[RoadTransferOrder] = []
diff --git a/gen/aircraft.py b/gen/aircraft.py
index 024a49c4..b4893704 100644
--- a/gen/aircraft.py
+++ b/gen/aircraft.py
@@ -72,7 +72,7 @@ from dcs.task import (
)
from dcs.terrain.terrain import Airport, NoParkingSlotError
from dcs.triggers import Event, TriggerOnce, TriggerRule
-from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup
+from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup, VehicleGroup
from dcs.unittype import FlyingType, UnitType
from game import db
@@ -88,6 +88,7 @@ from game.theater.controlpoint import (
OffMapSpawn,
)
from game.theater.theatergroundobject import TheaterGroundObject
+from game.transfers import Convoy, RoadTransferOrder
from game.unitmap import UnitMap
from game.utils import Distance, meters, nautical_miles
from gen.ato import AirTaskingOrder, Package
@@ -1691,25 +1692,30 @@ class BaiIngressBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
+ # TODO: Add common "UnitGroupTarget" base type.
target_group = self.package.target
if isinstance(target_group, TheaterGroundObject):
- tgroup = self.mission.find_group(target_group.group_name)
- if tgroup is not None:
- task = AttackGroup(tgroup.id, weapon_type=WeaponType.Auto)
- task.params["attackQtyLimit"] = False
- task.params["directionEnabled"] = False
- task.params["altitudeEnabled"] = False
- task.params["groupAttack"] = True
- waypoint.tasks.append(task)
- else:
- logging.error(
- "Could not find group for BAI mission %s", target_group.group_name
- )
+ group_name = target_group.group_name
+ elif isinstance(target_group, Convoy):
+ group_name = target_group.transfer.name
else:
logging.error(
"Unexpected target type for BAI mission: %s",
target_group.__class__.__name__,
)
+ return waypoint
+
+ group = self.mission.find_group(group_name)
+ if group is None:
+ logging.error("Could not find group for BAI mission %s", group_name)
+ return waypoint
+
+ task = AttackGroup(group.id, weapon_type=WeaponType.Auto)
+ task.params["attackQtyLimit"] = False
+ task.params["directionEnabled"] = False
+ task.params["altitudeEnabled"] = False
+ task.params["groupAttack"] = True
+ waypoint.tasks.append(task)
return waypoint
diff --git a/gen/convoys.py b/gen/convoys.py
index 520902ea..9f021135 100644
--- a/gen/convoys.py
+++ b/gen/convoys.py
@@ -12,6 +12,7 @@ from dcs.unittype import VehicleType
from game.transfers import RoadTransferOrder
from game.unitmap import UnitMap
+from game.utils import kph
if TYPE_CHECKING:
from game import Game
@@ -26,24 +27,26 @@ class ConvoyGenerator:
def generate(self) -> None:
# Reset the count to make generation deterministic.
- self.count = itertools.count()
for transfer in self.game.transfers:
self.generate_convoy_for(transfer)
- def generate_convoy_for(self, transfer: RoadTransferOrder) -> None:
+ def generate_convoy_for(self, transfer: RoadTransferOrder) -> VehicleGroup:
next_hop = transfer.path()[0]
origin = transfer.position.convoy_spawns[next_hop]
destination = next_hop.convoy_spawns[transfer.position]
group = self._create_mixed_unit_group(
- f"Convoy {next(self.count)}",
+ transfer.name,
origin,
transfer.units,
transfer.player,
)
- group.add_waypoint(destination, move_formation=PointAction.OnRoad)
+ group.add_waypoint(
+ destination, speed=kph(40).kph, move_formation=PointAction.OnRoad
+ )
self.make_drivable(group)
self.unit_map.add_convoy_units(group, transfer)
+ return group
def _create_mixed_unit_group(
self,
diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py
index 85c2895a..d447e3ed 100644
--- a/gen/flights/flightplan.py
+++ b/gen/flights/flightplan.py
@@ -30,8 +30,8 @@ from game.theater import (
SamGroundObject,
TheaterGroundObject,
)
-from game.theater.supplyroutes import SupplyRouteLink
from game.theater.theatergroundobject import EwrGroundObject
+from game.transfers import Convoy
from game.utils import Distance, Speed, feet, meters, nautical_miles
from .closestairfields import ObjectiveDistanceCache
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
@@ -467,25 +467,6 @@ class CasFlightPlan(PatrollingFlightPlan):
return self.patrol_end
-@dataclass(frozen=True)
-class ConvoyInterdictionFlightPlan(PatrollingFlightPlan):
- takeoff: FlightWaypoint
- land: FlightWaypoint
- divert: Optional[FlightWaypoint]
-
- def iter_waypoints(self) -> Iterator[FlightWaypoint]:
- yield self.takeoff
- yield from self.nav_to
- yield from [
- self.patrol_start,
- self.patrol_end,
- ]
- yield from self.nav_from
- yield self.land
- if self.divert is not None:
- yield self.divert
-
-
@dataclass(frozen=True)
class TarCapFlightPlan(PatrollingFlightPlan):
takeoff: FlightWaypoint
@@ -1042,75 +1023,22 @@ class FlightPlanBuilder:
"""
location = self.package.target
- if isinstance(location, SupplyRouteLink):
- return self.generate_supply_route_bai(flight, location)
-
- if not isinstance(location, TheaterGroundObject):
- raise InvalidObjectiveLocation(flight.flight_type, location)
-
targets: List[StrikeTarget] = []
- for group in location.groups:
- if group.units:
- targets.append(StrikeTarget(f"{group.name} at {location.name}", group))
+ if isinstance(location, TheaterGroundObject):
+ for group in location.groups:
+ if group.units:
+ targets.append(
+ StrikeTarget(f"{group.name} at {location.name}", group)
+ )
+ elif isinstance(location, Convoy):
+ targets.append(StrikeTarget(location.name, location))
+ else:
+ raise InvalidObjectiveLocation(flight.flight_type, location)
return self.strike_flightplan(
flight, location, FlightWaypointType.INGRESS_BAI, targets
)
- def generate_supply_route_bai(
- self, flight: Flight, location: SupplyRouteLink
- ) -> ConvoyInterdictionFlightPlan:
- """Generates a BAI flight plan for attacking a supply route.
-
- These flight plans are extremely rough because we do not know where the roads
- are. For now they're mostly only usable by players. The flight plan includes a
- start and end patrol point matching the end points of the convoy's route and a
- 30 minute time on station. It is up to the player to find the target.
-
- Args:
- flight: The flight to generate the flight plan for.
- location: The supply route link to attack.
- """
-
- origin = self.package_airfield()
- a_dist = origin.distance_to(location.control_point_a)
- b_dist = origin.distance_to(location.control_point_b)
- if a_dist < b_dist:
- near = location.control_point_a
- far = location.control_point_b
- else:
- near = location.control_point_b
- far = location.control_point_a
-
- patrol_alt = meters(
- random.randint(
- int(self.doctrine.min_patrol_altitude.meters),
- int(self.doctrine.max_patrol_altitude.meters),
- )
- )
-
- builder = WaypointBuilder(flight, self.game, self.is_player)
- start, end = builder.convoy_search(near, far, patrol_alt)
-
- return ConvoyInterdictionFlightPlan(
- self.package,
- flight,
- takeoff=builder.takeoff(flight.departure),
- nav_to=builder.nav_path(
- flight.departure.position, near.position, patrol_alt
- ),
- nav_from=builder.nav_path(
- far.position, flight.arrival.position, patrol_alt
- ),
- patrol_start=start,
- patrol_end=end,
- land=builder.land(flight.arrival),
- divert=builder.divert(flight.divert),
- # Not relevant because player only.
- engagement_distance=meters(0),
- patrol_duration=timedelta(minutes=30),
- )
-
def generate_anti_ship(self, flight: Flight) -> StrikeFlightPlan:
"""Generates an anti-ship flight plan.
diff --git a/gen/flights/waypointbuilder.py b/gen/flights/waypointbuilder.py
index ad1db18a..4559ca20 100644
--- a/gen/flights/waypointbuilder.py
+++ b/gen/flights/waypointbuilder.py
@@ -16,6 +16,8 @@ from dcs.mapping import Point
from dcs.unit import Unit
from dcs.unitgroup import Group, VehicleGroup
+from game.transfers import Convoy
+
if TYPE_CHECKING:
from game import Game
@@ -32,7 +34,7 @@ from .flight import Flight, FlightWaypoint, FlightWaypointType
@dataclass(frozen=True)
class StrikeTarget:
name: str
- target: Union[VehicleGroup, TheaterGroundObject, Unit, Group]
+ target: Union[VehicleGroup, TheaterGroundObject, Unit, Group, Convoy]
class WaypointBuilder:
@@ -349,63 +351,6 @@ class WaypointBuilder:
self.race_track_end(end, altitude),
)
- @staticmethod
- def convoy_search_start(
- control_point: ControlPoint, altitude: Distance
- ) -> FlightWaypoint:
- """Creates a convoy search start waypoint.
-
- Args:
- control_point: Control point for the beginning of the search.
- altitude: Altitude of the racetrack.
- """
- waypoint = FlightWaypoint(
- FlightWaypointType.INGRESS_BAI,
- control_point.position.x,
- control_point.position.y,
- altitude,
- )
- waypoint.name = control_point.name
- waypoint.description = "Beginning of convoy search area"
- waypoint.pretty_name = "Search start"
- return waypoint
-
- @staticmethod
- def convoy_search_end(
- control_point: ControlPoint, altitude: Distance
- ) -> FlightWaypoint:
- """Creates a convoy search start waypoint.
-
- Args:
- control_point: Control point for the beginning of the search.
- altitude: Altitude of the racetrack.
- """
- waypoint = FlightWaypoint(
- FlightWaypointType.EGRESS,
- control_point.position.x,
- control_point.position.y,
- altitude,
- )
- waypoint.name = control_point.name
- waypoint.description = "End of convoy search area"
- waypoint.pretty_name = "Search end"
- return waypoint
-
- def convoy_search(
- self, start: ControlPoint, end: ControlPoint, altitude: Distance
- ) -> Tuple[FlightWaypoint, FlightWaypoint]:
- """Creates two waypoint for a convoy search path.
-
- Args:
- start: The beginning convoy search waypoint.
- end: The ending convoy search waypoint.
- altitude: The convoy search altitude.
- """
- return (
- self.convoy_search_start(start, altitude),
- self.convoy_search_end(end, altitude),
- )
-
@staticmethod
def orbit(start: Point, altitude: Distance) -> FlightWaypoint:
"""Creates an circular orbit point.
@@ -463,7 +408,7 @@ class WaypointBuilder:
end: The end of the sweep.
altitude: The sweep altitude.
"""
- return (self.sweep_start(start, altitude), self.sweep_end(end, altitude))
+ return self.sweep_start(start, altitude), self.sweep_end(end, altitude)
def escort(
self, ingress: Point, target: MissionTarget, egress: Point
diff --git a/gen/naming.py b/gen/naming.py
index f1b14114..dcd09c2c 100644
--- a/gen/naming.py
+++ b/gen/naming.py
@@ -250,6 +250,7 @@ class NameGenerator:
number = 0
infantry_number = 0
aircraft_number = 0
+ convoy_number = 0
ANIMALS = ANIMALS
existing_alphas: List[str] = []
@@ -258,6 +259,7 @@ class NameGenerator:
def reset(cls):
cls.number = 0
cls.infantry_number = 0
+ cls.convoy_number = 0
cls.ANIMALS = ANIMALS
cls.existing_alphas = []
@@ -266,6 +268,7 @@ class NameGenerator:
cls.number = 0
cls.infantry_number = 0
cls.aircraft_number = 0
+ cls.convoy_number = 0
@classmethod
def next_aircraft_name(cls, country: Country, parent_base_id: int, flight: Flight):
@@ -327,6 +330,11 @@ class NameGenerator:
cls.number += 1
return "carrier|{}|{}|0|".format(country.id, cls.number)
+ @classmethod
+ def next_convoy_name(cls) -> str:
+ cls.convoy_number += 1
+ return f"Convoy {cls.convoy_number:04}"
+
@classmethod
def random_objective_name(cls):
if len(cls.ANIMALS) == 0:
diff --git a/qt_ui/widgets/QTopPanel.py b/qt_ui/widgets/QTopPanel.py
index f08e84e0..f43657f7 100644
--- a/qt_ui/widgets/QTopPanel.py
+++ b/qt_ui/widgets/QTopPanel.py
@@ -16,8 +16,6 @@ import qt_ui.uiconstants as CONST
from game import Game
from game.event.airwar import AirWarEvent
from gen.ato import Package
-from gen.flights.flight import FlightType
-from gen.flights.flightplan import ConvoyInterdictionFlightPlan
from gen.flights.traveltime import TotEstimator
from qt_ui.models import GameModel
from qt_ui.widgets.QBudgetBox import QBudgetBox
@@ -201,36 +199,6 @@ class QTopPanel(QFrame):
)
return result == QMessageBox.Yes
- def ato_has_ai_convoy_interdiction(self) -> bool:
- for package in self.game.blue_ato.packages:
- for flight in package.flights:
- if (
- isinstance(flight.flight_plan, ConvoyInterdictionFlightPlan)
- and not flight.client_count
- ):
- return True
- return False
-
- def confirm_ai_convoy_interdiction_launch(self) -> bool:
- result = QMessageBox.question(
- self,
- "Continue with AI convoy interdiction missions?",
- (
- "AI only convoy interdiction missions were planned. AI behavior for "
- "these missions has not been developed so they will probably get "
- "themselves killed. Continuing is not recommended.
"
- "
"
- "To remove AI convoy interdiction missions, delete any BAI flights "
- "that are planned against supply route objectives.
"
- "
"
- "Click 'Yes' to continue with AI only convoy interdiction missions."
- "
Click 'No' to cancel and revise your flight planning."
- ),
- QMessageBox.No,
- QMessageBox.Yes,
- )
- return result == QMessageBox.Yes
-
def confirm_negative_start_time(self, negative_starts: List[Package]) -> bool:
formatted = "
".join(
[f"{p.primary_task} {p.target.name}" for p in negative_starts]
@@ -273,12 +241,6 @@ class QTopPanel(QFrame):
if not self.ato_has_clients() and not self.confirm_no_client_launch():
return
- if (
- self.ato_has_ai_convoy_interdiction()
- and not self.confirm_ai_convoy_interdiction_launch()
- ):
- return
-
negative_starts = self.negative_start_packages()
if negative_starts:
if not self.confirm_negative_start_time(negative_starts):
diff --git a/qt_ui/widgets/map/SupplyRouteSegment.py b/qt_ui/widgets/map/SupplyRouteSegment.py
index 58fda738..68a23cfc 100644
--- a/qt_ui/widgets/map/SupplyRouteSegment.py
+++ b/qt_ui/widgets/map/SupplyRouteSegment.py
@@ -4,18 +4,12 @@ from typing import List, Optional
from PySide2.QtCore import Qt
from PySide2.QtGui import QColor, QPen
from PySide2.QtWidgets import (
- QAction,
QGraphicsItem,
QGraphicsLineItem,
- QGraphicsSceneContextMenuEvent,
- QGraphicsSceneHoverEvent,
- QMenu,
)
from game.theater import ControlPoint
-from game.theater.supplyroutes import SupplyRouteLink
from game.transfers import RoadTransferOrder
-from qt_ui.dialogs import Dialog
from qt_ui.uiconstants import COLORS
@@ -39,12 +33,16 @@ class SupplyRouteSegment(QGraphicsLineItem):
self.setToolTip(self.make_tooltip())
self.setAcceptHoverEvents(True)
+ @property
+ def has_convoys(self) -> bool:
+ return bool(self.convoys)
+
@cached_property
def convoy_size(self) -> int:
return sum(sum(c.units.values()) for c in self.convoys)
def make_tooltip(self) -> str:
- if not self.convoys:
+ if not self.has_convoys:
return "No convoys present on this supply route."
units = "units" if self.convoy_size > 1 else "unit"
@@ -77,37 +75,3 @@ class SupplyRouteSegment(QGraphicsLineItem):
pen.setStyle(self.line_style)
pen.setWidth(6)
return pen
-
- @property
- def has_convoys(self) -> bool:
- return bool(self.convoys)
-
- @property
- def targetable(self) -> bool:
- return self.convoys and not self.control_point_a.captured
-
- def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None:
- # Can only plan missions against enemy supply routes that have convoys.
- if not self.targetable:
- super().contextMenuEvent(event)
- return
-
- menu = QMenu("Menu")
-
- new_package_action = QAction(f"New package")
- new_package_action.triggered.connect(self.open_new_package_dialog)
- menu.addAction(new_package_action)
-
- menu.exec_(event.screenPos())
-
- def open_new_package_dialog(self) -> None:
- """Opens the dialog for planning a new mission package."""
- Dialog.open_new_package_dialog(
- SupplyRouteLink(self.control_point_a, self.control_point_b)
- )
-
- def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent):
- if self.targetable:
- self.setCursor(Qt.PointingHandCursor)
- else:
- super().hoverEnterEvent(event)
diff --git a/qt_ui/windows/basemenu/DepartingConvoysMenu.py b/qt_ui/windows/basemenu/DepartingConvoysMenu.py
new file mode 100644
index 00000000..193aafdd
--- /dev/null
+++ b/qt_ui/windows/basemenu/DepartingConvoysMenu.py
@@ -0,0 +1,101 @@
+from PySide2.QtCore import Qt
+from PySide2.QtWidgets import (
+ QFrame,
+ QGridLayout,
+ QGroupBox,
+ QLabel,
+ QPushButton,
+ QScrollArea,
+ QVBoxLayout,
+ QWidget,
+)
+
+from game import db
+from game.theater import ControlPoint
+from game.transfers import Convoy, RoadTransferOrder
+from qt_ui.dialogs import Dialog
+from qt_ui.models import GameModel
+from qt_ui.uiconstants import VEHICLES_ICONS
+
+
+class DepartingConvoyInfo(QGroupBox):
+ def __init__(self, convoy: RoadTransferOrder, game_model: GameModel) -> None:
+ super().__init__(f"To {convoy.destination}")
+ self.convoy = convoy
+
+ main_layout = QVBoxLayout()
+ self.setLayout(main_layout)
+
+ unit_layout = QGridLayout()
+ main_layout.addLayout(unit_layout)
+
+ for idx, (unit_type, count) in enumerate(convoy.units.items()):
+ icon = QLabel()
+ if unit_type.id in VEHICLES_ICONS.keys():
+ icon.setPixmap(VEHICLES_ICONS[unit_type.id])
+ else:
+ icon.setText("" + unit_type.id[:8] + "")
+ icon.setProperty("style", "icon-armor")
+ unit_layout.addWidget(icon, idx, 0)
+ unit_display_name = db.unit_get_expanded_info(
+ game_model.game.enemy_country, unit_type, "name"
+ )
+ unit_layout.addWidget(
+ QLabel(f"{count} x {unit_display_name}"),
+ idx,
+ 1,
+ )
+
+ if not convoy.units:
+ unit_layout.addWidget(QLabel("/"), 0, 0)
+
+ attack_button = QPushButton("Attack")
+ attack_button.setProperty("style", "btn-danger")
+ attack_button.setMaximumWidth(180)
+ attack_button.clicked.connect(self.on_attack)
+ main_layout.addWidget(attack_button, 0, Qt.AlignLeft)
+
+ def on_attack(self):
+ # TODO: Maintain Convoy list in Game.
+ # The fact that we create these here makes some of the other bookkeeping
+ # complicated. We could instead generate this at the start of the turn (and
+ # update whenever transfers are created or canceled) and also use that time to
+ # precalculate things like the next stop and group names.
+ Dialog.open_new_package_dialog(Convoy(self.convoy), parent=self.window())
+
+
+class DepartingConvoysList(QFrame):
+ def __init__(self, cp: ControlPoint, game_model: GameModel):
+ super().__init__()
+ self.cp = cp
+ self.game_model = game_model
+ self.setMinimumWidth(500)
+
+ layout = QVBoxLayout()
+ self.setLayout(layout)
+
+ scroll_content = QWidget()
+ task_box_layout = QGridLayout()
+ scroll_content.setLayout(task_box_layout)
+
+ for convoy in game_model.game.transfers:
+ if convoy.position != cp:
+ continue
+ group_info = DepartingConvoyInfo(convoy, game_model)
+ task_box_layout.addWidget(group_info)
+
+ scroll_content.setLayout(task_box_layout)
+ scroll = QScrollArea()
+ scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+ scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
+ scroll.setWidgetResizable(True)
+ scroll.setWidget(scroll_content)
+ layout.addWidget(scroll)
+
+
+class DepartingConvoysMenu(QFrame):
+ def __init__(self, cp: ControlPoint, game_model: GameModel):
+ super().__init__()
+ layout = QVBoxLayout()
+ layout.addWidget(DepartingConvoysList(cp, game_model))
+ self.setLayout(layout)
diff --git a/qt_ui/windows/basemenu/QBaseMenuTabs.py b/qt_ui/windows/basemenu/QBaseMenuTabs.py
index c6c35603..fe80dbe3 100644
--- a/qt_ui/windows/basemenu/QBaseMenuTabs.py
+++ b/qt_ui/windows/basemenu/QBaseMenuTabs.py
@@ -2,6 +2,7 @@ from PySide2.QtWidgets import QTabWidget
from game.theater import ControlPoint, OffMapSpawn, Fob
from qt_ui.models import GameModel
+from qt_ui.windows.basemenu.DepartingConvoysMenu import DepartingConvoysMenu
from qt_ui.windows.basemenu.airfield.QAirfieldCommand import QAirfieldCommand
from qt_ui.windows.basemenu.base_defenses.QBaseDefensesHQ import QBaseDefensesHQ
from qt_ui.windows.basemenu.ground_forces.QGroundForcesHQ import QGroundForcesHQ
@@ -18,27 +19,28 @@ class QBaseMenuTabs(QTabWidget):
self.addTab(self.base_defenses_hq, "Base Defenses")
self.intel = QIntelInfo(cp, game_model.game)
self.addTab(self.intel, "Intel")
+
+ self.departing_convoys = DepartingConvoysMenu(cp, game_model)
+ self.addTab(self.departing_convoys, "Departing Convoys")
+ return
+
+ if isinstance(cp, Fob):
+ self.ground_forces_hq = QGroundForcesHQ(cp, game_model)
+ self.addTab(self.ground_forces_hq, "Ground Forces HQ")
+ if cp.helipads:
+ self.airfield_command = QAirfieldCommand(cp, game_model)
+ self.addTab(self.airfield_command, "Heliport")
+ self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
+ self.addTab(self.base_defenses_hq, "Base Defenses")
else:
+ self.airfield_command = QAirfieldCommand(cp, game_model)
+ self.addTab(self.airfield_command, "Airfield Command")
- if cp:
- if isinstance(cp, Fob):
- self.ground_forces_hq = QGroundForcesHQ(cp, game_model)
- self.addTab(self.ground_forces_hq, "Ground Forces HQ")
- if cp.helipads:
- self.airfield_command = QAirfieldCommand(cp, game_model)
- self.addTab(self.airfield_command, "Heliport")
- self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
- self.addTab(self.base_defenses_hq, "Base Defenses")
- else:
-
- self.airfield_command = QAirfieldCommand(cp, game_model)
- self.addTab(self.airfield_command, "Airfield Command")
-
- if cp.is_carrier:
- self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
- self.addTab(self.base_defenses_hq, "Fleet")
- elif not isinstance(cp, OffMapSpawn):
- self.ground_forces_hq = QGroundForcesHQ(cp, game_model)
- self.addTab(self.ground_forces_hq, "Ground Forces HQ")
- self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
- self.addTab(self.base_defenses_hq, "Base Defenses")
+ if cp.is_carrier:
+ self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
+ self.addTab(self.base_defenses_hq, "Fleet")
+ elif not isinstance(cp, OffMapSpawn):
+ self.ground_forces_hq = QGroundForcesHQ(cp, game_model)
+ self.addTab(self.ground_forces_hq, "Ground Forces HQ")
+ self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
+ self.addTab(self.base_defenses_hq, "Base Defenses")