Merge remote-tracking branch 'upstream/develop' into new-plugin-system

This commit is contained in:
David Pierron 2020-10-12 17:29:54 +02:00
commit 3c4ccd7d57
27 changed files with 1041 additions and 462 deletions

View File

@ -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
## Fixes :

View File

@ -57,14 +57,14 @@ from dcs.task import (
)
from dcs.terrain.terrain import Airport, NoParkingSlotError
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.unittype import FlyingType, UnitType
from game import db
from game.data.cap_capabilities_db import GUNFIGHTERS
from game.settings import Settings
from game.utils import meter_to_nm, nm_to_meter
from game.utils import nm_to_meter
from gen.airfields import RunwayData
from gen.airsupportgen import AirSupport
from gen.ato import AirTaskingOrder, Package
@ -76,9 +76,10 @@ from gen.flights.flight import (
FlightWaypointType,
)
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 .conflictgen import Conflict
from .flights.traveltime import PackageWaypointTiming, TotEstimator
from .naming import namegen
WARM_START_HELI_AIRSPEED = 120
@ -86,8 +87,6 @@ WARM_START_HELI_ALT = 500
WARM_START_ALTITUDE = 3000
WARM_START_AIRSPEED = 550
CAP_DURATION = 30 # minutes
RTB_ALTITUDE = 800
RTB_DISTANCE = 5000
HELI_ALT = 500
@ -217,7 +216,7 @@ class FlightData:
#: True if this flight belongs to the player's coalition.
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
#: 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"]
@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:
def __init__(self, mission: Mission, conflict: Conflict, settings: Settings,
game, radio_registry: RadioRegistry):
@ -706,8 +563,13 @@ class AircraftConflictGenerator:
except KeyError:
return get_fallback_channel(airframe)
def _start_type(self) -> StartType:
return self.settings.cold_start and StartType.Cold or StartType.Warm
@staticmethod
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],
flight: Flight, dynamic_runways: Dict[str, RunwayData]):
@ -813,15 +675,10 @@ class AircraftConflictGenerator:
return runways[0]
def _generate_at_airport(self, name: str, side: Country,
unit_type: FlyingType, count: int,
client_count: int,
airport: Optional[Airport] = None,
start_type=None) -> FlyingGroup:
unit_type: FlyingType, count: int, start_type: str,
airport: Optional[Airport] = None) -> FlyingGroup:
assert count > 0
if start_type is None:
start_type = self._start_type()
logging.info("airgen: {} for {} at {}".format(unit_type, side.id, airport))
return self.m.flight_group_from_airport(
country=side,
@ -829,11 +686,11 @@ class AircraftConflictGenerator:
aircraft_type=unit_type,
airport=airport,
maintask=None,
start_type=start_type,
start_type=self._start_type(start_type),
group_size=count,
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
if unit_type in helicopters.helicopter_map.values():
@ -855,21 +712,16 @@ class AircraftConflictGenerator:
altitude=alt,
speed=speed,
maintask=None,
start_type=self._start_type(),
group_size=count)
group.points[0].alt_type = "RADIO"
return group
def _generate_at_group(self, name: str, side: Country,
unit_type: FlyingType, count: int, client_count: int,
at: Union[ShipGroup, StaticGroup],
start_type=None) -> FlyingGroup:
unit_type: FlyingType, count: int, start_type: str,
at: Union[ShipGroup, StaticGroup]) -> FlyingGroup:
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))
return self.m.flight_group_from_unit(
country=side,
@ -877,34 +729,9 @@ class AircraftConflictGenerator:
aircraft_type=unit_type,
pad_group=at,
maintask=None,
start_type=start_type,
start_type=self._start_type(start_type),
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):
point = group.add_waypoint(position, altitude, airspeed)
point.alt_type = "RADIO"
@ -975,110 +802,91 @@ class AircraftConflictGenerator:
logging.info(f"Generating flight: {flight.unit_type}")
group = self.generate_planned_flight(flight.from_cp, country,
flight)
self.setup_flight_group(group, flight, timing, dynamic_runways)
self.setup_group_activation_trigger(flight, group)
self.setup_flight_group(group, package, flight, timing,
dynamic_runways)
def setup_group_activation_trigger(self, flight, group):
if flight.scheduled_in > 0 and flight.client_count == 0:
def set_activation_time(self, flight: Flight, group: FlyingGroup,
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]:
group.late_activation = False
group.uncontrolled = True
activation_trigger = TriggerOnce(
Event.NoEvent, f"FlightLateActivationTrigger{group.id}")
activation_trigger.add_condition(TimeAfter(seconds=delay))
activation_trigger = TriggerOnce(Event.NoEvent, "FlightStartTrigger" + str(group.id))
activation_trigger.add_condition(TimeAfter(seconds=flight.scheduled_in * 60))
if (flight.from_cp.cptype == ControlPointType.AIRBASE):
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))
self.prevent_spawn_at_hostile_airbase(flight, activation_trigger)
activation_trigger.add_action(ActivateGroup(group.id))
self.m.triggerrules.triggers.append(activation_trigger)
if flight.flight_type == FlightType.INTERCEPTION:
self.setup_interceptor_triggers(group, flight, activation_trigger)
def set_startup_time(self, flight: Flight, group: FlyingGroup,
delay: int) -> None:
# Uncontrolled causes the AI unit to spawn, but not begin startup.
group.uncontrolled = True
group.add_trigger_action(StartCommand())
activation_trigger.add_action(AITaskPush(group.id, len(group.tasks)))
activation_trigger = TriggerOnce(Event.NoEvent,
f"FlightStartTrigger{group.id}")
activation_trigger.add_condition(TimeAfter(seconds=delay))
self.m.triggerrules.triggers.append(activation_trigger)
else:
group.late_activation = True
activation_trigger = TriggerOnce(Event.NoEvent, "FlightLateActivationTrigger" + str(group.id))
activation_trigger.add_condition(TimeAfter(seconds=flight.scheduled_in*60))
self.prevent_spawn_at_hostile_airbase(flight, activation_trigger)
group.add_trigger_action(StartCommand())
activation_trigger.add_action(AITaskPush(group.id, len(group.tasks)))
self.m.triggerrules.triggers.append(activation_trigger)
if(flight.from_cp.cptype == ControlPointType.AIRBASE):
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))
def prevent_spawn_at_hostile_airbase(self, flight: Flight,
trigger: TriggerRule) -> None:
# Prevent delayed flights from spawning at airbases if they were
# captured before they've spawned.
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:
activation_trigger.add_condition(PartOfCoalitionInZone(self.game.get_enemy_color(), detection_zone.id)) # TODO : support unit type in part of coalition
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))
coalition = self.game.get_player_coalition_id()
else:
activation_trigger.add_condition(PartOfCoalitionInZone(self.game.get_player_color(), detection_zone.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))
coalition = self.game.get_enemy_coalition_id()
trigger.add_condition(
CoalitionHasAirdrome(coalition, flight.from_cp.id))
def generate_planned_flight(self, cp, country, flight:Flight):
try:
if flight.client_count == 0 and self.game.settings.perf_ai_parking_start:
flight.start_type = "Cold"
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),
side=country,
unit_type=flight.unit_type,
count=flight.count,
client_count=0,
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:
st = StartType.Runway
if flight.start_type == "Cold":
st = StartType.Cold
elif flight.start_type == "Warm":
st = StartType.Warm
if cp.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP]:
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)
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,
start_type=flight.start_type,
airport=cp.airport)
except Exception as e:
# Generated when there is no place on Runway or on Parking Slots
logging.error(e)
logging.warning("No room on runway or parking slots. Starting from the air.")
flight.start_type = "In Flight"
group = self._generate_group(
group = self._generate_inflight(
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
side=country,
unit_type=flight.unit_type,
count=flight.count,
client_count=0,
at=cp.position)
group.points[0].alt = 1500
@ -1184,8 +992,8 @@ class AircraftConflictGenerator:
logging.error(f"Unhandled flight type: {flight.flight_type.name}")
self.configure_behavior(group)
def setup_flight_group(self, group: FlyingGroup, flight: Flight,
timing: PackageWaypointTiming,
def setup_flight_group(self, group: FlyingGroup, package: Package,
flight: Flight, timing: PackageWaypointTiming,
dynamic_runways: Dict[str, RunwayData]) -> None:
flight_type = flight.flight_type
if flight_type in [FlightType.CAP, FlightType.BARCAP, FlightType.TARCAP,
@ -1209,6 +1017,9 @@ class AircraftConflictGenerator:
for waypoint in flight.points:
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:
if point.only_for_player and not flight.client_count:
continue
@ -1219,9 +1030,53 @@ class AircraftConflictGenerator:
# Set here rather than when the FlightData is created so they waypoints
# have their TOTs set.
self.flights[-1].waypoints = flight.points
self.flights[-1].waypoints = [takeoff_point] + flight.points
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:
def __init__(self, waypoint: FlightWaypoint, group: FlyingGroup,
@ -1248,10 +1103,8 @@ class PydcsWaypointBuilder:
waypoint.speed_locked = False
@classmethod
def for_waypoint(cls, waypoint: FlightWaypoint,
group: FlyingGroup,
flight: Flight,
timing: PackageWaypointTiming,
def for_waypoint(cls, waypoint: FlightWaypoint, group: FlyingGroup,
flight: Flight, timing: PackageWaypointTiming,
mission: Mission) -> PydcsWaypointBuilder:
builders = {
FlightWaypointType.EGRESS: EgressPointBuilder,
@ -1396,9 +1249,7 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
pattern=OrbitAction.OrbitPattern.RaceTrack
))
start = self.timing.race_track_start
if start is not None:
self.set_waypoint_tot(waypoint, start)
self.set_waypoint_tot(waypoint, self.timing.race_track_start)
racetrack.stop_after_time(self.timing.race_track_end)
waypoint.add_task(racetrack)
return waypoint

View File

@ -52,7 +52,7 @@ class Package:
delay: int = field(default=0)
#: 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)

View File

@ -1,3 +1,4 @@
import datetime
import os
from collections import defaultdict
from dataclasses import dataclass
@ -116,9 +117,10 @@ class BriefingGenerator(MissionInfoGenerator):
assert not flight.client_units
aircraft = flight.aircraft_type
flight_unit_name = db.unit_type_name(aircraft)
delay = datetime.timedelta(seconds=flight.departure_delay)
self.description += (
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):

View File

@ -33,6 +33,7 @@ from gen.flights.flight import (
FlightType,
)
from gen.flights.flightplan import FlightPlanBuilder
from gen.flights.traveltime import TotEstimator
from theater import (
ControlPoint,
FrontLine,
@ -185,11 +186,13 @@ class PackageBuilder:
def __init__(self, location: MissionTarget,
closest_airfields: ClosestAirfields,
global_inventory: GlobalAircraftInventory,
is_player: bool) -> None:
is_player: bool,
start_type: str) -> None:
self.package = Package(location)
self.allocator = AircraftAllocator(closest_airfields, global_inventory,
is_player)
self.global_inventory = global_inventory
self.start_type = start_type
def plan_flight(self, plan: ProposedFlight) -> bool:
"""Allocates aircraft for the given flight and adds them to the package.
@ -203,7 +206,8 @@ class PackageBuilder:
if assignment is None:
return False
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)
flight.targetPoint = self.package.target
return True
@ -445,11 +449,18 @@ class CoalitionMissionPlanner:
def plan_mission(self, mission: ProposedMission) -> None:
"""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(
mission.location,
self.objective_finder.closest_airfields_to(mission.location),
self.game.aircraft_inventory,
self.is_player
self.is_player,
start_type
)
missing_types: Set[FlightType] = set()
@ -498,16 +509,18 @@ class CoalitionMissionPlanner:
margin=5
)
for package in self.ato.packages:
tot = TotEstimator(package).earliest_tot()
if package.primary_task in dca_types:
# All CAP missions should be on station in 15-25 minutes.
package.time_over_target = (20 + random.randint(-5, 5)) * 60
# All CAP missions should be on station ASAP.
package.time_over_target = tot
else:
# But other packages should be spread out a bit.
package.delay = next(start_time)
# TODO: Compute TOT based on package.
package.time_over_target = (package.delay + 40) * 60
for flight in package.flights:
flight.scheduled_in = package.delay
# But other packages should be spread out a bit. Note that take
# times are delayed, but all aircraft will become active at
# mission start. This makes it more worthwhile to attack enemy
# airfields to hit grounded aircraft, since they're more likely
# to be present. Runway and air started aircraft will be
# delayed until their takeoff time by AirConflictGenerator.
package.time_over_target = next(start_time) * 60 + tot
def message(self, title, text) -> None:
"""Emits a planning message to the player.

View File

@ -97,7 +97,6 @@ class FlightWaypoint:
# flight's offset in the UI.
self.tot: Optional[int] = None
@classmethod
def from_pydcs(cls, point: MovingPoint,
from_cp: ControlPoint) -> "FlightWaypoint":
@ -130,14 +129,11 @@ class Flight:
client_count: int = 0
use_custom_loadout = False
preset_loadout_name = ""
start_type = "Runway"
group = False # Contains DCS Mission group data after mission has been generated
targetPoint = None # Contains either None or a Strike/SEAD target point location
# How long before this flight should take off
scheduled_in = 0
def __init__(self, unit_type: UnitType, count: int, from_cp: ControlPoint, flight_type: FlightType):
def __init__(self, unit_type: UnitType, count: int, from_cp: ControlPoint,
flight_type: FlightType, start_type: str) -> None:
self.unit_type = unit_type
self.count = count
self.from_cp = from_cp
@ -145,11 +141,16 @@ class Flight:
self.points: List[FlightWaypoint] = []
self.targets: List[MissionTarget] = []
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):
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
@ -158,6 +159,6 @@ if __name__ == '__main__':
from theater import ControlPoint, Point, List
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
print(f)

285
gen/flights/traveltime.py Normal file
View 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)

View File

@ -58,7 +58,7 @@ class Dialog:
flight: Flight) -> None:
"""Opens the dialog to edit the given flight."""
cls.edit_flight_dialog = QEditFlightDialog(
cls.game_model.game,
cls.game_model,
package_model.package,
flight
)

65
qt_ui/displayoptions.py Normal file
View 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

View File

@ -1,7 +1,6 @@
"""Qt data models for game objects."""
import datetime
from enum import auto, IntEnum
from typing import Any, Callable, Dict, Iterator, TypeVar, Optional
from typing import Any, Callable, Dict, Iterator, Optional, TypeVar
from PySide2.QtCore import (
QAbstractListModel,
@ -15,6 +14,7 @@ from game import db
from game.game import Game
from gen.ato import AirTaskingOrder, Package
from gen.flights.flight import Flight
from gen.flights.traveltime import TotEstimator
from qt_ui.uiconstants import AIRCRAFT_ICONS
from theater.missiontarget import MissionTarget
@ -95,6 +95,8 @@ class NullListModel(QAbstractListModel):
class PackageModel(QAbstractListModel):
"""The model for an ATO package."""
FlightRole = Qt.UserRole
#: Emitted when this package is being deleted from the ATO.
deleted = Signal()
@ -113,17 +115,19 @@ class PackageModel(QAbstractListModel):
return self.text_for_flight(flight)
if role == Qt.DecorationRole:
return self.icon_for_flight(flight)
elif role == PackageModel.FlightRole:
return flight
return None
@staticmethod
def text_for_flight(flight: Flight) -> str:
def text_for_flight(self, flight: Flight) -> str:
"""Returns the text that should be displayed for the flight."""
task = flight.flight_type.name
count = flight.count
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
return f"[{task}] {count} x {name} from {origin} in {delay} minutes"
return f"[{task}] {count} x {name} from {origin} in {delay}"
@staticmethod
def icon_for_flight(flight: Flight) -> Optional[QIcon]:
@ -185,6 +189,8 @@ class AtoModel(QAbstractListModel):
PackageRole = Qt.UserRole
client_slots_changed = Signal()
def __init__(self, game: Optional[Game], ato: AirTaskingOrder) -> None:
super().__init__()
self.game = game

View File

@ -31,14 +31,20 @@ COLORS: Dict[str, QColor] = {
"white_transparent": QColor(255, 255, 255, 35),
"grey_transparent": QColor(150, 150, 150, 30),
"light_red": QColor(231, 92, 83, 90),
"red": QColor(200, 80, 80),
"dark_red": QColor(140, 20, 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),
"dark_blue": QColor(45, 62, 80),
"blue_transparent": QColor(0, 132, 255, 20),
"purple": QColor(187, 137, 255),
"yellow": QColor(238, 225, 123),
"bright_red": QColor(150, 80, 80),
"super_red": QColor(227, 32, 0),

View File

@ -1,4 +1,6 @@
"""A layout containing a widget with an associated label."""
from typing import Optional
from PySide2.QtCore import Qt
from PySide2.QtWidgets import QHBoxLayout, QLabel, QWidget
@ -10,8 +12,13 @@ class QLabeledWidget(QHBoxLayout):
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__()
self.addWidget(QLabel(text))
label = QLabel(text)
self.addWidget(label)
self.addStretch()
self.addWidget(widget, alignment=Qt.AlignRight)
if tooltip is not None:
label.setToolTip(tooltip)
widget.setToolTip(tooltip)

View File

@ -11,9 +11,11 @@ from PySide2.QtWidgets import (
import qt_ui.uiconstants as CONST
from game import Game
from game.event import CAP, CAS, FrontlineAttackEvent
from qt_ui.models import GameModel
from qt_ui.widgets.QBudgetBox import QBudgetBox
from qt_ui.widgets.QFactionsInfos import QFactionsInfos
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.QWaitingForMissionResultWindow import \
QWaitingForMissionResultWindow
@ -23,14 +25,18 @@ from qt_ui.windows.stats.QStatsWindow import QStatsWindow
class QTopPanel(QFrame):
def __init__(self, game: Game):
def __init__(self, game_model: GameModel):
super(QTopPanel, self).__init__()
self.game = game
self.game_model = game_model
self.setMaximumHeight(70)
self.init_ui()
GameUpdateSignal.get_instance().gameupdated.connect(self.setGame)
GameUpdateSignal.get_instance().budgetupdated.connect(self.budget_update)
@property
def game(self) -> Optional[Game]:
return self.game_model.game
def init_ui(self):
self.turnCounter = QTurnCounter()
@ -68,6 +74,8 @@ class QTopPanel(QFrame):
self.proceedBox = QGroupBox("Proceed")
self.proceedBoxLayout = QHBoxLayout()
self.proceedBoxLayout.addLayout(
MaxPlayerCount(self.game_model.ato_model))
self.proceedBoxLayout.addWidget(self.passTurnButton)
self.proceedBoxLayout.addWidget(self.proceedButton)
self.proceedBox.setLayout(self.proceedBoxLayout)
@ -84,16 +92,17 @@ class QTopPanel(QFrame):
self.setLayout(self.layout)
def setGame(self, game: Optional[Game]):
self.game = game
if game is not None:
self.turnCounter.setCurrentTurn(self.game.turn, self.game.current_day)
self.budgetBox.setGame(self.game)
self.factionsInfos.setGame(self.game)
if game is None:
return
if self.game and self.game.turn == 0:
self.proceedButton.setEnabled(False)
else:
self.proceedButton.setEnabled(True)
self.turnCounter.setCurrentTurn(game.turn, game.current_day)
self.budgetBox.setGame(game)
self.factionsInfos.setGame(game)
if game and game.turn == 0:
self.proceedButton.setEnabled(False)
else:
self.proceedButton.setEnabled(True)
def openSettings(self):
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():
return
# TODO: Verify no negative start times.
# TODO: Refactor this nonsense.
game_event = None
for event in self.game.events:

View File

@ -10,7 +10,7 @@ from PySide2.QtCore import (
QSize,
Qt,
)
from PySide2.QtGui import QFont, QFontMetrics, QPainter
from PySide2.QtGui import QFont, QFontMetrics, QIcon, QPainter
from PySide2.QtWidgets import (
QAbstractItemView,
QGroupBox,
@ -18,15 +18,115 @@ from PySide2.QtWidgets import (
QListView,
QPushButton,
QSplitter,
QStyleOptionViewItem, QStyledItemDelegate, QVBoxLayout,
QStyle, QStyleOptionViewItem, QStyledItemDelegate, QVBoxLayout,
)
from game import db
from gen.ato import Package
from gen.flights.flight import Flight
from gen.flights.traveltime import TotEstimator
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
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):
"""List view for displaying the flights of a package."""
@ -34,6 +134,8 @@ class QFlightList(QListView):
super().__init__()
self.package_model = model
self.set_package(model)
if model is not None:
self.setItemDelegate(FlightDelegate(model.package))
self.setIconSize(QSize(91, 24))
self.setSelectionBehavior(QAbstractItemView.SelectItems)
@ -43,6 +145,7 @@ class QFlightList(QListView):
self.disconnect_model()
else:
self.package_model = model
self.setItemDelegate(FlightDelegate(model.package))
self.setModel(model)
# noinspection PyUnresolvedReferences
model.deleted.connect(self.disconnect_model)
@ -109,6 +212,7 @@ class QFlightPanel(QGroupBox):
"""Sets the package model to display."""
self.package_model = model
self.flight_list.set_package(model)
self.selection_changed.connect(self.on_selection_changed)
self.on_selection_changed()
@property
@ -122,6 +226,15 @@ class QFlightPanel(QGroupBox):
enabled = index.isValid()
self.edit_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:
"""Opens the flight edit dialog."""
@ -196,6 +309,15 @@ class PackageDelegate(QStyledItemDelegate):
line2 = rect.adjusted(0, rect.height() / 2, 0, rect.height() / 2)
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,
index: QModelIndex) -> QSize:
metrics = QFontMetrics(self.get_font(option))
@ -270,6 +392,18 @@ class QPackagePanel(QGroupBox):
enabled = index.isValid()
self.edit_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:
"""Opens the package edit dialog."""

View 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))

View File

@ -1,5 +1,8 @@
from __future__ import annotations
import datetime
import logging
from typing import Dict, List, Optional, Tuple
from typing import List, Optional, Tuple
from PySide2.QtCore import Qt
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
from game import Game, db
from game.data.aaa_db import AAA_UNITS
from game.data.radar_db import UNITS_WITH_RADAR
from gen import Conflict
from gen.flights.flight import Flight
from game.utils import meter_to_feet
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.widgets.map.QFrontLine import QFrontLine
from qt_ui.widgets.map.QLiberationScene import QLiberationScene
from qt_ui.widgets.map.QMapControlPoint import QMapControlPoint
from qt_ui.widgets.map.QMapGroundObject import QMapGroundObject
from qt_ui.widgets.map.QFrontLine import QFrontLine
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from theater import ControlPoint, FrontLine
@ -30,15 +37,7 @@ from theater import ControlPoint, FrontLine
class QLiberationMap(QGraphicsView):
WAYPOINT_SIZE = 4
instance = None
display_rules: Dict[str, bool] = {
"cp": True,
"go": True,
"lines": True,
"events": True,
"sam": True,
"flight_paths": False
}
instance: Optional[QLiberationMap] = None
def __init__(self, game_model: GameModel):
super(QLiberationMap, self).__init__()
@ -47,6 +46,8 @@ class QLiberationMap(QGraphicsView):
self.game: Optional[Game] = game_model.game
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.setMaximumHeight(2160)
@ -61,6 +62,25 @@ class QLiberationMap(QGraphicsView):
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):
scene = QLiberationScene(self)
self.setScene(scene)
@ -161,27 +181,44 @@ class QLiberationMap(QGraphicsView):
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))
if ground_object.category == "aa" and self.get_display_rule("sam"):
max_range = 0
has_radar = False
is_aa = ground_object.category == "aa"
if is_aa and DisplayOptions.sam_ranges:
threat_range = 0
detection_range = 0
can_fire = False
if ground_object.groups:
for g in ground_object.groups:
for u in g.units:
unit = db.unit_type_from_name(u.type)
if unit in UNITS_WITH_RADAR:
has_radar = True
if unit.threat_range > max_range:
max_range = unit.threat_range
if has_radar:
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"])
if unit in UNITS_WITH_RADAR or unit in AAA_UNITS:
can_fire = True
if unit.detection_range > detection_range:
detection_range = unit.detection_range
if unit.threat_range > threat_range:
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)
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)
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.draw_flight_plans(scene)
@ -202,37 +239,94 @@ class QLiberationMap(QGraphicsView):
# Something may have caused those items to already be removed.
pass
self.flight_path_items.clear()
if not self.get_display_rule("flight_paths"):
if DisplayOptions.flight_paths.hide:
return
for package in self.game_model.ato_model.packages:
for flight in package.flights:
self.draw_flight_plan(scene, flight)
packages = list(self.game_model.ato_model.packages)
for p_idx, package_model in enumerate(packages):
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
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)
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))
self.draw_flight_path(scene, prev_pos, new_pos, is_player)
self.draw_waypoint(scene, new_pos, is_player)
self.draw_flight_path(scene, prev_pos, 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)
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],
player: bool) -> None:
waypoint_pen = self.waypoint_pen(player)
waypoint_brush = self.waypoint_brush(player)
player: bool, selected: bool) -> None:
waypoint_pen = self.waypoint_pen(player, selected)
waypoint_brush = self.waypoint_brush(player, selected)
self.flight_path_items.append(scene.addEllipse(
position[0], position[1], self.WAYPOINT_SIZE,
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],
pos1: Tuple[int, int], player: bool):
flight_path_pen = self.flight_path_pen(player)
pos1: Tuple[int, int], player: bool,
selected: bool) -> None:
flight_path_pen = self.flight_path_pen(player, selected)
# Draw the line to the *middle* of the waypoint.
offset = self.WAYPOINT_SIZE // 2
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
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:
if player:
return self.game.get_player_color()
else:
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)
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)
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)
color = CONST.COLORS[name]
pen = QPen(brush=color)
@ -367,18 +488,3 @@ class QLiberationMap(QGraphicsView):
effect = QGraphicsOpacityEffect()
effect.setOpacity(0.3)
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]

View File

@ -7,6 +7,7 @@ from qt_ui.models import GameModel
from qt_ui.windows.basemenu.QBaseMenu2 import QBaseMenu2
from theater import ControlPoint
from .QMapObject import QMapObject
from ...displayoptions import DisplayOptions
class QMapControlPoint(QMapObject):
@ -21,7 +22,7 @@ class QMapControlPoint(QMapObject):
self.base_details_dialog: Optional[QBaseMenu2] = None
def paint(self, painter, option, widget=None) -> None:
if self.parent.get_display_rule("cp"):
if DisplayOptions.control_points:
painter.save()
painter.setRenderHint(QPainter.Antialiasing)
painter.setBrush(self.brush_color)

View File

@ -8,8 +8,9 @@ import qt_ui.uiconstants as const
from game import Game
from game.data.building_data import FORTIFICATION_BUILDINGS
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
from theater import TheaterGroundObject, ControlPoint
from theater import ControlPoint, TheaterGroundObject
from .QMapObject import QMapObject
from ...displayoptions import DisplayOptions
class QMapGroundObject(QMapObject):
@ -50,7 +51,7 @@ class QMapGroundObject(QMapObject):
player_icons = "_blue"
enemy_icons = ""
if self.parent.get_display_rule("go"):
if DisplayOptions.ground_objects:
painter.save()
cat = self.ground_object.category

View File

@ -1,6 +1,6 @@
from __future__ import annotations
from typing import Optional
from typing import Optional, Tuple
from PySide2.QtCore import QObject, Signal
@ -24,11 +24,21 @@ class GameUpdateSignal(QObject):
debriefingReceived = Signal(DebriefingSignal)
flight_paths_changed = Signal()
package_selection_changed = Signal(int) # Optional[int]
flight_selection_changed = Signal(int) # Optional[int]
def __init__(self):
super(GameUpdateSignal, self).__init__()
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:
# noinspection PyUnresolvedReferences
self.flight_paths_changed.emit()

View File

@ -1,16 +1,16 @@
import logging
import sys
import webbrowser
from typing import Optional
from typing import Optional, Union
from PySide2.QtCore import Qt
from PySide2.QtGui import QIcon
from PySide2.QtWidgets import (
QAction,
QDesktopWidget,
QActionGroup, QDesktopWidget,
QFileDialog,
QMainWindow,
QMessageBox,
QMenu, QMessageBox,
QSplitter,
QVBoxLayout,
QWidget,
@ -19,6 +19,7 @@ from PySide2.QtWidgets import (
import qt_ui.uiconstants as CONST
from game import Game, persistency
from qt_ui.dialogs import Dialog
from qt_ui.displayoptions import DisplayGroup, DisplayOptions, DisplayRule
from qt_ui.models import GameModel
from qt_ui.uiconstants import URLS
from qt_ui.widgets.QTopPanel import QTopPanel
@ -76,7 +77,7 @@ class QLiberationWindow(QMainWindow):
vbox = QVBoxLayout()
vbox.setMargin(0)
vbox.addWidget(QTopPanel(self.game))
vbox.addWidget(QTopPanel(self.game_model))
vbox.addWidget(hbox)
central_widget = QWidget()
@ -134,48 +135,23 @@ class QLiberationWindow(QMainWindow):
file_menu.addSeparator()
file_menu.addAction(self.showLiberationPrefDialogAction)
file_menu.addSeparator()
#file_menu.addAction("Close Current Game", lambda: self.closeGame()) # Not working
file_menu.addAction("E&xit" , lambda: self.exit())
displayMenu = self.menu.addMenu("&Display")
tg_cp_visibility = QAction('&Control Point', displayMenu)
tg_cp_visibility.setCheckable(True)
tg_cp_visibility.setChecked(True)
tg_cp_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("cp", tg_cp_visibility.isChecked()))
tg_go_visibility = QAction('&Ground Objects', displayMenu)
tg_go_visibility.setCheckable(True)
tg_go_visibility.setChecked(True)
tg_go_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("go", tg_go_visibility.isChecked()))
tg_line_visibility = QAction('&Lines', displayMenu)
tg_line_visibility.setCheckable(True)
tg_line_visibility.setChecked(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)
last_was_group = True
for item in DisplayOptions.menu_items():
if isinstance(item, DisplayRule):
displayMenu.addAction(self.make_display_rule_action(item))
last_was_group = False
elif isinstance(item, DisplayGroup):
if not last_was_group:
displayMenu.addSeparator()
group = QActionGroup(displayMenu)
for display_rule in item:
displayMenu.addAction(
self.make_display_rule_action(display_rule, group))
last_was_group = True
help_menu = self.menu.addMenu("&Help")
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.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):
wizard = NewGameWizard(self)
wizard.show()

View File

@ -4,9 +4,9 @@ from PySide2.QtWidgets import (
QVBoxLayout,
)
from game import Game
from gen.ato import Package
from gen.flights.flight import Flight
from qt_ui.models import GameModel
from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
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):
"""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__()
self.game = game
self.game_model = game_model
self.setWindowTitle("Create flight")
self.setWindowIcon(EVENT_ICONS["strike"])
layout = QVBoxLayout()
self.flight_planner = QFlightPlanner(package, flight, game)
self.flight_planner = QFlightPlanner(package, flight, game_model.game)
layout.addWidget(self.flight_planner)
self.setLayout(layout)
self.finished.connect(self.on_close)
@staticmethod
def on_close(_result) -> None:
def on_close(self, _result) -> None:
GameUpdateSignal.get_instance().redraw_flight_paths()
self.game_model.ato_model.client_slots_changed.emit()

View File

@ -1,26 +1,28 @@
import datetime
from PySide2.QtGui import QStandardItem, QIcon
from game import db
from gen.ato import Package
from gen.flights.flight import Flight
from gen.flights.traveltime import TotEstimator
from qt_ui.uiconstants import AIRCRAFT_ICONS
# TODO: Replace with QFlightList.
class QFlightItem(QStandardItem):
def __init__(self, flight:Flight):
def __init__(self, package: Package, flight: Flight):
super(QFlightItem, self).__init__()
self.package = package
self.flight = flight
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)]))
self.setIcon(icon)
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])+"] "
+ str(self.flight.count) + " x " + db.unit_type_name(self.flight.unit_type)
+ " in " + str(self.flight.scheduled_in) + " minutes")
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")
+ " in " + str(delay))

