mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Merge pull request #184 from DanAlbert/waypoint-planning
Improve automated mission planning.
This commit is contained in:
commit
9101dae38a
@ -1,95 +1,101 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from game.utils import nm_to_meter, feet_to_meter
|
from game.utils import nm_to_meter, feet_to_meter
|
||||||
|
|
||||||
MODERN_DOCTRINE = {
|
|
||||||
|
|
||||||
"GENERATORS": {
|
@dataclass(frozen=True)
|
||||||
"CAS": True,
|
class Doctrine:
|
||||||
"CAP": True,
|
cas: bool
|
||||||
"SEAD": True,
|
cap: bool
|
||||||
"STRIKE": True,
|
sead: bool
|
||||||
"ANTISHIP": True,
|
strike: bool
|
||||||
},
|
antiship: bool
|
||||||
|
|
||||||
"STRIKE_MAX_RANGE": 1500000,
|
strike_max_range: int
|
||||||
"SEAD_MAX_RANGE": 1500000,
|
sead_max_range: int
|
||||||
|
|
||||||
"CAP_EVERY_X_MINUTES": 20,
|
rendezvous_altitude: int
|
||||||
"CAS_EVERY_X_MINUTES": 30,
|
join_distance: int
|
||||||
"SEAD_EVERY_X_MINUTES": 40,
|
split_distance: int
|
||||||
"STRIKE_EVERY_X_MINUTES": 40,
|
ingress_egress_distance: int
|
||||||
|
ingress_altitude: int
|
||||||
|
egress_altitude: int
|
||||||
|
|
||||||
"INGRESS_EGRESS_DISTANCE": nm_to_meter(45),
|
min_patrol_altitude: int
|
||||||
"INGRESS_ALT": feet_to_meter(20000),
|
max_patrol_altitude: int
|
||||||
"EGRESS_ALT": feet_to_meter(20000),
|
pattern_altitude: int
|
||||||
"PATROL_ALT_RANGE": (feet_to_meter(15000), feet_to_meter(33000)),
|
|
||||||
"PATTERN_ALTITUDE": feet_to_meter(5000),
|
|
||||||
|
|
||||||
"CAP_PATTERN_LENGTH": (nm_to_meter(15), nm_to_meter(40)),
|
cap_min_track_length: int
|
||||||
"FRONTLINE_CAP_DISTANCE_FROM_FRONTLINE": (nm_to_meter(6), nm_to_meter(15)),
|
cap_max_track_length: int
|
||||||
"CAP_DISTANCE_FROM_CP": (nm_to_meter(10), nm_to_meter(40)),
|
cap_min_distance_from_cp: int
|
||||||
|
cap_max_distance_from_cp: int
|
||||||
|
|
||||||
"MAX_NUMBER_OF_INTERCEPTION_GROUP": 3,
|
|
||||||
}
|
|
||||||
|
|
||||||
COLDWAR_DOCTRINE = {
|
MODERN_DOCTRINE = Doctrine(
|
||||||
|
cap=True,
|
||||||
|
cas=True,
|
||||||
|
sead=True,
|
||||||
|
strike=True,
|
||||||
|
antiship=True,
|
||||||
|
strike_max_range=1500000,
|
||||||
|
sead_max_range=1500000,
|
||||||
|
rendezvous_altitude=feet_to_meter(25000),
|
||||||
|
join_distance=nm_to_meter(20),
|
||||||
|
split_distance=nm_to_meter(20),
|
||||||
|
ingress_egress_distance=nm_to_meter(45),
|
||||||
|
ingress_altitude=feet_to_meter(20000),
|
||||||
|
egress_altitude=feet_to_meter(20000),
|
||||||
|
min_patrol_altitude=feet_to_meter(15000),
|
||||||
|
max_patrol_altitude=feet_to_meter(33000),
|
||||||
|
pattern_altitude=feet_to_meter(5000),
|
||||||
|
cap_min_track_length=nm_to_meter(15),
|
||||||
|
cap_max_track_length=nm_to_meter(40),
|
||||||
|
cap_min_distance_from_cp=nm_to_meter(10),
|
||||||
|
cap_max_distance_from_cp=nm_to_meter(40),
|
||||||
|
)
|
||||||
|
|
||||||
"GENERATORS": {
|
COLDWAR_DOCTRINE = Doctrine(
|
||||||
"CAS": True,
|
cap=True,
|
||||||
"CAP": True,
|
cas=True,
|
||||||
"SEAD": True,
|
sead=True,
|
||||||
"STRIKE": True,
|
strike=True,
|
||||||
"ANTISHIP": True,
|
antiship=True,
|
||||||
},
|
strike_max_range=1500000,
|
||||||
|
sead_max_range=1500000,
|
||||||
|
rendezvous_altitude=feet_to_meter(22000),
|
||||||
|
join_distance=nm_to_meter(10),
|
||||||
|
split_distance=nm_to_meter(10),
|
||||||
|
ingress_egress_distance=nm_to_meter(30),
|
||||||
|
ingress_altitude=feet_to_meter(18000),
|
||||||
|
egress_altitude=feet_to_meter(18000),
|
||||||
|
min_patrol_altitude=feet_to_meter(10000),
|
||||||
|
max_patrol_altitude=feet_to_meter(24000),
|
||||||
|
pattern_altitude=feet_to_meter(5000),
|
||||||
|
cap_min_track_length=nm_to_meter(12),
|
||||||
|
cap_max_track_length=nm_to_meter(24),
|
||||||
|
cap_min_distance_from_cp=nm_to_meter(8),
|
||||||
|
cap_max_distance_from_cp=nm_to_meter(25),
|
||||||
|
)
|
||||||
|
|
||||||
"STRIKE_MAX_RANGE": 1500000,
|
WWII_DOCTRINE = Doctrine(
|
||||||
"SEAD_MAX_RANGE": 1500000,
|
cap=True,
|
||||||
|
cas=True,
|
||||||
"CAP_EVERY_X_MINUTES": 20,
|
sead=False,
|
||||||
"CAS_EVERY_X_MINUTES": 30,
|
strike=True,
|
||||||
"SEAD_EVERY_X_MINUTES": 40,
|
antiship=True,
|
||||||
"STRIKE_EVERY_X_MINUTES": 40,
|
strike_max_range=1500000,
|
||||||
|
sead_max_range=1500000,
|
||||||
"INGRESS_EGRESS_DISTANCE": nm_to_meter(30),
|
join_distance=nm_to_meter(5),
|
||||||
"INGRESS_ALT": feet_to_meter(18000),
|
split_distance=nm_to_meter(5),
|
||||||
"EGRESS_ALT": feet_to_meter(18000),
|
rendezvous_altitude=feet_to_meter(10000),
|
||||||
"PATROL_ALT_RANGE": (feet_to_meter(10000), feet_to_meter(24000)),
|
ingress_egress_distance=nm_to_meter(7),
|
||||||
"PATTERN_ALTITUDE": feet_to_meter(5000),
|
ingress_altitude=feet_to_meter(8000),
|
||||||
|
egress_altitude=feet_to_meter(8000),
|
||||||
"CAP_PATTERN_LENGTH": (nm_to_meter(12), nm_to_meter(24)),
|
min_patrol_altitude=feet_to_meter(4000),
|
||||||
"FRONTLINE_CAP_DISTANCE_FROM_FRONTLINE": (nm_to_meter(2), nm_to_meter(8)),
|
max_patrol_altitude=feet_to_meter(15000),
|
||||||
"CAP_DISTANCE_FROM_CP": (nm_to_meter(8), nm_to_meter(25)),
|
pattern_altitude=feet_to_meter(5000),
|
||||||
|
cap_min_track_length=nm_to_meter(8),
|
||||||
"MAX_NUMBER_OF_INTERCEPTION_GROUP": 3,
|
cap_max_track_length=nm_to_meter(18),
|
||||||
}
|
cap_min_distance_from_cp=nm_to_meter(0),
|
||||||
|
cap_max_distance_from_cp=nm_to_meter(5),
|
||||||
WWII_DOCTRINE = {
|
)
|
||||||
|
|
||||||
"GENERATORS": {
|
|
||||||
"CAS": True,
|
|
||||||
"CAP": True,
|
|
||||||
"SEAD": False,
|
|
||||||
"STRIKE": True,
|
|
||||||
"ANTISHIP": True,
|
|
||||||
},
|
|
||||||
|
|
||||||
"STRIKE_MAX_RANGE": 1500000,
|
|
||||||
"SEAD_MAX_RANGE": 1500000,
|
|
||||||
|
|
||||||
"CAP_EVERY_X_MINUTES": 20,
|
|
||||||
"CAS_EVERY_X_MINUTES": 30,
|
|
||||||
"SEAD_EVERY_X_MINUTES": 40,
|
|
||||||
"STRIKE_EVERY_X_MINUTES": 40,
|
|
||||||
|
|
||||||
"INGRESS_EGRESS_DISTANCE": nm_to_meter(7),
|
|
||||||
"INGRESS_ALT": feet_to_meter(8000),
|
|
||||||
"EGRESS_ALT": feet_to_meter(8000),
|
|
||||||
"PATROL_ALT_RANGE": (feet_to_meter(4000), feet_to_meter(15000)),
|
|
||||||
"PATTERN_ALTITUDE": feet_to_meter(5000),
|
|
||||||
|
|
||||||
"CAP_PATTERN_LENGTH": (nm_to_meter(8), nm_to_meter(18)),
|
|
||||||
"FRONTLINE_CAP_DISTANCE_FROM_FRONTLINE": (nm_to_meter(1), nm_to_meter(6)),
|
|
||||||
"CAP_DISTANCE_FROM_CP": (nm_to_meter(0), nm_to_meter(5)),
|
|
||||||
|
|
||||||
"MAX_NUMBER_OF_INTERCEPTION_GROUP": 3,
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@ -807,7 +807,7 @@ CARRIER_TAKEOFF_BAN = [
|
|||||||
Units separated by country.
|
Units separated by country.
|
||||||
country : DCS Country name
|
country : DCS Country name
|
||||||
"""
|
"""
|
||||||
FACTIONS = {
|
FACTIONS: typing.Dict[str, typing.Dict[str, typing.Any]] = {
|
||||||
|
|
||||||
"Bluefor Modern": BLUEFOR_MODERN,
|
"Bluefor Modern": BLUEFOR_MODERN,
|
||||||
"Bluefor Cold War 1970s": BLUEFOR_COLDWAR,
|
"Bluefor Cold War 1970s": BLUEFOR_COLDWAR,
|
||||||
|
|||||||
22
game/game.py
22
game/game.py
@ -4,11 +4,13 @@ from game.db import REWARDS, PLAYER_BUDGET_BASE, sys
|
|||||||
from game.inventory import GlobalAircraftInventory
|
from game.inventory import GlobalAircraftInventory
|
||||||
from game.models.game_stats import GameStats
|
from game.models.game_stats import GameStats
|
||||||
from gen.ato import AirTaskingOrder
|
from gen.ato import AirTaskingOrder
|
||||||
from gen.flights.ai_flight_planner import FlightPlanner
|
from gen.flights.ai_flight_planner import CoalitionMissionPlanner
|
||||||
|
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||||
from gen.ground_forces.ai_ground_planner import GroundPlanner
|
from gen.ground_forces.ai_ground_planner import GroundPlanner
|
||||||
from .event import *
|
from .event import *
|
||||||
from .settings import Settings
|
from .settings import Settings
|
||||||
|
|
||||||
|
|
||||||
COMMISION_UNIT_VARIETY = 4
|
COMMISION_UNIT_VARIETY = 4
|
||||||
COMMISION_LIMITS_SCALE = 1.5
|
COMMISION_LIMITS_SCALE = 1.5
|
||||||
COMMISION_LIMITS_FACTORS = {
|
COMMISION_LIMITS_FACTORS = {
|
||||||
@ -70,7 +72,6 @@ class Game:
|
|||||||
self.date = datetime(start_date.year, start_date.month, start_date.day)
|
self.date = datetime(start_date.year, start_date.month, start_date.day)
|
||||||
self.game_stats = GameStats()
|
self.game_stats = GameStats()
|
||||||
self.game_stats.update(self)
|
self.game_stats.update(self)
|
||||||
self.planners = {}
|
|
||||||
self.ground_planners = {}
|
self.ground_planners = {}
|
||||||
self.informations = []
|
self.informations = []
|
||||||
self.informations.append(Information("Game Start", "-" * 40, 0))
|
self.informations.append(Information("Game Start", "-" * 40, 0))
|
||||||
@ -88,6 +89,7 @@ class Game:
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.sanitize_sides()
|
self.sanitize_sides()
|
||||||
|
self.on_load()
|
||||||
|
|
||||||
|
|
||||||
def sanitize_sides(self):
|
def sanitize_sides(self):
|
||||||
@ -104,11 +106,11 @@ class Game:
|
|||||||
self.enemy_country = "Russia"
|
self.enemy_country = "Russia"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def player_faction(self):
|
def player_faction(self) -> Dict[str, Any]:
|
||||||
return db.FACTIONS[self.player_name]
|
return db.FACTIONS[self.player_name]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def enemy_faction(self):
|
def enemy_faction(self) -> Dict[str, Any]:
|
||||||
return db.FACTIONS[self.enemy_name]
|
return db.FACTIONS[self.enemy_name]
|
||||||
|
|
||||||
def _roll(self, prob, mult):
|
def _roll(self, prob, mult):
|
||||||
@ -203,8 +205,10 @@ class Game:
|
|||||||
else:
|
else:
|
||||||
return event and event.name and event.name == self.player_name
|
return event and event.name and event.name == self.player_name
|
||||||
|
|
||||||
def pass_turn(self, no_action=False, ignored_cps: typing.Collection[ControlPoint] = None):
|
def on_load(self) -> None:
|
||||||
|
ObjectiveDistanceCache.set_theater(self.theater)
|
||||||
|
|
||||||
|
def pass_turn(self, no_action=False, ignored_cps: typing.Collection[ControlPoint] = None):
|
||||||
logging.info("Pass turn")
|
logging.info("Pass turn")
|
||||||
self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0))
|
self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0))
|
||||||
self.turn = self.turn + 1
|
self.turn = self.turn + 1
|
||||||
@ -244,16 +248,12 @@ class Game:
|
|||||||
|
|
||||||
# Plan flights & combat for next turn
|
# Plan flights & combat for next turn
|
||||||
self.__culling_points = self.compute_conflicts_position()
|
self.__culling_points = self.compute_conflicts_position()
|
||||||
self.planners = {}
|
|
||||||
self.ground_planners = {}
|
self.ground_planners = {}
|
||||||
self.blue_ato.clear()
|
self.blue_ato.clear()
|
||||||
self.red_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:
|
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:
|
if cp.has_frontline:
|
||||||
gplanner = GroundPlanner(cp, self)
|
gplanner = GroundPlanner(cp, self)
|
||||||
gplanner.plan_groundwar()
|
gplanner.plan_groundwar()
|
||||||
|
|||||||
@ -182,22 +182,17 @@ class Operation:
|
|||||||
airsupportgen.generate(self.is_awacs_enabled)
|
airsupportgen.generate(self.is_awacs_enabled)
|
||||||
|
|
||||||
# Generate Activity on the map
|
# Generate Activity on the map
|
||||||
airgen = AircraftConflictGenerator(
|
self.airgen.generate_flights(
|
||||||
self.current_mission, self.conflict, self.game.settings, self.game,
|
self.current_mission.country(self.game.player_country),
|
||||||
radio_registry)
|
self.game.blue_ato,
|
||||||
for cp in self.game.theater.controlpoints:
|
self.groundobjectgen.runways
|
||||||
side = cp.captured
|
)
|
||||||
if side:
|
self.airgen.generate_flights(
|
||||||
country = self.current_mission.country(self.game.player_country)
|
self.current_mission.country(self.game.enemy_country),
|
||||||
else:
|
self.game.red_ato,
|
||||||
country = self.current_mission.country(self.game.enemy_country)
|
self.groundobjectgen.runways
|
||||||
if cp.id in self.game.planners.keys():
|
)
|
||||||
airgen.generate_flights(
|
|
||||||
cp,
|
|
||||||
country,
|
|
||||||
self.game.planners[cp.id],
|
|
||||||
groundobjectgen.runways
|
|
||||||
)
|
|
||||||
|
|
||||||
# Generate ground units on frontline everywhere
|
# Generate ground units on frontline everywhere
|
||||||
jtacs: List[JtacInfo] = []
|
jtacs: List[JtacInfo] = []
|
||||||
|
|||||||
@ -14,8 +14,8 @@ from game.settings import Settings
|
|||||||
from game.utils import nm_to_meter
|
from game.utils import nm_to_meter
|
||||||
from gen.airfields import RunwayData
|
from gen.airfields import RunwayData
|
||||||
from gen.airsupportgen import AirSupport
|
from gen.airsupportgen import AirSupport
|
||||||
|
from gen.ato import AirTaskingOrder
|
||||||
from gen.callsigns import create_group_callsign_from_unit
|
from gen.callsigns import create_group_callsign_from_unit
|
||||||
from gen.flights.ai_flight_planner import FlightPlanner
|
|
||||||
from gen.flights.flight import (
|
from gen.flights.flight import (
|
||||||
Flight,
|
Flight,
|
||||||
FlightType,
|
FlightType,
|
||||||
@ -751,31 +751,28 @@ class AircraftConflictGenerator:
|
|||||||
else:
|
else:
|
||||||
logging.warning("Pylon not found ! => Pylon" + key + " on " + str(flight.unit_type))
|
logging.warning("Pylon not found ! => Pylon" + key + " on " + str(flight.unit_type))
|
||||||
|
|
||||||
|
def clear_parking_slots(self) -> None:
|
||||||
def generate_flights(self, cp, country, flight_planner: FlightPlanner,
|
for cp in self.game.theater.controlpoints:
|
||||||
dynamic_runways: Dict[str, RunwayData]):
|
|
||||||
# Clear pydcs parking slots
|
|
||||||
if cp.airport is not None:
|
|
||||||
logging.info("CLEARING SLOTS @ " + cp.airport.name)
|
|
||||||
logging.info("===============")
|
|
||||||
if cp.airport is not None:
|
if cp.airport is not None:
|
||||||
for ps in cp.airport.parking_slots:
|
for parking_slot in cp.airport.parking_slots:
|
||||||
logging.info("SLOT : " + str(ps.unit_id))
|
parking_slot.unit_id = None
|
||||||
ps.unit_id = None
|
|
||||||
logging.info("----------------")
|
|
||||||
logging.info("===============")
|
|
||||||
|
|
||||||
for flight in flight_planner.flights:
|
def generate_flights(self, country, ato: AirTaskingOrder,
|
||||||
|
dynamic_runways: Dict[str, RunwayData]) -> None:
|
||||||
if flight.client_count == 0 and self.game.position_culled(flight.from_cp.position):
|
self.clear_parking_slots()
|
||||||
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)
|
|
||||||
|
|
||||||
|
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(flight.from_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):
|
def setup_group_activation_trigger(self, flight, group):
|
||||||
if flight.scheduled_in > 0 and flight.client_count == 0:
|
if flight.scheduled_in > 0 and flight.client_count == 0:
|
||||||
@ -932,6 +929,14 @@ class AircraftConflictGenerator:
|
|||||||
group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
|
group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
|
||||||
group.points[0].tasks.append(OptROE(OptROE.Values.OpenFire))
|
group.points[0].tasks.append(OptROE(OptROE.Values.OpenFire))
|
||||||
group.points[0].tasks.append(OptRestrictJettison(True))
|
group.points[0].tasks.append(OptRestrictJettison(True))
|
||||||
|
elif flight_type == FlightType.ESCORT:
|
||||||
|
group.task = Escort.name
|
||||||
|
self._setup_group(group, Escort, flight, dynamic_runways)
|
||||||
|
# TODO: Cleanup duplication...
|
||||||
|
group.points[0].tasks.clear()
|
||||||
|
group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
|
||||||
|
group.points[0].tasks.append(OptROE(OptROE.Values.OpenFire))
|
||||||
|
group.points[0].tasks.append(OptRestrictJettison(True))
|
||||||
|
|
||||||
group.points[0].tasks.append(OptRTBOnBingoFuel(True))
|
group.points[0].tasks.append(OptRTBOnBingoFuel(True))
|
||||||
group.points[0].tasks.append(OptRestrictAfterburner(True))
|
group.points[0].tasks.append(OptRestrictAfterburner(True))
|
||||||
|
|||||||
28
gen/ato.py
28
gen/ato.py
@ -11,8 +11,9 @@ the single CAP flight.
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, List
|
from typing import Dict, Iterator, List, Optional
|
||||||
|
|
||||||
|
from dcs.mapping import Point
|
||||||
from .flights.flight import Flight, FlightType
|
from .flights.flight import Flight, FlightType
|
||||||
from theater.missiontarget import MissionTarget
|
from theater.missiontarget import MissionTarget
|
||||||
|
|
||||||
@ -39,6 +40,11 @@ class Package:
|
|||||||
#: The set of flights in the package.
|
#: The set of flights in the package.
|
||||||
flights: List[Flight] = field(default_factory=list)
|
flights: List[Flight] = field(default_factory=list)
|
||||||
|
|
||||||
|
join_point: Optional[Point] = field(default=None, init=False, hash=False)
|
||||||
|
split_point: Optional[Point] = field(default=None, init=False, hash=False)
|
||||||
|
ingress_point: Optional[Point] = field(default=None, init=False, hash=False)
|
||||||
|
egress_point: Optional[Point] = field(default=None, init=False, hash=False)
|
||||||
|
|
||||||
def add_flight(self, flight: Flight) -> None:
|
def add_flight(self, flight: Flight) -> None:
|
||||||
"""Adds a flight to the package."""
|
"""Adds a flight to the package."""
|
||||||
self.flights.append(flight)
|
self.flights.append(flight)
|
||||||
@ -46,12 +52,14 @@ class Package:
|
|||||||
def remove_flight(self, flight: Flight) -> None:
|
def remove_flight(self, flight: Flight) -> None:
|
||||||
"""Removes a flight from the package."""
|
"""Removes a flight from the package."""
|
||||||
self.flights.remove(flight)
|
self.flights.remove(flight)
|
||||||
|
if not self.flights:
|
||||||
|
self.ingress_point = None
|
||||||
|
self.egress_point = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def package_description(self) -> str:
|
def primary_task(self) -> Optional[FlightType]:
|
||||||
"""Generates a package description based on flight composition."""
|
|
||||||
if not self.flights:
|
if not self.flights:
|
||||||
return "No mission"
|
return None
|
||||||
|
|
||||||
flight_counts: Dict[FlightType, int] = defaultdict(lambda: 0)
|
flight_counts: Dict[FlightType, int] = defaultdict(lambda: 0)
|
||||||
for flight in self.flights:
|
for flight in self.flights:
|
||||||
@ -84,13 +92,21 @@ class Package:
|
|||||||
]
|
]
|
||||||
for task in task_priorities:
|
for task in task_priorities:
|
||||||
if flight_counts[task]:
|
if flight_counts[task]:
|
||||||
return task.name
|
return task
|
||||||
|
|
||||||
# If we get here, our task_priorities list above is incomplete. Log the
|
# If we get here, our task_priorities list above is incomplete. Log the
|
||||||
# issue and return the type of *any* flight in the package.
|
# issue and return the type of *any* flight in the package.
|
||||||
some_mission = next(iter(self.flights)).flight_type
|
some_mission = next(iter(self.flights)).flight_type
|
||||||
logging.warning(f"Unhandled mission type: {some_mission}")
|
logging.warning(f"Unhandled mission type: {some_mission}")
|
||||||
return some_mission.name
|
return some_mission
|
||||||
|
|
||||||
|
@property
|
||||||
|
def package_description(self) -> str:
|
||||||
|
"""Generates a package description based on flight composition."""
|
||||||
|
task = self.primary_task
|
||||||
|
if task is None:
|
||||||
|
return "No mission"
|
||||||
|
return task.name
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
def __hash__(self) -> int:
|
||||||
# TODO: Far from perfect. Number packages?
|
# TODO: Far from perfect. Number packages?
|
||||||
|
|||||||
@ -106,7 +106,7 @@ class BriefingGenerator(MissionInfoGenerator):
|
|||||||
aircraft = flight.aircraft_type
|
aircraft = flight.aircraft_type
|
||||||
flight_unit_name = db.unit_type_name(aircraft)
|
flight_unit_name = db.unit_type_name(aircraft)
|
||||||
self.description += "-" * 50 + "\n"
|
self.description += "-" * 50 + "\n"
|
||||||
self.description += f"{flight_unit_name} x {flight.size + 2}\n\n"
|
self.description += f"{flight_unit_name} x {flight.size}\n\n"
|
||||||
|
|
||||||
for i, wpt in enumerate(flight.waypoints):
|
for i, wpt in enumerate(flight.waypoints):
|
||||||
self.description += f"#{i + 1} -- {wpt.name} : {wpt.description}\n"
|
self.description += f"#{i + 1} -- {wpt.name} : {wpt.description}\n"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
51
gen/flights/closestairfields.py
Normal file
51
gen/flights/closestairfields.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
"""Objective adjacency lists."""
|
||||||
|
from typing import Dict, Iterator, List, Optional
|
||||||
|
|
||||||
|
from theater import ConflictTheater, ControlPoint, MissionTarget
|
||||||
|
|
||||||
|
|
||||||
|
class ClosestAirfields:
|
||||||
|
"""Precalculates which control points are closes to the given target."""
|
||||||
|
|
||||||
|
def __init__(self, target: MissionTarget,
|
||||||
|
all_control_points: List[ControlPoint]) -> None:
|
||||||
|
self.target = target
|
||||||
|
self.closest_airfields: List[ControlPoint] = sorted(
|
||||||
|
all_control_points, key=lambda c: self.target.distance_to(c)
|
||||||
|
)
|
||||||
|
|
||||||
|
def airfields_within(self, meters: int) -> Iterator[ControlPoint]:
|
||||||
|
"""Iterates over all airfields within the given range of the target.
|
||||||
|
|
||||||
|
Note that this iterates over *all* airfields, not just friendly
|
||||||
|
airfields.
|
||||||
|
"""
|
||||||
|
for cp in self.closest_airfields:
|
||||||
|
if cp.distance_to(self.target) < meters:
|
||||||
|
yield cp
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectiveDistanceCache:
|
||||||
|
theater: Optional[ConflictTheater] = None
|
||||||
|
closest_airfields: Dict[str, ClosestAirfields] = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def set_theater(cls, theater: ConflictTheater) -> None:
|
||||||
|
if cls.theater is not None:
|
||||||
|
cls.closest_airfields = {}
|
||||||
|
cls.theater = theater
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_closest_airfields(cls, location: MissionTarget) -> ClosestAirfields:
|
||||||
|
if cls.theater is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Call ObjectiveDistanceCache.set_theater before using"
|
||||||
|
)
|
||||||
|
|
||||||
|
if location.name not in cls.closest_airfields:
|
||||||
|
cls.closest_airfields[location.name] = ClosestAirfields(
|
||||||
|
location, cls.theater.controlpoints
|
||||||
|
)
|
||||||
|
return cls.closest_airfields[location.name]
|
||||||
@ -47,6 +47,8 @@ class FlightWaypointType(Enum):
|
|||||||
TARGET_GROUP_LOC = 13 # A target group approximate location
|
TARGET_GROUP_LOC = 13 # A target group approximate location
|
||||||
TARGET_SHIP = 14 # A target ship known location
|
TARGET_SHIP = 14 # A target ship known location
|
||||||
CUSTOM = 15 # User waypoint (no specific behaviour)
|
CUSTOM = 15 # User waypoint (no specific behaviour)
|
||||||
|
JOIN = 16
|
||||||
|
SPLIT = 17
|
||||||
|
|
||||||
|
|
||||||
class PredefinedWaypointCategory(Enum):
|
class PredefinedWaypointCategory(Enum):
|
||||||
|
|||||||
440
gen/flights/flightplan.py
Normal file
440
gen/flights/flightplan.py
Normal file
@ -0,0 +1,440 @@
|
|||||||
|
"""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 dcs.mapping import Point
|
||||||
|
from dcs.unit import Unit
|
||||||
|
|
||||||
|
from game.data.doctrine import Doctrine, MODERN_DOCTRINE
|
||||||
|
from game.utils import nm_to_meter
|
||||||
|
from gen.ato import Package
|
||||||
|
from theater import ControlPoint, FrontLine, MissionTarget, TheaterGroundObject
|
||||||
|
from .closestairfields import ObjectiveDistanceCache
|
||||||
|
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
|
||||||
|
from .waypointbuilder import WaypointBuilder
|
||||||
|
from ..conflictgen import Conflict
|
||||||
|
|
||||||
|
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, package: Package, is_player: bool) -> None:
|
||||||
|
self.game = game
|
||||||
|
self.package = package
|
||||||
|
self.is_player = is_player
|
||||||
|
if is_player:
|
||||||
|
faction = self.game.player_faction
|
||||||
|
else:
|
||||||
|
faction = self.game.enemy_faction
|
||||||
|
self.doctrine: Doctrine = faction.get("doctrine", MODERN_DOCTRINE)
|
||||||
|
|
||||||
|
def populate_flight_plan(
|
||||||
|
self, flight: Flight,
|
||||||
|
# TODO: Custom targets should be an attribute of the flight.
|
||||||
|
custom_targets: Optional[List[Unit]] = None) -> None:
|
||||||
|
"""Creates a default flight plan for the given mission."""
|
||||||
|
if flight not in self.package.flights:
|
||||||
|
raise RuntimeError("Flight must be a part of the package")
|
||||||
|
self.generate_missing_package_waypoints()
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
elif task == FlightType.CAP:
|
||||||
|
self.generate_barcap(flight)
|
||||||
|
elif task == FlightType.CAS:
|
||||||
|
self.generate_cas(flight)
|
||||||
|
elif task == FlightType.DEAD:
|
||||||
|
self.generate_sead(flight, custom_targets)
|
||||||
|
elif task == FlightType.ELINT:
|
||||||
|
logging.error("ELINT flight plan generation not implemented")
|
||||||
|
elif task == FlightType.ESCORT:
|
||||||
|
self.generate_escort(flight)
|
||||||
|
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, custom_targets)
|
||||||
|
elif task == FlightType.STRIKE:
|
||||||
|
self.generate_strike(flight)
|
||||||
|
elif task == FlightType.TARCAP:
|
||||||
|
self.generate_frontline_cap(flight)
|
||||||
|
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_missing_package_waypoints(self) -> None:
|
||||||
|
if self.package.ingress_point is None:
|
||||||
|
self.package.ingress_point = self._ingress_point()
|
||||||
|
if self.package.egress_point is None:
|
||||||
|
self.package.egress_point = self._egress_point()
|
||||||
|
if self.package.join_point is None:
|
||||||
|
self.package.join_point = self._join_point()
|
||||||
|
if self.package.split_point is None:
|
||||||
|
self.package.split_point = self._split_point()
|
||||||
|
|
||||||
|
def generate_strike(self, flight: Flight) -> None:
|
||||||
|
"""Generates a strike flight plan.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
flight: The flight to generate the flight plan for.
|
||||||
|
"""
|
||||||
|
location = self.package.target
|
||||||
|
|
||||||
|
# TODO: Support airfield strikes.
|
||||||
|
if not isinstance(location, TheaterGroundObject):
|
||||||
|
raise InvalidObjectiveLocation(flight.flight_type, location)
|
||||||
|
|
||||||
|
builder = WaypointBuilder(self.doctrine)
|
||||||
|
builder.ascent(flight.from_cp)
|
||||||
|
builder.join(self.package.join_point)
|
||||||
|
builder.ingress_strike(self.package.ingress_point, location)
|
||||||
|
|
||||||
|
if len(location.groups) > 0 and location.dcs_identifier == "AA":
|
||||||
|
# TODO: Replace with DEAD?
|
||||||
|
# Strike missions on SEAD targets target units.
|
||||||
|
for g in location.groups:
|
||||||
|
for j, u in enumerate(g.units):
|
||||||
|
builder.strike_point(u, f"{u.type} #{j}", location)
|
||||||
|
else:
|
||||||
|
# TODO: Does this actually happen?
|
||||||
|
# ConflictTheater is built with the belief that multiple ground
|
||||||
|
# objects have the same name. If that's the case,
|
||||||
|
# TheaterGroundObject needs some refactoring because it behaves very
|
||||||
|
# differently for SAM sites than it does for strike targets.
|
||||||
|
buildings = self.game.theater.find_ground_objects_by_obj_name(
|
||||||
|
location.obj_name
|
||||||
|
)
|
||||||
|
for building in buildings:
|
||||||
|
if building.is_dead:
|
||||||
|
continue
|
||||||
|
|
||||||
|
builder.strike_point(
|
||||||
|
building,
|
||||||
|
f"{building.obj_name} {building.category}",
|
||||||
|
location
|
||||||
|
)
|
||||||
|
|
||||||
|
builder.egress(self.package.egress_point, location)
|
||||||
|
builder.split(self.package.split_point)
|
||||||
|
builder.rtb(flight.from_cp)
|
||||||
|
|
||||||
|
flight.points = builder.build()
|
||||||
|
|
||||||
|
def generate_barcap(self, flight: Flight) -> None:
|
||||||
|
"""Generate a BARCAP flight at a given location.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
flight: The flight to generate the flight plan for.
|
||||||
|
"""
|
||||||
|
location = self.package.target
|
||||||
|
|
||||||
|
if isinstance(location, FrontLine):
|
||||||
|
raise InvalidObjectiveLocation(flight.flight_type, location)
|
||||||
|
|
||||||
|
patrol_alt = random.randint(
|
||||||
|
self.doctrine.min_patrol_altitude,
|
||||||
|
self.doctrine.max_patrol_altitude
|
||||||
|
)
|
||||||
|
|
||||||
|
closest_cache = ObjectiveDistanceCache.get_closest_airfields(location)
|
||||||
|
for airfield in closest_cache.closest_airfields:
|
||||||
|
if airfield.captured != self.is_player:
|
||||||
|
closest_airfield = airfield
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
logging.error("Could not find any enemy airfields")
|
||||||
|
return
|
||||||
|
|
||||||
|
heading = location.position.heading_between_point(
|
||||||
|
closest_airfield.position
|
||||||
|
)
|
||||||
|
|
||||||
|
end = location.position.point_from_heading(
|
||||||
|
heading,
|
||||||
|
random.randint(self.doctrine.cap_min_distance_from_cp,
|
||||||
|
self.doctrine.cap_max_distance_from_cp)
|
||||||
|
)
|
||||||
|
diameter = random.randint(
|
||||||
|
self.doctrine.cap_min_track_length,
|
||||||
|
self.doctrine.cap_max_track_length
|
||||||
|
)
|
||||||
|
start = end.point_from_heading(heading - 180, diameter)
|
||||||
|
|
||||||
|
builder = WaypointBuilder(self.doctrine)
|
||||||
|
builder.ascent(flight.from_cp)
|
||||||
|
builder.race_track(start, end, patrol_alt)
|
||||||
|
builder.rtb(flight.from_cp)
|
||||||
|
flight.points = builder.build()
|
||||||
|
|
||||||
|
def generate_frontline_cap(self, flight: Flight) -> None:
|
||||||
|
"""Generate a CAP flight plan for the given front line.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
flight: The flight to generate the flight plan for.
|
||||||
|
"""
|
||||||
|
location = self.package.target
|
||||||
|
|
||||||
|
if not isinstance(location, FrontLine):
|
||||||
|
raise InvalidObjectiveLocation(flight.flight_type, location)
|
||||||
|
|
||||||
|
ally_cp, enemy_cp = location.control_points
|
||||||
|
patrol_alt = random.randint(self.doctrine.min_patrol_altitude,
|
||||||
|
self.doctrine.max_patrol_altitude)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
builder = WaypointBuilder(self.doctrine)
|
||||||
|
builder.ascent(flight.from_cp)
|
||||||
|
builder.race_track(orbit0p, orbit1p, patrol_alt)
|
||||||
|
builder.rtb(flight.from_cp)
|
||||||
|
flight.points = builder.build()
|
||||||
|
|
||||||
|
def generate_sead(self, flight: Flight,
|
||||||
|
custom_targets: Optional[List[Unit]]) -> None:
|
||||||
|
"""Generate a SEAD/DEAD flight at a given location.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
flight: The flight to generate the flight plan for.
|
||||||
|
custom_targets: Specific radar equipped units selected by the user.
|
||||||
|
"""
|
||||||
|
location = self.package.target
|
||||||
|
|
||||||
|
if not isinstance(location, TheaterGroundObject):
|
||||||
|
raise InvalidObjectiveLocation(flight.flight_type, location)
|
||||||
|
|
||||||
|
if custom_targets is None:
|
||||||
|
custom_targets = []
|
||||||
|
|
||||||
|
builder = WaypointBuilder(self.doctrine)
|
||||||
|
builder.ascent(flight.from_cp)
|
||||||
|
builder.join(self.package.join_point)
|
||||||
|
builder.ingress_sead(self.package.ingress_point, location)
|
||||||
|
|
||||||
|
# TODO: Unify these.
|
||||||
|
# There doesn't seem to be any reason to treat the UI fragged missions
|
||||||
|
# different from the automatic missions.
|
||||||
|
if custom_targets:
|
||||||
|
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:
|
||||||
|
builder.dead_point(target, location.name, location)
|
||||||
|
else:
|
||||||
|
builder.sead_point(target, location.name, location)
|
||||||
|
else:
|
||||||
|
if flight.flight_type == FlightType.DEAD:
|
||||||
|
builder.dead_area(location)
|
||||||
|
else:
|
||||||
|
builder.sead_area(location)
|
||||||
|
|
||||||
|
builder.egress(self.package.egress_point, location)
|
||||||
|
builder.split(self.package.split_point)
|
||||||
|
builder.rtb(flight.from_cp)
|
||||||
|
|
||||||
|
flight.points = builder.build()
|
||||||
|
|
||||||
|
def generate_escort(self, flight: Flight) -> None:
|
||||||
|
# TODO: Decide common waypoints for the package ahead of time.
|
||||||
|
# Packages should determine some common points like push, ingress,
|
||||||
|
# egress, and split points ahead of time so they can be shared by all
|
||||||
|
# flights.
|
||||||
|
|
||||||
|
patrol_alt = random.randint(
|
||||||
|
self.doctrine.min_patrol_altitude,
|
||||||
|
self.doctrine.max_patrol_altitude
|
||||||
|
)
|
||||||
|
|
||||||
|
builder = WaypointBuilder(self.doctrine)
|
||||||
|
builder.ascent(flight.from_cp)
|
||||||
|
builder.join(self.package.join_point)
|
||||||
|
builder.race_track(
|
||||||
|
self.package.ingress_point,
|
||||||
|
self.package.egress_point,
|
||||||
|
patrol_alt
|
||||||
|
)
|
||||||
|
builder.split(self.package.split_point)
|
||||||
|
builder.rtb(flight.from_cp)
|
||||||
|
|
||||||
|
flight.points = builder.build()
|
||||||
|
|
||||||
|
def generate_cas(self, flight: Flight) -> None:
|
||||||
|
"""Generate a CAS flight plan for the given target.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
flight: The flight to generate the flight plan for.
|
||||||
|
"""
|
||||||
|
location = self.package.target
|
||||||
|
|
||||||
|
if not isinstance(location, FrontLine):
|
||||||
|
raise InvalidObjectiveLocation(flight.flight_type, location)
|
||||||
|
|
||||||
|
is_helo = getattr(flight.unit_type, "helicopter", False)
|
||||||
|
cap_alt = 500 if is_helo else 1000
|
||||||
|
|
||||||
|
ingress, heading, distance = Conflict.frontline_vector(
|
||||||
|
location.control_points[0], location.control_points[1],
|
||||||
|
self.game.theater
|
||||||
|
)
|
||||||
|
center = ingress.point_from_heading(heading, distance / 2)
|
||||||
|
egress = ingress.point_from_heading(heading, distance)
|
||||||
|
|
||||||
|
builder = WaypointBuilder(self.doctrine)
|
||||||
|
builder.ascent(flight.from_cp, is_helo)
|
||||||
|
builder.join(self.package.join_point)
|
||||||
|
builder.ingress_cas(ingress, location)
|
||||||
|
builder.cas(center, cap_alt)
|
||||||
|
builder.egress(egress, location)
|
||||||
|
builder.split(self.package.split_point)
|
||||||
|
builder.rtb(flight.from_cp, is_helo)
|
||||||
|
|
||||||
|
flight.points = builder.build()
|
||||||
|
|
||||||
|
# TODO: Make a model for the waypoint builder and use that in the UI.
|
||||||
|
def generate_ascend_point(self, departure: ControlPoint) -> FlightWaypoint:
|
||||||
|
"""Generate ascend point.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
departure: Departure airfield or carrier.
|
||||||
|
"""
|
||||||
|
builder = WaypointBuilder(self.doctrine)
|
||||||
|
builder.ascent(departure)
|
||||||
|
return builder.build()[0]
|
||||||
|
|
||||||
|
def generate_descend_point(self, arrival: ControlPoint) -> FlightWaypoint:
|
||||||
|
"""Generate approach/descend point.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
arrival: Arrival airfield or carrier.
|
||||||
|
"""
|
||||||
|
builder = WaypointBuilder(self.doctrine)
|
||||||
|
builder.descent(arrival)
|
||||||
|
return builder.build()[0]
|
||||||
|
|
||||||
|
def generate_rtb_waypoint(self, arrival: ControlPoint) -> FlightWaypoint:
|
||||||
|
"""Generate RTB landing point.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
arrival: Arrival airfield or carrier.
|
||||||
|
"""
|
||||||
|
builder = WaypointBuilder(self.doctrine)
|
||||||
|
builder.land(arrival)
|
||||||
|
return builder.build()[0]
|
||||||
|
|
||||||
|
def _join_point(self) -> Point:
|
||||||
|
ingress_point = self.package.ingress_point
|
||||||
|
heading = self._heading_to_package_airfield(ingress_point)
|
||||||
|
return ingress_point.point_from_heading(heading,
|
||||||
|
-self.doctrine.join_distance)
|
||||||
|
|
||||||
|
def _split_point(self) -> Point:
|
||||||
|
egress_point = self.package.egress_point
|
||||||
|
heading = self._heading_to_package_airfield(egress_point)
|
||||||
|
return egress_point.point_from_heading(heading,
|
||||||
|
-self.doctrine.split_distance)
|
||||||
|
|
||||||
|
def _ingress_point(self) -> Point:
|
||||||
|
heading = self._target_heading_to_package_airfield()
|
||||||
|
return self.package.target.position.point_from_heading(
|
||||||
|
heading - 180 + 25, self.doctrine.ingress_egress_distance
|
||||||
|
)
|
||||||
|
|
||||||
|
def _egress_point(self) -> Point:
|
||||||
|
heading = self._target_heading_to_package_airfield()
|
||||||
|
return self.package.target.position.point_from_heading(
|
||||||
|
heading - 180 - 25, self.doctrine.ingress_egress_distance
|
||||||
|
)
|
||||||
|
|
||||||
|
def _target_heading_to_package_airfield(self) -> int:
|
||||||
|
return self._heading_to_package_airfield(self.package.target.position)
|
||||||
|
|
||||||
|
def _heading_to_package_airfield(self, point: Point) -> int:
|
||||||
|
return self.package_airfield().position.heading_between_point(point)
|
||||||
|
|
||||||
|
# TODO: Set ingress/egress/join/split points in the Package.
|
||||||
|
def package_airfield(self) -> ControlPoint:
|
||||||
|
# We'll always have a package, but if this is being planned via the UI
|
||||||
|
# it could be the first flight in the package.
|
||||||
|
if not self.package.flights:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Cannot determine source airfield for package with no flights"
|
||||||
|
)
|
||||||
|
|
||||||
|
# The package airfield is either the flight's airfield (when there is no
|
||||||
|
# package) or the closest airfield to the objective that is the
|
||||||
|
# departure airfield for some flight in the package.
|
||||||
|
cache = ObjectiveDistanceCache.get_closest_airfields(
|
||||||
|
self.package.target
|
||||||
|
)
|
||||||
|
for airfield in cache.closest_airfields:
|
||||||
|
for flight in self.package.flights:
|
||||||
|
if flight.from_cp == airfield:
|
||||||
|
return airfield
|
||||||
|
raise RuntimeError(
|
||||||
|
"Could not find any airfield assigned to this package"
|
||||||
|
)
|
||||||
294
gen/flights/waypointbuilder.py
Normal file
294
gen/flights/waypointbuilder.py
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
|
from dcs.mapping import Point
|
||||||
|
from dcs.unit import Unit
|
||||||
|
|
||||||
|
from game.data.doctrine import Doctrine
|
||||||
|
from game.utils import nm_to_meter
|
||||||
|
from theater import ControlPoint, MissionTarget, TheaterGroundObject
|
||||||
|
from .flight import FlightWaypoint, FlightWaypointType
|
||||||
|
|
||||||
|
|
||||||
|
class WaypointBuilder:
|
||||||
|
def __init__(self, doctrine: Doctrine) -> None:
|
||||||
|
self.doctrine = doctrine
|
||||||
|
self.waypoints: List[FlightWaypoint] = []
|
||||||
|
self.ingress_point: Optional[FlightWaypoint] = None
|
||||||
|
|
||||||
|
def build(self) -> List[FlightWaypoint]:
|
||||||
|
return self.waypoints
|
||||||
|
|
||||||
|
def ascent(self, departure: ControlPoint, is_helo: bool = False) -> None:
|
||||||
|
"""Create ascent waypoint for the given departure airfield or carrier.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
departure: Departure airfield or carrier.
|
||||||
|
"""
|
||||||
|
# TODO: Pick runway based on wind direction.
|
||||||
|
heading = departure.heading
|
||||||
|
position = departure.position.point_from_heading(
|
||||||
|
heading, nm_to_meter(5)
|
||||||
|
)
|
||||||
|
waypoint = FlightWaypoint(
|
||||||
|
FlightWaypointType.ASCEND_POINT,
|
||||||
|
position.x,
|
||||||
|
position.y,
|
||||||
|
500 if is_helo else self.doctrine.pattern_altitude
|
||||||
|
)
|
||||||
|
waypoint.name = "ASCEND"
|
||||||
|
waypoint.alt_type = "RADIO"
|
||||||
|
waypoint.description = "Ascend"
|
||||||
|
waypoint.pretty_name = "Ascend"
|
||||||
|
self.waypoints.append(waypoint)
|
||||||
|
|
||||||
|
def descent(self, arrival: ControlPoint, is_helo: bool = False) -> None:
|
||||||
|
"""Create descent waypoint for the given arrival airfield or carrier.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
arrival: Arrival airfield or carrier.
|
||||||
|
"""
|
||||||
|
# TODO: Pick runway based on wind direction.
|
||||||
|
# ControlPoint.heading is the departure heading.
|
||||||
|
heading = (arrival.heading + 180) % 360
|
||||||
|
position = arrival.position.point_from_heading(
|
||||||
|
heading, nm_to_meter(5)
|
||||||
|
)
|
||||||
|
waypoint = FlightWaypoint(
|
||||||
|
FlightWaypointType.DESCENT_POINT,
|
||||||
|
position.x,
|
||||||
|
position.y,
|
||||||
|
300 if is_helo else self.doctrine.pattern_altitude
|
||||||
|
)
|
||||||
|
waypoint.name = "DESCEND"
|
||||||
|
waypoint.alt_type = "RADIO"
|
||||||
|
waypoint.description = "Descend to pattern altitude"
|
||||||
|
waypoint.pretty_name = "Ascend"
|
||||||
|
self.waypoints.append(waypoint)
|
||||||
|
|
||||||
|
def land(self, arrival: ControlPoint) -> None:
|
||||||
|
"""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"
|
||||||
|
self.waypoints.append(waypoint)
|
||||||
|
|
||||||
|
def join(self, position: Point) -> None:
|
||||||
|
waypoint = FlightWaypoint(
|
||||||
|
FlightWaypointType.JOIN,
|
||||||
|
position.x,
|
||||||
|
position.y,
|
||||||
|
self.doctrine.ingress_altitude
|
||||||
|
)
|
||||||
|
waypoint.pretty_name = "Join"
|
||||||
|
waypoint.description = "Rendezvous with package"
|
||||||
|
waypoint.name = "JOIN"
|
||||||
|
self.waypoints.append(waypoint)
|
||||||
|
|
||||||
|
def split(self, position: Point) -> None:
|
||||||
|
waypoint = FlightWaypoint(
|
||||||
|
FlightWaypointType.SPLIT,
|
||||||
|
position.x,
|
||||||
|
position.y,
|
||||||
|
self.doctrine.ingress_altitude
|
||||||
|
)
|
||||||
|
waypoint.pretty_name = "Split"
|
||||||
|
waypoint.description = "Depart from package"
|
||||||
|
waypoint.name = "SPLIT"
|
||||||
|
self.waypoints.append(waypoint)
|
||||||
|
|
||||||
|
def ingress_cas(self, position: Point, objective: MissionTarget) -> None:
|
||||||
|
self._ingress(FlightWaypointType.INGRESS_CAS, position, objective)
|
||||||
|
|
||||||
|
def ingress_sead(self, position: Point, objective: MissionTarget) -> None:
|
||||||
|
self._ingress(FlightWaypointType.INGRESS_SEAD, position, objective)
|
||||||
|
|
||||||
|
def ingress_strike(self, position: Point, objective: MissionTarget) -> None:
|
||||||
|
self._ingress(FlightWaypointType.INGRESS_STRIKE, position, objective)
|
||||||
|
|
||||||
|
def _ingress(self, ingress_type: FlightWaypointType, position: Point,
|
||||||
|
objective: MissionTarget) -> None:
|
||||||
|
if self.ingress_point is not None:
|
||||||
|
raise RuntimeError("A flight plan can have only one ingress point.")
|
||||||
|
|
||||||
|
waypoint = FlightWaypoint(
|
||||||
|
ingress_type,
|
||||||
|
position.x,
|
||||||
|
position.y,
|
||||||
|
self.doctrine.ingress_altitude
|
||||||
|
)
|
||||||
|
waypoint.pretty_name = "INGRESS on " + objective.name
|
||||||
|
waypoint.description = "INGRESS on " + objective.name
|
||||||
|
waypoint.name = "INGRESS"
|
||||||
|
self.waypoints.append(waypoint)
|
||||||
|
self.ingress_point = waypoint
|
||||||
|
|
||||||
|
def egress(self, position: Point, target: MissionTarget) -> None:
|
||||||
|
waypoint = FlightWaypoint(
|
||||||
|
FlightWaypointType.EGRESS,
|
||||||
|
position.x,
|
||||||
|
position.y,
|
||||||
|
self.doctrine.ingress_altitude
|
||||||
|
)
|
||||||
|
waypoint.pretty_name = "EGRESS from " + target.name
|
||||||
|
waypoint.description = "EGRESS from " + target.name
|
||||||
|
waypoint.name = "EGRESS"
|
||||||
|
self.waypoints.append(waypoint)
|
||||||
|
|
||||||
|
def dead_point(self, target: Union[TheaterGroundObject, Unit], name: str,
|
||||||
|
location: MissionTarget) -> None:
|
||||||
|
self._target_point(target, name, f"STRIKE [{location.name}]: {name}",
|
||||||
|
location)
|
||||||
|
# TODO: Seems fishy.
|
||||||
|
self.ingress_point.targetGroup = location
|
||||||
|
|
||||||
|
def sead_point(self, target: Union[TheaterGroundObject, Unit], name: str,
|
||||||
|
location: MissionTarget) -> None:
|
||||||
|
self._target_point(target, name, f"STRIKE [{location.name}]: {name}",
|
||||||
|
location)
|
||||||
|
# TODO: Seems fishy.
|
||||||
|
self.ingress_point.targetGroup = location
|
||||||
|
|
||||||
|
def strike_point(self, target: Union[TheaterGroundObject, Unit], name: str,
|
||||||
|
location: MissionTarget) -> None:
|
||||||
|
self._target_point(target, name, f"STRIKE [{location.name}]: {name}",
|
||||||
|
location)
|
||||||
|
|
||||||
|
def _target_point(self, target: Union[TheaterGroundObject, Unit], name: str,
|
||||||
|
description: str, location: MissionTarget) -> None:
|
||||||
|
if self.ingress_point is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"An ingress point must be added before target points."
|
||||||
|
)
|
||||||
|
|
||||||
|
waypoint = FlightWaypoint(
|
||||||
|
FlightWaypointType.TARGET_POINT,
|
||||||
|
target.position.x,
|
||||||
|
target.position.y,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
waypoint.description = description
|
||||||
|
waypoint.pretty_name = description
|
||||||
|
waypoint.name = name
|
||||||
|
waypoint.only_for_player = True
|
||||||
|
self.waypoints.append(waypoint)
|
||||||
|
# TODO: This seems wrong, but it's what was there before.
|
||||||
|
self.ingress_point.targets.append(location)
|
||||||
|
|
||||||
|
def sead_area(self, target: MissionTarget) -> None:
|
||||||
|
self._target_area(f"SEAD on {target.name}", target)
|
||||||
|
# TODO: Seems fishy.
|
||||||
|
self.ingress_point.targetGroup = target
|
||||||
|
|
||||||
|
def dead_area(self, target: MissionTarget) -> None:
|
||||||
|
self._target_area(f"DEAD on {target.name}", target)
|
||||||
|
# TODO: Seems fishy.
|
||||||
|
self.ingress_point.targetGroup = target
|
||||||
|
|
||||||
|
def _target_area(self, name: str, location: MissionTarget) -> None:
|
||||||
|
if self.ingress_point is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"An ingress point must be added before target points."
|
||||||
|
)
|
||||||
|
|
||||||
|
waypoint = FlightWaypoint(
|
||||||
|
FlightWaypointType.TARGET_GROUP_LOC,
|
||||||
|
location.position.x,
|
||||||
|
location.position.y,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
waypoint.description = name
|
||||||
|
waypoint.pretty_name = name
|
||||||
|
waypoint.name = name
|
||||||
|
waypoint.only_for_player = True
|
||||||
|
self.waypoints.append(waypoint)
|
||||||
|
# TODO: This seems wrong, but it's what was there before.
|
||||||
|
self.ingress_point.targets.append(location)
|
||||||
|
|
||||||
|
def cas(self, position: Point, altitude: int) -> None:
|
||||||
|
waypoint = FlightWaypoint(
|
||||||
|
FlightWaypointType.CAS,
|
||||||
|
position.x,
|
||||||
|
position.y,
|
||||||
|
altitude
|
||||||
|
)
|
||||||
|
waypoint.alt_type = "RADIO"
|
||||||
|
waypoint.description = "Provide CAS"
|
||||||
|
waypoint.name = "CAS"
|
||||||
|
waypoint.pretty_name = "CAS"
|
||||||
|
self.waypoints.append(waypoint)
|
||||||
|
|
||||||
|
def race_track_start(self, position: Point, altitude: int) -> None:
|
||||||
|
"""Creates a racetrack start waypoint.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
position: Position of the waypoint.
|
||||||
|
altitude: Altitude of the racetrack in meters.
|
||||||
|
"""
|
||||||
|
waypoint = FlightWaypoint(
|
||||||
|
FlightWaypointType.PATROL_TRACK,
|
||||||
|
position.x,
|
||||||
|
position.y,
|
||||||
|
altitude
|
||||||
|
)
|
||||||
|
waypoint.name = "RACETRACK START"
|
||||||
|
waypoint.description = "Orbit between this point and the next point"
|
||||||
|
waypoint.pretty_name = "Race-track start"
|
||||||
|
self.waypoints.append(waypoint)
|
||||||
|
|
||||||
|
# TODO: Does this actually do anything?
|
||||||
|
# orbit0.targets.append(location)
|
||||||
|
# Note: Targets of PATROL TRACK waypoints are the points to be defended.
|
||||||
|
# orbit0.targets.append(flight.from_cp)
|
||||||
|
# orbit0.targets.append(center)
|
||||||
|
|
||||||
|
def race_track_end(self, position: Point, altitude: int) -> None:
|
||||||
|
"""Creates a racetrack end waypoint.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
position: Position of the waypoint.
|
||||||
|
altitude: Altitude of the racetrack in meters.
|
||||||
|
"""
|
||||||
|
waypoint = FlightWaypoint(
|
||||||
|
FlightWaypointType.PATROL,
|
||||||
|
position.x,
|
||||||
|
position.y,
|
||||||
|
altitude
|
||||||
|
)
|
||||||
|
waypoint.name = "RACETRACK END"
|
||||||
|
waypoint.description = "Orbit between this point and the previous point"
|
||||||
|
waypoint.pretty_name = "Race-track end"
|
||||||
|
self.waypoints.append(waypoint)
|
||||||
|
|
||||||
|
def race_track(self, start: Point, end: Point, altitude: int) -> None:
|
||||||
|
"""Creates two waypoint for a racetrack orbit.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
start: The beginning racetrack waypoint.
|
||||||
|
end: The ending racetrack waypoint.
|
||||||
|
altitude: The racetrack altitude.
|
||||||
|
"""
|
||||||
|
self.race_track_start(start, altitude)
|
||||||
|
self.race_track_end(end, altitude)
|
||||||
|
|
||||||
|
def rtb(self, arrival: ControlPoint, is_helo: bool = False) -> None:
|
||||||
|
"""Creates descent ant landing waypoints for the given control point.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
arrival: Arrival airfield or carrier.
|
||||||
|
"""
|
||||||
|
self.descent(arrival, is_helo)
|
||||||
|
self.land(arrival)
|
||||||
@ -54,7 +54,12 @@ class Dialog:
|
|||||||
cls.edit_package_dialog.show()
|
cls.edit_package_dialog.show()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def open_edit_flight_dialog(cls, flight: Flight):
|
def open_edit_flight_dialog(cls, package_model: PackageModel,
|
||||||
|
flight: Flight) -> None:
|
||||||
"""Opens the dialog to edit the given flight."""
|
"""Opens the dialog to edit the given flight."""
|
||||||
cls.edit_flight_dialog = QEditFlightDialog(cls.game_model.game, flight)
|
cls.edit_flight_dialog = QEditFlightDialog(
|
||||||
|
cls.game_model.game,
|
||||||
|
package_model.package,
|
||||||
|
flight
|
||||||
|
)
|
||||||
cls.edit_flight_dialog.show()
|
cls.edit_flight_dialog.show()
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
from PySide2.QtWidgets import QFrame, QGroupBox, QHBoxLayout, QPushButton
|
from PySide2.QtWidgets import QFrame, QGroupBox, QHBoxLayout, QPushButton
|
||||||
|
|
||||||
import qt_ui.uiconstants as CONST
|
import qt_ui.uiconstants as CONST
|
||||||
@ -74,7 +76,7 @@ class QTopPanel(QFrame):
|
|||||||
self.layout.setContentsMargins(0,0,0,0)
|
self.layout.setContentsMargins(0,0,0,0)
|
||||||
self.setLayout(self.layout)
|
self.setLayout(self.layout)
|
||||||
|
|
||||||
def setGame(self, game:Game):
|
def setGame(self, game: Optional[Game]):
|
||||||
self.game = game
|
self.game = game
|
||||||
if game is not None:
|
if game is not None:
|
||||||
self.turnCounter.setCurrentTurn(self.game.turn, self.game.current_day)
|
self.turnCounter.setCurrentTurn(self.game.turn, self.game.current_day)
|
||||||
|
|||||||
@ -16,6 +16,7 @@ from PySide2.QtWidgets import (
|
|||||||
from gen.ato import Package
|
from gen.ato import Package
|
||||||
from gen.flights.flight import Flight
|
from gen.flights.flight import Flight
|
||||||
from ..models import AtoModel, GameModel, NullListModel, PackageModel
|
from ..models import AtoModel, GameModel, NullListModel, PackageModel
|
||||||
|
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||||
|
|
||||||
|
|
||||||
class QFlightList(QListView):
|
class QFlightList(QListView):
|
||||||
@ -122,7 +123,7 @@ class QFlightPanel(QGroupBox):
|
|||||||
return
|
return
|
||||||
from qt_ui.dialogs import Dialog
|
from qt_ui.dialogs import Dialog
|
||||||
Dialog.open_edit_flight_dialog(
|
Dialog.open_edit_flight_dialog(
|
||||||
self.package_model.flight_at_index(index)
|
self.package_model, self.package_model.flight_at_index(index)
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_delete(self) -> None:
|
def on_delete(self) -> None:
|
||||||
@ -134,6 +135,7 @@ class QFlightPanel(QGroupBox):
|
|||||||
self.game_model.game.aircraft_inventory.return_from_flight(
|
self.game_model.game.aircraft_inventory.return_from_flight(
|
||||||
self.flight_list.selected_item)
|
self.flight_list.selected_item)
|
||||||
self.package_model.delete_flight_at_index(index)
|
self.package_model.delete_flight_at_index(index)
|
||||||
|
GameUpdateSignal.get_instance().redraw_flight_paths()
|
||||||
|
|
||||||
|
|
||||||
class QPackageList(QListView):
|
class QPackageList(QListView):
|
||||||
@ -217,6 +219,7 @@ class QPackagePanel(QGroupBox):
|
|||||||
logging.error(f"Cannot delete package when no package is selected.")
|
logging.error(f"Cannot delete package when no package is selected.")
|
||||||
return
|
return
|
||||||
self.ato_model.delete_package_at_index(index)
|
self.ato_model.delete_package_at_index(index)
|
||||||
|
GameUpdateSignal.get_instance().redraw_flight_paths()
|
||||||
|
|
||||||
|
|
||||||
class QAirTaskingOrderPanel(QSplitter):
|
class QAirTaskingOrderPanel(QSplitter):
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import typing
|
from typing import Dict, List, Optional, Tuple
|
||||||
from typing import Dict, Tuple
|
|
||||||
|
|
||||||
from PySide2.QtCore import Qt
|
from PySide2.QtCore import Qt
|
||||||
from PySide2.QtGui import QBrush, QColor, QPen, QPixmap, QWheelEvent
|
from PySide2.QtGui import QBrush, QColor, QPen, QPixmap, QWheelEvent
|
||||||
from PySide2.QtWidgets import (
|
from PySide2.QtWidgets import (
|
||||||
QFrame,
|
QFrame,
|
||||||
|
QGraphicsItem,
|
||||||
QGraphicsOpacityEffect,
|
QGraphicsOpacityEffect,
|
||||||
QGraphicsScene,
|
QGraphicsScene,
|
||||||
QGraphicsView,
|
QGraphicsView,
|
||||||
@ -43,6 +43,9 @@ class QLiberationMap(QGraphicsView):
|
|||||||
super(QLiberationMap, self).__init__()
|
super(QLiberationMap, self).__init__()
|
||||||
QLiberationMap.instance = self
|
QLiberationMap.instance = self
|
||||||
self.game_model = game_model
|
self.game_model = game_model
|
||||||
|
self.game: Optional[Game] = game_model.game
|
||||||
|
|
||||||
|
self.flight_path_items: List[QGraphicsItem] = []
|
||||||
|
|
||||||
self.setMinimumSize(800,600)
|
self.setMinimumSize(800,600)
|
||||||
self.setMaximumHeight(2160)
|
self.setMaximumHeight(2160)
|
||||||
@ -53,6 +56,10 @@ class QLiberationMap(QGraphicsView):
|
|||||||
self.connectSignals()
|
self.connectSignals()
|
||||||
self.setGame(game_model.game)
|
self.setGame(game_model.game)
|
||||||
|
|
||||||
|
GameUpdateSignal.get_instance().flight_paths_changed.connect(
|
||||||
|
lambda: self.draw_flight_plans(self.scene())
|
||||||
|
)
|
||||||
|
|
||||||
def init_scene(self):
|
def init_scene(self):
|
||||||
scene = QLiberationScene(self)
|
scene = QLiberationScene(self)
|
||||||
self.setScene(scene)
|
self.setScene(scene)
|
||||||
@ -65,7 +72,7 @@ class QLiberationMap(QGraphicsView):
|
|||||||
def connectSignals(self):
|
def connectSignals(self):
|
||||||
GameUpdateSignal.get_instance().gameupdated.connect(self.setGame)
|
GameUpdateSignal.get_instance().gameupdated.connect(self.setGame)
|
||||||
|
|
||||||
def setGame(self, game: Game):
|
def setGame(self, game: Optional[Game]):
|
||||||
self.game = game
|
self.game = game
|
||||||
print("Reloading Map Canvas")
|
print("Reloading Map Canvas")
|
||||||
if self.game is not None:
|
if self.game is not None:
|
||||||
@ -176,8 +183,7 @@ class QLiberationMap(QGraphicsView):
|
|||||||
if self.get_display_rule("lines"):
|
if self.get_display_rule("lines"):
|
||||||
self.scene_create_lines_for_cp(cp, playerColor, enemyColor)
|
self.scene_create_lines_for_cp(cp, playerColor, enemyColor)
|
||||||
|
|
||||||
if self.get_display_rule("flight_paths"):
|
self.draw_flight_plans(scene)
|
||||||
self.draw_flight_plans(scene)
|
|
||||||
|
|
||||||
for cp in self.game.theater.controlpoints:
|
for cp in self.game.theater.controlpoints:
|
||||||
pos = self._transform_point(cp.position)
|
pos = self._transform_point(cp.position)
|
||||||
@ -188,6 +194,15 @@ class QLiberationMap(QGraphicsView):
|
|||||||
text.setPos(pos[0] + CONST.CP_SIZE + 1, pos[1] - CONST.CP_SIZE / 2 + 1)
|
text.setPos(pos[0] + CONST.CP_SIZE + 1, pos[1] - CONST.CP_SIZE / 2 + 1)
|
||||||
|
|
||||||
def draw_flight_plans(self, scene) -> None:
|
def draw_flight_plans(self, scene) -> None:
|
||||||
|
for item in self.flight_path_items:
|
||||||
|
try:
|
||||||
|
scene.removeItem(item)
|
||||||
|
except RuntimeError:
|
||||||
|
# Something may have caused those items to already be removed.
|
||||||
|
pass
|
||||||
|
self.flight_path_items.clear()
|
||||||
|
if not self.get_display_rule("flight_paths"):
|
||||||
|
return
|
||||||
for package in self.game_model.ato_model.packages:
|
for package in self.game_model.ato_model.packages:
|
||||||
for flight in package.flights:
|
for flight in package.flights:
|
||||||
self.draw_flight_plan(scene, flight)
|
self.draw_flight_plan(scene, flight)
|
||||||
@ -209,17 +224,21 @@ class QLiberationMap(QGraphicsView):
|
|||||||
player: bool) -> None:
|
player: bool) -> None:
|
||||||
waypoint_pen = self.waypoint_pen(player)
|
waypoint_pen = self.waypoint_pen(player)
|
||||||
waypoint_brush = self.waypoint_brush(player)
|
waypoint_brush = self.waypoint_brush(player)
|
||||||
scene.addEllipse(position[0], position[1], self.WAYPOINT_SIZE,
|
self.flight_path_items.append(scene.addEllipse(
|
||||||
self.WAYPOINT_SIZE, waypoint_pen, waypoint_brush)
|
position[0], position[1], self.WAYPOINT_SIZE,
|
||||||
|
self.WAYPOINT_SIZE, waypoint_pen, waypoint_brush
|
||||||
|
))
|
||||||
|
|
||||||
def draw_flight_path(self, scene: QGraphicsScene, pos0: Tuple[int, int],
|
def draw_flight_path(self, scene: QGraphicsScene, pos0: Tuple[int, int],
|
||||||
pos1: Tuple[int, int], player: bool):
|
pos1: Tuple[int, int], player: bool):
|
||||||
flight_path_pen = self.flight_path_pen(player)
|
flight_path_pen = self.flight_path_pen(player)
|
||||||
# Draw the line to the *middle* of the waypoint.
|
# Draw the line to the *middle* of the waypoint.
|
||||||
offset = self.WAYPOINT_SIZE // 2
|
offset = self.WAYPOINT_SIZE // 2
|
||||||
scene.addLine(pos0[0] + offset, pos0[1] + offset,
|
self.flight_path_items.append(scene.addLine(
|
||||||
pos1[0] + offset, pos1[1] + offset,
|
pos0[0] + offset, pos0[1] + offset,
|
||||||
flight_path_pen)
|
pos1[0] + offset, pos1[1] + offset,
|
||||||
|
flight_path_pen
|
||||||
|
))
|
||||||
|
|
||||||
def scene_create_lines_for_cp(self, cp: ControlPoint, playerColor, enemyColor):
|
def scene_create_lines_for_cp(self, cp: ControlPoint, playerColor, enemyColor):
|
||||||
scene = self.scene()
|
scene = self.scene()
|
||||||
|
|||||||
@ -1,3 +1,7 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from PySide2.QtCore import QObject, Signal
|
from PySide2.QtCore import QObject, Signal
|
||||||
|
|
||||||
from game import Game
|
from game import Game
|
||||||
@ -19,21 +23,31 @@ class GameUpdateSignal(QObject):
|
|||||||
budgetupdated = Signal(Game)
|
budgetupdated = Signal(Game)
|
||||||
debriefingReceived = Signal(DebriefingSignal)
|
debriefingReceived = Signal(DebriefingSignal)
|
||||||
|
|
||||||
|
flight_paths_changed = Signal()
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(GameUpdateSignal, self).__init__()
|
super(GameUpdateSignal, self).__init__()
|
||||||
GameUpdateSignal.instance = self
|
GameUpdateSignal.instance = self
|
||||||
|
|
||||||
def updateGame(self, game: Game):
|
def redraw_flight_paths(self) -> None:
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
|
self.flight_paths_changed.emit()
|
||||||
|
|
||||||
|
def updateGame(self, game: Optional[Game]):
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
self.gameupdated.emit(game)
|
self.gameupdated.emit(game)
|
||||||
|
|
||||||
def updateBudget(self, game: Game):
|
def updateBudget(self, game: Game):
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
self.budgetupdated.emit(game)
|
self.budgetupdated.emit(game)
|
||||||
|
|
||||||
def sendDebriefing(self, game: Game, gameEvent: Event, debriefing: Debriefing):
|
def sendDebriefing(self, game: Game, gameEvent: Event, debriefing: Debriefing):
|
||||||
sig = DebriefingSignal(game, gameEvent, debriefing)
|
sig = DebriefingSignal(game, gameEvent, debriefing)
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
self.gameupdated.emit(game)
|
self.gameupdated.emit(game)
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
self.debriefingReceived.emit(sig)
|
self.debriefingReceived.emit(sig)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_instance():
|
def get_instance() -> GameUpdateSignal:
|
||||||
return GameUpdateSignal.instance
|
return GameUpdateSignal.instance
|
||||||
|
|||||||
@ -232,6 +232,8 @@ class QLiberationWindow(QMainWindow):
|
|||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
def setGame(self, game: Optional[Game]):
|
def setGame(self, game: Optional[Game]):
|
||||||
|
if game is not None:
|
||||||
|
game.on_load()
|
||||||
self.game = game
|
self.game = game
|
||||||
if self.info_panel:
|
if self.info_panel:
|
||||||
self.info_panel.setGame(game)
|
self.info_panel.setGame(game)
|
||||||
|
|||||||
@ -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)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -5,15 +5,17 @@ from PySide2.QtWidgets import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from game import Game
|
from game import Game
|
||||||
|
from gen.ato import Package
|
||||||
from gen.flights.flight import Flight
|
from gen.flights.flight import Flight
|
||||||
from qt_ui.uiconstants import EVENT_ICONS
|
from qt_ui.uiconstants import EVENT_ICONS
|
||||||
|
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||||
from qt_ui.windows.mission.flight.QFlightPlanner import QFlightPlanner
|
from qt_ui.windows.mission.flight.QFlightPlanner import QFlightPlanner
|
||||||
|
|
||||||
|
|
||||||
class QEditFlightDialog(QDialog):
|
class QEditFlightDialog(QDialog):
|
||||||
"""Dialog window for editing flight plans and loadouts."""
|
"""Dialog window for editing flight plans and loadouts."""
|
||||||
|
|
||||||
def __init__(self, game: Game, flight: Flight) -> None:
|
def __init__(self, game: Game, package: Package, flight: Flight) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.game = game
|
self.game = game
|
||||||
@ -23,7 +25,12 @@ class QEditFlightDialog(QDialog):
|
|||||||
|
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
self.flight_planner = QFlightPlanner(flight, game)
|
self.flight_planner = QFlightPlanner(package, flight, game)
|
||||||
layout.addWidget(self.flight_planner)
|
layout.addWidget(self.flight_planner)
|
||||||
|
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
|
self.finished.connect(self.on_close)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def on_close(_result) -> None:
|
||||||
|
GameUpdateSignal.get_instance().redraw_flight_paths()
|
||||||
|
|||||||
@ -14,9 +14,11 @@ from PySide2.QtWidgets import (
|
|||||||
from game.game import Game
|
from game.game import Game
|
||||||
from gen.ato import Package
|
from gen.ato import Package
|
||||||
from gen.flights.flight import Flight
|
from gen.flights.flight import Flight
|
||||||
|
from gen.flights.flightplan import FlightPlanBuilder
|
||||||
from qt_ui.models import AtoModel, PackageModel
|
from qt_ui.models import AtoModel, PackageModel
|
||||||
from qt_ui.uiconstants import EVENT_ICONS
|
from qt_ui.uiconstants import EVENT_ICONS
|
||||||
from qt_ui.widgets.ato import QFlightList
|
from qt_ui.widgets.ato import QFlightList
|
||||||
|
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||||
from qt_ui.windows.mission.flight.QFlightCreator import QFlightCreator
|
from qt_ui.windows.mission.flight.QFlightCreator import QFlightCreator
|
||||||
from theater.missiontarget import MissionTarget
|
from theater.missiontarget import MissionTarget
|
||||||
|
|
||||||
@ -86,6 +88,12 @@ class QPackageDialog(QDialog):
|
|||||||
|
|
||||||
self.setLayout(self.layout)
|
self.setLayout(self.layout)
|
||||||
|
|
||||||
|
self.finished.connect(self.on_close)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def on_close(_result) -> None:
|
||||||
|
GameUpdateSignal.get_instance().redraw_flight_paths()
|
||||||
|
|
||||||
def on_selection_changed(self, selected: QItemSelection,
|
def on_selection_changed(self, selected: QItemSelection,
|
||||||
_deselected: QItemSelection) -> None:
|
_deselected: QItemSelection) -> None:
|
||||||
"""Updates the state of the delete button."""
|
"""Updates the state of the delete button."""
|
||||||
@ -93,15 +101,17 @@ class QPackageDialog(QDialog):
|
|||||||
|
|
||||||
def on_add_flight(self) -> None:
|
def on_add_flight(self) -> None:
|
||||||
"""Opens the new flight dialog."""
|
"""Opens the new flight dialog."""
|
||||||
self.add_flight_dialog = QFlightCreator(
|
self.add_flight_dialog = QFlightCreator(self.game,
|
||||||
self.game, self.package_model.package
|
self.package_model.package)
|
||||||
)
|
|
||||||
self.add_flight_dialog.created.connect(self.add_flight)
|
self.add_flight_dialog.created.connect(self.add_flight)
|
||||||
self.add_flight_dialog.show()
|
self.add_flight_dialog.show()
|
||||||
|
|
||||||
def add_flight(self, flight: Flight) -> None:
|
def add_flight(self, flight: Flight) -> None:
|
||||||
"""Adds the new flight to the package."""
|
"""Adds the new flight to the package."""
|
||||||
self.package_model.add_flight(flight)
|
self.package_model.add_flight(flight)
|
||||||
|
planner = FlightPlanBuilder(self.game, self.package_model.package,
|
||||||
|
is_player=True)
|
||||||
|
planner.populate_flight_plan(flight)
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
self.package_changed.emit()
|
self.package_changed.emit()
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import logging
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from PySide2.QtCore import Qt, Signal
|
from PySide2.QtCore import Qt, Signal
|
||||||
@ -11,15 +10,14 @@ from dcs.planes import PlaneType
|
|||||||
|
|
||||||
from game import Game
|
from game import Game
|
||||||
from gen.ato import Package
|
from gen.ato import Package
|
||||||
from gen.flights.ai_flight_planner import FlightPlanner
|
from gen.flights.flight import Flight
|
||||||
from gen.flights.flight import Flight, FlightType
|
|
||||||
from qt_ui.uiconstants import EVENT_ICONS
|
from qt_ui.uiconstants import EVENT_ICONS
|
||||||
from qt_ui.widgets.QFlightSizeSpinner import QFlightSizeSpinner
|
from qt_ui.widgets.QFlightSizeSpinner import QFlightSizeSpinner
|
||||||
from qt_ui.widgets.QLabeledWidget import QLabeledWidget
|
from qt_ui.widgets.QLabeledWidget import QLabeledWidget
|
||||||
from qt_ui.widgets.combos.QAircraftTypeSelector import QAircraftTypeSelector
|
from qt_ui.widgets.combos.QAircraftTypeSelector import QAircraftTypeSelector
|
||||||
from qt_ui.widgets.combos.QFlightTypeComboBox import QFlightTypeComboBox
|
from qt_ui.widgets.combos.QFlightTypeComboBox import QFlightTypeComboBox
|
||||||
from qt_ui.widgets.combos.QOriginAirfieldSelector import QOriginAirfieldSelector
|
from qt_ui.widgets.combos.QOriginAirfieldSelector import QOriginAirfieldSelector
|
||||||
from theater import ControlPoint, FrontLine, TheaterGroundObject
|
from theater import ControlPoint
|
||||||
|
|
||||||
|
|
||||||
class QFlightCreator(QDialog):
|
class QFlightCreator(QDialog):
|
||||||
@ -29,7 +27,6 @@ class QFlightCreator(QDialog):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.game = game
|
self.game = game
|
||||||
self.package = package
|
|
||||||
|
|
||||||
self.setWindowTitle("Create flight")
|
self.setWindowTitle("Create flight")
|
||||||
self.setWindowIcon(EVENT_ICONS["strike"])
|
self.setWindowIcon(EVENT_ICONS["strike"])
|
||||||
@ -37,7 +34,7 @@ class QFlightCreator(QDialog):
|
|||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
self.task_selector = QFlightTypeComboBox(
|
self.task_selector = QFlightTypeComboBox(
|
||||||
self.game.theater, self.package.target
|
self.game.theater, package.target
|
||||||
)
|
)
|
||||||
self.task_selector.setCurrentIndex(0)
|
self.task_selector.setCurrentIndex(0)
|
||||||
layout.addLayout(QLabeledWidget("Task:", self.task_selector))
|
layout.addLayout(QLabeledWidget("Task:", self.task_selector))
|
||||||
@ -93,7 +90,6 @@ class QFlightCreator(QDialog):
|
|||||||
size = self.flight_size_spinner.value()
|
size = self.flight_size_spinner.value()
|
||||||
|
|
||||||
flight = Flight(aircraft, size, origin, task)
|
flight = Flight(aircraft, size, origin, task)
|
||||||
self.populate_flight_plan(flight, task)
|
|
||||||
|
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
self.created.emit(flight)
|
self.created.emit(flight)
|
||||||
@ -102,77 +98,3 @@ class QFlightCreator(QDialog):
|
|||||||
def on_aircraft_changed(self, index: int) -> None:
|
def on_aircraft_changed(self, index: int) -> None:
|
||||||
new_aircraft = self.aircraft_selector.itemData(index)
|
new_aircraft = self.aircraft_selector.itemData(index)
|
||||||
self.airfield_selector.change_aircraft(new_aircraft)
|
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)
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ from PySide2.QtCore import Signal
|
|||||||
from PySide2.QtWidgets import QTabWidget
|
from PySide2.QtWidgets import QTabWidget
|
||||||
|
|
||||||
from game import Game
|
from game import Game
|
||||||
|
from gen.ato import Package
|
||||||
from gen.flights.flight import Flight
|
from gen.flights.flight import Flight
|
||||||
from qt_ui.windows.mission.flight.payload.QFlightPayloadTab import \
|
from qt_ui.windows.mission.flight.payload.QFlightPayloadTab import \
|
||||||
QFlightPayloadTab
|
QFlightPayloadTab
|
||||||
@ -15,14 +16,14 @@ class QFlightPlanner(QTabWidget):
|
|||||||
|
|
||||||
on_planned_flight_changed = Signal()
|
on_planned_flight_changed = Signal()
|
||||||
|
|
||||||
def __init__(self, flight: Flight, game: Game):
|
def __init__(self, package: Package, flight: Flight, game: Game):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.general_settings_tab = QGeneralFlightSettingsTab(game, flight)
|
self.general_settings_tab = QGeneralFlightSettingsTab(game, flight)
|
||||||
self.general_settings_tab.on_flight_settings_changed.connect(
|
self.general_settings_tab.on_flight_settings_changed.connect(
|
||||||
lambda: self.on_planned_flight_changed.emit())
|
lambda: self.on_planned_flight_changed.emit())
|
||||||
self.payload_tab = QFlightPayloadTab(flight, game)
|
self.payload_tab = QFlightPayloadTab(flight, game)
|
||||||
self.waypoint_tab = QFlightWaypointTab(game, flight)
|
self.waypoint_tab = QFlightWaypointTab(game, package, flight)
|
||||||
self.waypoint_tab.on_flight_changed.connect(
|
self.waypoint_tab.on_flight_changed.connect(
|
||||||
lambda: self.on_planned_flight_changed.emit())
|
lambda: self.on_planned_flight_changed.emit())
|
||||||
self.addTab(self.general_settings_tab, "General Flight settings")
|
self.addTab(self.general_settings_tab, "General Flight settings")
|
||||||
|
|||||||
@ -1,43 +0,0 @@
|
|||||||
from PySide2.QtCore import Qt
|
|
||||||
from PySide2.QtWidgets import QDialog, QPushButton
|
|
||||||
|
|
||||||
from game import Game
|
|
||||||
from gen.flights.flight import Flight
|
|
||||||
from qt_ui.uiconstants import EVENT_ICONS
|
|
||||||
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointInfoBox import QFlightWaypointInfoBox
|
|
||||||
|
|
||||||
|
|
||||||
class QAbstractMissionGenerator(QDialog):
|
|
||||||
|
|
||||||
def __init__(self, game: Game, flight: Flight, flight_waypoint_list, title):
|
|
||||||
super(QAbstractMissionGenerator, self).__init__()
|
|
||||||
self.game = game
|
|
||||||
self.flight = flight
|
|
||||||
self.setWindowFlags(Qt.WindowStaysOnTopHint)
|
|
||||||
self.setMinimumSize(400, 250)
|
|
||||||
self.setModal(True)
|
|
||||||
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.selected_waypoints = []
|
|
||||||
self.wpt_info = QFlightWaypointInfoBox()
|
|
||||||
|
|
||||||
self.ok_button = QPushButton("Ok")
|
|
||||||
self.ok_button.clicked.connect(self.apply)
|
|
||||||
|
|
||||||
def on_select_wpt_changed(self):
|
|
||||||
self.selected_waypoints = self.wpt_selection_box.get_selected_waypoints(False)
|
|
||||||
if self.selected_waypoints is None or len(self.selected_waypoints) <= 0:
|
|
||||||
self.ok_button.setDisabled(True)
|
|
||||||
else:
|
|
||||||
self.wpt_info.set_flight_waypoint(self.selected_waypoints[0])
|
|
||||||
self.ok_button.setDisabled(False)
|
|
||||||
|
|
||||||
def apply(self):
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
from PySide2.QtWidgets import QLabel, QHBoxLayout, QVBoxLayout
|
|
||||||
|
|
||||||
from game import Game
|
|
||||||
from gen.flights.flight import Flight, PredefinedWaypointCategory
|
|
||||||
from qt_ui.widgets.combos.QPredefinedWaypointSelectionComboBox import QPredefinedWaypointSelectionComboBox
|
|
||||||
from qt_ui.windows.mission.flight.generator.QAbstractMissionGenerator import QAbstractMissionGenerator
|
|
||||||
|
|
||||||
|
|
||||||
class QCAPMissionGenerator(QAbstractMissionGenerator):
|
|
||||||
|
|
||||||
def __init__(self, game: Game, flight: Flight, flight_waypoint_list):
|
|
||||||
super(QCAPMissionGenerator, self).__init__(game, flight, flight_waypoint_list, "CAP Generator")
|
|
||||||
|
|
||||||
self.wpt_selection_box = QPredefinedWaypointSelectionComboBox(self.game, self, False, True, True, False, False, True)
|
|
||||||
self.wpt_selection_box.setMinimumWidth(200)
|
|
||||||
self.wpt_selection_box.currentTextChanged.connect(self.on_select_wpt_changed)
|
|
||||||
|
|
||||||
self.init_ui()
|
|
||||||
self.on_select_wpt_changed()
|
|
||||||
|
|
||||||
def init_ui(self):
|
|
||||||
layout = QVBoxLayout()
|
|
||||||
|
|
||||||
wpt_layout = QHBoxLayout()
|
|
||||||
wpt_layout.addWidget(QLabel("CAP mission on : "))
|
|
||||||
wpt_layout.addWidget(self.wpt_selection_box)
|
|
||||||
wpt_layout.addStretch()
|
|
||||||
|
|
||||||
layout.addLayout(wpt_layout)
|
|
||||||
layout.addWidget(self.wpt_info)
|
|
||||||
layout.addStretch()
|
|
||||||
layout.addWidget(self.ok_button)
|
|
||||||
|
|
||||||
self.setLayout(layout)
|
|
||||||
|
|
||||||
def apply(self):
|
|
||||||
self.flight.points = []
|
|
||||||
|
|
||||||
wpt = self.selected_waypoints[0]
|
|
||||||
if wpt.category == PredefinedWaypointCategory.FRONTLINE:
|
|
||||||
self.planner.generate_frontline_cap(self.flight, wpt.data[0], wpt.data[1])
|
|
||||||
elif wpt.category == PredefinedWaypointCategory.ALLY_CP:
|
|
||||||
self.planner.generate_barcap(self.flight, wpt.data)
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.flight_waypoint_list.update_list()
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
from PySide2.QtGui import Qt
|
|
||||||
from PySide2.QtWidgets import QLabel, QHBoxLayout, QVBoxLayout, QGroupBox
|
|
||||||
from dcs import Point
|
|
||||||
|
|
||||||
from game import Game
|
|
||||||
from game.utils import meter_to_nm
|
|
||||||
from gen.flights.flight import Flight
|
|
||||||
from qt_ui.widgets.combos.QPredefinedWaypointSelectionComboBox import QPredefinedWaypointSelectionComboBox
|
|
||||||
from qt_ui.windows.mission.flight.generator.QAbstractMissionGenerator import QAbstractMissionGenerator
|
|
||||||
|
|
||||||
|
|
||||||
class QCASMissionGenerator(QAbstractMissionGenerator):
|
|
||||||
|
|
||||||
def __init__(self, game: Game, flight: Flight, flight_waypoint_list):
|
|
||||||
super(QCASMissionGenerator, self).__init__(game, flight, flight_waypoint_list, "CAS Generator")
|
|
||||||
|
|
||||||
self.wpt_selection_box = QPredefinedWaypointSelectionComboBox(self.game, self, False, False, True, False, False)
|
|
||||||
self.wpt_selection_box.setMinimumWidth(200)
|
|
||||||
self.wpt_selection_box.currentTextChanged.connect(self.on_select_wpt_changed)
|
|
||||||
|
|
||||||
self.distanceToTargetLabel = QLabel("0 nm")
|
|
||||||
self.init_ui()
|
|
||||||
self.on_select_wpt_changed()
|
|
||||||
|
|
||||||
def on_select_wpt_changed(self):
|
|
||||||
super(QCASMissionGenerator, self).on_select_wpt_changed()
|
|
||||||
wpts = self.wpt_selection_box.get_selected_waypoints()
|
|
||||||
|
|
||||||
if len(wpts) > 0:
|
|
||||||
self.distanceToTargetLabel.setText("~" + str(meter_to_nm(self.flight.from_cp.position.distance_to_point(Point(wpts[0].x, wpts[0].y)))) + " nm")
|
|
||||||
else:
|
|
||||||
self.distanceToTargetLabel.setText("??? nm")
|
|
||||||
|
|
||||||
def init_ui(self):
|
|
||||||
layout = QVBoxLayout()
|
|
||||||
|
|
||||||
wpt_layout = QHBoxLayout()
|
|
||||||
wpt_layout.addWidget(QLabel("CAS : "))
|
|
||||||
wpt_layout.addWidget(self.wpt_selection_box)
|
|
||||||
wpt_layout.addStretch()
|
|
||||||
|
|
||||||
distToTargetBox = QGroupBox("Infos :")
|
|
||||||
distToTarget = QHBoxLayout()
|
|
||||||
distToTarget.addWidget(QLabel("Distance to target : "))
|
|
||||||
distToTarget.addStretch()
|
|
||||||
distToTarget.addWidget(self.distanceToTargetLabel, alignment=Qt.AlignRight)
|
|
||||||
distToTargetBox.setLayout(distToTarget)
|
|
||||||
|
|
||||||
layout.addLayout(wpt_layout)
|
|
||||||
layout.addWidget(self.wpt_info)
|
|
||||||
layout.addWidget(distToTargetBox)
|
|
||||||
layout.addStretch()
|
|
||||||
layout.addWidget(self.ok_button)
|
|
||||||
|
|
||||||
self.setLayout(layout)
|
|
||||||
|
|
||||||
def apply(self):
|
|
||||||
self.flight.points = []
|
|
||||||
self.planner.generate_cas(self.flight, self.selected_waypoints[0].data[0], self.selected_waypoints[0].data[1])
|
|
||||||
self.flight_waypoint_list.update_list()
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,84 +0,0 @@
|
|||||||
from PySide2.QtGui import Qt
|
|
||||||
from PySide2.QtWidgets import QLabel, QHBoxLayout, QVBoxLayout, QGroupBox
|
|
||||||
|
|
||||||
from game import Game
|
|
||||||
from game.utils import meter_to_nm
|
|
||||||
from gen.flights.flight import Flight
|
|
||||||
from qt_ui.widgets.combos.QSEADTargetSelectionComboBox import QSEADTargetSelectionComboBox
|
|
||||||
from qt_ui.widgets.views.QSeadTargetInfoView import QSeadTargetInfoView
|
|
||||||
from qt_ui.windows.mission.flight.generator.QAbstractMissionGenerator import QAbstractMissionGenerator
|
|
||||||
|
|
||||||
|
|
||||||
class QSEADMissionGenerator(QAbstractMissionGenerator):
|
|
||||||
|
|
||||||
def __init__(self, game: Game, flight: Flight, flight_waypoint_list):
|
|
||||||
super(QSEADMissionGenerator, self).__init__(game, flight, flight_waypoint_list, "SEAD/DEAD Generator")
|
|
||||||
|
|
||||||
self.tgt_selection_box = QSEADTargetSelectionComboBox(self.game)
|
|
||||||
self.tgt_selection_box.setMinimumWidth(200)
|
|
||||||
self.tgt_selection_box.currentTextChanged.connect(self.on_selected_target_changed)
|
|
||||||
|
|
||||||
self.distanceToTargetLabel = QLabel("0 nm")
|
|
||||||
self.threatRangeLabel = QLabel("0 nm")
|
|
||||||
self.detectionRangeLabel = QLabel("0 nm")
|
|
||||||
self.seadTargetInfoView = QSeadTargetInfoView(None)
|
|
||||||
self.init_ui()
|
|
||||||
self.on_selected_target_changed()
|
|
||||||
|
|
||||||
def on_selected_target_changed(self):
|
|
||||||
target = self.tgt_selection_box.get_selected_target()
|
|
||||||
if target is not None:
|
|
||||||
self.distanceToTargetLabel.setText("~" + str(meter_to_nm(self.flight.from_cp.position.distance_to_point(target.location.position))) + " nm")
|
|
||||||
self.threatRangeLabel.setText(str(meter_to_nm(target.threat_range)) + " nm")
|
|
||||||
self.detectionRangeLabel.setText(str(meter_to_nm(target.detection_range)) + " nm")
|
|
||||||
self.seadTargetInfoView.setTarget(target)
|
|
||||||
|
|
||||||
def init_ui(self):
|
|
||||||
layout = QVBoxLayout()
|
|
||||||
|
|
||||||
wpt_layout = QHBoxLayout()
|
|
||||||
wpt_layout.addWidget(QLabel("SEAD/DEAD target : "))
|
|
||||||
wpt_layout.addStretch()
|
|
||||||
wpt_layout.addWidget(self.tgt_selection_box, alignment=Qt.AlignRight)
|
|
||||||
|
|
||||||
distThreatBox = QGroupBox("Infos :")
|
|
||||||
threatLayout = QVBoxLayout()
|
|
||||||
|
|
||||||
distToTarget = QHBoxLayout()
|
|
||||||
distToTarget.addWidget(QLabel("Distance to site : "))
|
|
||||||
distToTarget.addStretch()
|
|
||||||
distToTarget.addWidget(self.distanceToTargetLabel, alignment=Qt.AlignRight)
|
|
||||||
|
|
||||||
threatRangeLayout = QHBoxLayout()
|
|
||||||
threatRangeLayout.addWidget(QLabel("Site threat range : "))
|
|
||||||
threatRangeLayout.addStretch()
|
|
||||||
threatRangeLayout.addWidget(self.threatRangeLabel, alignment=Qt.AlignRight)
|
|
||||||
|
|
||||||
detectionRangeLayout = QHBoxLayout()
|
|
||||||
detectionRangeLayout.addWidget(QLabel("Site radar detection range: "))
|
|
||||||
detectionRangeLayout.addStretch()
|
|
||||||
detectionRangeLayout.addWidget(self.detectionRangeLabel, alignment=Qt.AlignRight)
|
|
||||||
|
|
||||||
threatLayout.addLayout(distToTarget)
|
|
||||||
threatLayout.addLayout(threatRangeLayout)
|
|
||||||
threatLayout.addLayout(detectionRangeLayout)
|
|
||||||
distThreatBox.setLayout(threatLayout)
|
|
||||||
|
|
||||||
layout.addLayout(wpt_layout)
|
|
||||||
layout.addWidget(self.seadTargetInfoView)
|
|
||||||
layout.addWidget(distThreatBox)
|
|
||||||
layout.addStretch()
|
|
||||||
layout.addWidget(self.ok_button)
|
|
||||||
|
|
||||||
self.setLayout(layout)
|
|
||||||
|
|
||||||
def apply(self):
|
|
||||||
self.flight.points = []
|
|
||||||
target = self.tgt_selection_box.get_selected_target()
|
|
||||||
self.planner.generate_sead(self.flight, target.location, target.radars)
|
|
||||||
self.flight_waypoint_list.update_list()
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
from PySide2.QtGui import Qt
|
|
||||||
from PySide2.QtWidgets import QLabel, QHBoxLayout, QVBoxLayout, QGroupBox
|
|
||||||
|
|
||||||
from game import Game
|
|
||||||
from game.utils import meter_to_nm
|
|
||||||
from gen.flights.flight import Flight
|
|
||||||
from qt_ui.widgets.combos.QStrikeTargetSelectionComboBox import QStrikeTargetSelectionComboBox
|
|
||||||
from qt_ui.widgets.views.QStrikeTargetInfoView import QStrikeTargetInfoView
|
|
||||||
from qt_ui.windows.mission.flight.generator.QAbstractMissionGenerator import QAbstractMissionGenerator
|
|
||||||
|
|
||||||
|
|
||||||
class QSTRIKEMissionGenerator(QAbstractMissionGenerator):
|
|
||||||
|
|
||||||
def __init__(self, game: Game, flight: Flight, flight_waypoint_list):
|
|
||||||
super(QSTRIKEMissionGenerator, self).__init__(game, flight, flight_waypoint_list, "Strike Generator")
|
|
||||||
|
|
||||||
self.tgt_selection_box = QStrikeTargetSelectionComboBox(self.game)
|
|
||||||
self.tgt_selection_box.setMinimumWidth(200)
|
|
||||||
self.tgt_selection_box.currentTextChanged.connect(self.on_selected_target_changed)
|
|
||||||
|
|
||||||
|
|
||||||
self.distanceToTargetLabel = QLabel("0 nm")
|
|
||||||
self.strike_infos = QStrikeTargetInfoView(None)
|
|
||||||
self.init_ui()
|
|
||||||
self.on_selected_target_changed()
|
|
||||||
|
|
||||||
def on_selected_target_changed(self):
|
|
||||||
target = self.tgt_selection_box.get_selected_target()
|
|
||||||
self.distanceToTargetLabel.setText("~" + str(meter_to_nm(self.flight.from_cp.position.distance_to_point(target.location.position))) + " nm")
|
|
||||||
self.strike_infos.setTarget(target)
|
|
||||||
|
|
||||||
def init_ui(self):
|
|
||||||
layout = QVBoxLayout()
|
|
||||||
|
|
||||||
wpt_layout = QHBoxLayout()
|
|
||||||
wpt_layout.addWidget(QLabel("Target : "))
|
|
||||||
wpt_layout.addStretch()
|
|
||||||
wpt_layout.addWidget(self.tgt_selection_box, alignment=Qt.AlignRight)
|
|
||||||
|
|
||||||
distToTargetBox = QGroupBox("Infos :")
|
|
||||||
distToTarget = QHBoxLayout()
|
|
||||||
distToTarget.addWidget(QLabel("Distance to target : "))
|
|
||||||
distToTarget.addStretch()
|
|
||||||
distToTarget.addWidget(self.distanceToTargetLabel, alignment=Qt.AlignRight)
|
|
||||||
distToTargetBox.setLayout(distToTarget)
|
|
||||||
|
|
||||||
layout.addLayout(wpt_layout)
|
|
||||||
layout.addWidget(self.strike_infos)
|
|
||||||
layout.addWidget(distToTargetBox)
|
|
||||||
layout.addStretch()
|
|
||||||
layout.addWidget(self.ok_button)
|
|
||||||
|
|
||||||
self.setLayout(layout)
|
|
||||||
|
|
||||||
def apply(self):
|
|
||||||
self.flight.points = []
|
|
||||||
target = self.tgt_selection_box.get_selected_target()
|
|
||||||
self.planner.generate_strike(self.flight, target.location)
|
|
||||||
self.flight_waypoint_list.update_list()
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,75 +1,98 @@
|
|||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
from PySide2.QtCore import Signal
|
from PySide2.QtCore import Signal
|
||||||
from PySide2.QtWidgets import QFrame, QGridLayout, QLabel, QPushButton, QVBoxLayout
|
from PySide2.QtWidgets import (
|
||||||
|
QFrame,
|
||||||
|
QGridLayout,
|
||||||
|
QLabel,
|
||||||
|
QMessageBox,
|
||||||
|
QPushButton,
|
||||||
|
QVBoxLayout,
|
||||||
|
)
|
||||||
|
|
||||||
from game import Game
|
from game import Game
|
||||||
from gen.flights.flight import Flight
|
from gen.ato import Package
|
||||||
from qt_ui.windows.mission.flight.generator.QCAPMissionGenerator import QCAPMissionGenerator
|
from gen.flights.flight import Flight, FlightType
|
||||||
from qt_ui.windows.mission.flight.generator.QCASMissionGenerator import QCASMissionGenerator
|
from gen.flights.flightplan import FlightPlanBuilder
|
||||||
from qt_ui.windows.mission.flight.generator.QSEADMissionGenerator import QSEADMissionGenerator
|
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointList import \
|
||||||
from qt_ui.windows.mission.flight.generator.QSTRIKEMissionGenerator import QSTRIKEMissionGenerator
|
QFlightWaypointList
|
||||||
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointList import QFlightWaypointList
|
from qt_ui.windows.mission.flight.waypoints.QPredefinedWaypointSelectionWindow import \
|
||||||
from qt_ui.windows.mission.flight.waypoints.QPredefinedWaypointSelectionWindow import QPredefinedWaypointSelectionWindow
|
QPredefinedWaypointSelectionWindow
|
||||||
|
from theater import ControlPoint, FrontLine
|
||||||
|
|
||||||
|
|
||||||
class QFlightWaypointTab(QFrame):
|
class QFlightWaypointTab(QFrame):
|
||||||
|
|
||||||
on_flight_changed = Signal()
|
on_flight_changed = Signal()
|
||||||
|
|
||||||
def __init__(self, game: Game, flight: Flight):
|
def __init__(self, game: Game, package: Package, flight: Flight):
|
||||||
super(QFlightWaypointTab, self).__init__()
|
super(QFlightWaypointTab, self).__init__()
|
||||||
self.flight = flight
|
|
||||||
self.game = game
|
self.game = game
|
||||||
self.planner = self.game.planners[self.flight.from_cp.id]
|
self.package = package
|
||||||
|
self.flight = flight
|
||||||
|
self.planner = FlightPlanBuilder(self.game, package, is_player=True)
|
||||||
|
|
||||||
|
self.flight_waypoint_list: Optional[QFlightWaypointList] = None
|
||||||
|
self.ascend_waypoint: Optional[QPushButton] = None
|
||||||
|
self.descend_waypoint: Optional[QPushButton] = None
|
||||||
|
self.rtb_waypoint: Optional[QPushButton] = None
|
||||||
|
self.delete_selected: Optional[QPushButton] = None
|
||||||
|
self.open_fast_waypoint_button: Optional[QPushButton] = None
|
||||||
|
self.recreate_buttons: List[QPushButton] = []
|
||||||
self.init_ui()
|
self.init_ui()
|
||||||
|
|
||||||
def init_ui(self):
|
def init_ui(self):
|
||||||
layout = QGridLayout()
|
layout = QGridLayout()
|
||||||
rlayout = QVBoxLayout()
|
|
||||||
self.flight_waypoint_list = QFlightWaypointList(self.flight)
|
self.flight_waypoint_list = QFlightWaypointList(self.flight)
|
||||||
self.open_fast_waypoint_button = QPushButton("Add Waypoint")
|
|
||||||
self.open_fast_waypoint_button.clicked.connect(self.on_fast_waypoint)
|
|
||||||
|
|
||||||
self.cas_generator = QPushButton("Gen. CAS")
|
|
||||||
self.cas_generator.clicked.connect(self.on_cas_generator)
|
|
||||||
|
|
||||||
self.cap_generator = QPushButton("Gen. CAP")
|
|
||||||
self.cap_generator.clicked.connect(self.on_cap_generator)
|
|
||||||
|
|
||||||
self.sead_generator = QPushButton("Gen. SEAD/DEAD")
|
|
||||||
self.sead_generator.clicked.connect(self.on_sead_generator)
|
|
||||||
|
|
||||||
self.strike_generator = QPushButton("Gen. STRIKE")
|
|
||||||
self.strike_generator.clicked.connect(self.on_strike_generator)
|
|
||||||
|
|
||||||
self.rtb_waypoint = QPushButton("Add RTB Waypoint")
|
|
||||||
self.rtb_waypoint.clicked.connect(self.on_rtb_waypoint)
|
|
||||||
|
|
||||||
self.ascend_waypoint = QPushButton("Add Ascend Waypoint")
|
|
||||||
self.ascend_waypoint.clicked.connect(self.on_ascend_waypoint)
|
|
||||||
|
|
||||||
self.descend_waypoint = QPushButton("Add Descend Waypoint")
|
|
||||||
self.descend_waypoint.clicked.connect(self.on_descend_waypoint)
|
|
||||||
|
|
||||||
self.delete_selected = QPushButton("Delete Selected")
|
|
||||||
self.delete_selected.clicked.connect(self.on_delete_waypoint)
|
|
||||||
|
|
||||||
layout.addWidget(self.flight_waypoint_list, 0, 0)
|
layout.addWidget(self.flight_waypoint_list, 0, 0)
|
||||||
|
|
||||||
|
rlayout = QVBoxLayout()
|
||||||
|
layout.addLayout(rlayout, 0, 1)
|
||||||
|
|
||||||
rlayout.addWidget(QLabel("<strong>Generator :</strong>"))
|
rlayout.addWidget(QLabel("<strong>Generator :</strong>"))
|
||||||
rlayout.addWidget(QLabel("<small>AI compatible</small>"))
|
rlayout.addWidget(QLabel("<small>AI compatible</small>"))
|
||||||
rlayout.addWidget(self.cas_generator)
|
|
||||||
rlayout.addWidget(self.cap_generator)
|
self.recreate_buttons.clear()
|
||||||
rlayout.addWidget(self.sead_generator)
|
recreate_types = [
|
||||||
rlayout.addWidget(self.strike_generator)
|
FlightType.CAS,
|
||||||
|
FlightType.CAP,
|
||||||
|
FlightType.SEAD,
|
||||||
|
FlightType.STRIKE
|
||||||
|
]
|
||||||
|
for task in recreate_types:
|
||||||
|
def make_closure(arg):
|
||||||
|
def closure():
|
||||||
|
return self.confirm_recreate(arg)
|
||||||
|
return closure
|
||||||
|
button = QPushButton(f"Recreate as {task.name}")
|
||||||
|
button.clicked.connect(make_closure(task))
|
||||||
|
rlayout.addWidget(button)
|
||||||
|
self.recreate_buttons.append(button)
|
||||||
|
|
||||||
rlayout.addWidget(QLabel("<strong>Advanced : </strong>"))
|
rlayout.addWidget(QLabel("<strong>Advanced : </strong>"))
|
||||||
rlayout.addWidget(QLabel("<small>Do not use for AI flights</small>"))
|
rlayout.addWidget(QLabel("<small>Do not use for AI flights</small>"))
|
||||||
|
|
||||||
|
self.ascend_waypoint = QPushButton("Add Ascend Waypoint")
|
||||||
|
self.ascend_waypoint.clicked.connect(self.on_ascend_waypoint)
|
||||||
rlayout.addWidget(self.ascend_waypoint)
|
rlayout.addWidget(self.ascend_waypoint)
|
||||||
|
|
||||||
|
self.descend_waypoint = QPushButton("Add Descend Waypoint")
|
||||||
|
self.descend_waypoint.clicked.connect(self.on_descend_waypoint)
|
||||||
rlayout.addWidget(self.descend_waypoint)
|
rlayout.addWidget(self.descend_waypoint)
|
||||||
|
|
||||||
|
self.rtb_waypoint = QPushButton("Add RTB Waypoint")
|
||||||
|
self.rtb_waypoint.clicked.connect(self.on_rtb_waypoint)
|
||||||
rlayout.addWidget(self.rtb_waypoint)
|
rlayout.addWidget(self.rtb_waypoint)
|
||||||
rlayout.addWidget(self.open_fast_waypoint_button)
|
|
||||||
|
self.delete_selected = QPushButton("Delete Selected")
|
||||||
|
self.delete_selected.clicked.connect(self.on_delete_waypoint)
|
||||||
rlayout.addWidget(self.delete_selected)
|
rlayout.addWidget(self.delete_selected)
|
||||||
|
|
||||||
|
self.open_fast_waypoint_button = QPushButton("Add Waypoint")
|
||||||
|
self.open_fast_waypoint_button.clicked.connect(self.on_fast_waypoint)
|
||||||
|
rlayout.addWidget(self.open_fast_waypoint_button)
|
||||||
rlayout.addStretch()
|
rlayout.addStretch()
|
||||||
layout.addLayout(rlayout, 0, 1)
|
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
|
|
||||||
def on_delete_waypoint(self):
|
def on_delete_waypoint(self):
|
||||||
@ -102,25 +125,27 @@ class QFlightWaypointTab(QFrame):
|
|||||||
self.flight_waypoint_list.update_list()
|
self.flight_waypoint_list.update_list()
|
||||||
self.on_change()
|
self.on_change()
|
||||||
|
|
||||||
def on_cas_generator(self):
|
def confirm_recreate(self, task: FlightType) -> None:
|
||||||
self.subwindow = QCASMissionGenerator(self.game, self.flight, self.flight_waypoint_list)
|
result = QMessageBox.question(
|
||||||
self.subwindow.finished.connect(self.on_change)
|
self,
|
||||||
self.subwindow.show()
|
"Regenerate flight?",
|
||||||
|
("Changing the flight type will reset its flight plan. Do you want "
|
||||||
def on_cap_generator(self):
|
"to continue?"),
|
||||||
self.subwindow = QCAPMissionGenerator(self.game, self.flight, self.flight_waypoint_list)
|
QMessageBox.No,
|
||||||
self.subwindow.finished.connect(self.on_change)
|
QMessageBox.Yes
|
||||||
self.subwindow.show()
|
)
|
||||||
|
if result == QMessageBox.Yes:
|
||||||
def on_sead_generator(self):
|
# TODO: These should all be just CAP.
|
||||||
self.subwindow = QSEADMissionGenerator(self.game, self.flight, self.flight_waypoint_list)
|
if task == FlightType.CAP:
|
||||||
self.subwindow.finished.connect(self.on_change)
|
if isinstance(self.package.target, FrontLine):
|
||||||
self.subwindow.show()
|
task = FlightType.TARCAP
|
||||||
|
elif isinstance(self.package.target, ControlPoint):
|
||||||
def on_strike_generator(self):
|
if self.package.target.is_fleet:
|
||||||
self.subwindow = QSTRIKEMissionGenerator(self.game, self.flight, self.flight_waypoint_list)
|
task = FlightType.BARCAP
|
||||||
self.subwindow.finished.connect(self.on_change)
|
self.flight.flight_type = task
|
||||||
self.subwindow.show()
|
self.planner.populate_flight_plan(self.flight)
|
||||||
|
self.flight_waypoint_list.update_list()
|
||||||
|
self.on_change()
|
||||||
|
|
||||||
def on_change(self):
|
def on_change(self):
|
||||||
self.flight_waypoint_list.update_list()
|
self.flight_waypoint_list.update_list()
|
||||||
|
|||||||
@ -52,7 +52,7 @@ class ControlPoint(MissionTarget):
|
|||||||
self.id = id
|
self.id = id
|
||||||
self.name = " ".join(re.split(r" |-", name)[:2])
|
self.name = " ".join(re.split(r" |-", name)[:2])
|
||||||
self.full_name = name
|
self.full_name = name
|
||||||
self.position = position
|
self.position: Point = position
|
||||||
self.at = at
|
self.at = at
|
||||||
self.ground_objects = []
|
self.ground_objects = []
|
||||||
self.ships = []
|
self.ships = []
|
||||||
@ -212,3 +212,6 @@ class ControlPoint(MissionTarget):
|
|||||||
if g.obj_name == obj_name:
|
if g.obj_name == obj_name:
|
||||||
found.append(g)
|
found.append(g)
|
||||||
return found
|
return found
|
||||||
|
|
||||||
|
def is_friendly(self, to_player: bool) -> bool:
|
||||||
|
return self.captured == to_player
|
||||||
|
|||||||
@ -1,9 +1,14 @@
|
|||||||
"""Battlefield front lines."""
|
"""Battlefield front lines."""
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
|
from dcs.mapping import Point
|
||||||
from . import ControlPoint, MissionTarget
|
from . import ControlPoint, MissionTarget
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Dedup by moving everything to using this class.
|
||||||
|
FRONTLINE_MIN_CP_DISTANCE = 5000
|
||||||
|
|
||||||
|
|
||||||
class FrontLine(MissionTarget):
|
class FrontLine(MissionTarget):
|
||||||
"""Defines a front line location between two control points.
|
"""Defines a front line location between two control points.
|
||||||
|
|
||||||
@ -25,3 +30,16 @@ class FrontLine(MissionTarget):
|
|||||||
a = self.control_point_a.name
|
a = self.control_point_a.name
|
||||||
b = self.control_point_b.name
|
b = self.control_point_b.name
|
||||||
return f"Front line {a}/{b}"
|
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 abc import ABC, abstractmethod
|
||||||
|
from dcs.mapping import Point
|
||||||
|
|
||||||
|
|
||||||
class MissionTarget(ABC):
|
class MissionTarget(ABC):
|
||||||
@ -9,3 +12,12 @@ class MissionTarget(ABC):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
"""The name of the mission target."""
|
"""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