diff --git a/game/event/event.py b/game/event/event.py index 8cc4aea7..ea5f3b80 100644 --- a/game/event/event.py +++ b/game/event/event.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging import math -from typing import Dict, List, Optional, Type, TYPE_CHECKING +from typing import Dict, List, Optional, TYPE_CHECKING, Type from dcs.mapping import Point from dcs.task import Task @@ -11,12 +11,12 @@ from dcs.unittype import UnitType from game import db, persistency from game.debriefing import Debriefing from game.infos.information import Information -from game.operation.operation import Operation +from game.theater import ControlPoint from gen.ground_forces.combat_stance import CombatStance -from theater import ControlPoint if TYPE_CHECKING: from ..game import Game + from game.operation.operation import Operation DIFFICULTY_LOG_BASE = 1.1 EVENT_DEPARTURE_MAX_DISTANCE = 340000 diff --git a/game/game.py b/game/game.py index e9cc42fc..3b29c46c 100644 --- a/game/game.py +++ b/game/game.py @@ -160,9 +160,6 @@ class Game: def _budget_player(self): self.budget += self.budget_reward_amount - def awacs_expense_commit(self): - self.budget -= AWACS_BUDGET_COST - def units_delivery_event(self, to_cp: ControlPoint) -> UnitsDeliveryEvent: event = UnitsDeliveryEvent(attacker_name=self.player_name, defender_name=self.player_name, @@ -172,10 +169,6 @@ class Game: self.events.append(event) return event - def units_delivery_remove(self, event: Event): - if event in self.events: - self.events.remove(event) - def initiate_event(self, event: Event): #assert event in self.events logging.info("Generating {} (regular)".format(event)) @@ -202,12 +195,6 @@ class Game: LuaPluginManager.load_settings(self.settings) ObjectiveDistanceCache.set_theater(self.theater) - # Save game compatibility. - - # TODO: Remove in 2.3. - if not hasattr(self, "conditions"): - self.conditions = self.generate_conditions() - def pass_turn(self, no_action: bool = False) -> None: logging.info("Pass turn") self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0)) @@ -248,6 +235,7 @@ class Game: self.aircraft_inventory.reset() for cp in self.theater.controlpoints: + cp.pending_unit_deliveries = self.units_delivery_event(cp) self.aircraft_inventory.set_from_control_point(cp) # Plan flights & combat for next turn diff --git a/game/inventory.py b/game/inventory.py index 89f5afa1..3c92a80f 100644 --- a/game/inventory.py +++ b/game/inventory.py @@ -1,11 +1,15 @@ """Inventory management APIs.""" -from collections import defaultdict -from typing import Dict, Iterable, Iterator, Set, Tuple +from __future__ import annotations -from dcs.unittype import UnitType +from collections import defaultdict +from typing import Dict, Iterable, Iterator, Set, Tuple, TYPE_CHECKING + +from dcs.unittype import FlyingType from gen.flights.flight import Flight -from theater import ControlPoint + +if TYPE_CHECKING: + from game.theater import ControlPoint class ControlPointAircraftInventory: @@ -13,9 +17,9 @@ class ControlPointAircraftInventory: def __init__(self, control_point: ControlPoint) -> None: self.control_point = control_point - self.inventory: Dict[UnitType, int] = defaultdict(int) + self.inventory: Dict[FlyingType, int] = defaultdict(int) - def add_aircraft(self, aircraft: UnitType, count: int) -> None: + def add_aircraft(self, aircraft: FlyingType, count: int) -> None: """Adds aircraft to the inventory. Args: @@ -24,7 +28,7 @@ class ControlPointAircraftInventory: """ self.inventory[aircraft] += count - def remove_aircraft(self, aircraft: UnitType, count: int) -> None: + def remove_aircraft(self, aircraft: FlyingType, count: int) -> None: """Removes aircraft from the inventory. Args: @@ -43,7 +47,7 @@ class ControlPointAircraftInventory: ) self.inventory[aircraft] -= count - def available(self, aircraft: UnitType) -> int: + def available(self, aircraft: FlyingType) -> int: """Returns the number of available aircraft of the given type. Args: @@ -55,14 +59,14 @@ class ControlPointAircraftInventory: return 0 @property - def types_available(self) -> Iterator[UnitType]: + def types_available(self) -> Iterator[FlyingType]: """Iterates over all available aircraft types.""" for aircraft, count in self.inventory.items(): if count > 0: yield aircraft @property - def all_aircraft(self) -> Iterator[Tuple[UnitType, int]]: + def all_aircraft(self) -> Iterator[Tuple[FlyingType, int]]: """Iterates over all available aircraft types, including amounts.""" for aircraft, count in self.inventory.items(): if count > 0: @@ -102,9 +106,9 @@ class GlobalAircraftInventory: return self.inventories[control_point] @property - def available_types_for_player(self) -> Iterator[UnitType]: + def available_types_for_player(self) -> Iterator[FlyingType]: """Iterates over all aircraft types available to the player.""" - seen: Set[UnitType] = set() + seen: Set[FlyingType] = set() for control_point, inventory in self.inventories.items(): if control_point.captured: for aircraft in inventory.types_available: diff --git a/game/models/frontline_data.py b/game/models/frontline_data.py index 94947135..586ebd58 100644 --- a/game/models/frontline_data.py +++ b/game/models/frontline_data.py @@ -1,4 +1,4 @@ -from theater import ControlPoint +from game.theater import ControlPoint class FrontlineData: diff --git a/game/operation/operation.py b/game/operation/operation.py index 0ff06ebe..1e01065b 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -15,6 +15,7 @@ from dcs.triggers import TriggerStart from dcs.unittype import UnitType from game.plugins import LuaPluginManager +from game.theater import ControlPoint from gen import Conflict, FlightType, VisualGenerator from gen.aircraft import AIRCRAFT_DATA, AircraftConflictGenerator, FlightData from gen.airfields import AIRFIELD_DATA @@ -29,7 +30,6 @@ from gen.kneeboard import KneeboardGenerator from gen.radios import RadioFrequency, RadioRegistry from gen.tacan import TacanRegistry from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator -from theater import ControlPoint from .. import db from ..debriefing import Debriefing diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index 88d622a6..ca21d463 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -16,6 +16,7 @@ from dcs.ships import ( Type_071_Amphibious_Transport_Dock, ) from dcs.terrain.terrain import Airport +from dcs.unittype import FlyingType from game import db from gen.ground_forces.combat_stance import CombatStance @@ -32,6 +33,7 @@ from .theatergroundobject import ( if TYPE_CHECKING: from game import Game from gen.flights.flight import FlightType + from ..event import UnitsDeliveryEvent class ControlPointType(Enum): @@ -157,6 +159,7 @@ class ControlPoint(MissionTarget): self.cptype = cptype self.stances: Dict[int, CombatStance] = {} self.airport = None + self.pending_unit_deliveries: Optional[UnitsDeliveryEvent] = None @property def ground_objects(self) -> List[TheaterGroundObject]: @@ -366,6 +369,13 @@ class ControlPoint(MissionTarget): # TODO: FlightType.STRIKE ] + def can_land(self, aircraft: FlyingType) -> bool: + if self.is_carrier and aircraft not in db.CARRIER_CAPABLE: + return False + if self.is_lha and aircraft not in db.LHA_CAPABLE: + return False + return True + class OffMapSpawn(ControlPoint): def __init__(self, id: int, name: str, position: Point): @@ -379,3 +389,7 @@ class OffMapSpawn(ControlPoint): def mission_types(self, for_player: bool) -> Iterator[FlightType]: yield from [] + + @property + def available_aircraft_slots(self) -> int: + return 1000 diff --git a/game/theater/frontline.py b/game/theater/frontline.py deleted file mode 100644 index 3b57f9b6..00000000 --- a/game/theater/frontline.py +++ /dev/null @@ -1 +0,0 @@ -"""Only here to keep compatibility for save games generated in version 2.2.0""" diff --git a/game/theater/start_generator.py b/game/theater/start_generator.py index ed30750b..c5232a32 100644 --- a/game/theater/start_generator.py +++ b/game/theater/start_generator.py @@ -39,10 +39,11 @@ from gen.sam.sam_group_generator import ( generate_anti_air_group, generate_ewr_group, generate_shorad_group, ) -from theater import ( +from . import ( ConflictTheater, ControlPoint, - ControlPointType, OffMapSpawn, + ControlPointType, + OffMapSpawn, ) GroundObjectTemplates = Dict[str, Dict[str, Any]] diff --git a/game/weather.py b/game/weather.py index d6775614..e8efd6e7 100644 --- a/game/weather.py +++ b/game/weather.py @@ -10,7 +10,7 @@ from typing import Optional from dcs.weather import Weather as PydcsWeather, Wind from game.settings import Settings -from theater import ConflictTheater +from game.theater import ConflictTheater class TimeOfDay(Enum): diff --git a/gen/aircraft.py b/gen/aircraft.py index 9edc52f4..f3690915 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -70,6 +70,12 @@ 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.theater.controlpoint import ( + ControlPoint, + ControlPointType, + OffMapSpawn, +) +from game.theater.theatergroundobject import TheaterGroundObject from game.utils import knots_to_kph, nm_to_meter from gen.airsupportgen import AirSupport from gen.ato import AirTaskingOrder, Package @@ -83,12 +89,6 @@ from gen.flights.flight import ( ) from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio from gen.runways import RunwayData -from theater import TheaterGroundObject -from game.theater.controlpoint import ( - ControlPoint, - ControlPointType, - OffMapSpawn, -) from .conflictgen import Conflict from .flights.flightplan import ( CasFlightPlan, @@ -695,6 +695,18 @@ class AircraftConflictGenerator: return StartType.Cold return StartType.Warm + def determine_runway(self, cp: ControlPoint, dynamic_runways) -> RunwayData: + fallback = RunwayData(cp.full_name, runway_heading=0, runway_name="") + if cp.cptype == ControlPointType.AIRBASE: + assigner = RunwayAssigner(self.game.conditions) + return assigner.get_preferred_runway(cp.airport) + elif cp.is_fleet: + return dynamic_runways.get(cp.name, fallback) + else: + logging.warning( + f"Unhandled departure/arrival control point: {cp.cptype}") + return fallback + def _setup_group(self, group: FlyingGroup, for_task: Type[Task], package: Package, flight: Flight, dynamic_runways: Dict[str, RunwayData]) -> None: @@ -752,19 +764,9 @@ class AircraftConflictGenerator: channel = self.get_intra_flight_channel(unit_type) group.set_frequency(channel.mhz) - # TODO: Support for different departure/arrival airfields. - cp = flight.from_cp - fallback_runway = RunwayData(cp.full_name, runway_heading=0, - runway_name="") - if cp.cptype == ControlPointType.AIRBASE: - assigner = RunwayAssigner(self.game.conditions) - departure_runway = assigner.get_preferred_runway( - flight.from_cp.airport) - elif cp.is_fleet: - departure_runway = dynamic_runways.get(cp.name, fallback_runway) - else: - logging.warning(f"Unhandled departure control point: {cp.cptype}") - departure_runway = fallback_runway + divert = None + if flight.divert is not None: + divert = self.determine_runway(flight.divert, dynamic_runways) self.flights.append(FlightData( package=package, @@ -774,10 +776,9 @@ class AircraftConflictGenerator: friendly=flight.from_cp.captured, # Set later. departure_delay=timedelta(), - departure=departure_runway, - arrival=departure_runway, - # TODO: Support for divert airfields. - divert=None, + departure=self.determine_runway(flight.departure, dynamic_runways), + arrival=self.determine_runway(flight.arrival, dynamic_runways), + divert=divert, # Waypoints are added later, after they've had their TOTs set. waypoints=[], intra_flight_channel=channel diff --git a/gen/conflictgen.py b/gen/conflictgen.py index ecce03e4..2cfe8223 100644 --- a/gen/conflictgen.py +++ b/gen/conflictgen.py @@ -5,7 +5,8 @@ from typing import Tuple from dcs.country import Country from dcs.mapping import Point -from theater import ConflictTheater, ControlPoint, FrontLine +from game.theater.conflicttheater import ConflictTheater, FrontLine +from game.theater.controlpoint import ControlPoint AIR_DISTANCE = 40000 diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index 7d152efa..182e0455 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -16,11 +16,24 @@ from typing import ( Type, ) -from dcs.unittype import FlyingType, UnitType +from dcs.unittype import FlyingType from game import db from game.data.radar_db import UNITS_WITH_RADAR from game.infos.information import Information +from game.theater import ( + ControlPoint, + FrontLine, + MissionTarget, + OffMapSpawn, + SamGroundObject, + TheaterGroundObject, +) +# Avoid importing some types that cause circular imports unless type checking. +from game.theater.theatergroundobject import ( + EwrGroundObject, + NavalGroundObject, VehicleGroupGroundObject, +) from game.utils import nm_to_meter from gen import Conflict from gen.ato import Package @@ -46,19 +59,6 @@ from gen.flights.flight import ( ) from gen.flights.flightplan import FlightPlanBuilder from gen.flights.traveltime import TotEstimator -from theater import ( - ControlPoint, - FrontLine, - MissionTarget, - OffMapSpawn, TheaterGroundObject, - SamGroundObject, -) - -# Avoid importing some types that cause circular imports unless type checking. -from game.theater.theatergroundobject import ( - EwrGroundObject, - NavalGroundObject, VehicleGroupGroundObject, -) if TYPE_CHECKING: from game import Game @@ -119,7 +119,7 @@ class AircraftAllocator: def find_aircraft_for_flight( self, flight: ProposedFlight - ) -> Optional[Tuple[ControlPoint, UnitType]]: + ) -> Optional[Tuple[ControlPoint, FlyingType]]: """Finds aircraft suitable for the given mission. Searches for aircraft capable of performing the given mission within the @@ -190,7 +190,7 @@ class AircraftAllocator: def find_aircraft_of_type( self, flight: ProposedFlight, types: List[Type[FlyingType]], - ) -> Optional[Tuple[ControlPoint, UnitType]]: + ) -> Optional[Tuple[ControlPoint, FlyingType]]: airfields_in_range = self.closest_airfields.airfields_within( flight.max_distance ) @@ -214,6 +214,8 @@ class PackageBuilder: global_inventory: GlobalAircraftInventory, is_player: bool, start_type: str) -> None: + self.closest_airfields = closest_airfields + self.is_player = is_player self.package = Package(location) self.allocator = AircraftAllocator(closest_airfields, global_inventory, is_player) @@ -236,11 +238,28 @@ class PackageBuilder: start_type = "In Flight" else: start_type = self.start_type - flight = Flight(self.package, aircraft, plan.num_aircraft, airfield, - plan.task, start_type) + + flight = Flight(self.package, aircraft, plan.num_aircraft, plan.task, + start_type, departure=airfield, arrival=airfield, + divert=self.find_divert_field(aircraft, airfield)) self.package.add_flight(flight) return True + def find_divert_field(self, aircraft: FlyingType, + arrival: ControlPoint) -> Optional[ControlPoint]: + divert_limit = nm_to_meter(150) + for airfield in self.closest_airfields.airfields_within(divert_limit): + if airfield.captured != self.is_player: + continue + if airfield == arrival: + continue + if not airfield.can_land(aircraft): + continue + if isinstance(airfield, OffMapSpawn): + continue + return airfield + return None + def build(self) -> Package: """Returns the built package.""" return self.package diff --git a/gen/flights/closestairfields.py b/gen/flights/closestairfields.py index a6045dde..5bba28db 100644 --- a/gen/flights/closestairfields.py +++ b/gen/flights/closestairfields.py @@ -1,7 +1,7 @@ """Objective adjacency lists.""" from typing import Dict, Iterator, List, Optional -from theater import ConflictTheater, ControlPoint, MissionTarget +from game.theater import ConflictTheater, ControlPoint, MissionTarget class ClosestAirfields: diff --git a/gen/flights/flight.py b/gen/flights/flight.py index 2b5e35ea..56d9d04a 100644 --- a/gen/flights/flight.py +++ b/gen/flights/flight.py @@ -65,6 +65,7 @@ class FlightWaypointType(Enum): INGRESS_DEAD = 20 INGRESS_SWEEP = 21 INGRESS_BAI = 22 + DIVERT = 23 class FlightWaypoint: @@ -133,12 +134,15 @@ class FlightWaypoint: class Flight: def __init__(self, package: Package, unit_type: FlyingType, count: int, - from_cp: ControlPoint, flight_type: FlightType, - start_type: str) -> None: + flight_type: FlightType, start_type: str, + departure: ControlPoint, arrival: ControlPoint, + divert: Optional[ControlPoint]) -> None: self.package = package self.unit_type = unit_type self.count = count - self.from_cp = from_cp + self.departure = departure + self.arrival = arrival + self.divert = divert self.flight_type = flight_type # TODO: Replace with FlightPlan. self.targets: List[MissionTarget] = [] @@ -157,6 +161,10 @@ class Flight: custom_waypoints=[] ) + @property + def from_cp(self) -> ControlPoint: + return self.departure + @property def points(self) -> List[FlightWaypoint]: return self.flight_plan.waypoints[1:] diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index 918861e2..d8758e32 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -7,20 +7,19 @@ generating the waypoints for the mission. """ from __future__ import annotations -import math -from datetime import timedelta -from functools import cached_property import logging +import math import random from dataclasses import dataclass +from datetime import timedelta +from functools import cached_property from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple from dcs.mapping import Point from dcs.unit import Unit from game.data.doctrine import Doctrine -from game.utils import nm_to_meter -from theater import ( +from game.theater import ( ControlPoint, FrontLine, MissionTarget, @@ -28,6 +27,7 @@ from theater import ( TheaterGroundObject, ) from game.theater.theatergroundobject import EwrGroundObject +from game.utils import nm_to_meter from .closestairfields import ObjectiveDistanceCache from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType from .traveltime import GroundSpeed, TravelTime @@ -68,6 +68,10 @@ class FlightPlan: @property def waypoints(self) -> List[FlightWaypoint]: """A list of all waypoints in the flight plan, in order.""" + return list(self.iter_waypoints()) + + def iter_waypoints(self) -> Iterator[FlightWaypoint]: + """Iterates over all waypoints in the flight plan, in order.""" raise NotImplementedError @property @@ -166,8 +170,7 @@ class FlightPlan: class LoiterFlightPlan(FlightPlan): hold: FlightWaypoint - @property - def waypoints(self) -> List[FlightWaypoint]: + def iter_waypoints(self) -> Iterator[FlightWaypoint]: raise NotImplementedError @property @@ -193,8 +196,7 @@ class FormationFlightPlan(LoiterFlightPlan): join: FlightWaypoint split: FlightWaypoint - @property - def waypoints(self) -> List[FlightWaypoint]: + def iter_waypoints(self) -> Iterator[FlightWaypoint]: raise NotImplementedError @property @@ -295,8 +297,7 @@ class PatrollingFlightPlan(FlightPlan): return self.patrol_end_time return None - @property - def waypoints(self) -> List[FlightWaypoint]: + def iter_waypoints(self) -> Iterator[FlightWaypoint]: raise NotImplementedError @property @@ -312,15 +313,17 @@ class PatrollingFlightPlan(FlightPlan): class BarCapFlightPlan(PatrollingFlightPlan): takeoff: FlightWaypoint land: FlightWaypoint + divert: Optional[FlightWaypoint] - @property - def waypoints(self) -> List[FlightWaypoint]: - return [ + def iter_waypoints(self) -> Iterator[FlightWaypoint]: + yield from [ self.takeoff, self.patrol_start, self.patrol_end, self.land, ] + if self.divert is not None: + yield self.divert @dataclass(frozen=True) @@ -328,16 +331,18 @@ class CasFlightPlan(PatrollingFlightPlan): takeoff: FlightWaypoint target: FlightWaypoint land: FlightWaypoint + divert: Optional[FlightWaypoint] - @property - def waypoints(self) -> List[FlightWaypoint]: - return [ + def iter_waypoints(self) -> Iterator[FlightWaypoint]: + yield from [ self.takeoff, self.patrol_start, self.target, self.patrol_end, self.land, ] + if self.divert is not None: + yield self.divert def request_escort_at(self) -> Optional[FlightWaypoint]: return self.patrol_start @@ -350,16 +355,18 @@ class CasFlightPlan(PatrollingFlightPlan): class TarCapFlightPlan(PatrollingFlightPlan): takeoff: FlightWaypoint land: FlightWaypoint + divert: Optional[FlightWaypoint] lead_time: timedelta - @property - def waypoints(self) -> List[FlightWaypoint]: - return [ + def iter_waypoints(self) -> Iterator[FlightWaypoint]: + yield from [ self.takeoff, self.patrol_start, self.patrol_end, self.land, ] + if self.divert is not None: + yield self.divert @property def tot_offset(self) -> timedelta: @@ -386,10 +393,6 @@ class TarCapFlightPlan(PatrollingFlightPlan): return super().patrol_end_time -# TODO: Remove when breaking save compat. -FrontLineCapFlightPlan = TarCapFlightPlan - - @dataclass(frozen=True) class StrikeFlightPlan(FormationFlightPlan): takeoff: FlightWaypoint @@ -400,19 +403,23 @@ class StrikeFlightPlan(FormationFlightPlan): egress: FlightWaypoint split: FlightWaypoint land: FlightWaypoint + divert: Optional[FlightWaypoint] - @property - def waypoints(self) -> List[FlightWaypoint]: - return [ + def iter_waypoints(self) -> Iterator[FlightWaypoint]: + yield from [ self.takeoff, self.hold, self.join, self.ingress - ] + self.targets + [ + ] + yield from self.targets + yield from[ self.egress, self.split, self.land, ] + if self.divert is not None: + yield self.divert @property def package_speed_waypoints(self) -> Set[FlightWaypoint]: @@ -511,17 +518,19 @@ class SweepFlightPlan(LoiterFlightPlan): sweep_start: FlightWaypoint sweep_end: FlightWaypoint land: FlightWaypoint + divert: Optional[FlightWaypoint] lead_time: timedelta - @property - def waypoints(self) -> List[FlightWaypoint]: - return [ + def iter_waypoints(self) -> Iterator[FlightWaypoint]: + yield from [ self.takeoff, self.hold, self.sweep_start, self.sweep_end, self.land, ] + if self.divert is not None: + yield self.divert @property def tot_waypoint(self) -> Optional[FlightWaypoint]: @@ -567,9 +576,8 @@ class SweepFlightPlan(LoiterFlightPlan): class CustomFlightPlan(FlightPlan): custom_waypoints: List[FlightWaypoint] - @property - def waypoints(self) -> List[FlightWaypoint]: - return self.custom_waypoints + def iter_waypoints(self) -> Iterator[FlightWaypoint]: + yield from self.custom_waypoints @property def tot_waypoint(self) -> Optional[FlightWaypoint]: @@ -774,10 +782,11 @@ class FlightPlanBuilder: package=self.package, flight=flight, patrol_duration=self.doctrine.cap_duration, - takeoff=builder.takeoff(flight.from_cp), + takeoff=builder.takeoff(flight.departure), patrol_start=start, patrol_end=end, - land=builder.land(flight.from_cp) + land=builder.land(flight.arrival), + divert=builder.divert(flight.divert) ) def generate_sweep(self, flight: Flight) -> SweepFlightPlan: @@ -800,11 +809,12 @@ class FlightPlanBuilder: package=self.package, flight=flight, lead_time=timedelta(minutes=5), - takeoff=builder.takeoff(flight.from_cp), + takeoff=builder.takeoff(flight.departure), hold=builder.hold(self._hold_point(flight)), sweep_start=start, sweep_end=end, - land=builder.land(flight.from_cp) + land=builder.land(flight.arrival), + divert=builder.divert(flight.divert) ) def racetrack_for_objective(self, @@ -900,10 +910,11 @@ class FlightPlanBuilder: # requests an escort the CAP flight will remain on station for the # duration of the escorted mission, or until it is winchester/bingo. patrol_duration=self.doctrine.cap_duration, - takeoff=builder.takeoff(flight.from_cp), + takeoff=builder.takeoff(flight.departure), patrol_start=start, patrol_end=end, - land=builder.land(flight.from_cp) + land=builder.land(flight.arrival), + divert=builder.divert(flight.divert) ) def generate_dead(self, flight: Flight, @@ -965,14 +976,15 @@ class FlightPlanBuilder: return StrikeFlightPlan( package=self.package, flight=flight, - takeoff=builder.takeoff(flight.from_cp), + takeoff=builder.takeoff(flight.departure), hold=builder.hold(self._hold_point(flight)), join=builder.join(self.package.waypoints.join), ingress=ingress, targets=[target], egress=egress, split=builder.split(self.package.waypoints.split), - land=builder.land(flight.from_cp) + land=builder.land(flight.arrival), + divert=builder.divert(flight.divert) ) def generate_cas(self, flight: Flight) -> CasFlightPlan: @@ -999,11 +1011,12 @@ class FlightPlanBuilder: package=self.package, flight=flight, patrol_duration=self.doctrine.cas_duration, - takeoff=builder.takeoff(flight.from_cp), + takeoff=builder.takeoff(flight.departure), patrol_start=builder.ingress_cas(ingress, location), target=builder.cas(center), patrol_end=builder.egress(egress, location), - land=builder.land(flight.from_cp) + land=builder.land(flight.arrival), + divert=builder.divert(flight.divert) ) @staticmethod @@ -1030,7 +1043,7 @@ class FlightPlanBuilder: def _hold_point(self, flight: Flight) -> Point: assert self.package.waypoints is not None - origin = flight.from_cp.position + origin = flight.departure.position target = self.package.target.position join = self.package.waypoints.join origin_to_target = origin.distance_to_point(target) @@ -1118,14 +1131,15 @@ class FlightPlanBuilder: return StrikeFlightPlan( package=self.package, flight=flight, - takeoff=builder.takeoff(flight.from_cp), + takeoff=builder.takeoff(flight.departure), hold=builder.hold(self._hold_point(flight)), join=builder.join(self.package.waypoints.join), ingress=ingress, targets=target_waypoints, egress=builder.egress(self.package.waypoints.egress, location), split=builder.split(self.package.waypoints.split), - land=builder.land(flight.from_cp) + land=builder.land(flight.arrival), + divert=builder.divert(flight.divert) ) def _retreating_rendezvous_point(self, attack_transition: Point) -> Point: @@ -1201,7 +1215,7 @@ class FlightPlanBuilder: ) for airfield in cache.closest_airfields: for flight in self.package.flights: - if flight.from_cp == airfield: + if flight.departure == airfield: return airfield raise RuntimeError( "Could not find any airfield assigned to this package" diff --git a/gen/flights/waypointbuilder.py b/gen/flights/waypointbuilder.py index 346a4498..74731929 100644 --- a/gen/flights/waypointbuilder.py +++ b/gen/flights/waypointbuilder.py @@ -8,14 +8,13 @@ from dcs.unit import Unit from dcs.unitgroup import VehicleGroup from game.data.doctrine import Doctrine -from game.utils import feet_to_meter -from game.weather import Conditions -from theater import ( +from game.theater import ( ControlPoint, MissionTarget, OffMapSpawn, TheaterGroundObject, ) +from game.weather import Conditions from .flight import Flight, FlightWaypoint, FlightWaypointType @@ -104,6 +103,40 @@ class WaypointBuilder: waypoint.pretty_name = "Land" return waypoint + def divert(self, + divert: Optional[ControlPoint]) -> Optional[FlightWaypoint]: + """Create divert waypoint for the given arrival airfield or carrier. + + Args: + divert: Divert airfield or carrier. + """ + if divert is None: + return None + + position = divert.position + if isinstance(divert, OffMapSpawn): + if self.is_helo: + altitude = 500 + else: + altitude = self.doctrine.rendezvous_altitude + altitude_type = "BARO" + else: + altitude = 0 + altitude_type = "RADIO" + + waypoint = FlightWaypoint( + FlightWaypointType.DIVERT, + position.x, + position.y, + altitude + ) + waypoint.alt_type = altitude_type + waypoint.name = "DIVERT" + waypoint.description = "Divert" + waypoint.pretty_name = "Divert" + waypoint.only_for_player = True + return waypoint + def hold(self, position: Point) -> FlightWaypoint: waypoint = FlightWaypoint( FlightWaypointType.LOITER, diff --git a/gen/ground_forces/ai_ground_planner.py b/gen/ground_forces/ai_ground_planner.py index db1deb03..b0f14df4 100644 --- a/gen/ground_forces/ai_ground_planner.py +++ b/gen/ground_forces/ai_ground_planner.py @@ -2,12 +2,12 @@ import random from enum import Enum from typing import Dict, List -from dcs.vehicles import Armor, Artillery, Infantry, Unarmed from dcs.unittype import VehicleType +from dcs.vehicles import Armor, Artillery, Infantry, Unarmed import pydcs_extensions.frenchpack.frenchpack as frenchpack +from game.theater import ControlPoint from gen.ground_forces.combat_stance import CombatStance -from theater import ControlPoint TYPE_TANKS = [ Armor.MBT_T_55, diff --git a/gen/groundobjectsgen.py b/gen/groundobjectsgen.py index edd58e6d..09385c78 100644 --- a/gen/groundobjectsgen.py +++ b/gen/groundobjectsgen.py @@ -20,14 +20,14 @@ from dcs.task import ( EPLRS, OptAlarmState, ) -from dcs.unit import Ship, Vehicle, Unit +from dcs.unit import Ship, Unit, Vehicle from dcs.unitgroup import Group, ShipGroup, StaticGroup from dcs.unittype import StaticType, UnitType from game import db from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID from game.db import unit_type_from_name -from theater import ControlPoint, TheaterGroundObject +from game.theater import ControlPoint, TheaterGroundObject from game.theater.theatergroundobject import ( BuildingGroundObject, CarrierGroundObject, GenericCarrierGroundObject, diff --git a/gen/runways.py b/gen/runways.py index 5323c37b..658cc846 100644 --- a/gen/runways.py +++ b/gen/runways.py @@ -7,8 +7,8 @@ from typing import Iterator, Optional from dcs.terrain.terrain import Airport +from game.theater import ControlPoint, ControlPointType from game.weather import Conditions -from theater import ControlPoint, ControlPointType from .airfields import AIRFIELD_DATA from .radios import RadioFrequency from .tacan import TacanChannel diff --git a/qt_ui/widgets/base/QAirportInformation.py b/qt_ui/widgets/base/QAirportInformation.py deleted file mode 100644 index 4fc1474c..00000000 --- a/qt_ui/widgets/base/QAirportInformation.py +++ /dev/null @@ -1,52 +0,0 @@ -from PySide2.QtWidgets import QGridLayout, QLabel, QGroupBox, QVBoxLayout, QLCDNumber - -from theater import ControlPoint, Airport - - -class QAirportInformation(QGroupBox): - - def __init__(self, cp:ControlPoint, airport:Airport): - super(QAirportInformation, self).__init__(airport.name) - self.cp = cp - self.airport = airport - self.init_ui() - - def init_ui(self): - self.layout = QGridLayout() - - # Runway information - self.runways = QGroupBox("Runways") - self.runwayLayout = QGridLayout() - for i, runway in enumerate(self.airport.runways): - - # Seems like info is missing in pydcs, even if the attribute is there - lr = "" - if runway.leftright == 1: - lr = "L" - elif runway.leftright == 2: - lr = "R" - - self.runwayLayout.addWidget(QLabel("Runway " + str(runway.heading) + lr), i, 0) - - # Seems like info is missing in pydcs, even if the attribute is there - if runway.ils: - self.runwayLayout.addWidget(QLabel("ILS "), i, 1) - self.runwayLayout.addWidget(QLCDNumber(6, runway.ils), i, 1) - else: - self.runwayLayout.addWidget(QLabel("NO ILS"), i, 1) - - - self.runways.setLayout(self.runwayLayout) - self.layout.addWidget(self.runways, 0, 0) - - self.layout.addWidget(QLabel("Parking Slots :"), 1, 0) - self.layout.addWidget(QLabel(str(len(self.airport.parking_slots))), 1, 1) - - - stretch = QVBoxLayout() - stretch.addStretch() - - self.layout.addLayout(stretch, 2, 0) - self.setLayout(self.layout) - - diff --git a/qt_ui/widgets/combos/QAircraftTypeSelector.py b/qt_ui/widgets/combos/QAircraftTypeSelector.py index 1f490e4d..2be6e48c 100644 --- a/qt_ui/widgets/combos/QAircraftTypeSelector.py +++ b/qt_ui/widgets/combos/QAircraftTypeSelector.py @@ -3,13 +3,13 @@ from typing import Iterable from PySide2.QtWidgets import QComboBox -from dcs.planes import PlaneType +from dcs.unittype import FlyingType class QAircraftTypeSelector(QComboBox): """Combo box for selecting among the given aircraft types.""" - def __init__(self, aircraft_types: Iterable[PlaneType]) -> None: + def __init__(self, aircraft_types: Iterable[FlyingType]) -> None: super().__init__() for aircraft in aircraft_types: self.addItem(f"{aircraft.id}", userData=aircraft) diff --git a/qt_ui/widgets/combos/QArrivalAirfieldSelector.py b/qt_ui/widgets/combos/QArrivalAirfieldSelector.py new file mode 100644 index 00000000..c5d89b90 --- /dev/null +++ b/qt_ui/widgets/combos/QArrivalAirfieldSelector.py @@ -0,0 +1,40 @@ +"""Combo box for selecting a departure airfield.""" +from typing import Iterable + +from PySide2.QtWidgets import QComboBox +from dcs.unittype import FlyingType + +from game import db +from game.theater.controlpoint import ControlPoint + + +class QArrivalAirfieldSelector(QComboBox): + """A combo box for selecting a flight's arrival or divert airfield. + + The combo box will automatically be populated with all airfields the given + aircraft type is able to land at. + """ + + def __init__(self, destinations: Iterable[ControlPoint], + aircraft: FlyingType, optional_text: str) -> None: + super().__init__() + self.destinations = list(destinations) + self.aircraft = aircraft + self.optional_text = optional_text + self.rebuild_selector() + self.setCurrentIndex(0) + + def change_aircraft(self, aircraft: FlyingType) -> None: + if self.aircraft == aircraft: + return + self.aircraft = aircraft + self.rebuild_selector() + + def rebuild_selector(self) -> None: + self.clear() + for destination in self.destinations: + if destination.can_land(self.aircraft): + self.addItem(destination.name, destination) + self.model().sort(0) + self.insertItem(0, self.optional_text, None) + self.update() diff --git a/qt_ui/widgets/combos/QFlightTypeComboBox.py b/qt_ui/widgets/combos/QFlightTypeComboBox.py index 6ba9e455..1918dd4d 100644 --- a/qt_ui/widgets/combos/QFlightTypeComboBox.py +++ b/qt_ui/widgets/combos/QFlightTypeComboBox.py @@ -2,7 +2,7 @@ from PySide2.QtWidgets import QComboBox -from theater import ConflictTheater, MissionTarget +from game.theater import ConflictTheater, MissionTarget class QFlightTypeComboBox(QComboBox): diff --git a/qt_ui/widgets/combos/QOriginAirfieldSelector.py b/qt_ui/widgets/combos/QOriginAirfieldSelector.py index 14bdbb47..ce1c6301 100644 --- a/qt_ui/widgets/combos/QOriginAirfieldSelector.py +++ b/qt_ui/widgets/combos/QOriginAirfieldSelector.py @@ -3,7 +3,7 @@ from typing import Iterable from PySide2.QtCore import Signal from PySide2.QtWidgets import QComboBox -from dcs.planes import PlaneType +from dcs.unittype import FlyingType from game.inventory import GlobalAircraftInventory from game.theater.controlpoint import ControlPoint @@ -20,7 +20,7 @@ class QOriginAirfieldSelector(QComboBox): def __init__(self, global_inventory: GlobalAircraftInventory, origins: Iterable[ControlPoint], - aircraft: PlaneType) -> None: + aircraft: FlyingType) -> None: super().__init__() self.global_inventory = global_inventory self.origins = list(origins) @@ -28,7 +28,7 @@ class QOriginAirfieldSelector(QComboBox): self.rebuild_selector() self.currentIndexChanged.connect(self.index_changed) - def change_aircraft(self, aircraft: PlaneType) -> None: + def change_aircraft(self, aircraft: FlyingType) -> None: if self.aircraft == aircraft: return self.aircraft = aircraft diff --git a/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py b/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py index 8af3c3f4..8f40afde 100644 --- a/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py +++ b/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py @@ -1,10 +1,10 @@ from PySide2.QtGui import QStandardItem, QStandardItemModel from game import Game +from game.theater import ControlPointType from gen import BuildingGroundObject, Conflict, FlightWaypointType from gen.flights.flight import FlightWaypoint from qt_ui.widgets.combos.QFilteredComboBox import QFilteredComboBox -from theater import ControlPointType class QPredefinedWaypointSelectionComboBox(QFilteredComboBox): diff --git a/qt_ui/widgets/map/QFrontLine.py b/qt_ui/widgets/map/QFrontLine.py index 1849f5ff..2ca71953 100644 --- a/qt_ui/widgets/map/QFrontLine.py +++ b/qt_ui/widgets/map/QFrontLine.py @@ -13,11 +13,11 @@ from PySide2.QtWidgets import ( ) import qt_ui.uiconstants as const +from game.theater import FrontLine from qt_ui.dialogs import Dialog from qt_ui.models import GameModel from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.mission.QPackageDialog import QNewPackageDialog -from theater import FrontLine class QFrontLine(QGraphicsLineItem): diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py index fb5802c3..50fc5fba 100644 --- a/qt_ui/widgets/map/QLiberationMap.py +++ b/qt_ui/widgets/map/QLiberationMap.py @@ -3,7 +3,7 @@ from __future__ import annotations import datetime import logging import math -from typing import Iterable, List, Optional, Tuple, Iterator +from typing import Iterable, Iterator, List, Optional, Tuple from PySide2.QtCore import QPointF, Qt from PySide2.QtGui import ( @@ -27,6 +27,13 @@ from dcs.mapping import point_from_heading import qt_ui.uiconstants as CONST from game import Game, db +from game.theater import ControlPoint +from game.theater.conflicttheater import FrontLine +from game.theater.theatergroundobject import ( + EwrGroundObject, + MissileSiteGroundObject, + TheaterGroundObject, +) from game.utils import meter_to_feet from game.weather import TimeOfDay from gen import Conflict @@ -39,13 +46,7 @@ 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.windows.GameUpdateSignal import GameUpdateSignal -from theater import ControlPoint -from game.theater.conflicttheater import FrontLine -from game.theater.theatergroundobject import ( - EwrGroundObject, - MissileSiteGroundObject, - TheaterGroundObject, -) + def binomial(i: int, n: int) -> float: """Binomial coefficient""" @@ -373,6 +374,10 @@ class QLiberationMap(QGraphicsView): FlightWaypointType.TARGET_SHIP, ) for idx, point in enumerate(flight.flight_plan.waypoints[1:]): + if point.waypoint_type == FlightWaypointType.DIVERT: + # Don't clutter the map showing divert points. + continue + new_pos = self._transform_point(Point(point.x, point.y)) self.draw_flight_path(scene, prev_pos, new_pos, is_player, selected) @@ -386,7 +391,6 @@ class QLiberationMap(QGraphicsView): self.draw_waypoint_info(scene, idx + 1, point, new_pos, flight.flight_plan) prev_pos = tuple(new_pos) - self.draw_flight_path(scene, prev_pos, pos, is_player, selected) def draw_waypoint(self, scene: QGraphicsScene, position: Tuple[int, int], player: bool, selected: bool) -> None: diff --git a/qt_ui/widgets/map/QMapControlPoint.py b/qt_ui/widgets/map/QMapControlPoint.py index 7ef55952..0f88bf7e 100644 --- a/qt_ui/widgets/map/QMapControlPoint.py +++ b/qt_ui/widgets/map/QMapControlPoint.py @@ -4,9 +4,9 @@ from PySide2.QtGui import QColor, QPainter from PySide2.QtWidgets import QAction, QMenu import qt_ui.uiconstants as const +from game.theater import ControlPoint 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 from ...windows.GameUpdateSignal import GameUpdateSignal diff --git a/qt_ui/widgets/map/QMapGroundObject.py b/qt_ui/widgets/map/QMapGroundObject.py index a7d857f3..f1d3e542 100644 --- a/qt_ui/widgets/map/QMapGroundObject.py +++ b/qt_ui/widgets/map/QMapGroundObject.py @@ -8,8 +8,8 @@ import qt_ui.uiconstants as const from game import Game from game.data.building_data import FORTIFICATION_BUILDINGS from game.db import REWARDS +from game.theater import ControlPoint, TheaterGroundObject from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu -from theater import ControlPoint, TheaterGroundObject from .QMapObject import QMapObject from ...displayoptions import DisplayOptions diff --git a/qt_ui/windows/basemenu/QBaseMenu2.py b/qt_ui/windows/basemenu/QBaseMenu2.py index cfdb8128..f9f7c159 100644 --- a/qt_ui/windows/basemenu/QBaseMenu2.py +++ b/qt_ui/windows/basemenu/QBaseMenu2.py @@ -2,12 +2,12 @@ from PySide2.QtCore import Qt from PySide2.QtGui import QCloseEvent, QPixmap from PySide2.QtWidgets import QDialog, QGridLayout, QHBoxLayout, QLabel, QWidget +from game.theater import ControlPoint, ControlPointType from qt_ui.models import GameModel from qt_ui.uiconstants import EVENT_ICONS from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.basemenu.QBaseMenuTabs import QBaseMenuTabs from qt_ui.windows.basemenu.QRecruitBehaviour import QRecruitBehaviour -from theater import ControlPoint, ControlPointType class QBaseMenu2(QDialog): diff --git a/qt_ui/windows/basemenu/QBaseMenuTabs.py b/qt_ui/windows/basemenu/QBaseMenuTabs.py index fd2f7ae7..1e705372 100644 --- a/qt_ui/windows/basemenu/QBaseMenuTabs.py +++ b/qt_ui/windows/basemenu/QBaseMenuTabs.py @@ -1,11 +1,11 @@ -from PySide2.QtWidgets import QFrame, QGridLayout, QLabel, QTabWidget +from PySide2.QtWidgets import QTabWidget +from game.theater import ControlPoint, OffMapSpawn from qt_ui.models import GameModel from qt_ui.windows.basemenu.airfield.QAirfieldCommand import QAirfieldCommand from qt_ui.windows.basemenu.base_defenses.QBaseDefensesHQ import QBaseDefensesHQ from qt_ui.windows.basemenu.ground_forces.QGroundForcesHQ import QGroundForcesHQ from qt_ui.windows.basemenu.intel.QIntelInfo import QIntelInfo -from theater import ControlPoint, OffMapSpawn class QBaseMenuTabs(QTabWidget): diff --git a/qt_ui/windows/basemenu/QRecruitBehaviour.py b/qt_ui/windows/basemenu/QRecruitBehaviour.py index b41ac68a..5cb26a81 100644 --- a/qt_ui/windows/basemenu/QRecruitBehaviour.py +++ b/qt_ui/windows/basemenu/QRecruitBehaviour.py @@ -1,3 +1,5 @@ +import logging + from PySide2.QtWidgets import ( QGroupBox, QHBoxLayout, @@ -6,17 +8,17 @@ from PySide2.QtWidgets import ( QSizePolicy, QSpacerItem, ) -import logging from dcs.unittype import UnitType -from theater import db - +from game import db +from game.event import UnitsDeliveryEvent +from game.theater import ControlPoint +from qt_ui.models import GameModel class QRecruitBehaviour: - game = None - cp = None - deliveryEvent = None + game_model: GameModel + cp: ControlPoint existing_units_labels = None bought_amount_labels = None maximum_units = -1 @@ -24,12 +26,16 @@ class QRecruitBehaviour: BUDGET_FORMAT = "Available Budget: ${}M" def __init__(self) -> None: - self.deliveryEvent = None self.bought_amount_labels = {} self.existing_units_labels = {} self.recruitable_types = [] self.update_available_budget() + @property + def pending_deliveries(self) -> UnitsDeliveryEvent: + assert self.cp.pending_unit_deliveries + return self.cp.pending_unit_deliveries + @property def budget(self) -> int: return self.game_model.game.budget @@ -47,7 +53,7 @@ class QRecruitBehaviour: exist.setLayout(existLayout) existing_units = self.cp.base.total_units_of_type(unit_type) - scheduled_units = self.deliveryEvent.units.get(unit_type, 0) + scheduled_units = self.pending_deliveries.units.get(unit_type, 0) unitName = QLabel("" + db.unit_type_name_2(unit_type) + "") unitName.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)) @@ -103,7 +109,7 @@ class QRecruitBehaviour: def _update_count_label(self, unit_type: UnitType): self.bought_amount_labels[unit_type].setText("{}".format( - unit_type in self.deliveryEvent.units and "{}".format(self.deliveryEvent.units[unit_type]) or "0" + unit_type in self.pending_deliveries.units and "{}".format(self.pending_deliveries.units[unit_type]) or "0" )) self.existing_units_labels[unit_type].setText("{}".format( @@ -129,7 +135,7 @@ class QRecruitBehaviour: price = db.PRICES[unit_type] if self.budget >= price: - self.deliveryEvent.deliver({unit_type: 1}) + self.pending_deliveries.deliver({unit_type: 1}) self.budget -= price else: # TODO : display modal warning @@ -138,12 +144,12 @@ class QRecruitBehaviour: self.update_available_budget() def sell(self, unit_type): - if self.deliveryEvent.units.get(unit_type, 0) > 0: + if self.pending_deliveries.units.get(unit_type, 0) > 0: price = db.PRICES[unit_type] self.budget += price - self.deliveryEvent.units[unit_type] = self.deliveryEvent.units[unit_type] - 1 - if self.deliveryEvent.units[unit_type] == 0: - del self.deliveryEvent.units[unit_type] + self.pending_deliveries.units[unit_type] = self.pending_deliveries.units[unit_type] - 1 + if self.pending_deliveries.units[unit_type] == 0: + del self.pending_deliveries.units[unit_type] elif self.cp.base.total_units_of_type(unit_type) > 0: price = db.PRICES[unit_type] self.budget += price @@ -154,20 +160,14 @@ class QRecruitBehaviour: @property def total_units(self): - total = 0 for unit_type in self.recruitables_types: total += self.cp.base.total_units(unit_type) - print(unit_type, total, self.cp.base.total_units(unit_type)) - print("--------------------------------") - if self.deliveryEvent: - for unit_bought in self.deliveryEvent.units: + if self.pending_deliveries: + for unit_bought in self.pending_deliveries.units: if db.unit_task(unit_bought) in self.recruitables_types: - total += self.deliveryEvent.units[unit_bought] - print(unit_bought, total, self.deliveryEvent.units[unit_bought]) - - print("=============================") + total += self.pending_deliveries.units[unit_bought] return total @@ -181,4 +181,4 @@ class QRecruitBehaviour: """ Set the maximum number of units that can be bought """ - self.recruitables_types = recruitables_types \ No newline at end of file + self.recruitables_types = recruitables_types diff --git a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py index a01aaaa9..3e8fef9f 100644 --- a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py +++ b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py @@ -11,13 +11,14 @@ from PySide2.QtWidgets import ( QVBoxLayout, QWidget, ) +from dcs.task import CAP, CAS from dcs.unittype import UnitType -from game.event.event import UnitsDeliveryEvent +from game import db +from game.theater import ControlPoint from qt_ui.models import GameModel from qt_ui.uiconstants import ICONS from qt_ui.windows.basemenu.QRecruitBehaviour import QRecruitBehaviour -from theater import CAP, CAS, ControlPoint, db class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour): @@ -25,17 +26,10 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour): QFrame.__init__(self) self.cp = cp self.game_model = game_model - self.deliveryEvent: Optional[UnitsDeliveryEvent] = None self.bought_amount_labels = {} self.existing_units_labels = {} - for event in self.game_model.game.events: - if event.__class__ == UnitsDeliveryEvent and event.from_cp == self.cp: - self.deliveryEvent = event - if not self.deliveryEvent: - self.deliveryEvent = self.game_model.game.units_delivery_event(self.cp) - # Determine maximum number of aircrafts that can be bought self.set_maximum_units(self.cp.available_aircraft_slots) self.set_recruitable_types([CAP, CAS]) @@ -92,7 +86,7 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour): def sell(self, unit_type: UnitType): # Don't need to remove aircraft from the inventory if we're canceling # orders. - if self.deliveryEvent.units.get(unit_type, 0) <= 0: + if self.pending_deliveries.units.get(unit_type, 0) <= 0: global_inventory = self.game_model.game.aircraft_inventory inventory = global_inventory.for_control_point(self.cp) try: diff --git a/qt_ui/windows/basemenu/airfield/QAirfieldCommand.py b/qt_ui/windows/basemenu/airfield/QAirfieldCommand.py index 9965115a..97c804bf 100644 --- a/qt_ui/windows/basemenu/airfield/QAirfieldCommand.py +++ b/qt_ui/windows/basemenu/airfield/QAirfieldCommand.py @@ -1,10 +1,10 @@ from PySide2.QtWidgets import QFrame, QGridLayout, QGroupBox, QVBoxLayout +from game.theater import ControlPoint from qt_ui.models import GameModel from qt_ui.windows.basemenu.airfield.QAircraftRecruitmentMenu import \ QAircraftRecruitmentMenu from qt_ui.windows.mission.QPlannedFlightsView import QPlannedFlightsView -from theater import ControlPoint class QAirfieldCommand(QFrame): diff --git a/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py b/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py index 350cf5e8..6d46b35b 100644 --- a/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py +++ b/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py @@ -1,10 +1,16 @@ from PySide2.QtCore import Qt -from PySide2.QtWidgets import QGridLayout, QLabel, QGroupBox, QPushButton, QVBoxLayout +from PySide2.QtWidgets import ( + QGridLayout, + QGroupBox, + QLabel, + QPushButton, + QVBoxLayout, +) +from game.theater import ControlPoint, TheaterGroundObject from qt_ui.dialogs import Dialog from qt_ui.uiconstants import VEHICLES_ICONS from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu -from theater import ControlPoint, TheaterGroundObject class QBaseDefenseGroupInfo(QGroupBox): diff --git a/qt_ui/windows/basemenu/base_defenses/QBaseDefensesHQ.py b/qt_ui/windows/basemenu/base_defenses/QBaseDefensesHQ.py index 5ad1f6c9..75a45eb0 100644 --- a/qt_ui/windows/basemenu/base_defenses/QBaseDefensesHQ.py +++ b/qt_ui/windows/basemenu/base_defenses/QBaseDefensesHQ.py @@ -1,7 +1,9 @@ from PySide2.QtWidgets import QFrame, QGridLayout + from game import Game -from qt_ui.windows.basemenu.base_defenses.QBaseInformation import QBaseInformation -from theater import ControlPoint +from game.theater import ControlPoint +from qt_ui.windows.basemenu.base_defenses.QBaseInformation import \ + QBaseInformation class QBaseDefensesHQ(QFrame): diff --git a/qt_ui/windows/basemenu/base_defenses/QBaseInformation.py b/qt_ui/windows/basemenu/base_defenses/QBaseInformation.py index f5325887..50ec2f81 100644 --- a/qt_ui/windows/basemenu/base_defenses/QBaseInformation.py +++ b/qt_ui/windows/basemenu/base_defenses/QBaseInformation.py @@ -1,10 +1,15 @@ from PySide2.QtGui import Qt -from PySide2.QtWidgets import QGridLayout, QLabel, QGroupBox, QVBoxLayout, QFrame, QWidget, QScrollArea +from PySide2.QtWidgets import ( + QFrame, + QGridLayout, + QScrollArea, + QVBoxLayout, + QWidget, +) -from game import db -from qt_ui.uiconstants import AIRCRAFT_ICONS, VEHICLES_ICONS -from qt_ui.windows.basemenu.base_defenses.QBaseDefenseGroupInfo import QBaseDefenseGroupInfo -from theater import ControlPoint, Airport +from game.theater import Airport, ControlPoint +from qt_ui.windows.basemenu.base_defenses.QBaseDefenseGroupInfo import \ + QBaseDefenseGroupInfo class QBaseInformation(QFrame): diff --git a/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py b/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py index ec1cabf6..c359eaaf 100644 --- a/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py +++ b/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py @@ -6,11 +6,12 @@ from PySide2.QtWidgets import ( QVBoxLayout, QWidget, ) +from dcs.task import PinpointStrike -from game.event import UnitsDeliveryEvent +from game import db +from game.theater import ControlPoint from qt_ui.models import GameModel from qt_ui.windows.basemenu.QRecruitBehaviour import QRecruitBehaviour -from theater import ControlPoint, PinpointStrike, db class QArmorRecruitmentMenu(QFrame, QRecruitBehaviour): @@ -23,12 +24,6 @@ class QArmorRecruitmentMenu(QFrame, QRecruitBehaviour): self.bought_amount_labels = {} self.existing_units_labels = {} - for event in self.game_model.game.events: - if event.__class__ == UnitsDeliveryEvent and event.from_cp == self.cp: - self.deliveryEvent = event - if not self.deliveryEvent: - self.deliveryEvent = self.game_model.game.units_delivery_event(self.cp) - self.init_ui() def init_ui(self): @@ -61,4 +56,4 @@ class QArmorRecruitmentMenu(QFrame, QRecruitBehaviour): scroll.setWidgetResizable(True) scroll.setWidget(scroll_content) main_layout.addWidget(scroll) - self.setLayout(main_layout) \ No newline at end of file + self.setLayout(main_layout) diff --git a/qt_ui/windows/basemenu/ground_forces/QGroundForcesHQ.py b/qt_ui/windows/basemenu/ground_forces/QGroundForcesHQ.py index bb18594f..39cba843 100644 --- a/qt_ui/windows/basemenu/ground_forces/QGroundForcesHQ.py +++ b/qt_ui/windows/basemenu/ground_forces/QGroundForcesHQ.py @@ -1,11 +1,11 @@ from PySide2.QtWidgets import QFrame, QGridLayout +from game.theater import ControlPoint from qt_ui.models import GameModel from qt_ui.windows.basemenu.ground_forces.QArmorRecruitmentMenu import \ QArmorRecruitmentMenu from qt_ui.windows.basemenu.ground_forces.QGroundForcesStrategy import \ QGroundForcesStrategy -from theater import ControlPoint class QGroundForcesHQ(QFrame): diff --git a/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategy.py b/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategy.py index 0b7b4db6..3aee8c50 100644 --- a/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategy.py +++ b/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategy.py @@ -1,8 +1,9 @@ -from PySide2.QtWidgets import QLabel, QGroupBox, QVBoxLayout +from PySide2.QtWidgets import QGroupBox, QLabel, QVBoxLayout from game import Game -from qt_ui.windows.basemenu.ground_forces.QGroundForcesStrategySelector import QGroundForcesStrategySelector -from theater import ControlPoint +from game.theater import ControlPoint +from qt_ui.windows.basemenu.ground_forces.QGroundForcesStrategySelector import \ + QGroundForcesStrategySelector class QGroundForcesStrategy(QGroupBox): diff --git a/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategySelector.py b/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategySelector.py index 09c3fa5b..4acd8731 100644 --- a/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategySelector.py +++ b/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategySelector.py @@ -1,6 +1,6 @@ from PySide2.QtWidgets import QComboBox -from theater import ControlPoint, CombatStance +from game.theater import CombatStance, ControlPoint class QGroundForcesStrategySelector(QComboBox): diff --git a/qt_ui/windows/basemenu/intel/QIntelInfo.py b/qt_ui/windows/basemenu/intel/QIntelInfo.py index bc7cb13b..e422ef3a 100644 --- a/qt_ui/windows/basemenu/intel/QIntelInfo.py +++ b/qt_ui/windows/basemenu/intel/QIntelInfo.py @@ -1,11 +1,14 @@ +from PySide2.QtWidgets import ( + QFrame, + QGridLayout, + QGroupBox, + QLabel, + QVBoxLayout, +) +from dcs.task import CAP, CAS, Embarking, PinpointStrike - -from PySide2.QtWidgets import QLabel, QGroupBox, QVBoxLayout, QFrame, QGridLayout -from dcs.task import Embarking, CAS, PinpointStrike, CAP - -from game import Game -from qt_ui.windows.basemenu.ground_forces.QGroundForcesStrategySelector import QGroundForcesStrategySelector -from theater import ControlPoint, db +from game import Game, db +from game.theater import ControlPoint class QIntelInfo(QFrame): diff --git a/qt_ui/windows/groundobject/QGroundObjectMenu.py b/qt_ui/windows/groundobject/QGroundObjectMenu.py index a1fda851..abbf5c8c 100644 --- a/qt_ui/windows/groundobject/QGroundObjectMenu.py +++ b/qt_ui/windows/groundobject/QGroundObjectMenu.py @@ -2,20 +2,31 @@ import logging from PySide2 import QtCore from PySide2.QtGui import Qt -from PySide2.QtWidgets import QHBoxLayout, QDialog, QGridLayout, QLabel, QGroupBox, QVBoxLayout, QPushButton, \ - QComboBox, QSpinBox, QMessageBox +from PySide2.QtWidgets import ( + QComboBox, + QDialog, + QGridLayout, + QGroupBox, + QHBoxLayout, + QLabel, + QMessageBox, + QPushButton, + QSpinBox, + QVBoxLayout, +) from dcs import Point from game import Game, db from game.data.building_data import FORTIFICATION_BUILDINGS -from game.db import PRICES, REWARDS, unit_type_of, PinpointStrike -from gen.defenses.armor_group_generator import generate_armor_group_of_type_and_size +from game.db import PRICES, PinpointStrike, REWARDS, unit_type_of +from game.theater import ControlPoint, TheaterGroundObject +from gen.defenses.armor_group_generator import \ + generate_armor_group_of_type_and_size from gen.sam.sam_group_generator import get_faction_possible_sams_generator from qt_ui.uiconstants import EVENT_ICONS from qt_ui.widgets.QBudgetBox import QBudgetBox from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.groundobject.QBuildingInfo import QBuildingInfo -from theater import ControlPoint, TheaterGroundObject class QGroundObjectMenu(QDialog): diff --git a/qt_ui/windows/mission/flight/QFlightCreator.py b/qt_ui/windows/mission/flight/QFlightCreator.py index 80fbf219..0e0bf773 100644 --- a/qt_ui/windows/mission/flight/QFlightCreator.py +++ b/qt_ui/windows/mission/flight/QFlightCreator.py @@ -10,15 +10,17 @@ from PySide2.QtWidgets import ( from dcs.planes import PlaneType from game import Game +from game.theater import ControlPoint, OffMapSpawn from gen.ato import Package from gen.flights.flight import Flight from qt_ui.uiconstants import EVENT_ICONS from qt_ui.widgets.QFlightSizeSpinner import QFlightSizeSpinner from qt_ui.widgets.QLabeledWidget import QLabeledWidget from qt_ui.widgets.combos.QAircraftTypeSelector import QAircraftTypeSelector +from qt_ui.widgets.combos.QArrivalAirfieldSelector import \ + QArrivalAirfieldSelector from qt_ui.widgets.combos.QFlightTypeComboBox import QFlightTypeComboBox from qt_ui.widgets.combos.QOriginAirfieldSelector import QOriginAirfieldSelector -from theater import ControlPoint, OffMapSpawn class QFlightCreator(QDialog): @@ -49,16 +51,30 @@ class QFlightCreator(QDialog): self.on_aircraft_changed) layout.addLayout(QLabeledWidget("Aircraft:", self.aircraft_selector)) - self.airfield_selector = QOriginAirfieldSelector( + self.departure = QOriginAirfieldSelector( self.game.aircraft_inventory, [cp for cp in game.theater.controlpoints if cp.captured], self.aircraft_selector.currentData() ) - self.airfield_selector.availability_changed.connect(self.update_max_size) - layout.addLayout(QLabeledWidget("Airfield:", self.airfield_selector)) + self.departure.availability_changed.connect(self.update_max_size) + layout.addLayout(QLabeledWidget("Departure:", self.departure)) + + self.arrival = QArrivalAirfieldSelector( + [cp for cp in game.theater.controlpoints if cp.captured], + self.aircraft_selector.currentData(), + "Same as departure" + ) + layout.addLayout(QLabeledWidget("Arrival:", self.arrival)) + + self.divert = QArrivalAirfieldSelector( + [cp for cp in game.theater.controlpoints if cp.captured], + self.aircraft_selector.currentData(), + "None" + ) + layout.addLayout(QLabeledWidget("Divert:", self.divert)) self.flight_size_spinner = QFlightSizeSpinner() - self.update_max_size(self.airfield_selector.available) + self.update_max_size(self.departure.available) layout.addLayout(QLabeledWidget("Size:", self.flight_size_spinner)) self.client_slots_spinner = QFlightSizeSpinner( @@ -82,10 +98,16 @@ class QFlightCreator(QDialog): def verify_form(self) -> Optional[str]: aircraft: PlaneType = self.aircraft_selector.currentData() - origin: ControlPoint = self.airfield_selector.currentData() + origin: ControlPoint = self.departure.currentData() + arrival: ControlPoint = self.arrival.currentData() + divert: ControlPoint = self.divert.currentData() size: int = self.flight_size_spinner.value() if not origin.captured: return f"{origin.name} is not owned by your coalition." + if arrival is not None and not arrival.captured: + return f"{arrival.name} is not owned by your coalition." + if divert is not None and not divert.captured: + return f"{divert.name} is not owned by your coalition." available = origin.base.aircraft.get(aircraft, 0) if not available: return f"{origin.name} has no {aircraft.id} available." @@ -104,16 +126,22 @@ class QFlightCreator(QDialog): task = self.task_selector.currentData() aircraft = self.aircraft_selector.currentData() - origin = self.airfield_selector.currentData() + origin = self.departure.currentData() + arrival = self.arrival.currentData() + divert = self.divert.currentData() size = self.flight_size_spinner.value() + if arrival is None: + arrival = origin + if isinstance(origin, OffMapSpawn): start_type = "In Flight" elif self.game.settings.perf_ai_parking_start: start_type = "Cold" else: start_type = "Warm" - flight = Flight(self.package, aircraft, size, origin, task, start_type) + flight = Flight(self.package, aircraft, size, task, start_type, origin, + arrival, divert) flight.client_count = self.client_slots_spinner.value() # noinspection PyUnresolvedReferences @@ -122,7 +150,9 @@ class QFlightCreator(QDialog): def on_aircraft_changed(self, index: int) -> None: new_aircraft = self.aircraft_selector.itemData(index) - self.airfield_selector.change_aircraft(new_aircraft) + self.departure.change_aircraft(new_aircraft) + self.arrival.change_aircraft(new_aircraft) + self.divert.change_aircraft(new_aircraft) def update_max_size(self, available: int) -> None: self.flight_size_spinner.setMaximum(min(available, 4)) diff --git a/qt_ui/windows/newgame/QCampaignList.py b/qt_ui/windows/newgame/QCampaignList.py index 09839224..86ce0461 100644 --- a/qt_ui/windows/newgame/QCampaignList.py +++ b/qt_ui/windows/newgame/QCampaignList.py @@ -12,7 +12,7 @@ from PySide2.QtGui import QStandardItem, QStandardItemModel from PySide2.QtWidgets import QAbstractItemView, QListView import qt_ui.uiconstants as CONST -from theater import ConflictTheater +from game.theater import ConflictTheater @dataclass(frozen=True) diff --git a/theater/__init__.py b/theater/__init__.py deleted file mode 100644 index f6b256d8..00000000 --- a/theater/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# For save game compatibility. Remove before 2.3. -from game.theater import * diff --git a/theater/base.py b/theater/base.py deleted file mode 100644 index fc28c91b..00000000 --- a/theater/base.py +++ /dev/null @@ -1,2 +0,0 @@ -# For save compat. Remove in 2.3. -from game.theater.base import * diff --git a/theater/conflicttheater.py b/theater/conflicttheater.py deleted file mode 100644 index e1566178..00000000 --- a/theater/conflicttheater.py +++ /dev/null @@ -1,2 +0,0 @@ -# For save compat. Remove in 2.3. -from game.theater.conflicttheater import * diff --git a/theater/controlpoint.py b/theater/controlpoint.py deleted file mode 100644 index 90a6b164..00000000 --- a/theater/controlpoint.py +++ /dev/null @@ -1,2 +0,0 @@ -# For save compat. Remove in 2.3. -from game.theater.controlpoint import * diff --git a/theater/frontline.py b/theater/frontline.py deleted file mode 100644 index 5ddb5706..00000000 --- a/theater/frontline.py +++ /dev/null @@ -1,3 +0,0 @@ -# For save compat. Remove in 2.3. -from game.theater.frontline import * -from game.theater.conflicttheater import FrontLine \ No newline at end of file diff --git a/theater/theatergroundobject.py b/theater/theatergroundobject.py deleted file mode 100644 index 3c77455d..00000000 --- a/theater/theatergroundobject.py +++ /dev/null @@ -1,2 +0,0 @@ -# For save compat. Remove in 2.3. -from game.theater.theatergroundobject import *