Perform coalition-wide mission planning.

Mission planning on a per-control point basis lacked the context it
needed to make good decisions, and the ability to make larger missions
that pulled aircraft from multiple airfields.

The per-CP planners have been replaced in favor of a global planner
per coalition. The planner generates a list of potential missions in
order of priority and then allocates aircraft to the proposed flights
until no missions remain.

Mission planning behavior has changed:

* CAP flights will now only be generated for airfields within a
  predefined threat range of an enemy airfield.
* CAS, SEAD, and strike missions get escorts. Strike missions get a
  SEAD flight.
* CAS, SEAD, and strike missions will not be planned unless
  they have an escort available.
* Missions may originate from multiple airfields.

There's more to do:

* The range limitations imposed on the mission planner should take
  aircraft range limitations into account.
* Air superiority aircraft like the F-15 should be preferred for CAP
  over multi-role aircraft like the F/A-18 since otherwise we run the
  risk of running out of ground attack capable aircraft even though
  there are still unused aircraft.
* Mission priorities may need tuning.
* Target areas could be analyzed for potential threats, allowing
  escort flights to be optional or omitted if there is no threat to
  defend against. For example, late game a SEAD flight for a strike
  mission probably is not necessary.
* SAM threat should be judged by how close the extent of the SAM's
  range is to friendly locations, not the distance to the site itself.
  An SA-10 30 nm away is more threatening than an SA-6 25 nm away.
* Much of the planning behavior should be factored out into the
  coalition's doctrine.

But as-is this is an improvement over the existing behavior, so those
things can be follow ups.

The potential regression in behavior here is that we're no longer
planning multiple cycles of missions. Each objective will get one CAP.
I think this fits better with the turn cycle of the game, as a CAP
flight should be able to remain on station for the duration of the
turn (especially with refueling).

Note that this does break save compatibility as the old planner was a
part of the game object, and since that class is now gone it can't be
unpickled.
This commit is contained in:
Dan Albert 2020-09-26 13:17:34 -07:00
parent 1f240b02f4
commit 1e041b6249
13 changed files with 1070 additions and 814 deletions

View File

