mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Merge remote-tracking branch 'upstream/develop' into new-plugin-system
This commit is contained in:
commit
3c4ccd7d57
@ -1,3 +1,12 @@
|
|||||||
|
# 2.1.5
|
||||||
|
|
||||||
|
## Features/Improvements :
|
||||||
|
* **[Units/Factions]** Enabled EPLRS for ground units that supports it (so they appear on A-10C II TAD and Helmet)
|
||||||
|
|
||||||
|
## Fixes :
|
||||||
|
* **[UI]** Fixed an issue that prevent saving after aborting a mission
|
||||||
|
* **[Mission Generator]** Fixed aircraft landing point type being wrong
|
||||||
|
|
||||||
# 2.1.4
|
# 2.1.4
|
||||||
|
|
||||||
## Fixes :
|
## Fixes :
|
||||||
|
|||||||
405
gen/aircraft.py
405
gen/aircraft.py
@ -57,14 +57,14 @@ from dcs.task import (
|
|||||||
)
|
)
|
||||||
from dcs.terrain.terrain import Airport, NoParkingSlotError
|
from dcs.terrain.terrain import Airport, NoParkingSlotError
|
||||||
from dcs.translation import String
|
from dcs.translation import String
|
||||||
from dcs.triggers import Event, TriggerOnce
|
from dcs.triggers import Event, TriggerOnce, TriggerRule
|
||||||
from dcs.unitgroup import FlyingGroup, Group, ShipGroup, StaticGroup
|
from dcs.unitgroup import FlyingGroup, Group, ShipGroup, StaticGroup
|
||||||
from dcs.unittype import FlyingType, UnitType
|
from dcs.unittype import FlyingType, UnitType
|
||||||
|
|
||||||
from game import db
|
from game import db
|
||||||
from game.data.cap_capabilities_db import GUNFIGHTERS
|
from game.data.cap_capabilities_db import GUNFIGHTERS
|
||||||
from game.settings import Settings
|
from game.settings import Settings
|
||||||
from game.utils import meter_to_nm, 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, Package
|
from gen.ato import AirTaskingOrder, Package
|
||||||
@ -76,9 +76,10 @@ from gen.flights.flight import (
|
|||||||
FlightWaypointType,
|
FlightWaypointType,
|
||||||
)
|
)
|
||||||
from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
|
from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
|
||||||
from theater import MissionTarget, TheaterGroundObject
|
from theater import TheaterGroundObject
|
||||||
from theater.controlpoint import ControlPoint, ControlPointType
|
from theater.controlpoint import ControlPoint, ControlPointType
|
||||||
from .conflictgen import Conflict
|
from .conflictgen import Conflict
|
||||||
|
from .flights.traveltime import PackageWaypointTiming, TotEstimator
|
||||||
from .naming import namegen
|
from .naming import namegen
|
||||||
|
|
||||||
WARM_START_HELI_AIRSPEED = 120
|
WARM_START_HELI_AIRSPEED = 120
|
||||||
@ -86,8 +87,6 @@ WARM_START_HELI_ALT = 500
|
|||||||
WARM_START_ALTITUDE = 3000
|
WARM_START_ALTITUDE = 3000
|
||||||
WARM_START_AIRSPEED = 550
|
WARM_START_AIRSPEED = 550
|
||||||
|
|
||||||
CAP_DURATION = 30 # minutes
|
|
||||||
|
|
||||||
RTB_ALTITUDE = 800
|
RTB_ALTITUDE = 800
|
||||||
RTB_DISTANCE = 5000
|
RTB_DISTANCE = 5000
|
||||||
HELI_ALT = 500
|
HELI_ALT = 500
|
||||||
@ -217,7 +216,7 @@ class FlightData:
|
|||||||
#: True if this flight belongs to the player's coalition.
|
#: True if this flight belongs to the player's coalition.
|
||||||
friendly: bool
|
friendly: bool
|
||||||
|
|
||||||
#: Number of minutes after mission start the flight is set to depart.
|
#: Number of seconds after mission start the flight is set to depart.
|
||||||
departure_delay: int
|
departure_delay: int
|
||||||
|
|
||||||
#: Arrival airport.
|
#: Arrival airport.
|
||||||
@ -537,148 +536,6 @@ AIRCRAFT_DATA["P-51D-30-NA"] = AIRCRAFT_DATA["P-51D"]
|
|||||||
AIRCRAFT_DATA["P-47D-30"] = AIRCRAFT_DATA["P-51D"]
|
AIRCRAFT_DATA["P-47D-30"] = AIRCRAFT_DATA["P-51D"]
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class PackageWaypointTiming:
|
|
||||||
#: The package being scheduled.
|
|
||||||
package: Package
|
|
||||||
|
|
||||||
#: The package join time.
|
|
||||||
join: int
|
|
||||||
|
|
||||||
#: The ingress waypoint TOT.
|
|
||||||
ingress: int
|
|
||||||
|
|
||||||
#: The egress waypoint TOT.
|
|
||||||
egress: int
|
|
||||||
|
|
||||||
#: The package split time.
|
|
||||||
split: int
|
|
||||||
|
|
||||||
@property
|
|
||||||
def target(self) -> int:
|
|
||||||
"""The package time over target."""
|
|
||||||
assert self.package.time_over_target is not None
|
|
||||||
return self.package.time_over_target
|
|
||||||
|
|
||||||
@property
|
|
||||||
def race_track_start(self) -> Optional[int]:
|
|
||||||
cap_types = (FlightType.BARCAP, FlightType.CAP)
|
|
||||||
if self.package.primary_task in cap_types:
|
|
||||||
# CAP flights don't have hold points, and we don't calculate takeoff
|
|
||||||
# times yet or adjust the TOT based on when the flight can arrive,
|
|
||||||
# so if we set a TOT that gives the flight a lot of extra time it
|
|
||||||
# will just fly to the start point slowly, possibly slowly enough to
|
|
||||||
# stall and crash. Just don't set a TOT for these points and let the
|
|
||||||
# CAP get on station ASAP.
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return self.ingress
|
|
||||||
|
|
||||||
@property
|
|
||||||
def race_track_end(self) -> int:
|
|
||||||
cap_types = (FlightType.BARCAP, FlightType.CAP)
|
|
||||||
if self.package.primary_task in cap_types:
|
|
||||||
return self.target + CAP_DURATION * 60
|
|
||||||
else:
|
|
||||||
return self.egress
|
|
||||||
|
|
||||||
def push_time(self, flight: Flight, hold_point: Point) -> int:
|
|
||||||
assert self.package.waypoints is not None
|
|
||||||
return self.join - self.travel_time(
|
|
||||||
hold_point,
|
|
||||||
self.package.waypoints.join,
|
|
||||||
self.flight_ground_speed(flight)
|
|
||||||
)
|
|
||||||
|
|
||||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[int]:
|
|
||||||
target_types = (
|
|
||||||
FlightWaypointType.TARGET_GROUP_LOC,
|
|
||||||
FlightWaypointType.TARGET_POINT,
|
|
||||||
FlightWaypointType.TARGET_SHIP,
|
|
||||||
)
|
|
||||||
|
|
||||||
ingress_types = (
|
|
||||||
FlightWaypointType.INGRESS_CAS,
|
|
||||||
FlightWaypointType.INGRESS_SEAD,
|
|
||||||
FlightWaypointType.INGRESS_STRIKE,
|
|
||||||
)
|
|
||||||
|
|
||||||
if waypoint.waypoint_type == FlightWaypointType.JOIN:
|
|
||||||
return self.join
|
|
||||||
elif waypoint.waypoint_type in ingress_types:
|
|
||||||
return self.ingress
|
|
||||||
elif waypoint.waypoint_type in target_types:
|
|
||||||
return self.target
|
|
||||||
elif waypoint.waypoint_type == FlightWaypointType.EGRESS:
|
|
||||||
return self.egress
|
|
||||||
elif waypoint.waypoint_type == FlightWaypointType.SPLIT:
|
|
||||||
return self.split
|
|
||||||
elif waypoint.waypoint_type == FlightWaypointType.PATROL_TRACK:
|
|
||||||
return self.race_track_start
|
|
||||||
return None
|
|
||||||
|
|
||||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint,
|
|
||||||
flight: Flight) -> Optional[int]:
|
|
||||||
if waypoint.waypoint_type == FlightWaypointType.LOITER:
|
|
||||||
return self.push_time(flight, Point(waypoint.x, waypoint.y))
|
|
||||||
elif waypoint.waypoint_type == FlightWaypointType.PATROL:
|
|
||||||
return self.race_track_end
|
|
||||||
return None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def for_package(cls, package: Package) -> PackageWaypointTiming:
|
|
||||||
assert package.time_over_target is not None
|
|
||||||
assert package.waypoints is not None
|
|
||||||
|
|
||||||
group_ground_speed = cls.package_ground_speed(package)
|
|
||||||
|
|
||||||
ingress = package.time_over_target - cls.travel_time(
|
|
||||||
package.waypoints.ingress,
|
|
||||||
package.target.position,
|
|
||||||
group_ground_speed
|
|
||||||
)
|
|
||||||
|
|
||||||
join = ingress - cls.travel_time(
|
|
||||||
package.waypoints.join,
|
|
||||||
package.waypoints.ingress,
|
|
||||||
group_ground_speed
|
|
||||||
)
|
|
||||||
|
|
||||||
egress = package.time_over_target + cls.travel_time(
|
|
||||||
package.target.position,
|
|
||||||
package.waypoints.egress,
|
|
||||||
group_ground_speed
|
|
||||||
)
|
|
||||||
|
|
||||||
split = egress + cls.travel_time(
|
|
||||||
package.waypoints.egress,
|
|
||||||
package.waypoints.split,
|
|
||||||
group_ground_speed
|
|
||||||
)
|
|
||||||
|
|
||||||
return cls(package, join, ingress, egress, split)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def package_ground_speed(cls, package: Package) -> int:
|
|
||||||
speeds = []
|
|
||||||
for flight in package.flights:
|
|
||||||
speeds.append(cls.flight_ground_speed(flight))
|
|
||||||
return min(speeds) # knots
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def flight_ground_speed(_flight: Flight) -> int:
|
|
||||||
# TODO: Gather data so this is useful.
|
|
||||||
return 400 # knots
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def travel_time(a: Point, b: Point, speed: float) -> int:
|
|
||||||
error_factor = 1.1
|
|
||||||
distance = meter_to_nm(a.distance_to_point(b))
|
|
||||||
hours = distance / speed
|
|
||||||
seconds = hours * 3600
|
|
||||||
return int(seconds * error_factor)
|
|
||||||
|
|
||||||
|
|
||||||
class AircraftConflictGenerator:
|
class AircraftConflictGenerator:
|
||||||
def __init__(self, mission: Mission, conflict: Conflict, settings: Settings,
|
def __init__(self, mission: Mission, conflict: Conflict, settings: Settings,
|
||||||
game, radio_registry: RadioRegistry):
|
game, radio_registry: RadioRegistry):
|
||||||
@ -706,8 +563,13 @@ class AircraftConflictGenerator:
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
return get_fallback_channel(airframe)
|
return get_fallback_channel(airframe)
|
||||||
|
|
||||||
def _start_type(self) -> StartType:
|
@staticmethod
|
||||||
return self.settings.cold_start and StartType.Cold or StartType.Warm
|
def _start_type(start_type: str) -> StartType:
|
||||||
|
if start_type == "Runway":
|
||||||
|
return StartType.Runway
|
||||||
|
elif start_type == "Cold":
|
||||||
|
return StartType.Cold
|
||||||
|
return StartType.Warm
|
||||||
|
|
||||||
def _setup_group(self, group: FlyingGroup, for_task: Type[Task],
|
def _setup_group(self, group: FlyingGroup, for_task: Type[Task],
|
||||||
flight: Flight, dynamic_runways: Dict[str, RunwayData]):
|
flight: Flight, dynamic_runways: Dict[str, RunwayData]):
|
||||||
@ -813,15 +675,10 @@ class AircraftConflictGenerator:
|
|||||||
return runways[0]
|
return runways[0]
|
||||||
|
|
||||||
def _generate_at_airport(self, name: str, side: Country,
|
def _generate_at_airport(self, name: str, side: Country,
|
||||||
unit_type: FlyingType, count: int,
|
unit_type: FlyingType, count: int, start_type: str,
|
||||||
client_count: int,
|
airport: Optional[Airport] = None) -> FlyingGroup:
|
||||||
airport: Optional[Airport] = None,
|
|
||||||
start_type=None) -> FlyingGroup:
|
|
||||||
assert count > 0
|
assert count > 0
|
||||||
|
|
||||||
if start_type is None:
|
|
||||||
start_type = self._start_type()
|
|
||||||
|
|
||||||
logging.info("airgen: {} for {} at {}".format(unit_type, side.id, airport))
|
logging.info("airgen: {} for {} at {}".format(unit_type, side.id, airport))
|
||||||
return self.m.flight_group_from_airport(
|
return self.m.flight_group_from_airport(
|
||||||
country=side,
|
country=side,
|
||||||
@ -829,11 +686,11 @@ class AircraftConflictGenerator:
|
|||||||
aircraft_type=unit_type,
|
aircraft_type=unit_type,
|
||||||
airport=airport,
|
airport=airport,
|
||||||
maintask=None,
|
maintask=None,
|
||||||
start_type=start_type,
|
start_type=self._start_type(start_type),
|
||||||
group_size=count,
|
group_size=count,
|
||||||
parking_slots=None)
|
parking_slots=None)
|
||||||
|
|
||||||
def _generate_inflight(self, name: str, side: Country, unit_type: FlyingType, count: int, client_count: int, at: Point) -> FlyingGroup:
|
def _generate_inflight(self, name: str, side: Country, unit_type: FlyingType, count: int, at: Point) -> FlyingGroup:
|
||||||
assert count > 0
|
assert count > 0
|
||||||
|
|
||||||
if unit_type in helicopters.helicopter_map.values():
|
if unit_type in helicopters.helicopter_map.values():
|
||||||
@ -855,21 +712,16 @@ class AircraftConflictGenerator:
|
|||||||
altitude=alt,
|
altitude=alt,
|
||||||
speed=speed,
|
speed=speed,
|
||||||
maintask=None,
|
maintask=None,
|
||||||
start_type=self._start_type(),
|
|
||||||
group_size=count)
|
group_size=count)
|
||||||
|
|
||||||
group.points[0].alt_type = "RADIO"
|
group.points[0].alt_type = "RADIO"
|
||||||
return group
|
return group
|
||||||
|
|
||||||
def _generate_at_group(self, name: str, side: Country,
|
def _generate_at_group(self, name: str, side: Country,
|
||||||
unit_type: FlyingType, count: int, client_count: int,
|
unit_type: FlyingType, count: int, start_type: str,
|
||||||
at: Union[ShipGroup, StaticGroup],
|
at: Union[ShipGroup, StaticGroup]) -> FlyingGroup:
|
||||||
start_type=None) -> FlyingGroup:
|
|
||||||
assert count > 0
|
assert count > 0
|
||||||
|
|
||||||
if start_type is None:
|
|
||||||
start_type = self._start_type()
|
|
||||||
|
|
||||||
logging.info("airgen: {} for {} at unit {}".format(unit_type, side.id, at))
|
logging.info("airgen: {} for {} at unit {}".format(unit_type, side.id, at))
|
||||||
return self.m.flight_group_from_unit(
|
return self.m.flight_group_from_unit(
|
||||||
country=side,
|
country=side,
|
||||||
@ -877,34 +729,9 @@ class AircraftConflictGenerator:
|
|||||||
aircraft_type=unit_type,
|
aircraft_type=unit_type,
|
||||||
pad_group=at,
|
pad_group=at,
|
||||||
maintask=None,
|
maintask=None,
|
||||||
start_type=start_type,
|
start_type=self._start_type(start_type),
|
||||||
group_size=count)
|
group_size=count)
|
||||||
|
|
||||||
def _generate_group(self, name: str, side: Country, unit_type: FlyingType, count: int, client_count: int, at: db.StartingPosition):
|
|
||||||
if isinstance(at, Point):
|
|
||||||
return self._generate_inflight(name, side, unit_type, count, client_count, at)
|
|
||||||
elif isinstance(at, Group):
|
|
||||||
takeoff_ban = unit_type in db.CARRIER_TAKEOFF_BAN
|
|
||||||
ai_ban = client_count == 0 and self.settings.only_player_takeoff
|
|
||||||
|
|
||||||
if not takeoff_ban and not ai_ban:
|
|
||||||
return self._generate_at_group(name, side, unit_type, count, client_count, at)
|
|
||||||
else:
|
|
||||||
return self._generate_inflight(name, side, unit_type, count, client_count, at.position)
|
|
||||||
elif isinstance(at, Airport):
|
|
||||||
takeoff_ban = unit_type in db.TAKEOFF_BAN
|
|
||||||
ai_ban = client_count == 0 and self.settings.only_player_takeoff
|
|
||||||
|
|
||||||
if not takeoff_ban and not ai_ban:
|
|
||||||
try:
|
|
||||||
return self._generate_at_airport(name, side, unit_type, count, client_count, at)
|
|
||||||
except NoParkingSlotError:
|
|
||||||
logging.info("No parking slot found at " + at.name + ", switching to air start.")
|
|
||||||
pass
|
|
||||||
return self._generate_inflight(name, side, unit_type, count, client_count, at.position)
|
|
||||||
else:
|
|
||||||
assert False
|
|
||||||
|
|
||||||
def _add_radio_waypoint(self, group: FlyingGroup, position, altitude: int, airspeed: int = 600):
|
def _add_radio_waypoint(self, group: FlyingGroup, position, altitude: int, airspeed: int = 600):
|
||||||
point = group.add_waypoint(position, altitude, airspeed)
|
point = group.add_waypoint(position, altitude, airspeed)
|
||||||
point.alt_type = "RADIO"
|
point.alt_type = "RADIO"
|
||||||
@ -975,110 +802,91 @@ class AircraftConflictGenerator:
|
|||||||
logging.info(f"Generating flight: {flight.unit_type}")
|
logging.info(f"Generating flight: {flight.unit_type}")
|
||||||
group = self.generate_planned_flight(flight.from_cp, country,
|
group = self.generate_planned_flight(flight.from_cp, country,
|
||||||
flight)
|
flight)
|
||||||
self.setup_flight_group(group, flight, timing, dynamic_runways)
|
self.setup_flight_group(group, package, flight, timing,
|
||||||
self.setup_group_activation_trigger(flight, group)
|
dynamic_runways)
|
||||||
|
|
||||||
def setup_group_activation_trigger(self, flight, group):
|
def set_activation_time(self, flight: Flight, group: FlyingGroup,
|
||||||
if flight.scheduled_in > 0 and flight.client_count == 0:
|
delay: int) -> None:
|
||||||
|
# Note: Late activation causes the waypoint TOTs to look *weird* in the
|
||||||
|
# mission editor. Waypoint times will be relative to the group
|
||||||
|
# activation time rather than in absolute local time. A flight delayed
|
||||||
|
# until 09:10 when the overall mission start time is 09:00, with a join
|
||||||
|
# time of 09:30 will show the join time as 00:30, not 09:30.
|
||||||
|
group.late_activation = True
|
||||||
|
|
||||||
if flight.start_type != "In Flight" and flight.from_cp.cptype not in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP]:
|
activation_trigger = TriggerOnce(
|
||||||
group.late_activation = False
|
Event.NoEvent, f"FlightLateActivationTrigger{group.id}")
|
||||||
group.uncontrolled = True
|
activation_trigger.add_condition(TimeAfter(seconds=delay))
|
||||||
|
|
||||||
activation_trigger = TriggerOnce(Event.NoEvent, "FlightStartTrigger" + str(group.id))
|
self.prevent_spawn_at_hostile_airbase(flight, activation_trigger)
|
||||||
activation_trigger.add_condition(TimeAfter(seconds=flight.scheduled_in * 60))
|
activation_trigger.add_action(ActivateGroup(group.id))
|
||||||
if (flight.from_cp.cptype == ControlPointType.AIRBASE):
|
self.m.triggerrules.triggers.append(activation_trigger)
|
||||||
if flight.from_cp.captured:
|
|
||||||
activation_trigger.add_condition(
|
|
||||||
CoalitionHasAirdrome(self.game.get_player_coalition_id(), flight.from_cp.id))
|
|
||||||
else:
|
|
||||||
activation_trigger.add_condition(
|
|
||||||
CoalitionHasAirdrome(self.game.get_enemy_coalition_id(), flight.from_cp.id))
|
|
||||||
|
|
||||||
if flight.flight_type == FlightType.INTERCEPTION:
|
def set_startup_time(self, flight: Flight, group: FlyingGroup,
|
||||||
self.setup_interceptor_triggers(group, flight, activation_trigger)
|
delay: int) -> None:
|
||||||
|
# Uncontrolled causes the AI unit to spawn, but not begin startup.
|
||||||
|
group.uncontrolled = True
|
||||||
|
|
||||||
group.add_trigger_action(StartCommand())
|
activation_trigger = TriggerOnce(Event.NoEvent,
|
||||||
activation_trigger.add_action(AITaskPush(group.id, len(group.tasks)))
|
f"FlightStartTrigger{group.id}")
|
||||||
|
activation_trigger.add_condition(TimeAfter(seconds=delay))
|
||||||
|
|
||||||
self.m.triggerrules.triggers.append(activation_trigger)
|
self.prevent_spawn_at_hostile_airbase(flight, activation_trigger)
|
||||||
else:
|
group.add_trigger_action(StartCommand())
|
||||||
group.late_activation = True
|
activation_trigger.add_action(AITaskPush(group.id, len(group.tasks)))
|
||||||
activation_trigger = TriggerOnce(Event.NoEvent, "FlightLateActivationTrigger" + str(group.id))
|
self.m.triggerrules.triggers.append(activation_trigger)
|
||||||
activation_trigger.add_condition(TimeAfter(seconds=flight.scheduled_in*60))
|
|
||||||
|
|
||||||
if(flight.from_cp.cptype == ControlPointType.AIRBASE):
|
def prevent_spawn_at_hostile_airbase(self, flight: Flight,
|
||||||
if flight.from_cp.captured:
|
trigger: TriggerRule) -> None:
|
||||||
activation_trigger.add_condition(CoalitionHasAirdrome(self.game.get_player_coalition_id(), flight.from_cp.id))
|
# Prevent delayed flights from spawning at airbases if they were
|
||||||
else:
|
# captured before they've spawned.
|
||||||
activation_trigger.add_condition(CoalitionHasAirdrome(self.game.get_enemy_coalition_id(), flight.from_cp.id))
|
if flight.from_cp.cptype != ControlPointType.AIRBASE:
|
||||||
|
return
|
||||||
|
|
||||||
if flight.flight_type == FlightType.INTERCEPTION:
|
|
||||||
self.setup_interceptor_triggers(group, flight, activation_trigger)
|
|
||||||
|
|
||||||
activation_trigger.add_action(ActivateGroup(group.id))
|
|
||||||
self.m.triggerrules.triggers.append(activation_trigger)
|
|
||||||
|
|
||||||
def setup_interceptor_triggers(self, group, flight, activation_trigger):
|
|
||||||
|
|
||||||
detection_zone = self.m.triggers.add_triggerzone(flight.from_cp.position, radius=25000, hidden=False, name="ITZ")
|
|
||||||
if flight.from_cp.captured:
|
if flight.from_cp.captured:
|
||||||
activation_trigger.add_condition(PartOfCoalitionInZone(self.game.get_enemy_color(), detection_zone.id)) # TODO : support unit type in part of coalition
|
coalition = self.game.get_player_coalition_id()
|
||||||
activation_trigger.add_action(MessageToAll(String("WARNING : Enemy aircraft have been detected in the vicinity of " + flight.from_cp.name + ". Interceptors are taking off."), 20))
|
|
||||||
else:
|
else:
|
||||||
activation_trigger.add_condition(PartOfCoalitionInZone(self.game.get_player_color(), detection_zone.id))
|
coalition = self.game.get_enemy_coalition_id()
|
||||||
activation_trigger.add_action(MessageToAll(String("WARNING : We have detected that enemy aircraft are scrambling for an interception on " + flight.from_cp.name + " airbase."), 20))
|
|
||||||
|
trigger.add_condition(
|
||||||
|
CoalitionHasAirdrome(coalition, flight.from_cp.id))
|
||||||
|
|
||||||
def generate_planned_flight(self, cp, country, flight:Flight):
|
def generate_planned_flight(self, cp, country, flight:Flight):
|
||||||
try:
|
try:
|
||||||
if flight.client_count == 0 and self.game.settings.perf_ai_parking_start:
|
|
||||||
flight.start_type = "Cold"
|
|
||||||
|
|
||||||
if flight.start_type == "In Flight":
|
if flight.start_type == "In Flight":
|
||||||
group = self._generate_group(
|
group = self._generate_inflight(
|
||||||
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
|
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
|
||||||
side=country,
|
side=country,
|
||||||
unit_type=flight.unit_type,
|
unit_type=flight.unit_type,
|
||||||
count=flight.count,
|
count=flight.count,
|
||||||
client_count=0,
|
|
||||||
at=cp.position)
|
at=cp.position)
|
||||||
|
elif cp.is_fleet:
|
||||||
|
group_name = cp.get_carrier_group_name()
|
||||||
|
group = self._generate_at_group(
|
||||||
|
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
|
||||||
|
side=country,
|
||||||
|
unit_type=flight.unit_type,
|
||||||
|
count=flight.count,
|
||||||
|
start_type=flight.start_type,
|
||||||
|
at=self.m.find_group(group_name))
|
||||||
else:
|
else:
|
||||||
st = StartType.Runway
|
group = self._generate_at_airport(
|
||||||
if flight.start_type == "Cold":
|
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
|
||||||
st = StartType.Cold
|
side=country,
|
||||||
elif flight.start_type == "Warm":
|
unit_type=flight.unit_type,
|
||||||
st = StartType.Warm
|
count=flight.count,
|
||||||
|
start_type=flight.start_type,
|
||||||
if cp.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP]:
|
airport=cp.airport)
|
||||||
group_name = cp.get_carrier_group_name()
|
|
||||||
group = self._generate_at_group(
|
|
||||||
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
|
|
||||||
side=country,
|
|
||||||
unit_type=flight.unit_type,
|
|
||||||
count=flight.count,
|
|
||||||
client_count=0,
|
|
||||||
at=self.m.find_group(group_name),
|
|
||||||
start_type=st)
|
|
||||||
else:
|
|
||||||
group = self._generate_at_airport(
|
|
||||||
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
|
|
||||||
side=country,
|
|
||||||
unit_type=flight.unit_type,
|
|
||||||
count=flight.count,
|
|
||||||
client_count=0,
|
|
||||||
airport=cp.airport,
|
|
||||||
start_type=st)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Generated when there is no place on Runway or on Parking Slots
|
# Generated when there is no place on Runway or on Parking Slots
|
||||||
logging.error(e)
|
logging.error(e)
|
||||||
logging.warning("No room on runway or parking slots. Starting from the air.")
|
logging.warning("No room on runway or parking slots. Starting from the air.")
|
||||||
flight.start_type = "In Flight"
|
flight.start_type = "In Flight"
|
||||||
group = self._generate_group(
|
group = self._generate_inflight(
|
||||||
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
|
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
|
||||||
side=country,
|
side=country,
|
||||||
unit_type=flight.unit_type,
|
unit_type=flight.unit_type,
|
||||||
count=flight.count,
|
count=flight.count,
|
||||||
client_count=0,
|
|
||||||
at=cp.position)
|
at=cp.position)
|
||||||
group.points[0].alt = 1500
|
group.points[0].alt = 1500
|
||||||
|
|
||||||
@ -1184,8 +992,8 @@ class AircraftConflictGenerator:
|
|||||||
logging.error(f"Unhandled flight type: {flight.flight_type.name}")
|
logging.error(f"Unhandled flight type: {flight.flight_type.name}")
|
||||||
self.configure_behavior(group)
|
self.configure_behavior(group)
|
||||||
|
|
||||||
def setup_flight_group(self, group: FlyingGroup, flight: Flight,
|
def setup_flight_group(self, group: FlyingGroup, package: Package,
|
||||||
timing: PackageWaypointTiming,
|
flight: Flight, timing: PackageWaypointTiming,
|
||||||
dynamic_runways: Dict[str, RunwayData]) -> None:
|
dynamic_runways: Dict[str, RunwayData]) -> None:
|
||||||
flight_type = flight.flight_type
|
flight_type = flight.flight_type
|
||||||
if flight_type in [FlightType.CAP, FlightType.BARCAP, FlightType.TARCAP,
|
if flight_type in [FlightType.CAP, FlightType.BARCAP, FlightType.TARCAP,
|
||||||
@ -1209,6 +1017,9 @@ class AircraftConflictGenerator:
|
|||||||
for waypoint in flight.points:
|
for waypoint in flight.points:
|
||||||
waypoint.tot = None
|
waypoint.tot = None
|
||||||
|
|
||||||
|
takeoff_point = FlightWaypoint.from_pydcs(group.points[0],
|
||||||
|
flight.from_cp)
|
||||||
|
self.set_takeoff_time(takeoff_point, package, flight, group)
|
||||||
for point in flight.points:
|
for point in flight.points:
|
||||||
if point.only_for_player and not flight.client_count:
|
if point.only_for_player and not flight.client_count:
|
||||||
continue
|
continue
|
||||||
@ -1219,9 +1030,53 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
# Set here rather than when the FlightData is created so they waypoints
|
# Set here rather than when the FlightData is created so they waypoints
|
||||||
# have their TOTs set.
|
# have their TOTs set.
|
||||||
self.flights[-1].waypoints = flight.points
|
self.flights[-1].waypoints = [takeoff_point] + flight.points
|
||||||
self._setup_custom_payload(flight, group)
|
self._setup_custom_payload(flight, group)
|
||||||
|
|
||||||
|
def set_takeoff_time(self, waypoint: FlightWaypoint, package: Package,
|
||||||
|
flight: Flight, group: FlyingGroup) -> None:
|
||||||
|
estimator = TotEstimator(package)
|
||||||
|
start_time = estimator.mission_start_time(flight)
|
||||||
|
|
||||||
|
if start_time > 0:
|
||||||
|
if self.should_activate_late(flight):
|
||||||
|
# Late activation causes the aircraft to not be spawned until
|
||||||
|
# triggered.
|
||||||
|
self.set_activation_time(flight, group, start_time)
|
||||||
|
elif flight.start_type == "Cold":
|
||||||
|
# Setting the start time causes the AI to wait until the
|
||||||
|
# specified time to begin their startup sequence.
|
||||||
|
self.set_startup_time(flight, group, start_time)
|
||||||
|
|
||||||
|
# And setting *our* waypoint TOT causes the takeoff time to show up in
|
||||||
|
# the player's kneeboard.
|
||||||
|
waypoint.tot = estimator.takeoff_time_for_flight(flight)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def should_activate_late(flight: Flight) -> bool:
|
||||||
|
if flight.client_count:
|
||||||
|
# Never delay players. Note that cold start player flights with
|
||||||
|
# AI members will still be marked as uncontrolled until the start
|
||||||
|
# trigger fires to postpone engine start.
|
||||||
|
#
|
||||||
|
# Player flights that start on the runway or in the air will start
|
||||||
|
# immediately, and AI flight members will not be delayed.
|
||||||
|
return False
|
||||||
|
|
||||||
|
if flight.start_type != "Cold":
|
||||||
|
# Avoid spawning aircraft in the air or on the runway until it's
|
||||||
|
# time for their mission. Also avoid burning through gas spawning
|
||||||
|
# hot aircraft hours before their takeoff time.
|
||||||
|
return True
|
||||||
|
|
||||||
|
if flight.from_cp.is_fleet:
|
||||||
|
# Carrier spawns will crowd the carrier deck, especially without
|
||||||
|
# super carrier.
|
||||||
|
# TODO: Is there enough parking on the supercarrier?
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class PydcsWaypointBuilder:
|
class PydcsWaypointBuilder:
|
||||||
def __init__(self, waypoint: FlightWaypoint, group: FlyingGroup,
|
def __init__(self, waypoint: FlightWaypoint, group: FlyingGroup,
|
||||||
@ -1248,10 +1103,8 @@ class PydcsWaypointBuilder:
|
|||||||
waypoint.speed_locked = False
|
waypoint.speed_locked = False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def for_waypoint(cls, waypoint: FlightWaypoint,
|
def for_waypoint(cls, waypoint: FlightWaypoint, group: FlyingGroup,
|
||||||
group: FlyingGroup,
|
flight: Flight, timing: PackageWaypointTiming,
|
||||||
flight: Flight,
|
|
||||||
timing: PackageWaypointTiming,
|
|
||||||
mission: Mission) -> PydcsWaypointBuilder:
|
mission: Mission) -> PydcsWaypointBuilder:
|
||||||
builders = {
|
builders = {
|
||||||
FlightWaypointType.EGRESS: EgressPointBuilder,
|
FlightWaypointType.EGRESS: EgressPointBuilder,
|
||||||
@ -1396,9 +1249,7 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
|
|||||||
pattern=OrbitAction.OrbitPattern.RaceTrack
|
pattern=OrbitAction.OrbitPattern.RaceTrack
|
||||||
))
|
))
|
||||||
|
|
||||||
start = self.timing.race_track_start
|
self.set_waypoint_tot(waypoint, self.timing.race_track_start)
|
||||||
if start is not None:
|
|
||||||
self.set_waypoint_tot(waypoint, start)
|
|
||||||
racetrack.stop_after_time(self.timing.race_track_end)
|
racetrack.stop_after_time(self.timing.race_track_end)
|
||||||
waypoint.add_task(racetrack)
|
waypoint.add_task(racetrack)
|
||||||
return waypoint
|
return waypoint
|
||||||
|
|||||||
@ -52,7 +52,7 @@ class Package:
|
|||||||
delay: int = field(default=0)
|
delay: int = field(default=0)
|
||||||
|
|
||||||
#: Desired TOT measured in seconds from mission start.
|
#: Desired TOT measured in seconds from mission start.
|
||||||
time_over_target: Optional[int] = field(default=None)
|
time_over_target: int = field(default=0)
|
||||||
|
|
||||||
waypoints: Optional[PackageWaypoints] = field(default=None)
|
waypoints: Optional[PackageWaypoints] = field(default=None)
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import datetime
|
||||||
import os
|
import os
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@ -116,9 +117,10 @@ class BriefingGenerator(MissionInfoGenerator):
|
|||||||
assert not flight.client_units
|
assert not flight.client_units
|
||||||
aircraft = flight.aircraft_type
|
aircraft = flight.aircraft_type
|
||||||
flight_unit_name = db.unit_type_name(aircraft)
|
flight_unit_name = db.unit_type_name(aircraft)
|
||||||
|
delay = datetime.timedelta(seconds=flight.departure_delay)
|
||||||
self.description += (
|
self.description += (
|
||||||
f"{flight.flight_type.name} {flight_unit_name} x {flight.size}, "
|
f"{flight.flight_type.name} {flight_unit_name} x {flight.size}, "
|
||||||
f"departing in {flight.departure_delay} minutes\n"
|
f"departing in {delay}\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
def generate(self):
|
def generate(self):
|
||||||
|
|||||||
@ -33,6 +33,7 @@ from gen.flights.flight import (
|
|||||||
FlightType,
|
FlightType,
|
||||||
)
|
)
|
||||||
from gen.flights.flightplan import FlightPlanBuilder
|
from gen.flights.flightplan import FlightPlanBuilder
|
||||||
|
from gen.flights.traveltime import TotEstimator
|
||||||
from theater import (
|
from theater import (
|
||||||
ControlPoint,
|
ControlPoint,
|
||||||
FrontLine,
|
FrontLine,
|
||||||
@ -185,11 +186,13 @@ class PackageBuilder:
|
|||||||
def __init__(self, location: MissionTarget,
|
def __init__(self, location: MissionTarget,
|
||||||
closest_airfields: ClosestAirfields,
|
closest_airfields: ClosestAirfields,
|
||||||
global_inventory: GlobalAircraftInventory,
|
global_inventory: GlobalAircraftInventory,
|
||||||
is_player: bool) -> None:
|
is_player: bool,
|
||||||
|
start_type: str) -> None:
|
||||||
self.package = Package(location)
|
self.package = Package(location)
|
||||||
self.allocator = AircraftAllocator(closest_airfields, global_inventory,
|
self.allocator = AircraftAllocator(closest_airfields, global_inventory,
|
||||||
is_player)
|
is_player)
|
||||||
self.global_inventory = global_inventory
|
self.global_inventory = global_inventory
|
||||||
|
self.start_type = start_type
|
||||||
|
|
||||||
def plan_flight(self, plan: ProposedFlight) -> bool:
|
def plan_flight(self, plan: ProposedFlight) -> bool:
|
||||||
"""Allocates aircraft for the given flight and adds them to the package.
|
"""Allocates aircraft for the given flight and adds them to the package.
|
||||||
@ -203,7 +206,8 @@ class PackageBuilder:
|
|||||||
if assignment is None:
|
if assignment is None:
|
||||||
return False
|
return False
|
||||||
airfield, aircraft = assignment
|
airfield, aircraft = assignment
|
||||||
flight = Flight(aircraft, plan.num_aircraft, airfield, plan.task)
|
flight = Flight(aircraft, plan.num_aircraft, airfield, plan.task,
|
||||||
|
self.start_type)
|
||||||
self.package.add_flight(flight)
|
self.package.add_flight(flight)
|
||||||
flight.targetPoint = self.package.target
|
flight.targetPoint = self.package.target
|
||||||
return True
|
return True
|
||||||
@ -445,11 +449,18 @@ class CoalitionMissionPlanner:
|
|||||||
|
|
||||||
def plan_mission(self, mission: ProposedMission) -> None:
|
def plan_mission(self, mission: ProposedMission) -> None:
|
||||||
"""Allocates aircraft for a proposed mission and adds it to the ATO."""
|
"""Allocates aircraft for a proposed mission and adds it to the ATO."""
|
||||||
|
|
||||||
|
if self.game.settings.perf_ai_parking_start:
|
||||||
|
start_type = "Cold"
|
||||||
|
else:
|
||||||
|
start_type = "Warm"
|
||||||
|
|
||||||
builder = PackageBuilder(
|
builder = PackageBuilder(
|
||||||
mission.location,
|
mission.location,
|
||||||
self.objective_finder.closest_airfields_to(mission.location),
|
self.objective_finder.closest_airfields_to(mission.location),
|
||||||
self.game.aircraft_inventory,
|
self.game.aircraft_inventory,
|
||||||
self.is_player
|
self.is_player,
|
||||||
|
start_type
|
||||||
)
|
)
|
||||||
|
|
||||||
missing_types: Set[FlightType] = set()
|
missing_types: Set[FlightType] = set()
|
||||||
@ -498,16 +509,18 @@ class CoalitionMissionPlanner:
|
|||||||
margin=5
|
margin=5
|
||||||
)
|
)
|
||||||
for package in self.ato.packages:
|
for package in self.ato.packages:
|
||||||
|
tot = TotEstimator(package).earliest_tot()
|
||||||
if package.primary_task in dca_types:
|
if package.primary_task in dca_types:
|
||||||
# All CAP missions should be on station in 15-25 minutes.
|
# All CAP missions should be on station ASAP.
|
||||||
package.time_over_target = (20 + random.randint(-5, 5)) * 60
|
package.time_over_target = tot
|
||||||
else:
|
else:
|
||||||
# But other packages should be spread out a bit.
|
# But other packages should be spread out a bit. Note that take
|
||||||
package.delay = next(start_time)
|
# times are delayed, but all aircraft will become active at
|
||||||
# TODO: Compute TOT based on package.
|
# mission start. This makes it more worthwhile to attack enemy
|
||||||
package.time_over_target = (package.delay + 40) * 60
|
# airfields to hit grounded aircraft, since they're more likely
|
||||||
for flight in package.flights:
|
# to be present. Runway and air started aircraft will be
|
||||||
flight.scheduled_in = package.delay
|
# delayed until their takeoff time by AirConflictGenerator.
|
||||||
|
package.time_over_target = next(start_time) * 60 + tot
|
||||||
|
|
||||||
def message(self, title, text) -> None:
|
def message(self, title, text) -> None:
|
||||||
"""Emits a planning message to the player.
|
"""Emits a planning message to the player.
|
||||||
|
|||||||
@ -97,7 +97,6 @@ class FlightWaypoint:
|
|||||||
# flight's offset in the UI.
|
# flight's offset in the UI.
|
||||||
self.tot: Optional[int] = None
|
self.tot: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_pydcs(cls, point: MovingPoint,
|
def from_pydcs(cls, point: MovingPoint,
|
||||||
from_cp: ControlPoint) -> "FlightWaypoint":
|
from_cp: ControlPoint) -> "FlightWaypoint":
|
||||||
@ -130,14 +129,11 @@ class Flight:
|
|||||||
client_count: int = 0
|
client_count: int = 0
|
||||||
use_custom_loadout = False
|
use_custom_loadout = False
|
||||||
preset_loadout_name = ""
|
preset_loadout_name = ""
|
||||||
start_type = "Runway"
|
|
||||||
group = False # Contains DCS Mission group data after mission has been generated
|
group = False # Contains DCS Mission group data after mission has been generated
|
||||||
targetPoint = None # Contains either None or a Strike/SEAD target point location
|
targetPoint = None # Contains either None or a Strike/SEAD target point location
|
||||||
|
|
||||||
# How long before this flight should take off
|
def __init__(self, unit_type: UnitType, count: int, from_cp: ControlPoint,
|
||||||
scheduled_in = 0
|
flight_type: FlightType, start_type: str) -> None:
|
||||||
|
|
||||||
def __init__(self, unit_type: UnitType, count: int, from_cp: ControlPoint, flight_type: FlightType):
|
|
||||||
self.unit_type = unit_type
|
self.unit_type = unit_type
|
||||||
self.count = count
|
self.count = count
|
||||||
self.from_cp = from_cp
|
self.from_cp = from_cp
|
||||||
@ -145,11 +141,16 @@ class Flight:
|
|||||||
self.points: List[FlightWaypoint] = []
|
self.points: List[FlightWaypoint] = []
|
||||||
self.targets: List[MissionTarget] = []
|
self.targets: List[MissionTarget] = []
|
||||||
self.loadout: Dict[str, str] = {}
|
self.loadout: Dict[str, str] = {}
|
||||||
self.start_type = "Runway"
|
self.start_type = start_type
|
||||||
|
# Late activation delay in seconds from mission start. This is not
|
||||||
|
# the same as the flight's takeoff time. Takeoff time depends on the
|
||||||
|
# mission's TOT and the other flights in the package. Takeoff time is
|
||||||
|
# determined by AirConflictGenerator.
|
||||||
|
self.scheduled_in = 0
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return self.flight_type.name + " | " + str(self.count) + "x" + db.unit_type_name(self.unit_type) \
|
return self.flight_type.name + " | " + str(self.count) + "x" + db.unit_type_name(self.unit_type) \
|
||||||
+ " in " + str(self.scheduled_in) + " minutes (" + str(len(self.points)) + " wpt)"
|
+ " (" + str(len(self.points)) + " wpt)"
|
||||||
|
|
||||||
|
|
||||||
# Test
|
# Test
|
||||||
@ -158,6 +159,6 @@ if __name__ == '__main__':
|
|||||||
from theater import ControlPoint, Point, List
|
from theater import ControlPoint, Point, List
|
||||||
|
|
||||||
from_cp = ControlPoint(0, "AA", Point(0, 0), Point(0, 0), [], 0, 0)
|
from_cp = ControlPoint(0, "AA", Point(0, 0), Point(0, 0), [], 0, 0)
|
||||||
f = Flight(A_10C(), 4, from_cp, FlightType.CAS)
|
f = Flight(A_10C(), 4, from_cp, FlightType.CAS, "Cold")
|
||||||
f.scheduled_in = 50
|
f.scheduled_in = 50
|
||||||
print(f)
|
print(f)
|
||||||
|
|||||||
285
gen/flights/traveltime.py
Normal file
285
gen/flights/traveltime.py
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Iterable, Optional
|
||||||
|
|
||||||
|
from dcs.mapping import Point
|
||||||
|
|
||||||
|
from game.utils import meter_to_nm
|
||||||
|
from gen.ato import Package
|
||||||
|
from gen.flights.flight import (
|
||||||
|
Flight,
|
||||||
|
FlightType,
|
||||||
|
FlightWaypoint,
|
||||||
|
FlightWaypointType,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
CAP_DURATION = 30 # Minutes
|
||||||
|
CAP_TYPES = (FlightType.BARCAP, FlightType.CAP)
|
||||||
|
|
||||||
|
|
||||||
|
class GroundSpeed:
|
||||||
|
@classmethod
|
||||||
|
def for_package(cls, package: Package) -> int:
|
||||||
|
speeds = []
|
||||||
|
for flight in package.flights:
|
||||||
|
speeds.append(cls.for_flight(flight))
|
||||||
|
return min(speeds) # knots
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def for_flight(_flight: Flight) -> int:
|
||||||
|
# TODO: Gather data so this is useful.
|
||||||
|
# TODO: Expose both a cruise speed and target speed.
|
||||||
|
# The cruise speed can be used for ascent, hold, join, and RTB to save
|
||||||
|
# on fuel, but mission speed will be fast enough to keep the flight
|
||||||
|
# safer.
|
||||||
|
return 400 # knots
|
||||||
|
|
||||||
|
|
||||||
|
class TravelTime:
|
||||||
|
@staticmethod
|
||||||
|
def between_points(a: Point, b: Point, speed: float) -> int:
|
||||||
|
error_factor = 1.1
|
||||||
|
distance = meter_to_nm(a.distance_to_point(b))
|
||||||
|
hours = distance / speed
|
||||||
|
seconds = hours * 3600
|
||||||
|
return int(seconds * error_factor)
|
||||||
|
|
||||||
|
|
||||||
|
class TotEstimator:
|
||||||
|
# An extra five minutes given as wiggle room. Expected to be spent at the
|
||||||
|
# hold point performing any last minute configuration.
|
||||||
|
HOLD_TIME = 5 * 60
|
||||||
|
|
||||||
|
def __init__(self, package: Package) -> None:
|
||||||
|
self.package = package
|
||||||
|
self.timing = PackageWaypointTiming.for_package(package)
|
||||||
|
|
||||||
|
def mission_start_time(self, flight: Flight) -> int:
|
||||||
|
takeoff_time = self.takeoff_time_for_flight(flight)
|
||||||
|
startup_time = self.estimate_startup(flight)
|
||||||
|
ground_ops_time = self.estimate_ground_ops(flight)
|
||||||
|
return takeoff_time - startup_time - ground_ops_time
|
||||||
|
|
||||||
|
def takeoff_time_for_flight(self, flight: Flight) -> int:
|
||||||
|
stop_types = {FlightWaypointType.JOIN, FlightWaypointType.PATROL_TRACK}
|
||||||
|
travel_time = self.estimate_waypoints_to_target(flight, stop_types)
|
||||||
|
if travel_time is None:
|
||||||
|
logging.warning("Found no join point or patrol point. Cannot "
|
||||||
|
f"estimate takeoff time takeoff time for {flight}")
|
||||||
|
# Takeoff immediately.
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if self.package.primary_task in CAP_TYPES:
|
||||||
|
start_time = self.timing.race_track_start
|
||||||
|
else:
|
||||||
|
start_time = self.timing.join
|
||||||
|
return start_time - travel_time - self.HOLD_TIME
|
||||||
|
|
||||||
|
def earliest_tot(self) -> int:
|
||||||
|
return max((
|
||||||
|
self.earliest_tot_for_flight(f) for f in self.package.flights
|
||||||
|
)) + self.HOLD_TIME
|
||||||
|
|
||||||
|
def earliest_tot_for_flight(self, flight: Flight) -> int:
|
||||||
|
"""Estimate fastest time from mission start to the target position.
|
||||||
|
|
||||||
|
For CAP missions, this is time to race track start.
|
||||||
|
|
||||||
|
For other mission types this is the time to the mission target.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
flight: The flight to get the earliest TOT time for.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The earliest possible TOT for the given flight in seconds. Returns 0
|
||||||
|
if an ingress point cannot be found.
|
||||||
|
"""
|
||||||
|
stop_types = {
|
||||||
|
FlightWaypointType.PATROL_TRACK,
|
||||||
|
FlightWaypointType.INGRESS_CAS,
|
||||||
|
FlightWaypointType.INGRESS_SEAD,
|
||||||
|
FlightWaypointType.INGRESS_STRIKE,
|
||||||
|
}
|
||||||
|
time_to_ingress = self.estimate_waypoints_to_target(flight, stop_types)
|
||||||
|
if time_to_ingress is None:
|
||||||
|
logging.warning(
|
||||||
|
f"Found no ingress types. Cannot estimate TOT for {flight}")
|
||||||
|
# Return 0 so this flight's travel time does not affect the rest of
|
||||||
|
# the package.
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if self.package.primary_task in CAP_TYPES:
|
||||||
|
# The racetrack start *is* the target. The package target is the
|
||||||
|
# protected objective.
|
||||||
|
time_to_target = 0
|
||||||
|
else:
|
||||||
|
assert self.package.waypoints is not None
|
||||||
|
time_to_target = TravelTime.between_points(
|
||||||
|
self.package.waypoints.ingress, self.package.target.position,
|
||||||
|
GroundSpeed.for_package(self.package))
|
||||||
|
return sum([
|
||||||
|
self.estimate_startup(flight),
|
||||||
|
self.estimate_ground_ops(flight),
|
||||||
|
time_to_ingress,
|
||||||
|
time_to_target,
|
||||||
|
])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def estimate_startup(flight: Flight) -> int:
|
||||||
|
if flight.start_type == "Cold":
|
||||||
|
return 10 * 60
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def estimate_ground_ops(flight: Flight) -> int:
|
||||||
|
if flight.start_type in ("Runway", "In Flight"):
|
||||||
|
return 0
|
||||||
|
if flight.from_cp.is_fleet:
|
||||||
|
return 2 * 60
|
||||||
|
else:
|
||||||
|
return 5 * 60
|
||||||
|
|
||||||
|
def estimate_waypoints_to_target(
|
||||||
|
self, flight: Flight,
|
||||||
|
stop_types: Iterable[FlightWaypointType]) -> Optional[int]:
|
||||||
|
total = 0
|
||||||
|
previous_position = flight.from_cp.position
|
||||||
|
for waypoint in flight.points:
|
||||||
|
position = Point(waypoint.x, waypoint.y)
|
||||||
|
total += TravelTime.between_points(
|
||||||
|
previous_position, position,
|
||||||
|
self.speed_to_waypoint(flight, waypoint)
|
||||||
|
)
|
||||||
|
previous_position = position
|
||||||
|
if waypoint.waypoint_type in stop_types:
|
||||||
|
return total
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def speed_to_waypoint(self, flight: Flight,
|
||||||
|
waypoint: FlightWaypoint) -> int:
|
||||||
|
pre_join = (FlightWaypointType.LOITER, FlightWaypointType.JOIN)
|
||||||
|
if waypoint.waypoint_type == FlightWaypointType.ASCEND_POINT:
|
||||||
|
# Flights that start airborne already have some altitude and a good
|
||||||
|
# amount of speed.
|
||||||
|
factor = 1.0 if flight.start_type == "In Flight" else 0.5
|
||||||
|
return int(GroundSpeed.for_flight(flight) * factor)
|
||||||
|
elif waypoint.waypoint_type in pre_join:
|
||||||
|
return GroundSpeed.for_flight(flight)
|
||||||
|
return GroundSpeed.for_package(self.package)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PackageWaypointTiming:
|
||||||
|
#: The package being scheduled.
|
||||||
|
package: Package
|
||||||
|
|
||||||
|
#: The package join time.
|
||||||
|
join: int
|
||||||
|
|
||||||
|
#: The ingress waypoint TOT.
|
||||||
|
ingress: int
|
||||||
|
|
||||||
|
#: The egress waypoint TOT.
|
||||||
|
egress: int
|
||||||
|
|
||||||
|
#: The package split time.
|
||||||
|
split: int
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target(self) -> int:
|
||||||
|
"""The package time over target."""
|
||||||
|
assert self.package.time_over_target is not None
|
||||||
|
return self.package.time_over_target
|
||||||
|
|
||||||
|
@property
|
||||||
|
def race_track_start(self) -> int:
|
||||||
|
if self.package.primary_task in CAP_TYPES:
|
||||||
|
return self.package.time_over_target
|
||||||
|
else:
|
||||||
|
return self.ingress
|
||||||
|
|
||||||
|
@property
|
||||||
|
def race_track_end(self) -> int:
|
||||||
|
if self.package.primary_task in CAP_TYPES:
|
||||||
|
return self.target + CAP_DURATION * 60
|
||||||
|
else:
|
||||||
|
return self.egress
|
||||||
|
|
||||||
|
def push_time(self, flight: Flight, hold_point: Point) -> int:
|
||||||
|
assert self.package.waypoints is not None
|
||||||
|
return self.join - TravelTime.between_points(
|
||||||
|
hold_point,
|
||||||
|
self.package.waypoints.join,
|
||||||
|
GroundSpeed.for_flight(flight)
|
||||||
|
)
|
||||||
|
|
||||||
|
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[int]:
|
||||||
|
target_types = (
|
||||||
|
FlightWaypointType.TARGET_GROUP_LOC,
|
||||||
|
FlightWaypointType.TARGET_POINT,
|
||||||
|
FlightWaypointType.TARGET_SHIP,
|
||||||
|
)
|
||||||
|
|
||||||
|
ingress_types = (
|
||||||
|
FlightWaypointType.INGRESS_CAS,
|
||||||
|
FlightWaypointType.INGRESS_SEAD,
|
||||||
|
FlightWaypointType.INGRESS_STRIKE,
|
||||||
|
)
|
||||||
|
|
||||||
|
if waypoint.waypoint_type == FlightWaypointType.JOIN:
|
||||||
|
return self.join
|
||||||
|
elif waypoint.waypoint_type in ingress_types:
|
||||||
|
return self.ingress
|
||||||
|
elif waypoint.waypoint_type in target_types:
|
||||||
|
return self.target
|
||||||
|
elif waypoint.waypoint_type == FlightWaypointType.EGRESS:
|
||||||
|
return self.egress
|
||||||
|
elif waypoint.waypoint_type == FlightWaypointType.SPLIT:
|
||||||
|
return self.split
|
||||||
|
elif waypoint.waypoint_type == FlightWaypointType.PATROL_TRACK:
|
||||||
|
return self.race_track_start
|
||||||
|
return None
|
||||||
|
|
||||||
|
def depart_time_for_waypoint(self, waypoint: FlightWaypoint,
|
||||||
|
flight: Flight) -> Optional[int]:
|
||||||
|
if waypoint.waypoint_type == FlightWaypointType.LOITER:
|
||||||
|
return self.push_time(flight, Point(waypoint.x, waypoint.y))
|
||||||
|
elif waypoint.waypoint_type == FlightWaypointType.PATROL:
|
||||||
|
return self.race_track_end
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def for_package(cls, package: Package) -> PackageWaypointTiming:
|
||||||
|
assert package.waypoints is not None
|
||||||
|
|
||||||
|
group_ground_speed = GroundSpeed.for_package(package)
|
||||||
|
|
||||||
|
ingress = package.time_over_target - TravelTime.between_points(
|
||||||
|
package.waypoints.ingress,
|
||||||
|
package.target.position,
|
||||||
|
group_ground_speed
|
||||||
|
)
|
||||||
|
|
||||||
|
join = ingress - TravelTime.between_points(
|
||||||
|
package.waypoints.join,
|
||||||
|
package.waypoints.ingress,
|
||||||
|
group_ground_speed
|
||||||
|
)
|
||||||
|
|
||||||
|
egress = package.time_over_target + TravelTime.between_points(
|
||||||
|
package.target.position,
|
||||||
|
package.waypoints.egress,
|
||||||
|
group_ground_speed
|
||||||
|
)
|
||||||
|
|
||||||
|
split = egress + TravelTime.between_points(
|
||||||
|
package.waypoints.egress,
|
||||||
|
package.waypoints.split,
|
||||||
|
group_ground_speed
|
||||||
|
)
|
||||||
|
|
||||||
|
return cls(package, join, ingress, egress, split)
|
||||||
@ -58,7 +58,7 @@ class Dialog:
|
|||||||
flight: Flight) -> None:
|
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.edit_flight_dialog = QEditFlightDialog(
|
||||||
cls.game_model.game,
|
cls.game_model,
|
||||||
package_model.package,
|
package_model.package,
|
||||||
flight
|
flight
|
||||||
)
|
)
|
||||||
|
|||||||
65
qt_ui/displayoptions.py
Normal file
65
qt_ui/displayoptions.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
"""Visibility options for the game map."""
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Iterator, Optional, Union
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DisplayRule:
|
||||||
|
name: str
|
||||||
|
_value: bool
|
||||||
|
|
||||||
|
@property
|
||||||
|
def menu_text(self) -> str:
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self) -> bool:
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
@value.setter
|
||||||
|
def value(self, value: bool) -> None:
|
||||||
|
from qt_ui.widgets.map.QLiberationMap import QLiberationMap
|
||||||
|
self._value = value
|
||||||
|
QLiberationMap.instance.reload_scene()
|
||||||
|
QLiberationMap.instance.update()
|
||||||
|
|
||||||
|
def __bool__(self) -> bool:
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
|
class DisplayGroup:
|
||||||
|
def __init__(self, name: Optional[str]) -> None:
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
def __iter__(self) -> Iterator[DisplayRule]:
|
||||||
|
# Python 3.6 enforces that __dict__ is order preserving by default.
|
||||||
|
for value in self.__dict__.values():
|
||||||
|
if isinstance(value, DisplayRule):
|
||||||
|
yield value
|
||||||
|
|
||||||
|
|
||||||
|
class FlightPathOptions(DisplayGroup):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__("Flight Paths")
|
||||||
|
self.hide = DisplayRule("Hide Flight Paths", False)
|
||||||
|
self.only_selected = DisplayRule("Show Selected Flight Path", False)
|
||||||
|
self.all = DisplayRule("Show All Flight Paths", True)
|
||||||
|
|
||||||
|
|
||||||
|
class DisplayOptions:
|
||||||
|
ground_objects = DisplayRule("Ground Objects", True)
|
||||||
|
control_points = DisplayRule("Control Points", True)
|
||||||
|
lines = DisplayRule("Lines", True)
|
||||||
|
events = DisplayRule("Events", True)
|
||||||
|
sam_ranges = DisplayRule("SAM Ranges", True)
|
||||||
|
waypoint_info = DisplayRule("Waypoint Information", True)
|
||||||
|
flight_paths = FlightPathOptions()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def menu_items(cls) -> Iterator[Union[DisplayGroup, DisplayRule]]:
|
||||||
|
# Python 3.6 enforces that __dict__ is order preserving by default.
|
||||||
|
for value in cls.__dict__.values():
|
||||||
|
if isinstance(value, DisplayRule):
|
||||||
|
yield value
|
||||||
|
elif isinstance(value, DisplayGroup):
|
||||||
|
yield value
|
||||||
@ -1,7 +1,6 @@
|
|||||||
"""Qt data models for game objects."""
|
"""Qt data models for game objects."""
|
||||||
import datetime
|
import datetime
|
||||||
from enum import auto, IntEnum
|
from typing import Any, Callable, Dict, Iterator, Optional, TypeVar
|
||||||
from typing import Any, Callable, Dict, Iterator, TypeVar, Optional
|
|
||||||
|
|
||||||
from PySide2.QtCore import (
|
from PySide2.QtCore import (
|
||||||
QAbstractListModel,
|
QAbstractListModel,
|
||||||
@ -15,6 +14,7 @@ from game import db
|
|||||||
from game.game import Game
|
from game.game import Game
|
||||||
from gen.ato import AirTaskingOrder, Package
|
from gen.ato import AirTaskingOrder, Package
|
||||||
from gen.flights.flight import Flight
|
from gen.flights.flight import Flight
|
||||||
|
from gen.flights.traveltime import TotEstimator
|
||||||
from qt_ui.uiconstants import AIRCRAFT_ICONS
|
from qt_ui.uiconstants import AIRCRAFT_ICONS
|
||||||
from theater.missiontarget import MissionTarget
|
from theater.missiontarget import MissionTarget
|
||||||
|
|
||||||
@ -95,6 +95,8 @@ class NullListModel(QAbstractListModel):
|
|||||||
class PackageModel(QAbstractListModel):
|
class PackageModel(QAbstractListModel):
|
||||||
"""The model for an ATO package."""
|
"""The model for an ATO package."""
|
||||||
|
|
||||||
|
FlightRole = Qt.UserRole
|
||||||
|
|
||||||
#: Emitted when this package is being deleted from the ATO.
|
#: Emitted when this package is being deleted from the ATO.
|
||||||
deleted = Signal()
|
deleted = Signal()
|
||||||
|
|
||||||
@ -113,17 +115,19 @@ class PackageModel(QAbstractListModel):
|
|||||||
return self.text_for_flight(flight)
|
return self.text_for_flight(flight)
|
||||||
if role == Qt.DecorationRole:
|
if role == Qt.DecorationRole:
|
||||||
return self.icon_for_flight(flight)
|
return self.icon_for_flight(flight)
|
||||||
|
elif role == PackageModel.FlightRole:
|
||||||
|
return flight
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
def text_for_flight(self, flight: Flight) -> str:
|
||||||
def text_for_flight(flight: Flight) -> str:
|
|
||||||
"""Returns the text that should be displayed for the flight."""
|
"""Returns the text that should be displayed for the flight."""
|
||||||
task = flight.flight_type.name
|
task = flight.flight_type.name
|
||||||
count = flight.count
|
count = flight.count
|
||||||
name = db.unit_type_name(flight.unit_type)
|
name = db.unit_type_name(flight.unit_type)
|
||||||
delay = flight.scheduled_in
|
estimator = TotEstimator(self.package)
|
||||||
|
delay = datetime.timedelta(seconds=estimator.mission_start_time(flight))
|
||||||
origin = flight.from_cp.name
|
origin = flight.from_cp.name
|
||||||
return f"[{task}] {count} x {name} from {origin} in {delay} minutes"
|
return f"[{task}] {count} x {name} from {origin} in {delay}"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def icon_for_flight(flight: Flight) -> Optional[QIcon]:
|
def icon_for_flight(flight: Flight) -> Optional[QIcon]:
|
||||||
@ -185,6 +189,8 @@ class AtoModel(QAbstractListModel):
|
|||||||
|
|
||||||
PackageRole = Qt.UserRole
|
PackageRole = Qt.UserRole
|
||||||
|
|
||||||
|
client_slots_changed = Signal()
|
||||||
|
|
||||||
def __init__(self, game: Optional[Game], ato: AirTaskingOrder) -> None:
|
def __init__(self, game: Optional[Game], ato: AirTaskingOrder) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.game = game
|
self.game = game
|
||||||
|
|||||||
@ -31,14 +31,20 @@ COLORS: Dict[str, QColor] = {
|
|||||||
"white_transparent": QColor(255, 255, 255, 35),
|
"white_transparent": QColor(255, 255, 255, 35),
|
||||||
"grey_transparent": QColor(150, 150, 150, 30),
|
"grey_transparent": QColor(150, 150, 150, 30),
|
||||||
|
|
||||||
|
"light_red": QColor(231, 92, 83, 90),
|
||||||
"red": QColor(200, 80, 80),
|
"red": QColor(200, 80, 80),
|
||||||
"dark_red": QColor(140, 20, 20),
|
"dark_red": QColor(140, 20, 20),
|
||||||
"red_transparent": QColor(227, 32, 0, 20),
|
"red_transparent": QColor(227, 32, 0, 20),
|
||||||
|
"transparent": QColor(255, 255, 255, 0),
|
||||||
|
|
||||||
|
"light_blue": QColor(105, 182, 240, 90),
|
||||||
"blue": QColor(0, 132, 255),
|
"blue": QColor(0, 132, 255),
|
||||||
"dark_blue": QColor(45, 62, 80),
|
"dark_blue": QColor(45, 62, 80),
|
||||||
"blue_transparent": QColor(0, 132, 255, 20),
|
"blue_transparent": QColor(0, 132, 255, 20),
|
||||||
|
|
||||||
|
"purple": QColor(187, 137, 255),
|
||||||
|
"yellow": QColor(238, 225, 123),
|
||||||
|
|
||||||
"bright_red": QColor(150, 80, 80),
|
"bright_red": QColor(150, 80, 80),
|
||||||
"super_red": QColor(227, 32, 0),
|
"super_red": QColor(227, 32, 0),
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
"""A layout containing a widget with an associated label."""
|
"""A layout containing a widget with an associated label."""
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from PySide2.QtCore import Qt
|
from PySide2.QtCore import Qt
|
||||||
from PySide2.QtWidgets import QHBoxLayout, QLabel, QWidget
|
from PySide2.QtWidgets import QHBoxLayout, QLabel, QWidget
|
||||||
|
|
||||||
@ -10,8 +12,13 @@ class QLabeledWidget(QHBoxLayout):
|
|||||||
label is used to name the input.
|
label is used to name the input.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, text: str, widget: QWidget) -> None:
|
def __init__(self, text: str, widget: QWidget,
|
||||||
|
tooltip: Optional[str] = None) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.addWidget(QLabel(text))
|
label = QLabel(text)
|
||||||
|
self.addWidget(label)
|
||||||
self.addStretch()
|
self.addStretch()
|
||||||
self.addWidget(widget, alignment=Qt.AlignRight)
|
self.addWidget(widget, alignment=Qt.AlignRight)
|
||||||
|
if tooltip is not None:
|
||||||
|
label.setToolTip(tooltip)
|
||||||
|
widget.setToolTip(tooltip)
|
||||||
|
|||||||
@ -11,9 +11,11 @@ from PySide2.QtWidgets import (
|
|||||||
import qt_ui.uiconstants as CONST
|
import qt_ui.uiconstants as CONST
|
||||||
from game import Game
|
from game import Game
|
||||||
from game.event import CAP, CAS, FrontlineAttackEvent
|
from game.event import CAP, CAS, FrontlineAttackEvent
|
||||||
|
from qt_ui.models import GameModel
|
||||||
from qt_ui.widgets.QBudgetBox import QBudgetBox
|
from qt_ui.widgets.QBudgetBox import QBudgetBox
|
||||||
from qt_ui.widgets.QFactionsInfos import QFactionsInfos
|
from qt_ui.widgets.QFactionsInfos import QFactionsInfos
|
||||||
from qt_ui.widgets.QTurnCounter import QTurnCounter
|
from qt_ui.widgets.QTurnCounter import QTurnCounter
|
||||||
|
from qt_ui.widgets.clientslots import MaxPlayerCount
|
||||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||||
from qt_ui.windows.QWaitingForMissionResultWindow import \
|
from qt_ui.windows.QWaitingForMissionResultWindow import \
|
||||||
QWaitingForMissionResultWindow
|
QWaitingForMissionResultWindow
|
||||||
@ -23,14 +25,18 @@ from qt_ui.windows.stats.QStatsWindow import QStatsWindow
|
|||||||
|
|
||||||
class QTopPanel(QFrame):
|
class QTopPanel(QFrame):
|
||||||
|
|
||||||
def __init__(self, game: Game):
|
def __init__(self, game_model: GameModel):
|
||||||
super(QTopPanel, self).__init__()
|
super(QTopPanel, self).__init__()
|
||||||
self.game = game
|
self.game_model = game_model
|
||||||
self.setMaximumHeight(70)
|
self.setMaximumHeight(70)
|
||||||
self.init_ui()
|
self.init_ui()
|
||||||
GameUpdateSignal.get_instance().gameupdated.connect(self.setGame)
|
GameUpdateSignal.get_instance().gameupdated.connect(self.setGame)
|
||||||
GameUpdateSignal.get_instance().budgetupdated.connect(self.budget_update)
|
GameUpdateSignal.get_instance().budgetupdated.connect(self.budget_update)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def game(self) -> Optional[Game]:
|
||||||
|
return self.game_model.game
|
||||||
|
|
||||||
def init_ui(self):
|
def init_ui(self):
|
||||||
|
|
||||||
self.turnCounter = QTurnCounter()
|
self.turnCounter = QTurnCounter()
|
||||||
@ -68,6 +74,8 @@ class QTopPanel(QFrame):
|
|||||||
|
|
||||||
self.proceedBox = QGroupBox("Proceed")
|
self.proceedBox = QGroupBox("Proceed")
|
||||||
self.proceedBoxLayout = QHBoxLayout()
|
self.proceedBoxLayout = QHBoxLayout()
|
||||||
|
self.proceedBoxLayout.addLayout(
|
||||||
|
MaxPlayerCount(self.game_model.ato_model))
|
||||||
self.proceedBoxLayout.addWidget(self.passTurnButton)
|
self.proceedBoxLayout.addWidget(self.passTurnButton)
|
||||||
self.proceedBoxLayout.addWidget(self.proceedButton)
|
self.proceedBoxLayout.addWidget(self.proceedButton)
|
||||||
self.proceedBox.setLayout(self.proceedBoxLayout)
|
self.proceedBox.setLayout(self.proceedBoxLayout)
|
||||||
@ -84,16 +92,17 @@ class QTopPanel(QFrame):
|
|||||||
self.setLayout(self.layout)
|
self.setLayout(self.layout)
|
||||||
|
|
||||||
def setGame(self, game: Optional[Game]):
|
def setGame(self, game: Optional[Game]):
|
||||||
self.game = game
|
if game is None:
|
||||||
if game is not None:
|
return
|
||||||
self.turnCounter.setCurrentTurn(self.game.turn, self.game.current_day)
|
|
||||||
self.budgetBox.setGame(self.game)
|
|
||||||
self.factionsInfos.setGame(self.game)
|
|
||||||
|
|
||||||
if self.game and self.game.turn == 0:
|
self.turnCounter.setCurrentTurn(game.turn, game.current_day)
|
||||||
self.proceedButton.setEnabled(False)
|
self.budgetBox.setGame(game)
|
||||||
else:
|
self.factionsInfos.setGame(game)
|
||||||
self.proceedButton.setEnabled(True)
|
|
||||||
|
if game and game.turn == 0:
|
||||||
|
self.proceedButton.setEnabled(False)
|
||||||
|
else:
|
||||||
|
self.proceedButton.setEnabled(True)
|
||||||
|
|
||||||
def openSettings(self):
|
def openSettings(self):
|
||||||
self.subwindow = QSettingsWindow(self.game)
|
self.subwindow = QSettingsWindow(self.game)
|
||||||
@ -138,6 +147,8 @@ class QTopPanel(QFrame):
|
|||||||
if not self.ato_has_clients() and not self.confirm_no_client_launch():
|
if not self.ato_has_clients() and not self.confirm_no_client_launch():
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# TODO: Verify no negative start times.
|
||||||
|
|
||||||
# TODO: Refactor this nonsense.
|
# TODO: Refactor this nonsense.
|
||||||
game_event = None
|
game_event = None
|
||||||
for event in self.game.events:
|
for event in self.game.events:
|
||||||
|
|||||||
@ -10,7 +10,7 @@ from PySide2.QtCore import (
|
|||||||
QSize,
|
QSize,
|
||||||
Qt,
|
Qt,
|
||||||
)
|
)
|
||||||
from PySide2.QtGui import QFont, QFontMetrics, QPainter
|
from PySide2.QtGui import QFont, QFontMetrics, QIcon, QPainter
|
||||||
from PySide2.QtWidgets import (
|
from PySide2.QtWidgets import (
|
||||||
QAbstractItemView,
|
QAbstractItemView,
|
||||||
QGroupBox,
|
QGroupBox,
|
||||||
@ -18,15 +18,115 @@ from PySide2.QtWidgets import (
|
|||||||
QListView,
|
QListView,
|
||||||
QPushButton,
|
QPushButton,
|
||||||
QSplitter,
|
QSplitter,
|
||||||
QStyleOptionViewItem, QStyledItemDelegate, QVBoxLayout,
|
QStyle, QStyleOptionViewItem, QStyledItemDelegate, QVBoxLayout,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from game import db
|
||||||
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.traveltime import TotEstimator
|
||||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||||
from ..models import AtoModel, GameModel, NullListModel, PackageModel
|
from ..models import AtoModel, GameModel, NullListModel, PackageModel
|
||||||
|
|
||||||
|
|
||||||
|
class FlightDelegate(QStyledItemDelegate):
|
||||||
|
FONT_SIZE = 10
|
||||||
|
HMARGIN = 4
|
||||||
|
VMARGIN = 4
|
||||||
|
|
||||||
|
def __init__(self, package: Package) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.package = package
|
||||||
|
|
||||||
|
def get_font(self, option: QStyleOptionViewItem) -> QFont:
|
||||||
|
font = QFont(option.font)
|
||||||
|
font.setPointSize(self.FONT_SIZE)
|
||||||
|
return font
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def flight(index: QModelIndex) -> Flight:
|
||||||
|
return index.data(PackageModel.FlightRole)
|
||||||
|
|
||||||
|
def first_row_text(self, index: QModelIndex) -> str:
|
||||||
|
flight = self.flight(index)
|
||||||
|
task = flight.flight_type.name
|
||||||
|
count = flight.count
|
||||||
|
name = db.unit_type_name(flight.unit_type)
|
||||||
|
estimator = TotEstimator(self.package)
|
||||||
|
delay = datetime.timedelta(seconds=estimator.mission_start_time(flight))
|
||||||
|
return f"[{task}] {count} x {name} in {delay}"
|
||||||
|
|
||||||
|
def second_row_text(self, index: QModelIndex) -> str:
|
||||||
|
flight = self.flight(index)
|
||||||
|
origin = flight.from_cp.name
|
||||||
|
return f"From {origin}"
|
||||||
|
|
||||||
|
def paint(self, painter: QPainter, option: QStyleOptionViewItem,
|
||||||
|
index: QModelIndex) -> None:
|
||||||
|
# Draw the list item with all the default selection styling, but with an
|
||||||
|
# invalid index so text formatting is left to us.
|
||||||
|
super().paint(painter, option, QModelIndex())
|
||||||
|
|
||||||
|
rect = option.rect.adjusted(self.HMARGIN, self.VMARGIN, -self.HMARGIN,
|
||||||
|
-self.VMARGIN)
|
||||||
|
|
||||||
|
with painter_context(painter):
|
||||||
|
painter.setFont(self.get_font(option))
|
||||||
|
|
||||||
|
icon: Optional[QIcon] = index.data(Qt.DecorationRole)
|
||||||
|
if icon is not None:
|
||||||
|
icon.paint(painter, rect, Qt.AlignLeft | Qt.AlignVCenter,
|
||||||
|
self.icon_mode(option),
|
||||||
|
self.icon_state(option))
|
||||||
|
|
||||||
|
rect = rect.adjusted(self.icon_size(option).width() + self.HMARGIN,
|
||||||
|
0, 0, 0)
|
||||||
|
painter.drawText(rect, Qt.AlignLeft, self.first_row_text(index))
|
||||||
|
line2 = rect.adjusted(0, rect.height() / 2, 0, rect.height() / 2)
|
||||||
|
painter.drawText(line2, Qt.AlignLeft, self.second_row_text(index))
|
||||||
|
|
||||||
|
clients = self.num_clients(index)
|
||||||
|
if clients:
|
||||||
|
painter.drawText(rect, Qt.AlignRight,
|
||||||
|
f"Player Slots: {clients}")
|
||||||
|
|
||||||
|
def num_clients(self, index: QModelIndex) -> int:
|
||||||
|
flight = self.flight(index)
|
||||||
|
return flight.client_count
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def icon_mode(option: QStyleOptionViewItem) -> QIcon.Mode:
|
||||||
|
if not (option.state & QStyle.State_Enabled):
|
||||||
|
return QIcon.Disabled
|
||||||
|
elif option.state & QStyle.State_Selected:
|
||||||
|
return QIcon.Selected
|
||||||
|
elif option.state & QStyle.State_Active:
|
||||||
|
return QIcon.Active
|
||||||
|
return QIcon.Normal
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def icon_state(option: QStyleOptionViewItem) -> QIcon.State:
|
||||||
|
return QIcon.On if option.state & QStyle.State_Open else QIcon.Off
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def icon_size(option: QStyleOptionViewItem) -> QSize:
|
||||||
|
icon_size: Optional[QSize] = option.decorationSize
|
||||||
|
if icon_size is None:
|
||||||
|
return QSize(0, 0)
|
||||||
|
else:
|
||||||
|
return icon_size
|
||||||
|
|
||||||
|
def sizeHint(self, option: QStyleOptionViewItem,
|
||||||
|
index: QModelIndex) -> QSize:
|
||||||
|
left = self.icon_size(option).width() + self.HMARGIN
|
||||||
|
metrics = QFontMetrics(self.get_font(option))
|
||||||
|
first = metrics.size(0, self.first_row_text(index))
|
||||||
|
second = metrics.size(0, self.second_row_text(index))
|
||||||
|
text_width = max(first.width(), second.width())
|
||||||
|
return QSize(left + text_width + 2 * self.HMARGIN,
|
||||||
|
first.height() + second.height() + 2 * self.VMARGIN)
|
||||||
|
|
||||||
|
|
||||||
class QFlightList(QListView):
|
class QFlightList(QListView):
|
||||||
"""List view for displaying the flights of a package."""
|
"""List view for displaying the flights of a package."""
|
||||||
|
|
||||||
@ -34,6 +134,8 @@ class QFlightList(QListView):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.package_model = model
|
self.package_model = model
|
||||||
self.set_package(model)
|
self.set_package(model)
|
||||||
|
if model is not None:
|
||||||
|
self.setItemDelegate(FlightDelegate(model.package))
|
||||||
self.setIconSize(QSize(91, 24))
|
self.setIconSize(QSize(91, 24))
|
||||||
self.setSelectionBehavior(QAbstractItemView.SelectItems)
|
self.setSelectionBehavior(QAbstractItemView.SelectItems)
|
||||||
|
|
||||||
@ -43,6 +145,7 @@ class QFlightList(QListView):
|
|||||||
self.disconnect_model()
|
self.disconnect_model()
|
||||||
else:
|
else:
|
||||||
self.package_model = model
|
self.package_model = model
|
||||||
|
self.setItemDelegate(FlightDelegate(model.package))
|
||||||
self.setModel(model)
|
self.setModel(model)
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
model.deleted.connect(self.disconnect_model)
|
model.deleted.connect(self.disconnect_model)
|
||||||
@ -109,6 +212,7 @@ class QFlightPanel(QGroupBox):
|
|||||||
"""Sets the package model to display."""
|
"""Sets the package model to display."""
|
||||||
self.package_model = model
|
self.package_model = model
|
||||||
self.flight_list.set_package(model)
|
self.flight_list.set_package(model)
|
||||||
|
self.selection_changed.connect(self.on_selection_changed)
|
||||||
self.on_selection_changed()
|
self.on_selection_changed()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -122,6 +226,15 @@ class QFlightPanel(QGroupBox):
|
|||||||
enabled = index.isValid()
|
enabled = index.isValid()
|
||||||
self.edit_button.setEnabled(enabled)
|
self.edit_button.setEnabled(enabled)
|
||||||
self.delete_button.setEnabled(enabled)
|
self.delete_button.setEnabled(enabled)
|
||||||
|
self.change_map_flight_selection(index)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def change_map_flight_selection(index: QModelIndex) -> None:
|
||||||
|
if not index.isValid():
|
||||||
|
GameUpdateSignal.get_instance().select_flight(None)
|
||||||
|
return
|
||||||
|
|
||||||
|
GameUpdateSignal.get_instance().select_flight(index.row())
|
||||||
|
|
||||||
def on_edit(self) -> None:
|
def on_edit(self) -> None:
|
||||||
"""Opens the flight edit dialog."""
|
"""Opens the flight edit dialog."""
|
||||||
@ -196,6 +309,15 @@ class PackageDelegate(QStyledItemDelegate):
|
|||||||
line2 = rect.adjusted(0, rect.height() / 2, 0, rect.height() / 2)
|
line2 = rect.adjusted(0, rect.height() / 2, 0, rect.height() / 2)
|
||||||
painter.drawText(line2, Qt.AlignLeft, self.right_text(index))
|
painter.drawText(line2, Qt.AlignLeft, self.right_text(index))
|
||||||
|
|
||||||
|
clients = self.num_clients(index)
|
||||||
|
if clients:
|
||||||
|
painter.drawText(rect, Qt.AlignRight,
|
||||||
|
f"Player Slots: {clients}")
|
||||||
|
|
||||||
|
def num_clients(self, index: QModelIndex) -> int:
|
||||||
|
package = self.package(index)
|
||||||
|
return sum(f.client_count for f in package.flights)
|
||||||
|
|
||||||
def sizeHint(self, option: QStyleOptionViewItem,
|
def sizeHint(self, option: QStyleOptionViewItem,
|
||||||
index: QModelIndex) -> QSize:
|
index: QModelIndex) -> QSize:
|
||||||
metrics = QFontMetrics(self.get_font(option))
|
metrics = QFontMetrics(self.get_font(option))
|
||||||
@ -270,6 +392,18 @@ class QPackagePanel(QGroupBox):
|
|||||||
enabled = index.isValid()
|
enabled = index.isValid()
|
||||||
self.edit_button.setEnabled(enabled)
|
self.edit_button.setEnabled(enabled)
|
||||||
self.delete_button.setEnabled(enabled)
|
self.delete_button.setEnabled(enabled)
|
||||||
|
self.change_map_package_selection(index)
|
||||||
|
|
||||||
|
def change_map_package_selection(self, index: QModelIndex) -> None:
|
||||||
|
if not index.isValid():
|
||||||
|
GameUpdateSignal.get_instance().select_package(None)
|
||||||
|
return
|
||||||
|
|
||||||
|
package = self.ato_model.get_package_model(index)
|
||||||
|
if package.rowCount() == 0:
|
||||||
|
GameUpdateSignal.get_instance().select_package(None)
|
||||||
|
else:
|
||||||
|
GameUpdateSignal.get_instance().select_package(index.row())
|
||||||
|
|
||||||
def on_edit(self) -> None:
|
def on_edit(self) -> None:
|
||||||
"""Opens the package edit dialog."""
|
"""Opens the package edit dialog."""
|
||||||
|
|||||||
28
qt_ui/widgets/clientslots.py
Normal file
28
qt_ui/widgets/clientslots.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
"""Widgets for displaying client slots."""
|
||||||
|
from PySide2.QtWidgets import QLabel
|
||||||
|
|
||||||
|
from qt_ui.models import AtoModel
|
||||||
|
from qt_ui.widgets.QLabeledWidget import QLabeledWidget
|
||||||
|
|
||||||
|
|
||||||
|
class MaxPlayerCount(QLabeledWidget):
|
||||||
|
def __init__(self, ato_model: AtoModel) -> None:
|
||||||
|
self.ato_model = ato_model
|
||||||
|
self.slots_label = QLabel(str(self.count_client_slots))
|
||||||
|
self.ato_model.client_slots_changed.connect(self.update_count)
|
||||||
|
super().__init__(
|
||||||
|
"Max Players:", self.slots_label,
|
||||||
|
("Total number of client slots. To add client slots, edit a flight "
|
||||||
|
"using the panel on the left.")
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def count_client_slots(self) -> int:
|
||||||
|
slots = 0
|
||||||
|
for package in self.ato_model.packages:
|
||||||
|
for flight in package.flights:
|
||||||
|
slots += flight.client_count
|
||||||
|
return slots
|
||||||
|
|
||||||
|
def update_count(self) -> None:
|
||||||
|
self.slots_label.setText(str(self.count_client_slots))
|
||||||
@ -1,5 +1,8 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, List, Optional, Tuple
|
from typing import List, Optional, 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
|
||||||
@ -15,14 +18,18 @@ from dcs.mapping import point_from_heading
|
|||||||
|
|
||||||
import qt_ui.uiconstants as CONST
|
import qt_ui.uiconstants as CONST
|
||||||
from game import Game, db
|
from game import Game, db
|
||||||
|
from game.data.aaa_db import AAA_UNITS
|
||||||
from game.data.radar_db import UNITS_WITH_RADAR
|
from game.data.radar_db import UNITS_WITH_RADAR
|
||||||
from gen import Conflict
|
from game.utils import meter_to_feet
|
||||||
from gen.flights.flight import Flight
|
from gen import Conflict, PackageWaypointTiming
|
||||||
|
from gen.ato import Package
|
||||||
|
from gen.flights.flight import Flight, FlightWaypoint, FlightWaypointType
|
||||||
|
from qt_ui.displayoptions import DisplayOptions
|
||||||
from qt_ui.models import GameModel
|
from qt_ui.models import GameModel
|
||||||
|
from qt_ui.widgets.map.QFrontLine import QFrontLine
|
||||||
from qt_ui.widgets.map.QLiberationScene import QLiberationScene
|
from qt_ui.widgets.map.QLiberationScene import QLiberationScene
|
||||||
from qt_ui.widgets.map.QMapControlPoint import QMapControlPoint
|
from qt_ui.widgets.map.QMapControlPoint import QMapControlPoint
|
||||||
from qt_ui.widgets.map.QMapGroundObject import QMapGroundObject
|
from qt_ui.widgets.map.QMapGroundObject import QMapGroundObject
|
||||||
from qt_ui.widgets.map.QFrontLine import QFrontLine
|
|
||||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||||
from theater import ControlPoint, FrontLine
|
from theater import ControlPoint, FrontLine
|
||||||
|
|
||||||
@ -30,15 +37,7 @@ from theater import ControlPoint, FrontLine
|
|||||||
class QLiberationMap(QGraphicsView):
|
class QLiberationMap(QGraphicsView):
|
||||||
WAYPOINT_SIZE = 4
|
WAYPOINT_SIZE = 4
|
||||||
|
|
||||||
instance = None
|
instance: Optional[QLiberationMap] = None
|
||||||
display_rules: Dict[str, bool] = {
|
|
||||||
"cp": True,
|
|
||||||
"go": True,
|
|
||||||
"lines": True,
|
|
||||||
"events": True,
|
|
||||||
"sam": True,
|
|
||||||
"flight_paths": False
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, game_model: GameModel):
|
def __init__(self, game_model: GameModel):
|
||||||
super(QLiberationMap, self).__init__()
|
super(QLiberationMap, self).__init__()
|
||||||
@ -47,6 +46,8 @@ class QLiberationMap(QGraphicsView):
|
|||||||
self.game: Optional[Game] = game_model.game
|
self.game: Optional[Game] = game_model.game
|
||||||
|
|
||||||
self.flight_path_items: List[QGraphicsItem] = []
|
self.flight_path_items: List[QGraphicsItem] = []
|
||||||
|
# A tuple of (package index, flight index), or none.
|
||||||
|
self.selected_flight: Optional[Tuple[int, int]] = None
|
||||||
|
|
||||||
self.setMinimumSize(800,600)
|
self.setMinimumSize(800,600)
|
||||||
self.setMaximumHeight(2160)
|
self.setMaximumHeight(2160)
|
||||||
@ -61,6 +62,25 @@ class QLiberationMap(QGraphicsView):
|
|||||||
lambda: self.draw_flight_plans(self.scene())
|
lambda: self.draw_flight_plans(self.scene())
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def update_package_selection(index: Optional[int]) -> None:
|
||||||
|
self.selected_flight = index, 0
|
||||||
|
self.draw_flight_plans(self.scene())
|
||||||
|
|
||||||
|
GameUpdateSignal.get_instance().package_selection_changed.connect(
|
||||||
|
update_package_selection
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_flight_selection(index: Optional[int]) -> None:
|
||||||
|
if self.selected_flight is None:
|
||||||
|
logging.error("Flight was selected with no package selected")
|
||||||
|
return
|
||||||
|
self.selected_flight = self.selected_flight[0], index
|
||||||
|
self.draw_flight_plans(self.scene())
|
||||||
|
|
||||||
|
GameUpdateSignal.get_instance().flight_selection_changed.connect(
|
||||||
|
update_flight_selection
|
||||||
|
)
|
||||||
|
|
||||||
def init_scene(self):
|
def init_scene(self):
|
||||||
scene = QLiberationScene(self)
|
scene = QLiberationScene(self)
|
||||||
self.setScene(scene)
|
self.setScene(scene)
|
||||||
@ -161,27 +181,44 @@ class QLiberationMap(QGraphicsView):
|
|||||||
buildings = self.game.theater.find_ground_objects_by_obj_name(ground_object.obj_name)
|
buildings = self.game.theater.find_ground_objects_by_obj_name(ground_object.obj_name)
|
||||||
scene.addItem(QMapGroundObject(self, go_pos[0], go_pos[1], 14, 12, cp, ground_object, self.game, buildings))
|
scene.addItem(QMapGroundObject(self, go_pos[0], go_pos[1], 14, 12, cp, ground_object, self.game, buildings))
|
||||||
|
|
||||||
if ground_object.category == "aa" and self.get_display_rule("sam"):
|
is_aa = ground_object.category == "aa"
|
||||||
max_range = 0
|
if is_aa and DisplayOptions.sam_ranges:
|
||||||
has_radar = False
|
threat_range = 0
|
||||||
|
detection_range = 0
|
||||||
|
can_fire = False
|
||||||
if ground_object.groups:
|
if ground_object.groups:
|
||||||
for g in ground_object.groups:
|
for g in ground_object.groups:
|
||||||
for u in g.units:
|
for u in g.units:
|
||||||
unit = db.unit_type_from_name(u.type)
|
unit = db.unit_type_from_name(u.type)
|
||||||
if unit in UNITS_WITH_RADAR:
|
if unit in UNITS_WITH_RADAR or unit in AAA_UNITS:
|
||||||
has_radar = True
|
can_fire = True
|
||||||
if unit.threat_range > max_range:
|
if unit.detection_range > detection_range:
|
||||||
max_range = unit.threat_range
|
detection_range = unit.detection_range
|
||||||
if has_radar:
|
if unit.threat_range > threat_range:
|
||||||
scene.addEllipse(go_pos[0] - max_range/300.0 + 8, go_pos[1] - max_range/300.0 + 8, max_range/150.0, max_range/150.0, CONST.COLORS["white_transparent"], CONST.COLORS["grey_transparent"])
|
threat_range = unit.threat_range
|
||||||
|
if can_fire:
|
||||||
|
threat_pos = self._transform_point(Point(ground_object.position.x+threat_range,
|
||||||
|
ground_object.position.y+threat_range))
|
||||||
|
detection_pos = self._transform_point(Point(ground_object.position.x+detection_range,
|
||||||
|
ground_object.position.y+detection_range))
|
||||||
|
threat_radius = Point(*go_pos).distance_to_point(Point(*threat_pos))
|
||||||
|
detection_radius = Point(*go_pos).distance_to_point(Point(*detection_pos))
|
||||||
|
|
||||||
|
# Add detection range circle
|
||||||
|
scene.addEllipse(go_pos[0] - detection_radius/2 + 7, go_pos[1] - detection_radius/2 + 6,
|
||||||
|
detection_radius, detection_radius, self.detection_pen(cp.captured))
|
||||||
|
|
||||||
|
# Add threat range circle
|
||||||
|
scene.addEllipse(go_pos[0] - threat_radius / 2 + 7, go_pos[1] - threat_radius / 2 + 6,
|
||||||
|
threat_radius, threat_radius, self.threat_pen(cp.captured))
|
||||||
added_objects.append(ground_object.obj_name)
|
added_objects.append(ground_object.obj_name)
|
||||||
|
|
||||||
for cp in self.game.theater.enemy_points():
|
for cp in self.game.theater.enemy_points():
|
||||||
if self.get_display_rule("lines"):
|
if DisplayOptions.lines:
|
||||||
self.scene_create_lines_for_cp(cp, playerColor, enemyColor)
|
self.scene_create_lines_for_cp(cp, playerColor, enemyColor)
|
||||||
|
|
||||||
for cp in self.game.theater.player_points():
|
for cp in self.game.theater.player_points():
|
||||||
if self.get_display_rule("lines"):
|
if DisplayOptions.lines:
|
||||||
self.scene_create_lines_for_cp(cp, playerColor, enemyColor)
|
self.scene_create_lines_for_cp(cp, playerColor, enemyColor)
|
||||||
|
|
||||||
self.draw_flight_plans(scene)
|
self.draw_flight_plans(scene)
|
||||||
@ -202,37 +239,94 @@ class QLiberationMap(QGraphicsView):
|
|||||||
# Something may have caused those items to already be removed.
|
# Something may have caused those items to already be removed.
|
||||||
pass
|
pass
|
||||||
self.flight_path_items.clear()
|
self.flight_path_items.clear()
|
||||||
if not self.get_display_rule("flight_paths"):
|
if DisplayOptions.flight_paths.hide:
|
||||||
return
|
return
|
||||||
for package in self.game_model.ato_model.packages:
|
packages = list(self.game_model.ato_model.packages)
|
||||||
for flight in package.flights:
|
for p_idx, package_model in enumerate(packages):
|
||||||
self.draw_flight_plan(scene, flight)
|
for f_idx, flight in enumerate(package_model.flights):
|
||||||
|
selected = (p_idx, f_idx) == self.selected_flight
|
||||||
|
if DisplayOptions.flight_paths.only_selected and not selected:
|
||||||
|
continue
|
||||||
|
self.draw_flight_plan(scene, package_model.package, flight,
|
||||||
|
selected)
|
||||||
|
|
||||||
def draw_flight_plan(self, scene: QGraphicsScene, flight: Flight) -> None:
|
def draw_flight_plan(self, scene: QGraphicsScene, package: Package,
|
||||||
|
flight: Flight, selected: bool) -> None:
|
||||||
is_player = flight.from_cp.captured
|
is_player = flight.from_cp.captured
|
||||||
pos = self._transform_point(flight.from_cp.position)
|
pos = self._transform_point(flight.from_cp.position)
|
||||||
|
|
||||||
self.draw_waypoint(scene, pos, is_player)
|
self.draw_waypoint(scene, pos, is_player, selected)
|
||||||
prev_pos = tuple(pos)
|
prev_pos = tuple(pos)
|
||||||
for point in flight.points:
|
drew_target = False
|
||||||
|
target_types = (
|
||||||
|
FlightWaypointType.TARGET_GROUP_LOC,
|
||||||
|
FlightWaypointType.TARGET_POINT,
|
||||||
|
FlightWaypointType.TARGET_SHIP,
|
||||||
|
)
|
||||||
|
for idx, point in enumerate(flight.points):
|
||||||
new_pos = self._transform_point(Point(point.x, point.y))
|
new_pos = self._transform_point(Point(point.x, point.y))
|
||||||
self.draw_flight_path(scene, prev_pos, new_pos, is_player)
|
self.draw_flight_path(scene, prev_pos, new_pos, is_player,
|
||||||
self.draw_waypoint(scene, new_pos, is_player)
|
selected)
|
||||||
|
self.draw_waypoint(scene, new_pos, is_player, selected)
|
||||||
|
if selected and DisplayOptions.waypoint_info:
|
||||||
|
if point.waypoint_type in target_types:
|
||||||
|
if drew_target:
|
||||||
|
# Don't draw dozens of targets over each other.
|
||||||
|
continue
|
||||||
|
drew_target = True
|
||||||
|
self.draw_waypoint_info(scene, idx + 1, point, new_pos, package,
|
||||||
|
flight)
|
||||||
prev_pos = tuple(new_pos)
|
prev_pos = tuple(new_pos)
|
||||||
self.draw_flight_path(scene, prev_pos, pos, is_player)
|
self.draw_flight_path(scene, prev_pos, pos, is_player, selected)
|
||||||
|
|
||||||
def draw_waypoint(self, scene: QGraphicsScene, position: Tuple[int, int],
|
def draw_waypoint(self, scene: QGraphicsScene, position: Tuple[int, int],
|
||||||
player: bool) -> None:
|
player: bool, selected: bool) -> None:
|
||||||
waypoint_pen = self.waypoint_pen(player)
|
waypoint_pen = self.waypoint_pen(player, selected)
|
||||||
waypoint_brush = self.waypoint_brush(player)
|
waypoint_brush = self.waypoint_brush(player, selected)
|
||||||
self.flight_path_items.append(scene.addEllipse(
|
self.flight_path_items.append(scene.addEllipse(
|
||||||
position[0], position[1], self.WAYPOINT_SIZE,
|
position[0], position[1], self.WAYPOINT_SIZE,
|
||||||
self.WAYPOINT_SIZE, waypoint_pen, waypoint_brush
|
self.WAYPOINT_SIZE, waypoint_pen, waypoint_brush
|
||||||
))
|
))
|
||||||
|
|
||||||
|
def draw_waypoint_info(self, scene: QGraphicsScene, number: int,
|
||||||
|
waypoint: FlightWaypoint, position: Tuple[int, int],
|
||||||
|
package: Package, flight: Flight) -> None:
|
||||||
|
timing = PackageWaypointTiming.for_package(package)
|
||||||
|
|
||||||
|
altitude = meter_to_feet(waypoint.alt)
|
||||||
|
altitude_type = "AGL" if waypoint.alt_type == "RADIO" else "MSL"
|
||||||
|
|
||||||
|
prefix = "TOT"
|
||||||
|
time = timing.tot_for_waypoint(waypoint)
|
||||||
|
if time is None:
|
||||||
|
prefix = "Depart"
|
||||||
|
time = timing.depart_time_for_waypoint(waypoint, flight)
|
||||||
|
if time is None:
|
||||||
|
tot = ""
|
||||||
|
else:
|
||||||
|
tot = f"{prefix} T+{datetime.timedelta(seconds=time)}"
|
||||||
|
|
||||||
|
pen = QPen(QColor("black"), 0.3)
|
||||||
|
brush = QColor("white")
|
||||||
|
|
||||||
|
def draw_text(text: str, x: int, y: int) -> None:
|
||||||
|
item = scene.addSimpleText(text)
|
||||||
|
item.setBrush(brush)
|
||||||
|
item.setPen(pen)
|
||||||
|
item.moveBy(x, y)
|
||||||
|
item.setZValue(2)
|
||||||
|
self.flight_path_items.append(item)
|
||||||
|
|
||||||
|
draw_text(f"{number} {waypoint.name}", position[0] + 8,
|
||||||
|
position[1] - 15)
|
||||||
|
draw_text(f"{altitude} ft {altitude_type}", position[0] + 8,
|
||||||
|
position[1] - 5)
|
||||||
|
draw_text(tot, position[0] + 8, position[1] + 5)
|
||||||
|
|
||||||
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)
|
selected: bool) -> None:
|
||||||
|
flight_path_pen = self.flight_path_pen(player, selected)
|
||||||
# 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
|
||||||
self.flight_path_items.append(scene.addLine(
|
self.flight_path_items.append(scene.addLine(
|
||||||
@ -321,21 +415,48 @@ class QLiberationMap(QGraphicsView):
|
|||||||
|
|
||||||
return X > treshold and X or treshold, Y > treshold and Y or treshold
|
return X > treshold and X or treshold, Y > treshold and Y or treshold
|
||||||
|
|
||||||
|
def highlight_color(self, transparent: Optional[bool] = False) -> QColor:
|
||||||
|
return QColor(255, 255, 0, 20 if transparent else 255)
|
||||||
|
|
||||||
def base_faction_color_name(self, player: bool) -> str:
|
def base_faction_color_name(self, player: bool) -> str:
|
||||||
if player:
|
if player:
|
||||||
return self.game.get_player_color()
|
return self.game.get_player_color()
|
||||||
else:
|
else:
|
||||||
return self.game.get_enemy_color()
|
return self.game.get_enemy_color()
|
||||||
|
|
||||||
def waypoint_pen(self, player: bool) -> QPen:
|
def waypoint_pen(self, player: bool, selected: bool) -> QColor:
|
||||||
|
if selected and DisplayOptions.flight_paths.all:
|
||||||
|
return self.highlight_color()
|
||||||
name = self.base_faction_color_name(player)
|
name = self.base_faction_color_name(player)
|
||||||
return QPen(brush=CONST.COLORS[name])
|
return CONST.COLORS[name]
|
||||||
|
|
||||||
def waypoint_brush(self, player: bool) -> QColor:
|
def waypoint_brush(self, player: bool, selected: bool) -> QColor:
|
||||||
|
if selected and DisplayOptions.flight_paths.all:
|
||||||
|
return self.highlight_color(transparent=True)
|
||||||
name = self.base_faction_color_name(player)
|
name = self.base_faction_color_name(player)
|
||||||
return CONST.COLORS[f"{name}_transparent"]
|
return CONST.COLORS[f"{name}_transparent"]
|
||||||
|
|
||||||
def flight_path_pen(self, player: bool) -> QPen:
|
def threat_pen(self, player: bool) -> QPen:
|
||||||
|
if player:
|
||||||
|
color = "blue"
|
||||||
|
else:
|
||||||
|
color = "red"
|
||||||
|
qpen = QPen(CONST.COLORS[color])
|
||||||
|
return qpen
|
||||||
|
|
||||||
|
def detection_pen(self, player: bool) -> QPen:
|
||||||
|
if player:
|
||||||
|
color = "purple"
|
||||||
|
else:
|
||||||
|
color = "yellow"
|
||||||
|
qpen = QPen(CONST.COLORS[color])
|
||||||
|
qpen.setStyle(Qt.DotLine)
|
||||||
|
return qpen
|
||||||
|
|
||||||
|
def flight_path_pen(self, player: bool, selected: bool) -> QPen:
|
||||||
|
if selected and DisplayOptions.flight_paths.all:
|
||||||
|
return self.highlight_color()
|
||||||
|
|
||||||
name = self.base_faction_color_name(player)
|
name = self.base_faction_color_name(player)
|
||||||
color = CONST.COLORS[name]
|
color = CONST.COLORS[name]
|
||||||
pen = QPen(brush=color)
|
pen = QPen(brush=color)
|
||||||
@ -367,18 +488,3 @@ class QLiberationMap(QGraphicsView):
|
|||||||
effect = QGraphicsOpacityEffect()
|
effect = QGraphicsOpacityEffect()
|
||||||
effect.setOpacity(0.3)
|
effect.setOpacity(0.3)
|
||||||
overlay.setGraphicsEffect(effect)
|
overlay.setGraphicsEffect(effect)
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def set_display_rule(rule: str, value: bool):
|
|
||||||
QLiberationMap.display_rules[rule] = value
|
|
||||||
QLiberationMap.instance.reload_scene()
|
|
||||||
QLiberationMap.instance.update()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_display_rules() -> Dict[str, bool]:
|
|
||||||
return QLiberationMap.display_rules
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_display_rule(rule) -> bool:
|
|
||||||
return QLiberationMap.display_rules[rule]
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@ from qt_ui.models import GameModel
|
|||||||
from qt_ui.windows.basemenu.QBaseMenu2 import QBaseMenu2
|
from qt_ui.windows.basemenu.QBaseMenu2 import QBaseMenu2
|
||||||
from theater import ControlPoint
|
from theater import ControlPoint
|
||||||
from .QMapObject import QMapObject
|
from .QMapObject import QMapObject
|
||||||
|
from ...displayoptions import DisplayOptions
|
||||||
|
|
||||||
|
|
||||||
class QMapControlPoint(QMapObject):
|
class QMapControlPoint(QMapObject):
|
||||||
@ -21,7 +22,7 @@ class QMapControlPoint(QMapObject):
|
|||||||
self.base_details_dialog: Optional[QBaseMenu2] = None
|
self.base_details_dialog: Optional[QBaseMenu2] = None
|
||||||
|
|
||||||
def paint(self, painter, option, widget=None) -> None:
|
def paint(self, painter, option, widget=None) -> None:
|
||||||
if self.parent.get_display_rule("cp"):
|
if DisplayOptions.control_points:
|
||||||
painter.save()
|
painter.save()
|
||||||
painter.setRenderHint(QPainter.Antialiasing)
|
painter.setRenderHint(QPainter.Antialiasing)
|
||||||
painter.setBrush(self.brush_color)
|
painter.setBrush(self.brush_color)
|
||||||
|
|||||||
@ -8,8 +8,9 @@ import qt_ui.uiconstants as const
|
|||||||
from game import Game
|
from game import Game
|
||||||
from game.data.building_data import FORTIFICATION_BUILDINGS
|
from game.data.building_data import FORTIFICATION_BUILDINGS
|
||||||
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
|
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
|
||||||
from theater import TheaterGroundObject, ControlPoint
|
from theater import ControlPoint, TheaterGroundObject
|
||||||
from .QMapObject import QMapObject
|
from .QMapObject import QMapObject
|
||||||
|
from ...displayoptions import DisplayOptions
|
||||||
|
|
||||||
|
|
||||||
class QMapGroundObject(QMapObject):
|
class QMapGroundObject(QMapObject):
|
||||||
@ -50,7 +51,7 @@ class QMapGroundObject(QMapObject):
|
|||||||
player_icons = "_blue"
|
player_icons = "_blue"
|
||||||
enemy_icons = ""
|
enemy_icons = ""
|
||||||
|
|
||||||
if self.parent.get_display_rule("go"):
|
if DisplayOptions.ground_objects:
|
||||||
painter.save()
|
painter.save()
|
||||||
|
|
||||||
cat = self.ground_object.category
|
cat = self.ground_object.category
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
from PySide2.QtCore import QObject, Signal
|
from PySide2.QtCore import QObject, Signal
|
||||||
|
|
||||||
@ -24,11 +24,21 @@ class GameUpdateSignal(QObject):
|
|||||||
debriefingReceived = Signal(DebriefingSignal)
|
debriefingReceived = Signal(DebriefingSignal)
|
||||||
|
|
||||||
flight_paths_changed = Signal()
|
flight_paths_changed = Signal()
|
||||||
|
package_selection_changed = Signal(int) # Optional[int]
|
||||||
|
flight_selection_changed = Signal(int) # Optional[int]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(GameUpdateSignal, self).__init__()
|
super(GameUpdateSignal, self).__init__()
|
||||||
GameUpdateSignal.instance = self
|
GameUpdateSignal.instance = self
|
||||||
|
|
||||||
|
def select_package(self, index: Optional[int]) -> None:
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
|
self.package_selection_changed.emit(index)
|
||||||
|
|
||||||
|
def select_flight(self, index: Optional[int]) -> None:
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
|
self.flight_selection_changed.emit(index)
|
||||||
|
|
||||||
def redraw_flight_paths(self) -> None:
|
def redraw_flight_paths(self) -> None:
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
self.flight_paths_changed.emit()
|
self.flight_paths_changed.emit()
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
import webbrowser
|
import webbrowser
|
||||||
from typing import Optional
|
from typing import Optional, Union
|
||||||
|
|
||||||
from PySide2.QtCore import Qt
|
from PySide2.QtCore import Qt
|
||||||
from PySide2.QtGui import QIcon
|
from PySide2.QtGui import QIcon
|
||||||
from PySide2.QtWidgets import (
|
from PySide2.QtWidgets import (
|
||||||
QAction,
|
QAction,
|
||||||
QDesktopWidget,
|
QActionGroup, QDesktopWidget,
|
||||||
QFileDialog,
|
QFileDialog,
|
||||||
QMainWindow,
|
QMainWindow,
|
||||||
QMessageBox,
|
QMenu, QMessageBox,
|
||||||
QSplitter,
|
QSplitter,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
QWidget,
|
QWidget,
|
||||||
@ -19,6 +19,7 @@ from PySide2.QtWidgets import (
|
|||||||
import qt_ui.uiconstants as CONST
|
import qt_ui.uiconstants as CONST
|
||||||
from game import Game, persistency
|
from game import Game, persistency
|
||||||
from qt_ui.dialogs import Dialog
|
from qt_ui.dialogs import Dialog
|
||||||
|
from qt_ui.displayoptions import DisplayGroup, DisplayOptions, DisplayRule
|
||||||
from qt_ui.models import GameModel
|
from qt_ui.models import GameModel
|
||||||
from qt_ui.uiconstants import URLS
|
from qt_ui.uiconstants import URLS
|
||||||
from qt_ui.widgets.QTopPanel import QTopPanel
|
from qt_ui.widgets.QTopPanel import QTopPanel
|
||||||
@ -76,7 +77,7 @@ class QLiberationWindow(QMainWindow):
|
|||||||
|
|
||||||
vbox = QVBoxLayout()
|
vbox = QVBoxLayout()
|
||||||
vbox.setMargin(0)
|
vbox.setMargin(0)
|
||||||
vbox.addWidget(QTopPanel(self.game))
|
vbox.addWidget(QTopPanel(self.game_model))
|
||||||
vbox.addWidget(hbox)
|
vbox.addWidget(hbox)
|
||||||
|
|
||||||
central_widget = QWidget()
|
central_widget = QWidget()
|
||||||
@ -134,48 +135,23 @@ class QLiberationWindow(QMainWindow):
|
|||||||
file_menu.addSeparator()
|
file_menu.addSeparator()
|
||||||
file_menu.addAction(self.showLiberationPrefDialogAction)
|
file_menu.addAction(self.showLiberationPrefDialogAction)
|
||||||
file_menu.addSeparator()
|
file_menu.addSeparator()
|
||||||
#file_menu.addAction("Close Current Game", lambda: self.closeGame()) # Not working
|
|
||||||
file_menu.addAction("E&xit" , lambda: self.exit())
|
file_menu.addAction("E&xit" , lambda: self.exit())
|
||||||
|
|
||||||
displayMenu = self.menu.addMenu("&Display")
|
displayMenu = self.menu.addMenu("&Display")
|
||||||
|
|
||||||
tg_cp_visibility = QAction('&Control Point', displayMenu)
|
last_was_group = True
|
||||||
tg_cp_visibility.setCheckable(True)
|
for item in DisplayOptions.menu_items():
|
||||||
tg_cp_visibility.setChecked(True)
|
if isinstance(item, DisplayRule):
|
||||||
tg_cp_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("cp", tg_cp_visibility.isChecked()))
|
displayMenu.addAction(self.make_display_rule_action(item))
|
||||||
|
last_was_group = False
|
||||||
tg_go_visibility = QAction('&Ground Objects', displayMenu)
|
elif isinstance(item, DisplayGroup):
|
||||||
tg_go_visibility.setCheckable(True)
|
if not last_was_group:
|
||||||
tg_go_visibility.setChecked(True)
|
displayMenu.addSeparator()
|
||||||
tg_go_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("go", tg_go_visibility.isChecked()))
|
group = QActionGroup(displayMenu)
|
||||||
|
for display_rule in item:
|
||||||
tg_line_visibility = QAction('&Lines', displayMenu)
|
displayMenu.addAction(
|
||||||
tg_line_visibility.setCheckable(True)
|
self.make_display_rule_action(display_rule, group))
|
||||||
tg_line_visibility.setChecked(True)
|
last_was_group = True
|
||||||
tg_line_visibility.toggled.connect(
|
|
||||||
lambda: QLiberationMap.set_display_rule("lines", tg_line_visibility.isChecked()))
|
|
||||||
|
|
||||||
tg_event_visibility = QAction('&Events', displayMenu)
|
|
||||||
tg_event_visibility.setCheckable(True)
|
|
||||||
tg_event_visibility.setChecked(True)
|
|
||||||
tg_event_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("events", tg_event_visibility.isChecked()))
|
|
||||||
|
|
||||||
tg_sam_visibility = QAction('&SAM Range', displayMenu)
|
|
||||||
tg_sam_visibility.setCheckable(True)
|
|
||||||
tg_sam_visibility.setChecked(True)
|
|
||||||
tg_sam_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("sam", tg_sam_visibility.isChecked()))
|
|
||||||
|
|
||||||
tg_flight_path_visibility = QAction('&Flight Paths', displayMenu)
|
|
||||||
tg_flight_path_visibility.setCheckable(True)
|
|
||||||
tg_flight_path_visibility.setChecked(False)
|
|
||||||
tg_flight_path_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("flight_paths", tg_flight_path_visibility.isChecked()))
|
|
||||||
|
|
||||||
displayMenu.addAction(tg_go_visibility)
|
|
||||||
displayMenu.addAction(tg_cp_visibility)
|
|
||||||
displayMenu.addAction(tg_line_visibility)
|
|
||||||
displayMenu.addAction(tg_event_visibility)
|
|
||||||
displayMenu.addAction(tg_sam_visibility)
|
|
||||||
displayMenu.addAction(tg_flight_path_visibility)
|
|
||||||
|
|
||||||
help_menu = self.menu.addMenu("&Help")
|
help_menu = self.menu.addMenu("&Help")
|
||||||
help_menu.addAction("&Discord Server", lambda: webbrowser.open_new_tab("https://" + "discord.gg" + "/" + "bKrt" + "rkJ"))
|
help_menu.addAction("&Discord Server", lambda: webbrowser.open_new_tab("https://" + "discord.gg" + "/" + "bKrt" + "rkJ"))
|
||||||
@ -188,6 +164,21 @@ class QLiberationWindow(QMainWindow):
|
|||||||
help_menu.addSeparator()
|
help_menu.addSeparator()
|
||||||
help_menu.addAction(self.showAboutDialogAction)
|
help_menu.addAction(self.showAboutDialogAction)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def make_display_rule_action(
|
||||||
|
display_rule, group: Optional[QActionGroup] = None) -> QAction:
|
||||||
|
def make_check_closure():
|
||||||
|
def closure():
|
||||||
|
display_rule.value = action.isChecked()
|
||||||
|
|
||||||
|
return closure
|
||||||
|
|
||||||
|
action = QAction(f"&{display_rule.menu_text}", group)
|
||||||
|
action.setCheckable(True)
|
||||||
|
action.setChecked(display_rule.value)
|
||||||
|
action.toggled.connect(make_check_closure())
|
||||||
|
return action
|
||||||
|
|
||||||
def newGame(self):
|
def newGame(self):
|
||||||
wizard = NewGameWizard(self)
|
wizard = NewGameWizard(self)
|
||||||
wizard.show()
|
wizard.show()
|
||||||
|
|||||||
@ -4,9 +4,9 @@ from PySide2.QtWidgets import (
|
|||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
)
|
)
|
||||||
|
|
||||||
from 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 qt_ui.models import GameModel
|
||||||
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.GameUpdateSignal import GameUpdateSignal
|
||||||
from qt_ui.windows.mission.flight.QFlightPlanner import QFlightPlanner
|
from qt_ui.windows.mission.flight.QFlightPlanner import QFlightPlanner
|
||||||
@ -15,22 +15,22 @@ 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, package: Package, flight: Flight) -> None:
|
def __init__(self, game_model: GameModel, package: Package, flight: Flight) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.game = game
|
self.game_model = game_model
|
||||||
|
|
||||||
self.setWindowTitle("Create flight")
|
self.setWindowTitle("Create flight")
|
||||||
self.setWindowIcon(EVENT_ICONS["strike"])
|
self.setWindowIcon(EVENT_ICONS["strike"])
|
||||||
|
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
self.flight_planner = QFlightPlanner(package, flight, game)
|
self.flight_planner = QFlightPlanner(package, flight, game_model.game)
|
||||||
layout.addWidget(self.flight_planner)
|
layout.addWidget(self.flight_planner)
|
||||||
|
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
self.finished.connect(self.on_close)
|
self.finished.connect(self.on_close)
|
||||||
|
|
||||||
@staticmethod
|
def on_close(self, _result) -> None:
|
||||||
def on_close(_result) -> None:
|
|
||||||
GameUpdateSignal.get_instance().redraw_flight_paths()
|
GameUpdateSignal.get_instance().redraw_flight_paths()
|
||||||
|
self.game_model.ato_model.client_slots_changed.emit()
|
||||||
|
|||||||
@ -1,26 +1,28 @@
|
|||||||
|
import datetime
|
||||||
|
|
||||||
from PySide2.QtGui import QStandardItem, QIcon
|
from PySide2.QtGui import QStandardItem, QIcon
|
||||||
|
|
||||||
from game import db
|
from game import db
|
||||||
|
from gen.ato import Package
|
||||||
from gen.flights.flight import Flight
|
from gen.flights.flight import Flight
|
||||||
|
from gen.flights.traveltime import TotEstimator
|
||||||
from qt_ui.uiconstants import AIRCRAFT_ICONS
|
from qt_ui.uiconstants import AIRCRAFT_ICONS
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Replace with QFlightList.
|
||||||
class QFlightItem(QStandardItem):
|
class QFlightItem(QStandardItem):
|
||||||
|
|
||||||
def __init__(self, flight:Flight):
|
def __init__(self, package: Package, flight: Flight):
|
||||||
super(QFlightItem, self).__init__()
|
super(QFlightItem, self).__init__()
|
||||||
|
self.package = package
|
||||||
self.flight = flight
|
self.flight = flight
|
||||||
|
|
||||||
if db.unit_type_name(self.flight.unit_type).replace("/", " ") in AIRCRAFT_ICONS.keys():
|
if db.unit_type_name(self.flight.unit_type).replace("/", " ") in AIRCRAFT_ICONS.keys():
|
||||||
icon = QIcon((AIRCRAFT_ICONS[db.unit_type_name(self.flight.unit_type)]))
|
icon = QIcon((AIRCRAFT_ICONS[db.unit_type_name(self.flight.unit_type)]))
|
||||||
self.setIcon(icon)
|
self.setIcon(icon)
|
||||||
self.setEditable(False)
|
self.setEditable(False)
|
||||||
|
estimator = TotEstimator(self.package)
|
||||||
|
delay = datetime.timedelta(seconds=estimator.mission_start_time(flight))
|
||||||
self.setText("["+str(self.flight.flight_type.name[:6])+"] "
|
self.setText("["+str(self.flight.flight_type.name[:6])+"] "
|
||||||
+ str(self.flight.count) + " x " + db.unit_type_name(self.flight.unit_type)
|
+ str(self.flight.count) + " x " + db.unit_type_name(self.flight.unit_type)
|
||||||
+ " in " + str(self.flight.scheduled_in) + " minutes")
|
+ " in " + str(delay))
|
||||||
|
|
||||||
def update(self, flight):
|
|
||||||
self.flight = flight
|
|
||||||
self.setText("[" + str(self.flight.flight_type.name[:6]) + "] "
|
|
||||||
+ str(self.flight.count) + " x " + db.unit_type_name(self.flight.unit_type)
|
|
||||||
+ " in " + str(self.flight.scheduled_in) + " minutes")
|
|
||||||
|
|||||||
@ -116,11 +116,14 @@ class QPackageDialog(QDialog):
|
|||||||
|
|
||||||
self.finished.connect(self.on_close)
|
self.finished.connect(self.on_close)
|
||||||
|
|
||||||
def on_close(self, _result) -> None:
|
@staticmethod
|
||||||
|
def on_close(_result) -> None:
|
||||||
|
GameUpdateSignal.get_instance().redraw_flight_paths()
|
||||||
|
|
||||||
|
def save_tot(self) -> None:
|
||||||
time = self.tot_spinner.time()
|
time = self.tot_spinner.time()
|
||||||
seconds = time.hour() * 3600 + time.minute() * 60 + time.second()
|
seconds = time.hour() * 3600 + time.minute() * 60 + time.second()
|
||||||
self.package_model.update_tot(seconds)
|
self.package_model.update_tot(seconds)
|
||||||
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:
|
||||||
@ -182,6 +185,7 @@ class QNewPackageDialog(QPackageDialog):
|
|||||||
Empty packages may be created. They can be modified later, and will have
|
Empty packages may be created. They can be modified later, and will have
|
||||||
no effect if empty when the mission is generated.
|
no effect if empty when the mission is generated.
|
||||||
"""
|
"""
|
||||||
|
self.save_tot()
|
||||||
self.ato_model.add_package(self.package_model.package)
|
self.ato_model.add_package(self.package_model.package)
|
||||||
for flight in self.package_model.package.flights:
|
for flight in self.package_model.package.flights:
|
||||||
self.game.aircraft_inventory.claim_for_flight(flight)
|
self.game.aircraft_inventory.claim_for_flight(flight)
|
||||||
@ -227,6 +231,7 @@ class QEditPackageDialog(QPackageDialog):
|
|||||||
|
|
||||||
def on_done(self) -> None:
|
def on_done(self) -> None:
|
||||||
"""Closes the window."""
|
"""Closes the window."""
|
||||||
|
self.save_tot()
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def on_delete(self) -> None:
|
def on_delete(self) -> None:
|
||||||
|
|||||||
@ -90,7 +90,11 @@ class QFlightCreator(QDialog):
|
|||||||
origin = self.airfield_selector.currentData()
|
origin = self.airfield_selector.currentData()
|
||||||
size = self.flight_size_spinner.value()
|
size = self.flight_size_spinner.value()
|
||||||
|
|
||||||
flight = Flight(aircraft, size, origin, task)
|
if self.game.settings.perf_ai_parking_start:
|
||||||
|
start_type = "Cold"
|
||||||
|
else:
|
||||||
|
start_type = "Warm"
|
||||||
|
flight = Flight(aircraft, size, origin, task, start_type)
|
||||||
flight.scheduled_in = self.package.delay
|
flight.scheduled_in = self.package.delay
|
||||||
|
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QSpinBox
|
from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QSpinBox
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Remove?
|
||||||
class QFlightDepartureEditor(QGroupBox):
|
class QFlightDepartureEditor(QGroupBox):
|
||||||
|
|
||||||
def __init__(self, flight):
|
def __init__(self, flight):
|
||||||
@ -15,7 +16,7 @@ class QFlightDepartureEditor(QGroupBox):
|
|||||||
self.departure_delta = QSpinBox(self)
|
self.departure_delta = QSpinBox(self)
|
||||||
self.departure_delta.setMinimum(0)
|
self.departure_delta.setMinimum(0)
|
||||||
self.departure_delta.setMaximum(120)
|
self.departure_delta.setMaximum(120)
|
||||||
self.departure_delta.setValue(self.flight.scheduled_in)
|
self.departure_delta.setValue(self.flight.scheduled_in // 60)
|
||||||
self.departure_delta.valueChanged.connect(self.change_scheduled)
|
self.departure_delta.valueChanged.connect(self.change_scheduled)
|
||||||
|
|
||||||
layout.addWidget(self.depart_from)
|
layout.addWidget(self.depart_from)
|
||||||
@ -27,4 +28,4 @@ class QFlightDepartureEditor(QGroupBox):
|
|||||||
self.changed = self.departure_delta.valueChanged
|
self.changed = self.departure_delta.valueChanged
|
||||||
|
|
||||||
def change_scheduled(self):
|
def change_scheduled(self):
|
||||||
self.flight.scheduled_in = int(self.departure_delta.value())
|
self.flight.scheduled_in = int(self.departure_delta.value() * 60)
|
||||||
|
|||||||
@ -2,10 +2,10 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
from typing import List
|
from typing import List, Optional
|
||||||
|
|
||||||
from PySide2 import QtGui, QtWidgets
|
from PySide2 import QtGui, QtWidgets
|
||||||
from PySide2.QtCore import QItemSelectionModel, QPoint
|
from PySide2.QtCore import QItemSelectionModel, QPoint, Qt
|
||||||
from PySide2.QtWidgets import QVBoxLayout
|
from PySide2.QtWidgets import QVBoxLayout
|
||||||
from dcs.task import CAP, CAS
|
from dcs.task import CAP, CAS
|
||||||
|
|
||||||
@ -63,6 +63,7 @@ class NewGameWizard(QtWidgets.QWizard):
|
|||||||
no_player_navy = self.field("no_player_navy")
|
no_player_navy = self.field("no_player_navy")
|
||||||
no_enemy_navy = self.field("no_enemy_navy")
|
no_enemy_navy = self.field("no_enemy_navy")
|
||||||
invertMap = self.field("invertMap")
|
invertMap = self.field("invertMap")
|
||||||
|
starting_money = int(self.field("starting_money"))
|
||||||
|
|
||||||
player_name = blueFaction
|
player_name = blueFaction
|
||||||
enemy_name = redFaction
|
enemy_name = redFaction
|
||||||
@ -76,12 +77,12 @@ class NewGameWizard(QtWidgets.QWizard):
|
|||||||
settings.do_not_generate_enemy_navy = no_enemy_navy
|
settings.do_not_generate_enemy_navy = no_enemy_navy
|
||||||
|
|
||||||
self.generatedGame = self.start_new_game(player_name, enemy_name, conflictTheater, midGame, multiplier,
|
self.generatedGame = self.start_new_game(player_name, enemy_name, conflictTheater, midGame, multiplier,
|
||||||
timePeriod, settings)
|
timePeriod, settings, starting_money)
|
||||||
|
|
||||||
super(NewGameWizard, self).accept()
|
super(NewGameWizard, self).accept()
|
||||||
|
|
||||||
def start_new_game(self, player_name: str, enemy_name: str, conflictTheater: ConflictTheater,
|
def start_new_game(self, player_name: str, enemy_name: str, conflictTheater: ConflictTheater,
|
||||||
midgame: bool, multiplier: float, period: datetime, settings:Settings):
|
midgame: bool, multiplier: float, period: datetime, settings:Settings, starting_money: int):
|
||||||
|
|
||||||
# Reset name generator
|
# Reset name generator
|
||||||
namegen.reset()
|
namegen.reset()
|
||||||
@ -102,14 +103,10 @@ class NewGameWizard(QtWidgets.QWizard):
|
|||||||
|
|
||||||
print("-- Game Object generated")
|
print("-- Game Object generated")
|
||||||
start_generator.generate_groundobjects(conflictTheater, game)
|
start_generator.generate_groundobjects(conflictTheater, game)
|
||||||
game.budget = int(game.budget * multiplier)
|
game.budget = starting_money
|
||||||
game.settings.multiplier = multiplier
|
game.settings.multiplier = multiplier
|
||||||
game.settings.sams = True
|
game.settings.sams = True
|
||||||
game.settings.version = CONST.VERSION_STRING
|
game.settings.version = CONST.VERSION_STRING
|
||||||
|
|
||||||
if midgame:
|
|
||||||
game.budget = game.budget * 4 * len(list(conflictTheater.conflicts()))
|
|
||||||
|
|
||||||
return game
|
return game
|
||||||
|
|
||||||
|
|
||||||
@ -298,6 +295,44 @@ class TheaterConfiguration(QtWidgets.QWizardPage):
|
|||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
|
||||||
|
class CurrencySpinner(QtWidgets.QSpinBox):
|
||||||
|
def __init__(self, minimum: Optional[int] = None,
|
||||||
|
maximum: Optional[int] = None,
|
||||||
|
initial: Optional[int] = None) -> None:
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
if minimum is not None:
|
||||||
|
self.setMinimum(minimum)
|
||||||
|
if maximum is not None:
|
||||||
|
self.setMaximum(maximum)
|
||||||
|
if initial is not None:
|
||||||
|
self.setValue(initial)
|
||||||
|
|
||||||
|
def textFromValue(self, val: int) -> str:
|
||||||
|
return f"${val}"
|
||||||
|
|
||||||
|
|
||||||
|
class BudgetInputs(QtWidgets.QGridLayout):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.addWidget(QtWidgets.QLabel("Starting money"), 0, 0)
|
||||||
|
|
||||||
|
minimum = 0
|
||||||
|
maximum = 5000
|
||||||
|
initial = 650
|
||||||
|
|
||||||
|
slider = QtWidgets.QSlider(Qt.Horizontal)
|
||||||
|
slider.setMinimum(minimum)
|
||||||
|
slider.setMaximum(maximum)
|
||||||
|
slider.setValue(initial)
|
||||||
|
self.starting_money = CurrencySpinner(minimum, maximum, initial)
|
||||||
|
slider.valueChanged.connect(lambda x: self.starting_money.setValue(x))
|
||||||
|
self.starting_money.valueChanged.connect(lambda x: slider.setValue(x))
|
||||||
|
|
||||||
|
self.addWidget(slider, 1, 0)
|
||||||
|
self.addWidget(self.starting_money, 1, 1)
|
||||||
|
|
||||||
|
|
||||||
class MiscOptions(QtWidgets.QWizardPage):
|
class MiscOptions(QtWidgets.QWizardPage):
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super(MiscOptions, self).__init__(parent)
|
super(MiscOptions, self).__init__(parent)
|
||||||
@ -330,6 +365,13 @@ class MiscOptions(QtWidgets.QWizardPage):
|
|||||||
no_enemy_navy = QtWidgets.QCheckBox()
|
no_enemy_navy = QtWidgets.QCheckBox()
|
||||||
self.registerField('no_enemy_navy', no_enemy_navy)
|
self.registerField('no_enemy_navy', no_enemy_navy)
|
||||||
|
|
||||||
|
layout = QtWidgets.QGridLayout()
|
||||||
|
layout.addWidget(QtWidgets.QLabel("Start at mid game"), 1, 0)
|
||||||
|
layout.addWidget(midGame, 1, 1)
|
||||||
|
layout.addWidget(QtWidgets.QLabel("Ennemy forces multiplier [Disabled for Now]"), 2, 0)
|
||||||
|
layout.addWidget(multiplier, 2, 1)
|
||||||
|
miscSettingsGroup.setLayout(layout)
|
||||||
|
|
||||||
generatorLayout = QtWidgets.QGridLayout()
|
generatorLayout = QtWidgets.QGridLayout()
|
||||||
generatorLayout.addWidget(QtWidgets.QLabel("No Aircraft Carriers"), 1, 0)
|
generatorLayout.addWidget(QtWidgets.QLabel("No Aircraft Carriers"), 1, 0)
|
||||||
generatorLayout.addWidget(no_carrier, 1, 1)
|
generatorLayout.addWidget(no_carrier, 1, 1)
|
||||||
@ -343,16 +385,15 @@ class MiscOptions(QtWidgets.QWizardPage):
|
|||||||
generatorLayout.addWidget(no_enemy_navy, 5, 1)
|
generatorLayout.addWidget(no_enemy_navy, 5, 1)
|
||||||
generatorSettingsGroup.setLayout(generatorLayout)
|
generatorSettingsGroup.setLayout(generatorLayout)
|
||||||
|
|
||||||
layout = QtWidgets.QGridLayout()
|
budget_inputs = BudgetInputs()
|
||||||
layout.addWidget(QtWidgets.QLabel("Start at mid game"), 1, 0)
|
economySettingsGroup = QtWidgets.QGroupBox("Economy")
|
||||||
layout.addWidget(midGame, 1, 1)
|
economySettingsGroup.setLayout(budget_inputs)
|
||||||
layout.addWidget(QtWidgets.QLabel("Ennemy forces multiplier [Disabled for Now]"), 2, 0)
|
self.registerField('starting_money', budget_inputs.starting_money)
|
||||||
layout.addWidget(multiplier, 2, 1)
|
|
||||||
miscSettingsGroup.setLayout(layout)
|
|
||||||
|
|
||||||
mlayout = QVBoxLayout()
|
mlayout = QVBoxLayout()
|
||||||
mlayout.addWidget(miscSettingsGroup)
|
mlayout.addWidget(miscSettingsGroup)
|
||||||
mlayout.addWidget(generatorSettingsGroup)
|
mlayout.addWidget(generatorSettingsGroup)
|
||||||
|
mlayout.addWidget(economySettingsGroup)
|
||||||
self.setLayout(mlayout)
|
self.setLayout(mlayout)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -13,9 +13,8 @@ from PySide2.QtWidgets import (
|
|||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
)
|
)
|
||||||
|
|
||||||
import qt_ui.uiconstants as CONST
|
|
||||||
from qt_ui import liberation_install, liberation_theme
|
from qt_ui import liberation_install, liberation_theme
|
||||||
from qt_ui.liberation_theme import get_theme_index, set_theme_index
|
from qt_ui.liberation_theme import THEMES, get_theme_index, set_theme_index
|
||||||
|
|
||||||
|
|
||||||
class QLiberationPreferences(QFrame):
|
class QLiberationPreferences(QFrame):
|
||||||
@ -39,7 +38,7 @@ class QLiberationPreferences(QFrame):
|
|||||||
self.browse_install_dir = QPushButton("Browse...")
|
self.browse_install_dir = QPushButton("Browse...")
|
||||||
self.browse_install_dir.clicked.connect(self.on_browse_installation_dir)
|
self.browse_install_dir.clicked.connect(self.on_browse_installation_dir)
|
||||||
self.themeSelect = QComboBox()
|
self.themeSelect = QComboBox()
|
||||||
[self.themeSelect.addItem(y['themeName']) for x, y in CONST.THEMES.items()]
|
[self.themeSelect.addItem(y['themeName']) for x, y in THEMES.items()]
|
||||||
|
|
||||||
self.initUi()
|
self.initUi()
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user