mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Redo convoy attack flight plans.
The previous flight plan only makes sense if the convoy will make it a significant distance from its starting point. At road speeds over the typical mission duration this is not true, so we can actually plan this as if it was a strike mission near the origin point and that's close enough. There's some cleanup work to do here that I've added todos for. Fixes https://github.com/Khopa/dcs_liberation/issues/996
This commit is contained in:
parent
3f16c0378a
commit
50d8e08a34
@ -1,10 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from game.theater.theatergroundobject import TheaterGroundObject
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
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 import Mission
|
||||||
from dcs.action import DoScript, DoScriptFile
|
from dcs.action import DoScript, DoScriptFile
|
||||||
@ -14,7 +13,9 @@ from dcs.lua.parse import loads
|
|||||||
from dcs.mapping import Point
|
from dcs.mapping import Point
|
||||||
from dcs.translation import String
|
from dcs.translation import String
|
||||||
from dcs.triggers import TriggerStart
|
from dcs.triggers import TriggerStart
|
||||||
|
|
||||||
from game.plugins import LuaPluginManager
|
from game.plugins import LuaPluginManager
|
||||||
|
from game.theater.theatergroundobject import TheaterGroundObject
|
||||||
from gen import Conflict, FlightType, VisualGenerator
|
from gen import Conflict, FlightType, VisualGenerator
|
||||||
from gen.aircraft import AIRCRAFT_DATA, AircraftConflictGenerator, FlightData
|
from gen.aircraft import AIRCRAFT_DATA, AircraftConflictGenerator, FlightData
|
||||||
from gen.airfields import AIRFIELD_DATA
|
from gen.airfields import AIRFIELD_DATA
|
||||||
@ -31,7 +32,6 @@ from gen.naming import namegen
|
|||||||
from gen.radios import RadioFrequency, RadioRegistry
|
from gen.radios import RadioFrequency, RadioRegistry
|
||||||
from gen.tacan import TacanRegistry
|
from gen.tacan import TacanRegistry
|
||||||
from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator
|
from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator
|
||||||
|
|
||||||
from .. import db
|
from .. import db
|
||||||
from ..theater import Airfield
|
from ..theater import Airfield
|
||||||
from ..unitmap import UnitMap
|
from ..unitmap import UnitMap
|
||||||
@ -43,18 +43,13 @@ if TYPE_CHECKING:
|
|||||||
class Operation:
|
class Operation:
|
||||||
"""Static class for managing the final Mission generation"""
|
"""Static class for managing the final Mission generation"""
|
||||||
|
|
||||||
current_mission = None # type: Mission
|
current_mission: Mission
|
||||||
airgen = None # type: AircraftConflictGenerator
|
airgen: AircraftConflictGenerator
|
||||||
triggersgen = None # type: TriggersGenerator
|
airsupportgen: AirSupportConflictGenerator
|
||||||
airsupportgen = None # type: AirSupportConflictGenerator
|
groundobjectgen: GroundObjectsGenerator
|
||||||
visualgen = None # type: VisualGenerator
|
radio_registry: RadioRegistry
|
||||||
groundobjectgen = None # type: GroundObjectsGenerator
|
tacan_registry: TacanRegistry
|
||||||
briefinggen = None # type: BriefingGenerator
|
game: Game
|
||||||
forcedoptionsgen = None # type: ForcedOptionsGenerator
|
|
||||||
radio_registry: Optional[RadioRegistry] = None
|
|
||||||
tacan_registry: Optional[TacanRegistry] = None
|
|
||||||
game = None # type: Game
|
|
||||||
environment_settings = None
|
|
||||||
trigger_radius = TRIGGER_RADIUS_MEDIUM
|
trigger_radius = TRIGGER_RADIUS_MEDIUM
|
||||||
is_quick = None
|
is_quick = None
|
||||||
player_awacs_enabled = True
|
player_awacs_enabled = True
|
||||||
@ -309,13 +304,13 @@ class Operation:
|
|||||||
# Set mission time and weather conditions.
|
# Set mission time and weather conditions.
|
||||||
EnvironmentGenerator(cls.current_mission, cls.game.conditions).generate()
|
EnvironmentGenerator(cls.current_mission, cls.game.conditions).generate()
|
||||||
cls._generate_ground_units()
|
cls._generate_ground_units()
|
||||||
|
cls._generate_convoys()
|
||||||
cls._generate_destroyed_units()
|
cls._generate_destroyed_units()
|
||||||
cls._generate_air_units()
|
cls._generate_air_units()
|
||||||
cls.assign_channels_to_flights(
|
cls.assign_channels_to_flights(
|
||||||
cls.airgen.flights, cls.airsupportgen.air_support
|
cls.airgen.flights, cls.airsupportgen.air_support
|
||||||
)
|
)
|
||||||
cls._generate_ground_conflicts()
|
cls._generate_ground_conflicts()
|
||||||
cls._generate_convoys()
|
|
||||||
|
|
||||||
# Triggers
|
# Triggers
|
||||||
triggersgen = TriggersGenerator(cls.current_mission, cls.game)
|
triggersgen = TriggersGenerator(cls.current_mission, cls.game)
|
||||||
|
|||||||
@ -6,8 +6,6 @@ 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, Iterator, List, Optional
|
||||||
|
|
||||||
from dcs import Point
|
|
||||||
from game.theater import FlightType, MissionTarget
|
|
||||||
from game.theater.controlpoint import ControlPoint
|
from game.theater.controlpoint import ControlPoint
|
||||||
|
|
||||||
|
|
||||||
@ -99,25 +97,3 @@ class SupplyRoute:
|
|||||||
current = previous
|
current = previous
|
||||||
path.reverse()
|
path.reverse()
|
||||||
return path
|
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
|
|
||||||
|
|||||||
@ -3,8 +3,10 @@ from dataclasses import dataclass, field
|
|||||||
from typing import Dict, Iterator, List, Type
|
from typing import Dict, Iterator, List, Type
|
||||||
|
|
||||||
from dcs.unittype import VehicleType
|
from dcs.unittype import VehicleType
|
||||||
from game.theater import ControlPoint
|
from game.theater import ControlPoint, MissionTarget
|
||||||
from game.theater.supplyroutes import SupplyRoute
|
from game.theater.supplyroutes import SupplyRoute
|
||||||
|
from gen.naming import namegen
|
||||||
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -35,6 +37,8 @@ class RoadTransferOrder(TransferOrder):
|
|||||||
#: point a turn through the supply line.
|
#: point a turn through the supply line.
|
||||||
position: ControlPoint = field(init=False)
|
position: ControlPoint = field(init=False)
|
||||||
|
|
||||||
|
name: str = field(init=False, default_factory=namegen.next_convoy_name)
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
self.position = self.origin
|
self.position = self.origin
|
||||||
|
|
||||||
@ -46,6 +50,27 @@ class RoadTransferOrder(TransferOrder):
|
|||||||
return self.path()[0]
|
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:
|
class PendingTransfers:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.pending_transfers: List[RoadTransferOrder] = []
|
self.pending_transfers: List[RoadTransferOrder] = []
|
||||||
|
|||||||
@ -72,7 +72,7 @@ from dcs.task import (
|
|||||||
)
|
)
|
||||||
from dcs.terrain.terrain import Airport, NoParkingSlotError
|
from dcs.terrain.terrain import Airport, NoParkingSlotError
|
||||||
from dcs.triggers import Event, TriggerOnce, TriggerRule
|
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 dcs.unittype import FlyingType, UnitType
|
||||||
|
|
||||||
from game import db
|
from game import db
|
||||||
@ -88,6 +88,7 @@ from game.theater.controlpoint import (
|
|||||||
OffMapSpawn,
|
OffMapSpawn,
|
||||||
)
|
)
|
||||||
from game.theater.theatergroundobject import TheaterGroundObject
|
from game.theater.theatergroundobject import TheaterGroundObject
|
||||||
|
from game.transfers import Convoy, RoadTransferOrder
|
||||||
from game.unitmap import UnitMap
|
from game.unitmap import UnitMap
|
||||||
from game.utils import Distance, meters, nautical_miles
|
from game.utils import Distance, meters, nautical_miles
|
||||||
from gen.ato import AirTaskingOrder, Package
|
from gen.ato import AirTaskingOrder, Package
|
||||||
@ -1691,25 +1692,30 @@ class BaiIngressBuilder(PydcsWaypointBuilder):
|
|||||||
def build(self) -> MovingPoint:
|
def build(self) -> MovingPoint:
|
||||||
waypoint = super().build()
|
waypoint = super().build()
|
||||||
|
|
||||||
|
# TODO: Add common "UnitGroupTarget" base type.
|
||||||
target_group = self.package.target
|
target_group = self.package.target
|
||||||
if isinstance(target_group, TheaterGroundObject):
|
if isinstance(target_group, TheaterGroundObject):
|
||||||
tgroup = self.mission.find_group(target_group.group_name)
|
group_name = target_group.group_name
|
||||||
if tgroup is not None:
|
elif isinstance(target_group, Convoy):
|
||||||
task = AttackGroup(tgroup.id, weapon_type=WeaponType.Auto)
|
group_name = target_group.transfer.name
|
||||||
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
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
logging.error(
|
logging.error(
|
||||||
"Unexpected target type for BAI mission: %s",
|
"Unexpected target type for BAI mission: %s",
|
||||||
target_group.__class__.__name__,
|
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
|
return waypoint
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,7 @@ from dcs.unittype import VehicleType
|
|||||||
|
|
||||||
from game.transfers import RoadTransferOrder
|
from game.transfers import RoadTransferOrder
|
||||||
from game.unitmap import UnitMap
|
from game.unitmap import UnitMap
|
||||||
|
from game.utils import kph
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game import Game
|
from game import Game
|
||||||
@ -26,24 +27,26 @@ class ConvoyGenerator:
|
|||||||
|
|
||||||
def generate(self) -> None:
|
def generate(self) -> None:
|
||||||
# Reset the count to make generation deterministic.
|
# Reset the count to make generation deterministic.
|
||||||
self.count = itertools.count()
|
|
||||||
for transfer in self.game.transfers:
|
for transfer in self.game.transfers:
|
||||||
self.generate_convoy_for(transfer)
|
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]
|
next_hop = transfer.path()[0]
|
||||||
origin = transfer.position.convoy_spawns[next_hop]
|
origin = transfer.position.convoy_spawns[next_hop]
|
||||||
destination = next_hop.convoy_spawns[transfer.position]
|
destination = next_hop.convoy_spawns[transfer.position]
|
||||||
|
|
||||||
group = self._create_mixed_unit_group(
|
group = self._create_mixed_unit_group(
|
||||||
f"Convoy {next(self.count)}",
|
transfer.name,
|
||||||
origin,
|
origin,
|
||||||
transfer.units,
|
transfer.units,
|
||||||
transfer.player,
|
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.make_drivable(group)
|
||||||
self.unit_map.add_convoy_units(group, transfer)
|
self.unit_map.add_convoy_units(group, transfer)
|
||||||
|
return group
|
||||||
|
|
||||||
def _create_mixed_unit_group(
|
def _create_mixed_unit_group(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@ -30,8 +30,8 @@ from game.theater import (
|
|||||||
SamGroundObject,
|
SamGroundObject,
|
||||||
TheaterGroundObject,
|
TheaterGroundObject,
|
||||||
)
|
)
|
||||||
from game.theater.supplyroutes import SupplyRouteLink
|
|
||||||
from game.theater.theatergroundobject import EwrGroundObject
|
from game.theater.theatergroundobject import EwrGroundObject
|
||||||
|
from game.transfers import Convoy
|
||||||
from game.utils import Distance, Speed, feet, meters, nautical_miles
|
from game.utils import Distance, Speed, feet, meters, nautical_miles
|
||||||
from .closestairfields import ObjectiveDistanceCache
|
from .closestairfields import ObjectiveDistanceCache
|
||||||
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
|
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
|
||||||
@ -467,25 +467,6 @@ class CasFlightPlan(PatrollingFlightPlan):
|
|||||||
return self.patrol_end
|
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)
|
@dataclass(frozen=True)
|
||||||
class TarCapFlightPlan(PatrollingFlightPlan):
|
class TarCapFlightPlan(PatrollingFlightPlan):
|
||||||
takeoff: FlightWaypoint
|
takeoff: FlightWaypoint
|
||||||
@ -1042,75 +1023,22 @@ class FlightPlanBuilder:
|
|||||||
"""
|
"""
|
||||||
location = self.package.target
|
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] = []
|
targets: List[StrikeTarget] = []
|
||||||
for group in location.groups:
|
if isinstance(location, TheaterGroundObject):
|
||||||
if group.units:
|
for group in location.groups:
|
||||||
targets.append(StrikeTarget(f"{group.name} at {location.name}", group))
|
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(
|
return self.strike_flightplan(
|
||||||
flight, location, FlightWaypointType.INGRESS_BAI, targets
|
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:
|
def generate_anti_ship(self, flight: Flight) -> StrikeFlightPlan:
|
||||||
"""Generates an anti-ship flight plan.
|
"""Generates an anti-ship flight plan.
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,8 @@ from dcs.mapping import Point
|
|||||||
from dcs.unit import Unit
|
from dcs.unit import Unit
|
||||||
from dcs.unitgroup import Group, VehicleGroup
|
from dcs.unitgroup import Group, VehicleGroup
|
||||||
|
|
||||||
|
from game.transfers import Convoy
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game import Game
|
from game import Game
|
||||||
|
|
||||||
@ -32,7 +34,7 @@ from .flight import Flight, FlightWaypoint, FlightWaypointType
|
|||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class StrikeTarget:
|
class StrikeTarget:
|
||||||
name: str
|
name: str
|
||||||
target: Union[VehicleGroup, TheaterGroundObject, Unit, Group]
|
target: Union[VehicleGroup, TheaterGroundObject, Unit, Group, Convoy]
|
||||||
|
|
||||||
|
|
||||||
class WaypointBuilder:
|
class WaypointBuilder:
|
||||||
@ -349,63 +351,6 @@ class WaypointBuilder:
|
|||||||
self.race_track_end(end, altitude),
|
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
|
@staticmethod
|
||||||
def orbit(start: Point, altitude: Distance) -> FlightWaypoint:
|
def orbit(start: Point, altitude: Distance) -> FlightWaypoint:
|
||||||
"""Creates an circular orbit point.
|
"""Creates an circular orbit point.
|
||||||
@ -463,7 +408,7 @@ class WaypointBuilder:
|
|||||||
end: The end of the sweep.
|
end: The end of the sweep.
|
||||||
altitude: The sweep altitude.
|
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(
|
def escort(
|
||||||
self, ingress: Point, target: MissionTarget, egress: Point
|
self, ingress: Point, target: MissionTarget, egress: Point
|
||||||
|
|||||||
@ -250,6 +250,7 @@ class NameGenerator:
|
|||||||
number = 0
|
number = 0
|
||||||
infantry_number = 0
|
infantry_number = 0
|
||||||
aircraft_number = 0
|
aircraft_number = 0
|
||||||
|
convoy_number = 0
|
||||||
|
|
||||||
ANIMALS = ANIMALS
|
ANIMALS = ANIMALS
|
||||||
existing_alphas: List[str] = []
|
existing_alphas: List[str] = []
|
||||||
@ -258,6 +259,7 @@ class NameGenerator:
|
|||||||
def reset(cls):
|
def reset(cls):
|
||||||
cls.number = 0
|
cls.number = 0
|
||||||
cls.infantry_number = 0
|
cls.infantry_number = 0
|
||||||
|
cls.convoy_number = 0
|
||||||
cls.ANIMALS = ANIMALS
|
cls.ANIMALS = ANIMALS
|
||||||
cls.existing_alphas = []
|
cls.existing_alphas = []
|
||||||
|
|
||||||
@ -266,6 +268,7 @@ class NameGenerator:
|
|||||||
cls.number = 0
|
cls.number = 0
|
||||||
cls.infantry_number = 0
|
cls.infantry_number = 0
|
||||||
cls.aircraft_number = 0
|
cls.aircraft_number = 0
|
||||||
|
cls.convoy_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):
|
||||||
@ -327,6 +330,11 @@ class NameGenerator:
|
|||||||
cls.number += 1
|
cls.number += 1
|
||||||
return "carrier|{}|{}|0|".format(country.id, cls.number)
|
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
|
@classmethod
|
||||||
def random_objective_name(cls):
|
def random_objective_name(cls):
|
||||||
if len(cls.ANIMALS) == 0:
|
if len(cls.ANIMALS) == 0:
|
||||||
|
|||||||
@ -16,8 +16,6 @@ import qt_ui.uiconstants as CONST
|
|||||||
from game import Game
|
from game import Game
|
||||||
from game.event.airwar import AirWarEvent
|
from game.event.airwar import AirWarEvent
|
||||||
from gen.ato import Package
|
from gen.ato import Package
|
||||||
from gen.flights.flight import FlightType
|
|
||||||
from gen.flights.flightplan import ConvoyInterdictionFlightPlan
|
|
||||||
from gen.flights.traveltime import TotEstimator
|
from gen.flights.traveltime import TotEstimator
|
||||||
from qt_ui.models import GameModel
|
from qt_ui.models import GameModel
|
||||||
from qt_ui.widgets.QBudgetBox import QBudgetBox
|
from qt_ui.widgets.QBudgetBox import QBudgetBox
|
||||||
@ -201,36 +199,6 @@ class QTopPanel(QFrame):
|
|||||||
)
|
)
|
||||||
return result == QMessageBox.Yes
|
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.<br />"
|
|
||||||
"<br />"
|
|
||||||
"To remove AI convoy interdiction missions, delete any BAI flights "
|
|
||||||
"that are planned against supply route objectives.<br />"
|
|
||||||
"<br />"
|
|
||||||
"Click 'Yes' to continue with AI only convoy interdiction missions."
|
|
||||||
"<br /><br />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:
|
def confirm_negative_start_time(self, negative_starts: List[Package]) -> bool:
|
||||||
formatted = "<br />".join(
|
formatted = "<br />".join(
|
||||||
[f"{p.primary_task} {p.target.name}" for p in negative_starts]
|
[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():
|
if not self.ato_has_clients() and not self.confirm_no_client_launch():
|
||||||
return
|
return
|
||||||
|
|
||||||
if (
|
|
||||||
self.ato_has_ai_convoy_interdiction()
|
|
||||||
and not self.confirm_ai_convoy_interdiction_launch()
|
|
||||||
):
|
|
||||||
return
|
|
||||||
|
|
||||||
negative_starts = self.negative_start_packages()
|
negative_starts = self.negative_start_packages()
|
||||||
if negative_starts:
|
if negative_starts:
|
||||||
if not self.confirm_negative_start_time(negative_starts):
|
if not self.confirm_negative_start_time(negative_starts):
|
||||||
|
|||||||
@ -4,18 +4,12 @@ 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
|
||||||
from PySide2.QtWidgets import (
|
from PySide2.QtWidgets import (
|
||||||
QAction,
|
|
||||||
QGraphicsItem,
|
QGraphicsItem,
|
||||||
QGraphicsLineItem,
|
QGraphicsLineItem,
|
||||||
QGraphicsSceneContextMenuEvent,
|
|
||||||
QGraphicsSceneHoverEvent,
|
|
||||||
QMenu,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from game.theater import ControlPoint
|
from game.theater import ControlPoint
|
||||||
from game.theater.supplyroutes import SupplyRouteLink
|
|
||||||
from game.transfers import RoadTransferOrder
|
from game.transfers import RoadTransferOrder
|
||||||
from qt_ui.dialogs import Dialog
|
|
||||||
from qt_ui.uiconstants import COLORS
|
from qt_ui.uiconstants import COLORS
|
||||||
|
|
||||||
|
|
||||||
@ -39,12 +33,16 @@ class SupplyRouteSegment(QGraphicsLineItem):
|
|||||||
self.setToolTip(self.make_tooltip())
|
self.setToolTip(self.make_tooltip())
|
||||||
self.setAcceptHoverEvents(True)
|
self.setAcceptHoverEvents(True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_convoys(self) -> bool:
|
||||||
|
return bool(self.convoys)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def convoy_size(self) -> int:
|
def convoy_size(self) -> int:
|
||||||
return sum(sum(c.units.values()) for c in self.convoys)
|
return sum(sum(c.units.values()) for c in self.convoys)
|
||||||
|
|
||||||
def make_tooltip(self) -> str:
|
def make_tooltip(self) -> str:
|
||||||
if not self.convoys:
|
if not self.has_convoys:
|
||||||
return "No convoys present on this supply route."
|
return "No convoys present on this supply route."
|
||||||
units = "units" if self.convoy_size > 1 else "unit"
|
units = "units" if self.convoy_size > 1 else "unit"
|
||||||
|
|
||||||
@ -77,37 +75,3 @@ class SupplyRouteSegment(QGraphicsLineItem):
|
|||||||
pen.setStyle(self.line_style)
|
pen.setStyle(self.line_style)
|
||||||
pen.setWidth(6)
|
pen.setWidth(6)
|
||||||
return pen
|
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)
|
|
||||||
|
|||||||
101
qt_ui/windows/basemenu/DepartingConvoysMenu.py
Normal file
101
qt_ui/windows/basemenu/DepartingConvoysMenu.py
Normal file
@ -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("<b>" + unit_type.id[:8] + "</b>")
|
||||||
|
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 <strong>{unit_display_name}</strong>"),
|
||||||
|
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)
|
||||||
@ -2,6 +2,7 @@ from PySide2.QtWidgets import QTabWidget
|
|||||||
|
|
||||||
from game.theater import ControlPoint, OffMapSpawn, Fob
|
from game.theater import ControlPoint, OffMapSpawn, Fob
|
||||||
from qt_ui.models import GameModel
|
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.airfield.QAirfieldCommand import QAirfieldCommand
|
||||||
from qt_ui.windows.basemenu.base_defenses.QBaseDefensesHQ import QBaseDefensesHQ
|
from qt_ui.windows.basemenu.base_defenses.QBaseDefensesHQ import QBaseDefensesHQ
|
||||||
from qt_ui.windows.basemenu.ground_forces.QGroundForcesHQ import QGroundForcesHQ
|
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.addTab(self.base_defenses_hq, "Base Defenses")
|
||||||
self.intel = QIntelInfo(cp, game_model.game)
|
self.intel = QIntelInfo(cp, game_model.game)
|
||||||
self.addTab(self.intel, "Intel")
|
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:
|
else:
|
||||||
|
self.airfield_command = QAirfieldCommand(cp, game_model)
|
||||||
|
self.addTab(self.airfield_command, "Airfield Command")
|
||||||
|
|
||||||
if cp:
|
if cp.is_carrier:
|
||||||
if isinstance(cp, Fob):
|
self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
|
||||||
self.ground_forces_hq = QGroundForcesHQ(cp, game_model)
|
self.addTab(self.base_defenses_hq, "Fleet")
|
||||||
self.addTab(self.ground_forces_hq, "Ground Forces HQ")
|
elif not isinstance(cp, OffMapSpawn):
|
||||||
if cp.helipads:
|
self.ground_forces_hq = QGroundForcesHQ(cp, game_model)
|
||||||
self.airfield_command = QAirfieldCommand(cp, game_model)
|
self.addTab(self.ground_forces_hq, "Ground Forces HQ")
|
||||||
self.addTab(self.airfield_command, "Heliport")
|
self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
|
||||||
self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
|
self.addTab(self.base_defenses_hq, "Base Defenses")
|
||||||
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")
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user