@ -807,7 +807,7 @@ CARRIER_TAKEOFF_BAN = [
Units separated by country.
country : DCS Country name
"""
FACTIONS = {
FACTIONS: typing.Dict[str, typing.Dict[str, typing.Any]] = {
"Bluefor Modern": BLUEFOR_MODERN,
"Bluefor Cold War 1970s": BLUEFOR_COLDWAR,

View File

@ -4,11 +4,12 @@ from game.db import REWARDS, PLAYER_BUDGET_BASE, sys
from game.inventory import GlobalAircraftInventory
from game.models.game_stats import GameStats
from gen.ato import AirTaskingOrder
from gen.flights.ai_flight_planner import FlightPlanner
from gen.flights.ai_flight_planner import CoalitionMissionPlanner
from gen.ground_forces.ai_ground_planner import GroundPlanner
from .event import *
from .settings import Settings
COMMISION_UNIT_VARIETY = 4
COMMISION_LIMITS_SCALE = 1.5
COMMISION_LIMITS_FACTORS = {
@ -70,7 +71,6 @@ class Game:
self.date = datetime(start_date.year, start_date.month, start_date.day)
self.game_stats = GameStats()
self.game_stats.update(self)
self.planners = {}
self.ground_planners = {}
self.informations = []
self.informations.append(Information("Game Start", "-" * 40, 0))
@ -104,11 +104,11 @@ class Game:
self.enemy_country = "Russia"
@property
def player_faction(self):
def player_faction(self) -> Dict[str, Any]:
return db.FACTIONS[self.player_name]
@property
def enemy_faction(self):
def enemy_faction(self) -> Dict[str, Any]:
return db.FACTIONS[self.enemy_name]
def _roll(self, prob, mult):
@ -244,16 +244,12 @@ class Game:
# Plan flights & combat for next turn
self.__culling_points = self.compute_conflicts_position()
self.planners = {}
self.ground_planners = {}
self.blue_ato.clear()
self.red_ato.clear()
CoalitionMissionPlanner(self, is_player=True).plan_missions()
CoalitionMissionPlanner(self, is_player=False).plan_missions()
for cp in self.theater.controlpoints:
if cp.has_runway():
planner = FlightPlanner(cp, self)
planner.plan_flights()
self.planners[cp.id] = planner
if cp.has_frontline:
gplanner = GroundPlanner(cp, self)
gplanner.plan_groundwar()

View File

@ -189,15 +189,16 @@ class Operation:
side = cp.captured
if side:
country = self.current_mission.country(self.game.player_country)
ato = self.game.blue_ato
else:
country = self.current_mission.country(self.game.enemy_country)
if cp.id in self.game.planners.keys():
self.airgen.generate_flights(
cp,
country,
self.game.planners[cp.id],
self.groundobjectgen.runways
)
ato = self.game.red_ato
self.airgen.generate_flights(
cp,
country,
ato,
self.groundobjectgen.runways
)
# Generate ground units on frontline everywhere
jtacs: List[JtacInfo] = []

View File

@ -14,8 +14,8 @@ from game.settings import Settings
from game.utils import nm_to_meter
from gen.airfields import RunwayData
from gen.airsupportgen import AirSupport
from gen.ato import AirTaskingOrder
from gen.callsigns import create_group_callsign_from_unit
from gen.flights.ai_flight_planner import FlightPlanner
from gen.flights.flight import (
Flight,
FlightType,
@ -751,31 +751,27 @@ class AircraftConflictGenerator:
else:
logging.warning("Pylon not found ! => Pylon" + key + " on " + str(flight.unit_type))
def generate_flights(self, cp, country, flight_planner: FlightPlanner,
dynamic_runways: Dict[str, RunwayData]):
# Clear pydcs parking slots
if cp.airport is not None:
logging.info("CLEARING SLOTS @ " + cp.airport.name)
logging.info("===============")
def clear_parking_slots(self) -> None:
for cp in self.game.theater.controlpoints:
if cp.airport is not None:
for ps in cp.airport.parking_slots:
logging.info("SLOT : " + str(ps.unit_id))
ps.unit_id = None
logging.info("----------------")
logging.info("===============")
for parking_slot in cp.airport.parking_slots:
parking_slot.unit_id = None
for flight in flight_planner.flights:
if flight.client_count == 0 and self.game.position_culled(flight.from_cp.position):
logging.info("Flight not generated : culled")
continue
logging.info("Generating flight : " + str(flight.unit_type))
group = self.generate_planned_flight(cp, country, flight)
self.setup_flight_group(group, flight, flight.flight_type,
dynamic_runways)
self.setup_group_activation_trigger(flight, group)
def generate_flights(self, cp, country, ato: AirTaskingOrder,
dynamic_runways: Dict[str, RunwayData]) -> None:
self.clear_parking_slots()
for package in ato.packages:
for flight in package.flights:
culled = self.game.position_culled(flight.from_cp.position)
if flight.client_count == 0 and culled:
logging.info("Flight not generated: culled")
continue
logging.info(f"Generating flight: {flight.unit_type}")
group = self.generate_planned_flight(cp, country, flight)
self.setup_flight_group(group, flight, flight.flight_type,
dynamic_runways)
self.setup_group_activation_trigger(flight, group)
def setup_group_activation_trigger(self, flight, group):
if flight.scheduled_in > 0 and flight.client_count == 0:

File diff suppressed because it is too large Load Diff

582
gen/flights/flightplan.py Normal file
View File

@ -0,0 +1,582 @@
"""Flight plan generation.
Flights are first planned generically by either the player or by the
MissionPlanner. Those only plan basic information like the objective, aircraft
type, and the size of the flight. The FlightPlanBuilder is responsible for
generating the waypoints for the mission.
"""
from __future__ import annotations
import logging
import random
from typing import List, Optional, TYPE_CHECKING
from game.data.doctrine import MODERN_DOCTRINE
from .flight import Flight, FlightType, FlightWaypointType, FlightWaypoint
from ..conflictgen import Conflict
from theater import ControlPoint, FrontLine, MissionTarget, TheaterGroundObject
from game.utils import nm_to_meter
from dcs.unit import Unit
if TYPE_CHECKING:
from game import Game
class InvalidObjectiveLocation(RuntimeError):
"""Raised when the objective location is invalid for the mission type."""
def __init__(self, task: FlightType, location: MissionTarget) -> None:
super().__init__(
f"{location.name} is not valid for {task.name} missions."
)
class FlightPlanBuilder:
"""Generates flight plans for flights."""
def __init__(self, game: Game, is_player: bool) -> None:
self.game = game
if is_player:
faction = self.game.player_faction
else:
faction = self.game.enemy_faction
self.doctrine = faction.get("doctrine", MODERN_DOCTRINE)
def populate_flight_plan(self, flight: Flight,
objective_location: MissionTarget) -> None:
"""Creates a default flight plan for the given mission."""
# TODO: Flesh out mission types.
try:
task = flight.flight_type
if task == FlightType.ANTISHIP:
logging.error(
"Anti-ship flight plan generation not implemented"
)
elif task == FlightType.BAI:
logging.error("BAI flight plan generation not implemented")
elif task == FlightType.BARCAP:
self.generate_barcap(flight, objective_location)
elif task == FlightType.CAP:
self.generate_barcap(flight, objective_location)
elif task == FlightType.CAS:
self.generate_cas(flight, objective_location)
elif task == FlightType.DEAD:
self.generate_sead(flight, objective_location)
elif task == FlightType.ELINT:
logging.error("ELINT flight plan generation not implemented")
elif task == FlightType.EVAC:
logging.error("Evac flight plan generation not implemented")
elif task == FlightType.EWAR:
logging.error("EWar flight plan generation not implemented")
elif task == FlightType.INTERCEPTION:
logging.error(
"Intercept flight plan generation not implemented"
)
elif task == FlightType.LOGISTICS:
logging.error(
"Logistics flight plan generation not implemented"
)
elif task == FlightType.RECON:
logging.error("Recon flight plan generation not implemented")
elif task == FlightType.SEAD:
self.generate_sead(flight, objective_location)
elif task == FlightType.STRIKE:
self.generate_strike(flight, objective_location)
elif task == FlightType.TARCAP:
self.generate_frontline_cap(flight, objective_location)
elif task == FlightType.TROOP_TRANSPORT:
logging.error(
"Troop transport flight plan generation not implemented"
)
except InvalidObjectiveLocation as ex:
logging.error(f"Could not create flight plan: {ex}")
def generate_strike(self, flight: Flight, location: MissionTarget) -> None:
"""Generates a strike flight plan.
Args:
flight: The flight to generate the flight plan for.
location: The strike target location.
"""
# TODO: Support airfield strikes.
if not isinstance(location, TheaterGroundObject):
raise InvalidObjectiveLocation(flight.flight_type, location)
# TODO: Stop clobbering flight type.
flight.flight_type = FlightType.STRIKE
ascend = self.generate_ascend_point(flight.from_cp)
flight.points.append(ascend)
heading = flight.from_cp.position.heading_between_point(
location.position
)
ingress_heading = heading - 180 + 25
egress_heading = heading - 180 - 25
ingress_pos = location.position.point_from_heading(
ingress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"]
)
ingress_point = FlightWaypoint(
FlightWaypointType.INGRESS_STRIKE,
ingress_pos.x,
ingress_pos.y,
self.doctrine["INGRESS_ALT"]
)
ingress_point.pretty_name = "INGRESS on " + location.name
ingress_point.description = "INGRESS on " + location.name
ingress_point.name = "INGRESS"
flight.points.append(ingress_point)
if len(location.groups) > 0 and location.dcs_identifier == "AA":
for g in location.groups:
for j, u in enumerate(g.units):
point = FlightWaypoint(
FlightWaypointType.TARGET_POINT,
u.position.x,
u.position.y,
0
)
point.description = (
f"STRIKE [{location.name}] : {u.type} #{j}"
)
point.pretty_name = (
f"STRIKE [{location.name}] : {u.type} #{j}"
)
point.name = f"{location.name} #{j}"
point.only_for_player = True
ingress_point.targets.append(location)
flight.points.append(point)
else:
if hasattr(location, "obj_name"):
buildings = self.game.theater.find_ground_objects_by_obj_name(
location.obj_name
)
for building in buildings:
if building.is_dead:
continue
point = FlightWaypoint(
FlightWaypointType.TARGET_POINT,
building.position.x,
building.position.y,
0
)
point.description = (
f"STRIKE on {building.obj_name} {building.category} "
f"[{building.dcs_identifier}]"
)
point.pretty_name = (
f"STRIKE on {building.obj_name} {building.category} "
f"[{building.dcs_identifier}]"
)
point.name = building.obj_name
point.only_for_player = True
ingress_point.targets.append(building)
flight.points.append(point)
else:
point = FlightWaypoint(
FlightWaypointType.TARGET_GROUP_LOC,
location.position.x,
location.position.y,
0
)
point.description = "STRIKE on " + location.name
point.pretty_name = "STRIKE on " + location.name
point.name = location.name
point.only_for_player = True
ingress_point.targets.append(location)
flight.points.append(point)
egress_pos = location.position.point_from_heading(
egress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"]
)
egress_point = FlightWaypoint(
FlightWaypointType.EGRESS,
egress_pos.x,
egress_pos.y,
self.doctrine["EGRESS_ALT"]
)
egress_point.name = "EGRESS"
egress_point.pretty_name = "EGRESS from " + location.name
egress_point.description = "EGRESS from " + location.name
flight.points.append(egress_point)
descend = self.generate_descend_point(flight.from_cp)
flight.points.append(descend)
rtb = self.generate_rtb_waypoint(flight.from_cp)
flight.points.append(rtb)
def generate_barcap(self, flight: Flight, location: MissionTarget) -> None:
"""Generate a BARCAP flight at a given location.
Args:
flight: The flight to generate the flight plan for.
location: The control point to protect.
"""
if isinstance(location, FrontLine):
raise InvalidObjectiveLocation(flight.flight_type, location)
if isinstance(location, ControlPoint) and location.is_carrier:
flight.flight_type = FlightType.BARCAP
else:
flight.flight_type = FlightType.CAP
patrol_alt = random.randint(
self.doctrine["PATROL_ALT_RANGE"][0],
self.doctrine["PATROL_ALT_RANGE"][1]
)
loc = location.position.point_from_heading(
random.randint(0, 360),
random.randint(self.doctrine["CAP_DISTANCE_FROM_CP"][0],
self.doctrine["CAP_DISTANCE_FROM_CP"][1])
)
hdg = location.position.heading_between_point(loc)
radius = random.randint(
self.doctrine["CAP_PATTERN_LENGTH"][0],
self.doctrine["CAP_PATTERN_LENGTH"][1]
)
orbit0p = loc.point_from_heading(hdg - 90, radius)
orbit1p = loc.point_from_heading(hdg + 90, radius)
# Create points
ascend = self.generate_ascend_point(flight.from_cp)
flight.points.append(ascend)
orbit0 = FlightWaypoint(
FlightWaypointType.PATROL_TRACK,
orbit0p.x,
orbit0p.y,
patrol_alt
)
orbit0.name = "ORBIT 0"
orbit0.description = "Standby between this point and the next one"
orbit0.pretty_name = "Race-track start"
flight.points.append(orbit0)
orbit1 = FlightWaypoint(
FlightWaypointType.PATROL,
orbit1p.x,
orbit1p.y,
patrol_alt
)
orbit1.name = "ORBIT 1"
orbit1.description = "Standby between this point and the previous one"
orbit1.pretty_name = "Race-track end"
flight.points.append(orbit1)
orbit0.targets.append(location)
descend = self.generate_descend_point(flight.from_cp)
flight.points.append(descend)
rtb = self.generate_rtb_waypoint(flight.from_cp)
flight.points.append(rtb)
def generate_frontline_cap(self, flight: Flight,
location: MissionTarget) -> None:
"""Generate a CAP flight plan for the given front line.
Args:
flight: The flight to generate the flight plan for.
location: Front line to protect.
"""
if not isinstance(location, FrontLine):
raise InvalidObjectiveLocation(flight.flight_type, location)
ally_cp, enemy_cp = location.control_points
flight.flight_type = FlightType.CAP
patrol_alt = random.randint(self.doctrine["PATROL_ALT_RANGE"][0],
self.doctrine["PATROL_ALT_RANGE"][1])
# Find targets waypoints
ingress, heading, distance = Conflict.frontline_vector(
ally_cp, enemy_cp, self.game.theater
)
center = ingress.point_from_heading(heading, distance / 2)
orbit_center = center.point_from_heading(
heading - 90, random.randint(nm_to_meter(6), nm_to_meter(15))
)
combat_width = distance / 2
if combat_width > 500000:
combat_width = 500000
if combat_width < 35000:
combat_width = 35000
radius = combat_width*1.25
orbit0p = orbit_center.point_from_heading(heading, radius)
orbit1p = orbit_center.point_from_heading(heading + 180, radius)
# Create points
ascend = self.generate_ascend_point(flight.from_cp)
flight.points.append(ascend)
orbit0 = FlightWaypoint(
FlightWaypointType.PATROL_TRACK,
orbit0p.x,
orbit0p.y,
patrol_alt
)
orbit0.name = "ORBIT 0"
orbit0.description = "Standby between this point and the next one"
orbit0.pretty_name = "Race-track start"
flight.points.append(orbit0)
orbit1 = FlightWaypoint(
FlightWaypointType.PATROL,
orbit1p.x,
orbit1p.y,
patrol_alt
)
orbit1.name = "ORBIT 1"
orbit1.description = "Standby between this point and the previous one"
orbit1.pretty_name = "Race-track end"
flight.points.append(orbit1)
# Note: Targets of PATROL TRACK waypoints are the points to be defended.
orbit0.targets.append(flight.from_cp)
orbit0.targets.append(center)
descend = self.generate_descend_point(flight.from_cp)
flight.points.append(descend)
rtb = self.generate_rtb_waypoint(flight.from_cp)
flight.points.append(rtb)
def generate_sead(self, flight: Flight, location: MissionTarget,
custom_targets: Optional[List[Unit]] = None) -> None:
"""Generate a SEAD/DEAD flight at a given location.
Args:
flight: The flight to generate the flight plan for.
location: Location of the SAM site.
custom_targets: Specific radar equipped units selected by the user.
"""
if not isinstance(location, TheaterGroundObject):
raise InvalidObjectiveLocation(flight.flight_type, location)
if custom_targets is None:
custom_targets = []
flight.points = []
flight.flight_type = random.choice([FlightType.SEAD, FlightType.DEAD])
ascend = self.generate_ascend_point(flight.from_cp)
flight.points.append(ascend)
heading = flight.from_cp.position.heading_between_point(
location.position
)
ingress_heading = heading - 180 + 25
egress_heading = heading - 180 - 25
ingress_pos = location.position.point_from_heading(
ingress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"]
)
ingress_point = FlightWaypoint(
FlightWaypointType.INGRESS_SEAD,
ingress_pos.x,
ingress_pos.y,
self.doctrine["INGRESS_ALT"]
)
ingress_point.name = "INGRESS"
ingress_point.pretty_name = "INGRESS on " + location.name
ingress_point.description = "INGRESS on " + location.name
flight.points.append(ingress_point)
if len(custom_targets) > 0:
for target in custom_targets:
point = FlightWaypoint(
FlightWaypointType.TARGET_POINT,
target.position.x,
target.position.y,
0
)
point.alt_type = "RADIO"
if flight.flight_type == FlightType.DEAD:
point.description = "DEAD on " + target.type
point.pretty_name = "DEAD on " + location.name
point.only_for_player = True
else:
point.description = "SEAD on " + location.name
point.pretty_name = "SEAD on " + location.name
point.only_for_player = True
flight.points.append(point)
ingress_point.targets.append(location)
ingress_point.targetGroup = location
else:
point = FlightWaypoint(
FlightWaypointType.TARGET_GROUP_LOC,
location.position.x,
location.position.y,
0
)
point.alt_type = "RADIO"
if flight.flight_type == FlightType.DEAD:
point.description = "DEAD on " + location.name
point.pretty_name = "DEAD on " + location.name
point.only_for_player = True
else:
point.description = "SEAD on " + location.name
point.pretty_name = "SEAD on " + location.name
point.only_for_player = True
ingress_point.targets.append(location)
ingress_point.targetGroup = location
flight.points.append(point)
egress_pos = location.position.point_from_heading(
egress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"]
)
egress_point = FlightWaypoint(
FlightWaypointType.EGRESS,
egress_pos.x,
egress_pos.y,
self.doctrine["EGRESS_ALT"]
)
egress_point.name = "EGRESS"
egress_point.pretty_name = "EGRESS from " + location.name
egress_point.description = "EGRESS from " + location.name
flight.points.append(egress_point)
descend = self.generate_descend_point(flight.from_cp)
flight.points.append(descend)
rtb = self.generate_rtb_waypoint(flight.from_cp)
flight.points.append(rtb)
def generate_cas(self, flight: Flight, location: MissionTarget) -> None:
"""Generate a CAS flight plan for the given target.
Args:
flight: The flight to generate the flight plan for.
location: Front line with CAS targets.
"""
if not isinstance(location, FrontLine):
raise InvalidObjectiveLocation(flight.flight_type, location)
from_cp, location = location.control_points
is_helo = getattr(flight.unit_type, "helicopter", False)
cap_alt = 1000
flight.points = []
flight.flight_type = FlightType.CAS
ingress, heading, distance = Conflict.frontline_vector(
from_cp, location, self.game.theater
)
center = ingress.point_from_heading(heading, distance / 2)
egress = ingress.point_from_heading(heading, distance)
ascend = self.generate_ascend_point(flight.from_cp)
if is_helo:
cap_alt = 500
ascend.alt = 500
flight.points.append(ascend)
ingress_point = FlightWaypoint(
FlightWaypointType.INGRESS_CAS,
ingress.x,
ingress.y,
cap_alt
)
ingress_point.alt_type = "RADIO"
ingress_point.name = "INGRESS"
ingress_point.pretty_name = "INGRESS"
ingress_point.description = "Ingress into CAS area"
flight.points.append(ingress_point)
center_point = FlightWaypoint(
FlightWaypointType.CAS,
center.x,
center.y,
cap_alt
)
center_point.alt_type = "RADIO"
center_point.description = "Provide CAS"
center_point.name = "CAS"
center_point.pretty_name = "CAS"
flight.points.append(center_point)
egress_point = FlightWaypoint(
FlightWaypointType.EGRESS,
egress.x,
egress.y,
cap_alt
)
egress_point.alt_type = "RADIO"
egress_point.description = "Egress from CAS area"
egress_point.name = "EGRESS"
egress_point.pretty_name = "EGRESS"
flight.points.append(egress_point)
descend = self.generate_descend_point(flight.from_cp)
if is_helo:
descend.alt = 300
flight.points.append(descend)
rtb = self.generate_rtb_waypoint(flight.from_cp)
flight.points.append(rtb)
def generate_ascend_point(self, departure: ControlPoint) -> FlightWaypoint:
"""Generate ascend point.
Args:
departure: Departure airfield or carrier.
"""
ascend_heading = departure.heading
pos_ascend = departure.position.point_from_heading(
ascend_heading, 10000
)
ascend = FlightWaypoint(
FlightWaypointType.ASCEND_POINT,
pos_ascend.x,
pos_ascend.y,
self.doctrine["PATTERN_ALTITUDE"]
)
ascend.name = "ASCEND"
ascend.alt_type = "RADIO"
ascend.description = "Ascend"
ascend.pretty_name = "Ascend"
return ascend
def generate_descend_point(self, arrival: ControlPoint) -> FlightWaypoint:
"""Generate approach/descend point.
Args:
arrival: Arrival airfield or carrier.
"""
ascend_heading = arrival.heading
descend = arrival.position.point_from_heading(
ascend_heading - 180, 10000
)
descend = FlightWaypoint(
FlightWaypointType.DESCENT_POINT,
descend.x,
descend.y,
self.doctrine["PATTERN_ALTITUDE"]
)
descend.name = "DESCEND"
descend.alt_type = "RADIO"
descend.description = "Descend to pattern alt"
descend.pretty_name = "Descend to pattern alt"
return descend
@staticmethod
def generate_rtb_waypoint(arrival: ControlPoint) -> FlightWaypoint:
"""Generate RTB landing point.
Args:
arrival: Arrival airfield or carrier.
"""
rtb = arrival.position
rtb = FlightWaypoint(
FlightWaypointType.LANDING_POINT,
rtb.x,
rtb.y,
0
)
rtb.name = "LANDING"
rtb.alt_type = "RADIO"
rtb.description = "RTB"
rtb.pretty_name = "RTB"
return rtb

View File

@ -1,32 +0,0 @@
from PySide2.QtCore import Signal
from PySide2.QtWidgets import QGroupBox, QHBoxLayout, QComboBox, QLabel
from game import Game
class QChooseAirbase(QGroupBox):
selected_airbase_changed = Signal(str)
def __init__(self, game:Game, title=""):
super(QChooseAirbase, self).__init__(title)
self.game = game
self.layout = QHBoxLayout()
self.depart_from_label = QLabel("Airbase : ")
self.depart_from = QComboBox()
for i, cp in enumerate([b for b in self.game.theater.controlpoints if b.captured and b.id in self.game.planners]):
self.depart_from.addItem(str(cp.name), cp)
self.depart_from.setCurrentIndex(0)
self.depart_from.currentTextChanged.connect(self._on_airbase_selected)
self.layout.addWidget(self.depart_from_label)
self.layout.addWidget(self.depart_from)
self.setLayout(self.layout)
def _on_airbase_selected(self):
selected = self.depart_from.currentText()
self.selected_airbase_changed.emit(selected)

View File

@ -11,7 +11,7 @@ from dcs.planes import PlaneType
from game import Game
from gen.ato import Package
from gen.flights.ai_flight_planner import FlightPlanner
from gen.flights.flightplan import FlightPlanBuilder
from gen.flights.flight import Flight, FlightType
from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.widgets.QFlightSizeSpinner import QFlightSizeSpinner
@ -31,6 +31,8 @@ class QFlightCreator(QDialog):
self.game = game
self.package = package
self.planner = FlightPlanBuilder(self.game, is_player=True)
self.setWindowTitle("Create flight")
self.setWindowIcon(EVENT_ICONS["strike"])
@ -93,7 +95,7 @@ class QFlightCreator(QDialog):
size = self.flight_size_spinner.value()
flight = Flight(aircraft, size, origin, task)
self.populate_flight_plan(flight, task)
self.planner.populate_flight_plan(flight, self.package.target)
# noinspection PyUnresolvedReferences
self.created.emit(flight)
@ -102,77 +104,3 @@ class QFlightCreator(QDialog):
def on_aircraft_changed(self, index: int) -> None:
new_aircraft = self.aircraft_selector.itemData(index)
self.airfield_selector.change_aircraft(new_aircraft)
@property
def planner(self) -> FlightPlanner:
return self.game.planners[self.airfield_selector.currentData().id]
def populate_flight_plan(self, flight: Flight, task: FlightType) -> None:
# TODO: Flesh out mission types.
if task == FlightType.ANTISHIP:
logging.error("Anti-ship flight plan generation not implemented")
elif task == FlightType.BAI:
logging.error("BAI flight plan generation not implemented")
elif task == FlightType.BARCAP:
self.generate_cap(flight)
elif task == FlightType.CAP:
self.generate_cap(flight)
elif task == FlightType.CAS:
self.generate_cas(flight)
elif task == FlightType.DEAD:
self.generate_sead(flight)
elif task == FlightType.ELINT:
logging.error("ELINT flight plan generation not implemented")
elif task == FlightType.EVAC:
logging.error("Evac flight plan generation not implemented")
elif task == FlightType.EWAR:
logging.error("EWar flight plan generation not implemented")
elif task == FlightType.INTERCEPTION:
logging.error("Intercept flight plan generation not implemented")
elif task == FlightType.LOGISTICS:
logging.error("Logistics flight plan generation not implemented")
elif task == FlightType.RECON:
logging.error("Recon flight plan generation not implemented")
elif task == FlightType.SEAD:
self.generate_sead(flight)
elif task == FlightType.STRIKE:
self.generate_strike(flight)
elif task == FlightType.TARCAP:
self.generate_cap(flight)
elif task == FlightType.TROOP_TRANSPORT:
logging.error(
"Troop transport flight plan generation not implemented"
)
def generate_cas(self, flight: Flight) -> None:
if not isinstance(self.package.target, FrontLine):
logging.error(
"Could not create flight plan: CAS missions only valid for "
"front lines"
)
return
self.planner.generate_cas(flight, self.package.target)
def generate_cap(self, flight: Flight) -> None:
if isinstance(self.package.target, TheaterGroundObject):
logging.error(
"Could not create flight plan: CAP missions for strike targets "
"not implemented"
)
return
if isinstance(self.package.target, FrontLine):
self.planner.generate_frontline_cap(flight, self.package.target)
else:
self.planner.generate_barcap(flight, self.package.target)
def generate_sead(self, flight: Flight) -> None:
self.planner.generate_sead(flight, self.package.target)
def generate_strike(self, flight: Flight) -> None:
if not isinstance(self.package.target, TheaterGroundObject):
logging.error(
"Could not create flight plan: strike missions for capture "
"points not implemented"
)
return
self.planner.generate_strike(flight, self.package.target)

View File

@ -3,6 +3,7 @@ from PySide2.QtWidgets import QDialog, QPushButton
from game import Game
from gen.flights.flight import Flight
from gen.flights.flightplan import FlightPlanBuilder
from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointInfoBox import QFlightWaypointInfoBox
@ -19,7 +20,7 @@ class QAbstractMissionGenerator(QDialog):
self.setWindowTitle(title)
self.setWindowIcon(EVENT_ICONS["strike"])
self.flight_waypoint_list = flight_waypoint_list
self.planner = self.game.planners[self.flight.from_cp.id]
self.planner = FlightPlanBuilder(self.game, is_player=True)
self.selected_waypoints = []
self.wpt_info = QFlightWaypointInfoBox()

View File

@ -3,6 +3,7 @@ from PySide2.QtWidgets import QFrame, QGridLayout, QLabel, QPushButton, QVBoxLay
from game import Game
from gen.flights.flight import Flight
from gen.flights.flightplan import FlightPlanBuilder
from qt_ui.windows.mission.flight.generator.QCAPMissionGenerator import QCAPMissionGenerator
from qt_ui.windows.mission.flight.generator.QCASMissionGenerator import QCASMissionGenerator
from qt_ui.windows.mission.flight.generator.QSEADMissionGenerator import QSEADMissionGenerator
@ -19,7 +20,7 @@ class QFlightWaypointTab(QFrame):
super(QFlightWaypointTab, self).__init__()
self.flight = flight
self.game = game
self.planner = self.game.planners[self.flight.from_cp.id]
self.planner = FlightPlanBuilder(self.game, is_player=True)
self.init_ui()
def init_ui(self):

View File

@ -52,7 +52,7 @@ class ControlPoint(MissionTarget):
self.id = id
self.name = " ".join(re.split(r" |-", name)[:2])
self.full_name = name
self.position = position
self.position: Point = position
self.at = at
self.ground_objects = []
self.ships = []
@ -212,3 +212,6 @@ class ControlPoint(MissionTarget):
if g.obj_name == obj_name:
found.append(g)
return found
def is_friendly(self, to_player: bool) -> bool:
return self.captured == to_player

View File

@ -1,9 +1,14 @@
"""Battlefield front lines."""
from typing import Tuple
from dcs.mapping import Point
from . import ControlPoint, MissionTarget
# TODO: Dedup by moving everything to using this class.
FRONTLINE_MIN_CP_DISTANCE = 5000
class FrontLine(MissionTarget):
"""Defines a front line location between two control points.
@ -25,3 +30,16 @@ class FrontLine(MissionTarget):
a = self.control_point_a.name
b = self.control_point_b.name
return f"Front line {a}/{b}"
@property
def position(self) -> Point:
a = self.control_point_a.position
b = self.control_point_b.position
attack_heading = a.heading_between_point(b)
attack_distance = a.distance_to_point(b)
middle_point = a.point_from_heading(attack_heading, attack_distance / 2)
strength_delta = (self.control_point_a.base.strength - self.control_point_b.base.strength) / 1.0
position = middle_point.point_from_heading(attack_heading,
strength_delta * attack_distance / 2 - FRONTLINE_MIN_CP_DISTANCE)
return position

View File

@ -1,4 +1,7 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from dcs.mapping import Point
class MissionTarget(ABC):
@ -9,3 +12,12 @@ class MissionTarget(ABC):
@abstractmethod
def name(self) -> str:
"""The name of the mission target."""
@property
@abstractmethod
def position(self) -> Point:
"""The location of the mission target."""
def distance_to(self, other: MissionTarget) -> int:
"""Computes the distance to the given mission target."""
return self.position.distance_to_point(other.position)