Add off-map spawn locations.

The AI isn't making use of these yet, but it's not smart enough to do so
anyway.

Would benefit from an icon to differentiate it on the map.

I'm stretching the definition of "control point" quite a bit. We might
want to put a class above `ControlPoint` for `AirSpawnLocation` to
represent types of spawn locations that can't be captured and don't have
ground objectives.

Fixes https://github.com/Khopa/dcs_liberation/issues/274
This commit is contained in:
Dan Albert 2020-11-20 01:58:55 -08:00
parent 4e910c4b09
commit 18b6f7b84c
15 changed files with 178 additions and 99 deletions

View File

@ -26,7 +26,7 @@ from .event.frontlineattack import FrontlineAttackEvent
from .factions.faction import Faction
from .infos.information import Information
from .settings import Settings
from .theater import ConflictTheater, ControlPoint
from .theater import ConflictTheater, ControlPoint, OffMapSpawn
from .weather import Conditions, TimeOfDay
COMMISION_UNIT_VARIETY = 4
@ -289,6 +289,9 @@ class Game:
if len(potential_cp_armor) == 0:
potential_cp_armor = self.theater.enemy_points()
potential_cp_armor = [p for p in potential_cp_armor if
not isinstance(p, OffMapSpawn)]
i = 0
potential_units = db.FACTIONS[self.enemy_name].frontline_units

View File

