mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
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:
parent
1f240b02f4
commit
1e041b6249
@ -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,
|
||||
|
||||
16
game/game.py
16
game/game.py
@ -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()
|
||||
|
||||
@ -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] = []
|
||||
|
||||
@ -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
582
gen/flights/flightplan.py
Normal 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
|
||||
@ -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)
|
||||
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user