View File

@ -116,11 +116,14 @@ class QPackageDialog(QDialog):
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()
seconds = time.hour() * 3600 + time.minute() * 60 + time.second()
self.package_model.update_tot(seconds)
GameUpdateSignal.get_instance().redraw_flight_paths()
def on_selection_changed(self, selected: QItemSelection,
_deselected: QItemSelection) -> None:
@ -182,6 +185,7 @@ class QNewPackageDialog(QPackageDialog):
Empty packages may be created. They can be modified later, and will have
no effect if empty when the mission is generated.
"""
self.save_tot()
self.ato_model.add_package(self.package_model.package)
for flight in self.package_model.package.flights:
self.game.aircraft_inventory.claim_for_flight(flight)
@ -227,6 +231,7 @@ class QEditPackageDialog(QPackageDialog):
def on_done(self) -> None:
"""Closes the window."""
self.save_tot()
self.close()
def on_delete(self) -> None:

View File

@ -90,7 +90,11 @@ class QFlightCreator(QDialog):
origin = self.airfield_selector.currentData()
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
# noinspection PyUnresolvedReferences

View File

@ -1,6 +1,7 @@
from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QSpinBox
# TODO: Remove?
class QFlightDepartureEditor(QGroupBox):
def __init__(self, flight):
@ -15,7 +16,7 @@ class QFlightDepartureEditor(QGroupBox):
self.departure_delta = QSpinBox(self)
self.departure_delta.setMinimum(0)
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)
layout.addWidget(self.depart_from)
@ -27,4 +28,4 @@ class QFlightDepartureEditor(QGroupBox):
self.changed = self.departure_delta.valueChanged
def change_scheduled(self):
self.flight.scheduled_in = int(self.departure_delta.value())
self.flight.scheduled_in = int(self.departure_delta.value() * 60)

View File

@ -2,10 +2,10 @@ from __future__ import unicode_literals
import datetime
import logging
from typing import List
from typing import List, Optional
from PySide2 import QtGui, QtWidgets
from PySide2.QtCore import QItemSelectionModel, QPoint
from PySide2.QtCore import QItemSelectionModel, QPoint, Qt
from PySide2.QtWidgets import QVBoxLayout
from dcs.task import CAP, CAS
@ -63,6 +63,7 @@ class NewGameWizard(QtWidgets.QWizard):
no_player_navy = self.field("no_player_navy")
no_enemy_navy = self.field("no_enemy_navy")
invertMap = self.field("invertMap")
starting_money = int(self.field("starting_money"))
player_name = blueFaction
enemy_name = redFaction
@ -76,12 +77,12 @@ class NewGameWizard(QtWidgets.QWizard):
settings.do_not_generate_enemy_navy = no_enemy_navy
self.generatedGame = self.start_new_game(player_name, enemy_name, conflictTheater, midGame, multiplier,
timePeriod, settings)
timePeriod, settings, starting_money)
super(NewGameWizard, self).accept()
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
namegen.reset()
@ -102,14 +103,10 @@ class NewGameWizard(QtWidgets.QWizard):
print("-- Game Object generated")
start_generator.generate_groundobjects(conflictTheater, game)
game.budget = int(game.budget * multiplier)
game.budget = starting_money
game.settings.multiplier = multiplier
game.settings.sams = True
game.settings.version = CONST.VERSION_STRING
if midgame:
game.budget = game.budget * 4 * len(list(conflictTheater.conflicts()))
return game
@ -298,6 +295,44 @@ class TheaterConfiguration(QtWidgets.QWizardPage):
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):
def __init__(self, parent=None):
super(MiscOptions, self).__init__(parent)
@ -330,6 +365,13 @@ class MiscOptions(QtWidgets.QWizardPage):
no_enemy_navy = QtWidgets.QCheckBox()
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.addWidget(QtWidgets.QLabel("No Aircraft Carriers"), 1, 0)
generatorLayout.addWidget(no_carrier, 1, 1)
@ -343,16 +385,15 @@ class MiscOptions(QtWidgets.QWizardPage):
generatorLayout.addWidget(no_enemy_navy, 5, 1)
generatorSettingsGroup.setLayout(generatorLayout)
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)
budget_inputs = BudgetInputs()
economySettingsGroup = QtWidgets.QGroupBox("Economy")
economySettingsGroup.setLayout(budget_inputs)
self.registerField('starting_money', budget_inputs.starting_money)
mlayout = QVBoxLayout()
mlayout.addWidget(miscSettingsGroup)
mlayout.addWidget(generatorSettingsGroup)
mlayout.addWidget(economySettingsGroup)
self.setLayout(mlayout)

View File

@ -13,9 +13,8 @@ from PySide2.QtWidgets import (
QVBoxLayout,
)
import qt_ui.uiconstants as CONST
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):
@ -39,7 +38,7 @@ class QLiberationPreferences(QFrame):
self.browse_install_dir = QPushButton("Browse...")
self.browse_install_dir.clicked.connect(self.on_browse_installation_dir)
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()