@ -16,6 +16,7 @@ from dcs.countries import (
)
from dcs.country import Country
from dcs.mapping import Point
from dcs.planes import F_15C
from dcs.ships import (
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
@ -31,11 +32,17 @@ from dcs.terrain import (
thechannel,
)
from dcs.terrain.terrain import Airport, Terrain
from dcs.unitgroup import Group, ShipGroup, StaticGroup, VehicleGroup
from dcs.unitgroup import (
FlyingGroup,
Group,
ShipGroup,
StaticGroup,
VehicleGroup,
)
from dcs.vehicles import AirDefence, Armor
from gen.flights.flight import FlightType
from .controlpoint import ControlPoint, MissionTarget
from .controlpoint import ControlPoint, MissionTarget, OffMapSpawn
from .landmap import Landmap, load_landmap, poly_contains
from ..utils import nm_to_meter
@ -94,6 +101,8 @@ class MizCampaignLoader:
BLUE_COUNTRY = CombinedJointTaskForcesBlue()
RED_COUNTRY = CombinedJointTaskForcesRed()
OFF_MAP_UNIT_TYPE = F_15C.id
CV_UNIT_TYPE = CVN_74_John_C__Stennis.id
LHA_UNIT_TYPE = LHA_1_Tarawa.id
FRONT_LINE_UNIT_TYPE = Armor.APC_M113.id
@ -175,6 +184,11 @@ class MizCampaignLoader:
def red(self) -> Country:
return self.country(blue=False)
def off_map_spawns(self, blue: bool) -> Iterator[FlyingGroup]:
for group in self.country(blue).plane_group:
if group.units[0].type == self.OFF_MAP_UNIT_TYPE:
yield group
def carriers(self, blue: bool) -> Iterator[ShipGroup]:
for group in self.country(blue).ship_group:
if group.units[0].type == self.CV_UNIT_TYPE:
@ -236,6 +250,12 @@ class MizCampaignLoader:
control_points[control_point.id] = control_point
for blue in (False, True):
for group in self.off_map_spawns(blue):
control_point = OffMapSpawn(next(self.control_point_id),
str(group.name), group.position)
control_point.captured = blue
control_point.captured_invert = group.late_activation
control_points[control_point.id] = control_point
for group in self.carriers(blue):
# TODO: Name the carrier.
control_point = ControlPoint.carrier(

View File

@ -40,6 +40,7 @@ class ControlPointType(Enum):
LHA_GROUP = 2 # A group with a Tarawa carrier (Helicopters & Harrier)
FARP = 4 # A FARP, with slots for helicopters
FOB = 5 # A FOB (ground units only)
OFF_MAP = 6
class LocationType(Enum):
@ -364,3 +365,17 @@ class ControlPoint(MissionTarget):
yield from [
# TODO: FlightType.STRIKE
]
class OffMapSpawn(ControlPoint):
def __init__(self, id: int, name: str, position: Point):
from . import IMPORTANCE_MEDIUM, SIZE_REGULAR
super().__init__(id, name, position, at=position, radials=[],
size=SIZE_REGULAR, importance=IMPORTANCE_MEDIUM,
has_frontline=False, cptype=ControlPointType.OFF_MAP)
def capture(self, game: Game, for_player: bool) -> None:
raise RuntimeError("Off map control points cannot be captured")
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
yield from []

View File

@ -42,8 +42,7 @@ from gen.sam.sam_group_generator import (
from theater import (
ConflictTheater,
ControlPoint,
ControlPointType,
TheaterGroundObject,
ControlPointType, OffMapSpawn,
)
GroundObjectTemplates = Dict[str, Dict[str, Any]]
@ -139,7 +138,13 @@ class GameGenerator:
control_point.base.commision_points = {}
control_point.base.strength = 1
# The tasks here are confusing. PinpointStrike for some reason means
# ground units.
for task in [PinpointStrike, CAP, CAS, AirDefence]:
if isinstance(control_point, OffMapSpawn):
# Off-map spawn locations start with no aircraft.
continue
if IMPORTANCE_HIGH <= control_point.importance <= IMPORTANCE_LOW:
raise ValueError(
f"CP importance must be between {IMPORTANCE_LOW} and "
@ -366,6 +371,11 @@ class ControlPointGroundObjectGenerator:
self.control_point.connected_objectives.append(g)
class NoOpGroundObjectGenerator(ControlPointGroundObjectGenerator):
def generate(self) -> bool:
return True
class CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator):
def generate(self) -> bool:
if not super().generate():
@ -660,6 +670,8 @@ class GroundObjectGenerator:
generator = CarrierGroundObjectGenerator(self.game, control_point)
elif control_point.cptype == ControlPointType.LHA_GROUP:
generator = LhaGroundObjectGenerator(self.game, control_point)
elif isinstance(control_point, OffMapSpawn):
generator = NoOpGroundObjectGenerator(self.game, control_point)
else:
generator = AirbaseGroundObjectGenerator(self.game, control_point,
self.templates)

View File

@ -12,3 +12,7 @@ def meter_to_nm(value_in_meter: float) -> int:
def nm_to_meter(value_in_nm: float) -> int:
return int(value_in_nm * 1852)
def knots_to_kph(knots: float) -> int:
return int(knots * 1.852)

View File

@ -70,7 +70,7 @@ from dcs.unittype import FlyingType, UnitType
from game import db
from game.data.cap_capabilities_db import GUNFIGHTERS
from game.settings import Settings
from game.utils import nm_to_meter
from game.utils import knots_to_kph, nm_to_meter
from gen.airsupportgen import AirSupport
from gen.ato import AirTaskingOrder, Package
from gen.callsigns import create_group_callsign_from_unit
@ -84,7 +84,11 @@ from gen.flights.flight import (
from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
from gen.runways import RunwayData
from theater import TheaterGroundObject
from game.theater.controlpoint import ControlPoint, ControlPointType
from game.theater.controlpoint import (
ControlPoint,
ControlPointType,
OffMapSpawn,
)
from .conflictgen import Conflict
from .flights.flightplan import (
CasFlightPlan,
@ -92,7 +96,7 @@ from .flights.flightplan import (
PatrollingFlightPlan,
SweepFlightPlan,
)
from .flights.traveltime import TotEstimator
from .flights.traveltime import GroundSpeed, TotEstimator
from .naming import namegen
from .runways import RunwayAssigner
@ -804,31 +808,37 @@ class AircraftConflictGenerator:
group_size=count,
parking_slots=None)
def _generate_inflight(self, name: str, side: Country, unit_type: FlyingType, count: int, at: Point) -> FlyingGroup:
assert count > 0
def _generate_inflight(self, name: str, side: Country, flight: Flight,
origin: ControlPoint) -> FlyingGroup:
assert flight.count > 0
at = origin.position
if unit_type in helicopters.helicopter_map.values():
alt_type = "RADIO"
if isinstance(origin, OffMapSpawn):
alt = flight.flight_plan.waypoints[0].alt
alt_type = flight.flight_plan.waypoints[0].alt_type
elif flight.unit_type in helicopters.helicopter_map.values():
alt = WARM_START_HELI_ALT
speed = WARM_START_HELI_AIRSPEED
else:
alt = WARM_START_ALTITUDE
speed = WARM_START_AIRSPEED
speed = knots_to_kph(GroundSpeed.for_flight(flight, alt))
pos = Point(at.x + random.randint(100, 1000), at.y + random.randint(100, 1000))
logging.info("airgen: {} for {} at {} at {}".format(unit_type, side.id, alt, speed))
logging.info("airgen: {} for {} at {} at {}".format(flight.unit_type, side.id, alt, speed))
group = self.m.flight_group(
country=side,
name=name,
aircraft_type=unit_type,
aircraft_type=flight.unit_type,
airport=None,
position=pos,
altitude=alt,
speed=speed,
maintask=None,
group_size=count)
group_size=flight.count)
group.points[0].alt_type = "RADIO"
group.points[0].alt_type = alt_type
return group
def _generate_at_group(self, name: str, side: Country,
@ -974,9 +984,8 @@ class AircraftConflictGenerator:
group = self._generate_inflight(
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
side=country,
unit_type=flight.unit_type,
count=flight.count,
at=cp.position)
flight=flight,
origin=cp)
elif cp.is_fleet:
group_name = cp.get_carrier_group_name()
group = self._generate_at_group(
@ -1002,9 +1011,8 @@ class AircraftConflictGenerator:
group = self._generate_inflight(
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
side=country,
unit_type=flight.unit_type,
count=flight.count,
at=cp.position)
flight=flight,
origin=cp)
group.points[0].alt = 1500
return group

View File

@ -50,7 +50,7 @@ from theater import (
ControlPoint,
FrontLine,
MissionTarget,
TheaterGroundObject,
OffMapSpawn, TheaterGroundObject,
SamGroundObject,
)
@ -232,8 +232,12 @@ class PackageBuilder:
if assignment is None:
return False
airfield, aircraft = assignment
if isinstance(airfield, OffMapSpawn):
start_type = "In Flight"
else:
start_type = self.start_type
flight = Flight(self.package, aircraft, plan.num_aircraft, airfield,
plan.task, self.start_type)
plan.task, start_type)
self.package.add_flight(flight)
return True
@ -406,6 +410,9 @@ class ObjectiveFinder:
CP.
"""
for cp in self.friendly_control_points():
if isinstance(cp, OffMapSpawn):
# Off-map spawn locations don't need protection.
continue
airfields_in_proximity = self.closest_airfields_to(cp)
airfields_in_threat_range = airfields_in_proximity.airfields_within(
self.AIRFIELD_THREAT_RANGE

View File

@ -8,11 +8,15 @@ from dcs.unit import Unit
from dcs.unitgroup import VehicleGroup
from game.data.doctrine import Doctrine
from game.utils import nm_to_meter
from game.utils import feet_to_meter
from game.weather import Conditions
from theater import ControlPoint, MissionTarget, TheaterGroundObject
from theater import (
ControlPoint,
MissionTarget,
OffMapSpawn,
TheaterGroundObject,
)
from .flight import Flight, FlightWaypoint, FlightWaypointType
from ..runways import RunwayAssigner
@dataclass(frozen=True)
@ -34,8 +38,7 @@ class WaypointBuilder:
def is_helo(self) -> bool:
return getattr(self.flight.unit_type, "helicopter", False)
@staticmethod
def takeoff(departure: ControlPoint) -> FlightWaypoint:
def takeoff(self, departure: ControlPoint) -> FlightWaypoint:
"""Create takeoff waypoint for the given arrival airfield or carrier.
Note that the takeoff waypoint will automatically be created by pydcs
@ -46,36 +49,59 @@ class WaypointBuilder:
departure: Departure airfield or carrier.
"""
position = departure.position
waypoint = FlightWaypoint(
FlightWaypointType.TAKEOFF,
position.x,
position.y,
0
)
waypoint.name = "TAKEOFF"
waypoint.alt_type = "RADIO"
waypoint.description = "Takeoff"
waypoint.pretty_name = "Takeoff"
if isinstance(departure, OffMapSpawn):
waypoint = FlightWaypoint(
FlightWaypointType.NAV,
position.x,
position.y,
500 if self.is_helo else self.doctrine.rendezvous_altitude
)
waypoint.name = "NAV"
waypoint.alt_type = "BARO"
waypoint.description = "Enter theater"
waypoint.pretty_name = "Enter theater"
else:
waypoint = FlightWaypoint(
FlightWaypointType.TAKEOFF,
position.x,
position.y,
0
)
waypoint.name = "TAKEOFF"
waypoint.alt_type = "RADIO"
waypoint.description = "Takeoff"
waypoint.pretty_name = "Takeoff"
return waypoint
@staticmethod
def land(arrival: ControlPoint) -> FlightWaypoint:
def land(self, arrival: ControlPoint) -> FlightWaypoint:
"""Create descent waypoint for the given arrival airfield or carrier.
Args:
arrival: Arrival airfield or carrier.
"""
position = arrival.position
waypoint = FlightWaypoint(
FlightWaypointType.LANDING_POINT,
position.x,
position.y,
0
)
waypoint.name = "LANDING"
waypoint.alt_type = "RADIO"
waypoint.description = "Land"
waypoint.pretty_name = "Land"
if isinstance(arrival, OffMapSpawn):
waypoint = FlightWaypoint(
FlightWaypointType.NAV,
position.x,
position.y,
500 if self.is_helo else self.doctrine.rendezvous_altitude
)
waypoint.name = "NAV"
waypoint.alt_type = "BARO"
waypoint.description = "Exit theater"
waypoint.pretty_name = "Exit theater"
else:
waypoint = FlightWaypoint(
FlightWaypointType.LANDING_POINT,
position.x,
position.y,
0
)
waypoint.name = "LANDING"
waypoint.alt_type = "RADIO"
waypoint.description = "Land"
waypoint.pretty_name = "Land"
return waypoint
def hold(self, position: Point) -> FlightWaypoint:

View File

@ -79,11 +79,8 @@ class QMapControlPoint(QMapObject):
for connected in self.control_point.connected_points:
if connected.captured:
menu.addAction(self.capture_action)
break
else:
return
menu.addAction(self.capture_action)
def cheat_capture(self) -> None:
self.control_point.capture(self.game_model.game, for_player=True)

View File

@ -47,9 +47,12 @@ class QMapObject(QGraphicsRectItem):
object_details_action.triggered.connect(self.on_click)
menu.addAction(object_details_action)
new_package_action = QAction(f"New package")
new_package_action.triggered.connect(self.open_new_package_dialog)
menu.addAction(new_package_action)
# Not all locations have valid objetives. Off-map spawns, for example,
# have no mission types.
if list(self.mission_target.mission_types(for_player=True)):
new_package_action = QAction(f"New package")
new_package_action.triggered.connect(self.open_new_package_dialog)
menu.addAction(new_package_action)
self.add_context_menu_actions(menu)

View File

@ -18,7 +18,6 @@ class QBaseMenu2(QDialog):
# Attrs
self.cp = cp
self.game_model = game_model
self.is_carrier = self.cp.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP]
self.objectName = "menuDialogue"
# Widgets

View File

@ -5,39 +5,30 @@ 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
from qt_ui.windows.basemenu.intel.QIntelInfo import QIntelInfo
from theater import ControlPoint
from theater import ControlPoint, OffMapSpawn
class QBaseMenuTabs(QTabWidget):
def __init__(self, cp: ControlPoint, game_model: GameModel):
super(QBaseMenuTabs, self).__init__()
self.cp = cp
if cp:
if not cp.captured:
if not cp.is_carrier:
self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
self.addTab(self.base_defenses_hq, "Base Defenses")
self.intel = QIntelInfo(cp, game_model.game)
self.addTab(self.intel, "Intel")
else:
if cp.has_runway():
self.airfield_command = QAirfieldCommand(cp, game_model)
self.addTab(self.airfield_command, "Airfield Command")
if not cp.is_carrier:
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")
else:
self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
self.addTab(self.base_defenses_hq, "Fleet")
if not cp.captured:
if not cp.is_carrier and not isinstance(cp, OffMapSpawn):
self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
self.addTab(self.base_defenses_hq, "Base Defenses")
self.intel = QIntelInfo(cp, game_model.game)
self.addTab(self.intel, "Intel")
else:
tabError = QFrame()
l = QGridLayout()
l.addWidget(QLabel("No Control Point"))
tabError.setLayout(l)
self.addTab(tabError, "No Control Point")
if cp.has_runway():
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")

View File

@ -18,7 +18,7 @@ from qt_ui.widgets.QLabeledWidget import QLabeledWidget
from qt_ui.widgets.combos.QAircraftTypeSelector import QAircraftTypeSelector
from qt_ui.widgets.combos.QFlightTypeComboBox import QFlightTypeComboBox
from qt_ui.widgets.combos.QOriginAirfieldSelector import QOriginAirfieldSelector
from theater import ControlPoint
from theater import ControlPoint, OffMapSpawn
class QFlightCreator(QDialog):
@ -107,7 +107,9 @@ class QFlightCreator(QDialog):
origin = self.airfield_selector.currentData()
size = self.flight_size_spinner.value()
if self.game.settings.perf_ai_parking_start:
if isinstance(origin, OffMapSpawn):
start_type = "In Flight"
elif self.game.settings.perf_ai_parking_start:
start_type = "Cold"
else:
start_type = "Warm"

View File

@ -42,15 +42,7 @@ class QFlightWaypointList(QTableView):
self.model.setHorizontalHeaderLabels(["Name", "Alt", "TOT/DEPART"])
# The first waypoint is set up by pydcs at mission generation time, so
# we need to add that waypoint manually.
takeoff = FlightWaypoint(self.flight.from_cp.position.x,
self.flight.from_cp.position.y, 0)
takeoff.description = "Take Off"
takeoff.name = takeoff.pretty_name = "Take Off from " + self.flight.from_cp.name
takeoff.alt_type = "RADIO"
waypoints = itertools.chain([takeoff], self.flight.points)
waypoints = self.flight.flight_plan.waypoints
for row, waypoint in enumerate(waypoints):
self.add_waypoint_row(row, self.flight, waypoint)
self.selectionModel().setCurrentIndex(self.indexAt(QPoint(1, 1)),