Roadbase and ground spawn support (#132)

* Roadbase and ground spawn support

Implemented support for roadbases and ground spawn slots at airfields and FOBs. The ground spawn slots can be inserted in campaigns by placing either an A-10A or an AJS37 at a runway or ramp. This will cause an invisible FARP, an ammo dump and a fuel dump to be placed (behind the slot in case of A-10A, to the side in case of AJS37) in the generated campaigns. The ground spawn slot can be used by human controlled aircraft in generated missions. Also allowed the use of the four-slot FARP and the single helipad in campaigns, in addition to the invisible FARP. The first waypoint of the placed aircraft will be the center of a Remove Statics trigger zone (which might or might not work in multiplayer due to a DCS limitation).

Also implemented three new options in settings:
 - AI fixed-wing aircraft can use roadbases / bases with only ground spawns
   - This setting will allow the AI to use the roadbases for flights and transfers. AI flights will air-start from these bases, since the AI in DCS is not currently able to take off from ground spawns.
 - Spawn trucks at ground spawns in airbases instead of FARP statics
 - Spawn trucks at ground spawns in roadbases instead of FARP statics
   - These settings will replace the FARP statics with refueler and ammo trucks at roadbases. Enabling them might have a negative performance impact.

* Modified calculate_parking_slots() so it now takes into account also helicopter slots on FARPs and also ground start slots (but only if the aircraft is flyable or the "AI fixed-wing aircraft can use roadbases / bases with only ground spawns" option is enabled in settings).

* Improved the way parking slots are communicated on the basemenu window.

* Refactored helipad and ground spawn appends to static methods _add_helipad and _add_ground_spawn in mizcampaignloader.py
Added missing changelog entries.
Fixed tgogenerator.py imports.
Cleaned up ParkingType() construction.

* Added test_control_point_parking for testing that the correct number of parking slots are returned for control point in test_controlpoint.py

* Added test_parking_type_from_squadron to test the correct ParkingType object is returned for a squadron of Viggens in test_controlpoint.py

* Added test_parking_type_from_aircraft to test the correct ParkingType object is returned for Viggen aircraft type in test_controlpoint.py

---------

Co-authored-by: Raffson <Raffson@users.noreply.github.com>
This commit is contained in:
MetalStormGhost 2023-06-19 00:02:08 +03:00 committed by GitHub
parent 5a71806651
commit e273e93012
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1234 additions and 174 deletions

View File

@ -3,6 +3,9 @@
## Features/Improvements ## Features/Improvements
* **[Preset Groups]** Add SA-2 with ZSU-23/57 * **[Preset Groups]** Add SA-2 with ZSU-23/57
* **[Campaign Design]** Ability to define almost all possible settings in the campaign's yaml file. * **[Campaign Design]** Ability to define almost all possible settings in the campaign's yaml file.
* **[Campaign Design]** Ability to add roadbases and/or ground spawns to campaigns.
* **[Campaign Design]** Ability to define SCENERY REMOVE OBJECTS ZONE triggers with the roadbase objects in campaign miz. This might not work reliably in multiplayer due to DCS issues. FARPs can be used to remove scenery objects in multiplayer.
* **[Campaign Management]** Improved squadron retreat logic at longer ranges.
* **[Options]** Ability to load & save your settings. * **[Options]** Ability to load & save your settings.
* **[UI]** Added fuel selector in flight's edit window. * **[UI]** Added fuel selector in flight's edit window.
* **[Plugins]** Expose Splash Damage's "game_messages" option and set its default to false. * **[Plugins]** Expose Splash Damage's "game_messages" option and set its default to false.

View File

@ -9,13 +9,14 @@ from uuid import UUID
from dcs import Mission from dcs import Mission
from dcs.countries import CombinedJointTaskForcesBlue, CombinedJointTaskForcesRed from dcs.countries import CombinedJointTaskForcesBlue, CombinedJointTaskForcesRed
from dcs.country import Country from dcs.country import Country
from dcs.planes import F_15C from dcs.planes import F_15C, A_10A, AJS37
from dcs.ships import HandyWind, LHA_Tarawa, Stennis, USS_Arleigh_Burke_IIa from dcs.ships import HandyWind, LHA_Tarawa, Stennis, USS_Arleigh_Burke_IIa
from dcs.statics import Fortification, Warehouse from dcs.statics import Fortification, Warehouse
from dcs.terrain import Airport from dcs.terrain import Airport
from dcs.unitgroup import PlaneGroup, ShipGroup, StaticGroup, VehicleGroup from dcs.unitgroup import PlaneGroup, ShipGroup, StaticGroup, VehicleGroup
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
from game.point_with_heading import PointWithHeading
from game.positioned import Positioned from game.positioned import Positioned
from game.profiling import logged_duration from game.profiling import logged_duration
from game.scenery_group import SceneryGroup from game.scenery_group import SceneryGroup
@ -28,6 +29,7 @@ from game.theater.controlpoint import (
OffMapSpawn, OffMapSpawn,
) )
from game.theater.presetlocation import PresetLocation from game.theater.presetlocation import PresetLocation
from game.utils import Distance, meters, feet, Heading
from game.utils import Distance, meters, feet from game.utils import Distance, meters, feet
if TYPE_CHECKING: if TYPE_CHECKING:
@ -40,6 +42,8 @@ class MizCampaignLoader:
RED_COUNTRY = CombinedJointTaskForcesRed() RED_COUNTRY = CombinedJointTaskForcesRed()
OFF_MAP_UNIT_TYPE = F_15C.id OFF_MAP_UNIT_TYPE = F_15C.id
GROUND_SPAWN_UNIT_TYPE = A_10A.id
GROUND_SPAWN_ROADBASE_UNIT_TYPE = AJS37.id
CV_UNIT_TYPE = Stennis.id CV_UNIT_TYPE = Stennis.id
LHA_UNIT_TYPE = LHA_Tarawa.id LHA_UNIT_TYPE = LHA_Tarawa.id
@ -48,7 +52,7 @@ class MizCampaignLoader:
CP_CONVOY_SPAWN_TYPE = Armor.M1043_HMMWV_Armament.id CP_CONVOY_SPAWN_TYPE = Armor.M1043_HMMWV_Armament.id
FOB_UNIT_TYPE = Unarmed.SKP_11.id FOB_UNIT_TYPE = Unarmed.SKP_11.id
FARP_HELIPADS_TYPE = ["Invisible FARP", "SINGLE_HELIPAD"] FARP_HELIPADS_TYPE = ["Invisible FARP", "SINGLE_HELIPAD", "FARP"]
OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id
SHIP_UNIT_TYPE = USS_Arleigh_Burke_IIa.id SHIP_UNIT_TYPE = USS_Arleigh_Burke_IIa.id
@ -96,6 +100,8 @@ class MizCampaignLoader:
STRIKE_TARGET_UNIT_TYPE = Fortification.Tech_combine.id STRIKE_TARGET_UNIT_TYPE = Fortification.Tech_combine.id
GROUND_SPAWN_WAYPOINT_DISTANCE = 1000
def __init__(self, miz: Path, theater: ConflictTheater) -> None: def __init__(self, miz: Path, theater: ConflictTheater) -> None:
self.theater = theater self.theater = theater
self.mission = Mission() self.mission = Mission()
@ -224,6 +230,18 @@ class MizCampaignLoader:
if group.units[0].type in self.FARP_HELIPADS_TYPE: if group.units[0].type in self.FARP_HELIPADS_TYPE:
yield group yield group
@property
def ground_spawns_roadbase(self) -> Iterator[PlaneGroup]:
for group in itertools.chain(self.blue.plane_group, self.red.plane_group):
if group.units[0].type == self.GROUND_SPAWN_ROADBASE_UNIT_TYPE:
yield group
@property
def ground_spawns(self) -> Iterator[PlaneGroup]:
for group in itertools.chain(self.blue.plane_group, self.red.plane_group):
if group.units[0].type == self.GROUND_SPAWN_UNIT_TYPE:
yield group
@property @property
def factories(self) -> Iterator[StaticGroup]: def factories(self) -> Iterator[StaticGroup]:
for group in self.blue.static_group: for group in self.blue.static_group:
@ -362,6 +380,37 @@ class MizCampaignLoader:
closest = spawn closest = spawn
return closest return closest
@staticmethod
def _add_helipad(helipads: list[PointWithHeading], static: StaticGroup) -> None:
helipads.append(
PointWithHeading.from_point(
static.position, Heading.from_degrees(static.units[0].heading)
)
)
def _add_ground_spawn(
self,
ground_spawns: list[tuple[PointWithHeading, Point]],
plane_group: PlaneGroup,
) -> None:
if len(plane_group.points) >= 2:
first_waypoint = plane_group.points[1].position
else:
first_waypoint = plane_group.position.point_from_heading(
plane_group.units[0].heading,
self.GROUND_SPAWN_WAYPOINT_DISTANCE,
)
ground_spawns.append(
(
PointWithHeading.from_point(
plane_group.position,
Heading.from_degrees(plane_group.units[0].heading),
),
first_waypoint,
)
)
def add_supply_routes(self) -> None: def add_supply_routes(self) -> None:
for group in self.front_line_path_groups: for group in self.front_line_path_groups:
# The unit will have its first waypoint at the source CP and the final # The unit will have its first waypoint at the source CP and the final
@ -475,7 +524,20 @@ class MizCampaignLoader:
for static in self.helipads: for static in self.helipads:
closest, distance = self.objective_info(static) closest, distance = self.objective_info(static)
closest.helipads.append(PresetLocation.from_group(static)) if static.units[0].type == "SINGLE_HELIPAD":
self._add_helipad(closest.helipads, static)
elif static.units[0].type == "FARP":
self._add_helipad(closest.helipads_quad, static)
else:
self._add_helipad(closest.helipads_invisible, static)
for plane_group in self.ground_spawns_roadbase:
closest, distance = self.objective_info(plane_group)
self._add_ground_spawn(closest.ground_spawns_roadbase, plane_group)
for plane_group in self.ground_spawns:
closest, distance = self.objective_info(plane_group)
self._add_ground_spawn(closest.ground_spawns, plane_group)
for static in self.factories: for static in self.factories:
closest, distance = self.objective_info(static) closest, distance = self.objective_info(static)

View File

@ -13,6 +13,7 @@ from game.theater import (
FrontLine, FrontLine,
MissionTarget, MissionTarget,
OffMapSpawn, OffMapSpawn,
ParkingType,
) )
from game.theater.theatergroundobject import ( from game.theater.theatergroundobject import (
BuildingGroundObject, BuildingGroundObject,
@ -161,11 +162,21 @@ class ObjectiveFinder:
break break
def oca_targets(self, min_aircraft: int) -> Iterator[ControlPoint]: def oca_targets(self, min_aircraft: int) -> Iterator[ControlPoint]:
parking_type = ParkingType()
parking_type.include_rotary_wing = True
parking_type.include_fixed_wing = True
parking_type.include_fixed_wing_stol = True
airfields = [] airfields = []
for control_point in self.enemy_control_points(): for control_point in self.enemy_control_points():
if not isinstance(control_point, Airfield): if not isinstance(control_point, Airfield) and not isinstance(
control_point, Fob
):
continue continue
if control_point.allocated_aircraft().total_present >= min_aircraft: if (
control_point.allocated_aircraft(parking_type).total_present
>= min_aircraft
):
airfields.append(control_point) airfields.append(control_point)
return self._targets_by_range(airfields) return self._targets_by_range(airfields)

View File

@ -90,6 +90,8 @@ class TurnState(Enum):
class Game: class Game:
scenery_clear_zones: List[Point]
def __init__( def __init__(
self, self,
player_faction: Faction, player_faction: Faction,

View File

@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Any
from dcs.countries import countries_by_name from dcs.countries import countries_by_name
from game.ato.packagewaypoints import PackageWaypoints from game.ato.packagewaypoints import PackageWaypoints
from game.data.doctrine import MODERN_DOCTRINE, COLDWAR_DOCTRINE, WWII_DOCTRINE from game.data.doctrine import MODERN_DOCTRINE, COLDWAR_DOCTRINE, WWII_DOCTRINE
from game.theater import SeasonalConditions from game.theater import ParkingType, SeasonalConditions
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
@ -110,9 +110,13 @@ class Migrator:
s.country = countries_by_name[c]() s.country = countries_by_name[c]()
# code below is used to fix corruptions wrt overpopulation # code below is used to fix corruptions wrt overpopulation
if s.owned_aircraft < 0 or s.location.unclaimed_parking() < 0: parking_type = ParkingType().from_squadron(s)
if (
s.owned_aircraft < 0
or s.location.unclaimed_parking(parking_type) < 0
):
s.owned_aircraft = max( s.owned_aircraft = max(
0, s.location.unclaimed_parking() + s.owned_aircraft 0, s.location.unclaimed_parking(parking_type) + s.owned_aircraft
) )
def _update_factions(self) -> None: def _update_factions(self) -> None:

View File

@ -3,8 +3,9 @@ from __future__ import annotations
import logging import logging
from datetime import datetime from datetime import datetime
from functools import cached_property from functools import cached_property
from typing import Any, Dict, List, TYPE_CHECKING from typing import Any, Dict, List, TYPE_CHECKING, Tuple
from dcs import Point
from dcs.action import AITaskPush from dcs.action import AITaskPush
from dcs.condition import FlagIsTrue, GroupDead, Or, FlagIsFalse from dcs.condition import FlagIsTrue, GroupDead, Or, FlagIsFalse
from dcs.country import Country from dcs.country import Country
@ -54,7 +55,9 @@ class AircraftGenerator:
laser_code_registry: LaserCodeRegistry, laser_code_registry: LaserCodeRegistry,
unit_map: UnitMap, unit_map: UnitMap,
mission_data: MissionData, mission_data: MissionData,
helipads: dict[ControlPoint, StaticGroup], helipads: dict[ControlPoint, list[StaticGroup]],
ground_spawns_roadbase: dict[ControlPoint, list[Tuple[StaticGroup, Point]]],
ground_spawns: dict[ControlPoint, list[Tuple[StaticGroup, Point]]],
) -> None: ) -> None:
self.mission = mission self.mission = mission
self.settings = settings self.settings = settings
@ -67,6 +70,8 @@ class AircraftGenerator:
self.flights: List[FlightData] = [] self.flights: List[FlightData] = []
self.mission_data = mission_data self.mission_data = mission_data
self.helipads = helipads self.helipads = helipads
self.ground_spawns_roadbase = ground_spawns_roadbase
self.ground_spawns = ground_spawns
self.ewrj_package_dict: Dict[int, List[FlyingGroup[Any]]] = {} self.ewrj_package_dict: Dict[int, List[FlyingGroup[Any]]] = {}
self.ewrj = settings.plugins.get("ewrj") self.ewrj = settings.plugins.get("ewrj")
@ -186,7 +191,13 @@ class AircraftGenerator:
flight.state = Completed(flight, self.game.settings) flight.state = Completed(flight, self.game.settings)
group = FlightGroupSpawner( group = FlightGroupSpawner(
flight, country, self.mission, self.helipads, self.mission_data flight,
country,
self.mission,
self.helipads,
self.ground_spawns_roadbase,
self.ground_spawns,
self.mission_data,
).create_idle_aircraft() ).create_idle_aircraft()
AircraftPainter(flight, group).apply_livery() AircraftPainter(flight, group).apply_livery()
self.unit_map.add_aircraft(group, flight) self.unit_map.add_aircraft(group, flight)
@ -196,7 +207,13 @@ class AircraftGenerator:
) -> FlyingGroup[Any]: ) -> FlyingGroup[Any]:
"""Creates and configures the flight group in the mission.""" """Creates and configures the flight group in the mission."""
group = FlightGroupSpawner( group = FlightGroupSpawner(
flight, country, self.mission, self.helipads, self.mission_data flight,
country,
self.mission,
self.helipads,
self.ground_spawns_roadbase,
self.ground_spawns,
self.mission_data,
).create_flight_group() ).create_flight_group()
self.flights.append( self.flights.append(
FlightGroupConfigurator( FlightGroupConfigurator(
@ -216,10 +233,10 @@ class AircraftGenerator:
wpt = group.waypoint("LANDING") wpt = group.waypoint("LANDING")
if flight.is_helo and isinstance(flight.arrival, Fob) and wpt: if flight.is_helo and isinstance(flight.arrival, Fob) and wpt:
hpad = self.helipads[flight.arrival].units.pop(0) hpad = self.helipads[flight.arrival].pop(0)
wpt.helipad_id = hpad.id wpt.helipad_id = hpad.units[0].id
wpt.link_unit = hpad.id wpt.link_unit = hpad.units[0].id
self.helipads[flight.arrival].units.append(hpad) self.helipads[flight.arrival].append(hpad)
if self.ewrj: if self.ewrj:
self._track_ewrj_flight(flight, group) self._track_ewrj_flight(flight, group)

View File

@ -29,6 +29,8 @@ from .flightdata import FlightData
from .waypoints import WaypointGenerator from .waypoints import WaypointGenerator
from ...ato.flightplans.aewc import AewcFlightPlan from ...ato.flightplans.aewc import AewcFlightPlan
from ...ato.flightplans.theaterrefueling import TheaterRefuelingFlightPlan from ...ato.flightplans.theaterrefueling import TheaterRefuelingFlightPlan
from ...ato.flightwaypointtype import FlightWaypointType
from ...theater import Fob
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
@ -103,6 +105,19 @@ class FlightGroupConfigurator:
self.mission_data, self.mission_data,
).create_waypoints() ).create_waypoints()
# Special handling for landing waypoints when:
# 1. It's an AI-only flight
# 2. Aircraft are not helicopters/VTOL
# 3. Landing waypoint does not point to an airfield
if (
self.flight.client_count < 1
and not self.flight.unit_type.helicopter
and not self.flight.unit_type.lha_capable
and isinstance(self.flight.squadron.location, Fob)
):
# Need to set uncontrolled to false, otherwise the AI will skip the mission and just land
self.group.uncontrolled = False
return FlightData( return FlightData(
package=self.flight.package, package=self.flight.package,
aircraft_type=self.flight.unit_type, aircraft_type=self.flight.unit_type,

View File

@ -1,10 +1,10 @@
import logging import logging
import random import random
from typing import Any, Union from typing import Any, Union, Tuple, Optional
from dcs import Mission from dcs import Mission
from dcs.country import Country from dcs.country import Country
from dcs.mapping import Vector2 from dcs.mapping import Vector2, Point
from dcs.mission import StartType as DcsStartType from dcs.mission import StartType as DcsStartType
from dcs.planes import F_14A, Su_33 from dcs.planes import F_14A, Su_33
from dcs.point import PointAction from dcs.point import PointAction
@ -47,13 +47,17 @@ class FlightGroupSpawner:
flight: Flight, flight: Flight,
country: Country, country: Country,
mission: Mission, mission: Mission,
helipads: dict[ControlPoint, StaticGroup], helipads: dict[ControlPoint, list[StaticGroup]],
ground_spawns_roadbase: dict[ControlPoint, list[Tuple[StaticGroup, Point]]],
ground_spawns: dict[ControlPoint, list[Tuple[StaticGroup, Point]]],
mission_data: MissionData, mission_data: MissionData,
) -> None: ) -> None:
self.flight = flight self.flight = flight
self.country = country self.country = country
self.mission = mission self.mission = mission
self.helipads = helipads self.helipads = helipads
self.ground_spawns_roadbase = ground_spawns_roadbase
self.ground_spawns = ground_spawns
self.mission_data = mission_data self.mission_data = mission_data
def create_flight_group(self) -> FlyingGroup[Any]: def create_flight_group(self) -> FlyingGroup[Any]:
@ -88,11 +92,11 @@ class FlightGroupSpawner:
return grp return grp
def create_idle_aircraft(self) -> FlyingGroup[Any]: def create_idle_aircraft(self) -> FlyingGroup[Any]:
airport = self.flight.squadron.location.dcs_airport assert isinstance(self.flight.squadron.location, Airfield)
assert airport is not None airfield = self.flight.squadron.location
group = self._generate_at_airport( group = self._generate_at_airfield(
name=namegen.next_aircraft_name(self.country, self.flight), name=namegen.next_aircraft_name(self.country, self.flight),
airport=airport, airfield=airfield,
) )
group.uncontrolled = True group.uncontrolled = True
@ -121,18 +125,47 @@ class FlightGroupSpawner:
elif isinstance(cp, Fob): elif isinstance(cp, Fob):
is_heli = self.flight.squadron.aircraft.helicopter is_heli = self.flight.squadron.aircraft.helicopter
is_vtol = not is_heli and self.flight.squadron.aircraft.lha_capable is_vtol = not is_heli and self.flight.squadron.aircraft.lha_capable
if not is_heli and not is_vtol: if not is_heli and not is_vtol and not cp.has_ground_spawns:
raise RuntimeError( raise RuntimeError(
f"Cannot spawn non-VTOL aircraft at {cp} because it is a FOB" f"Cannot spawn fixed-wing aircraft at {cp} because of insufficient ground spawn slots."
) )
pilot_count = len(self.flight.roster.pilots) pilot_count = len(self.flight.roster.pilots)
if is_vtol and self.flight.roster.player_count != pilot_count: if (
not is_heli
and self.flight.roster.player_count != pilot_count
and not self.flight.coalition.game.settings.ground_start_ai_planes
):
raise RuntimeError( raise RuntimeError(
f"VTOL aircraft at {cp} must be piloted by humans exclusively." f"Fixed-wing aircraft at {cp} must be piloted by humans exclusively because"
f' the "AI fixed-wing aircraft can use roadbases / bases with only ground'
f' spawns" setting is currently disabled.'
) )
return self._generate_at_cp_helipad(name, cp) if cp.has_helipads and (is_heli or is_vtol):
pad_group = self._generate_at_cp_helipad(name, cp)
if pad_group is not None:
return pad_group
if cp.has_ground_spawns and (self.flight.client_count > 0 or is_heli):
pad_group = self._generate_at_cp_ground_spawn(name, cp)
if pad_group is not None:
return pad_group
return self._generate_over_departure(name, cp)
elif isinstance(cp, Airfield): elif isinstance(cp, Airfield):
return self._generate_at_airport(name, cp.airport) is_heli = self.flight.squadron.aircraft.helicopter
if cp.has_helipads and is_heli:
pad_group = self._generate_at_cp_helipad(name, cp)
if pad_group is not None:
return pad_group
if (
cp.has_ground_spawns
and len(self.ground_spawns[cp])
+ len(self.ground_spawns_roadbase[cp])
>= self.flight.count
and (self.flight.client_count > 0 or is_heli)
):
pad_group = self._generate_at_cp_ground_spawn(name, cp)
if pad_group is not None:
return pad_group
return self._generate_at_airfield(name, cp)
else: else:
raise NotImplementedError( raise NotImplementedError(
f"Aircraft spawn behavior not implemented for {cp} ({cp.__class__})" f"Aircraft spawn behavior not implemented for {cp} ({cp.__class__})"
@ -185,7 +218,7 @@ class FlightGroupSpawner:
group.points[0].alt_type = alt_type group.points[0].alt_type = alt_type
return group return group
def _generate_at_airport(self, name: str, airport: Airport) -> FlyingGroup[Any]: def _generate_at_airfield(self, name: str, airfield: Airfield) -> FlyingGroup[Any]:
# TODO: Delayed runway starts should be converted to air starts for multiplayer. # TODO: Delayed runway starts should be converted to air starts for multiplayer.
# Runway starts do not work with late activated aircraft in multiplayer. Instead # Runway starts do not work with late activated aircraft in multiplayer. Instead
# of spawning on the runway the aircraft will spawn on the taxiway, potentially # of spawning on the runway the aircraft will spawn on the taxiway, potentially
@ -193,13 +226,14 @@ class FlightGroupSpawner:
# starts or (less likely) downgrade to warm starts to avoid the issue when the # starts or (less likely) downgrade to warm starts to avoid the issue when the
# player is generating the mission for multiplayer (which would need a new # player is generating the mission for multiplayer (which would need a new
# option). # option).
self.flight.unit_type.dcs_unit_type.load_payloads()
return self.mission.flight_group_from_airport( return self.mission.flight_group_from_airport(
country=self.country, country=self.country,
name=name, name=name,
aircraft_type=self.flight.unit_type.dcs_unit_type, aircraft_type=self.flight.unit_type.dcs_unit_type,
airport=airport, airport=airfield.airport,
maintask=None, maintask=None,
start_type=self.dcs_start_type(), start_type=self._start_type_at_airfield(airfield),
group_size=self.flight.count, group_size=self.flight.count,
parking_slots=None, parking_slots=None,
) )
@ -253,26 +287,84 @@ class FlightGroupSpawner:
group_size=self.flight.count, group_size=self.flight.count,
) )
def _generate_at_cp_helipad(self, name: str, cp: ControlPoint) -> FlyingGroup[Any]: def _generate_at_cp_helipad(
self, name: str, cp: ControlPoint
) -> Optional[FlyingGroup[Any]]:
try: try:
helipad = self.helipads[cp] helipad = self.helipads[cp].pop()
except IndexError: except IndexError as ex:
raise NoParkingSlotError() logging.warning("Not enough helipads available at " + str(ex))
if isinstance(cp, Airfield):
return self._generate_at_airfield(name, cp)
else:
return None
# raise RuntimeError(f"Not enough helipads available at {cp}") from ex
group = self._generate_at_group(name, helipad) group = self._generate_at_group(name, helipad)
group.points[0].type = "TakeOffGround" # Note : A bit dirty, need better support in pydcs
group.points[0].action = PointAction.FromGroundArea group.points[0].action = PointAction.FromGroundArea
group.points[0].type = "TakeOffGround"
if self.start_type is StartType.WARM: group.units[0].heading = helipad.units[0].heading
group.points[0].type = "TakeOffGroundHot" if self.start_type is not StartType.COLD:
group.points[0].action = PointAction.FromGroundAreaHot group.points[0].action = PointAction.FromGroundAreaHot
hpad = helipad.units[0] group.points[0].type = "TakeOffGroundHot"
for i in range(self.flight.count):
pos = cp.helipads.pop(0) for i in range(self.flight.count - 1):
group.units[i].position = pos try:
group.units[i].heading = hpad.heading helipad = self.helipads[cp].pop()
cp.helipads.append(pos) terrain = cp.coalition.game.theater.terrain
group.units[1 + i].position = Point(
helipad.x, helipad.y, terrain=terrain
)
group.units[1 + i].heading = helipad.units[0].heading
except IndexError as ex:
logging.warning("Not enough helipads available at " + str(ex))
if isinstance(cp, Airfield):
return self._generate_at_airfield(name, cp)
else:
return None
return group
def _generate_at_cp_ground_spawn(
self, name: str, cp: ControlPoint
) -> Optional[FlyingGroup[Any]]:
try:
if len(self.ground_spawns_roadbase[cp]) > 0:
ground_spawn = self.ground_spawns_roadbase[cp].pop()
else:
ground_spawn = self.ground_spawns[cp].pop()
except IndexError as ex:
logging.warning("Not enough STOL slots available at " + str(ex))
return None
# raise RuntimeError(f"Not enough STOL slots available at {cp}") from ex
group = self._generate_at_group(name, ground_spawn[0])
# Note : A bit dirty, need better support in pydcs
group.points[0].action = PointAction.FromGroundArea
group.points[0].type = "TakeOffGround"
group.units[0].heading = ground_spawn[0].units[0].heading
try:
cp.coalition.game.scenery_clear_zones
except AttributeError:
cp.coalition.game.scenery_clear_zones = []
cp.coalition.game.scenery_clear_zones.append(ground_spawn[1])
for i in range(self.flight.count - 1):
try:
terrain = cp.coalition.game.theater.terrain
if len(self.ground_spawns_roadbase[cp]) > 0:
ground_spawn = self.ground_spawns_roadbase[cp].pop()
else:
ground_spawn = self.ground_spawns[cp].pop()
group.units[1 + i].position = Point(
ground_spawn[0].x, ground_spawn[0].y, terrain=terrain
)
group.units[1 + i].heading = ground_spawn[0].units[0].heading
except IndexError as ex:
raise RuntimeError(f"Not enough STOL slots available at {cp}") from ex
return group return group
def dcs_start_type(self) -> DcsStartType: def dcs_start_type(self) -> DcsStartType:
@ -284,6 +376,12 @@ class FlightGroupSpawner:
return DcsStartType.Warm return DcsStartType.Warm
raise ValueError(f"There is no pydcs StartType matching {self.start_type}") raise ValueError(f"There is no pydcs StartType matching {self.start_type}")
def _start_type_at_airfield(
self,
airfield: Airfield,
) -> DcsStartType:
return self.dcs_start_type()
def _start_type_at_group( def _start_type_at_group(
self, self,
at: Union[ShipGroup, StaticGroup], at: Union[ShipGroup, StaticGroup],

View File

@ -9,11 +9,12 @@ from game.missiongenerator.frontlineconflictdescription import (
) )
# Misc config settings for objects drawn in ME mission file (and F10 map) # Misc config settings for objects drawn in ME mission file (and F10 map)
from game.theater import TRIGGER_RADIUS_CAPTURE
FRONTLINE_COLORS = Rgba(255, 0, 0, 255) FRONTLINE_COLORS = Rgba(255, 0, 0, 255)
WHITE = Rgba(255, 255, 255, 255) WHITE = Rgba(255, 255, 255, 255)
CP_RED = Rgba(255, 0, 0, 80) CP_RED = Rgba(255, 0, 0, 80)
CP_BLUE = Rgba(0, 0, 255, 80) CP_BLUE = Rgba(0, 0, 255, 80)
CP_CIRCLE_RADIUS = 2500
BLUE_PATH_COLOR = Rgba(0, 0, 255, 100) BLUE_PATH_COLOR = Rgba(0, 0, 255, 100)
RED_PATH_COLOR = Rgba(255, 0, 0, 100) RED_PATH_COLOR = Rgba(255, 0, 0, 100)
ACTIVE_PATH_COLOR = Rgba(255, 80, 80, 100) ACTIVE_PATH_COLOR = Rgba(255, 80, 80, 100)
@ -40,7 +41,7 @@ class DrawingsGenerator:
color = CP_RED color = CP_RED
shape = self.player_layer.add_circle( shape = self.player_layer.add_circle(
cp.position, cp.position,
CP_CIRCLE_RADIUS, TRIGGER_RADIUS_CAPTURE,
line_thickness=2, line_thickness=2,
color=WHITE, color=WHITE,
fill=color, fill=color,

View File

@ -243,6 +243,8 @@ class MissionGenerator:
self.unit_map, self.unit_map,
mission_data=self.mission_data, mission_data=self.mission_data,
helipads=tgo_generator.helipads, helipads=tgo_generator.helipads,
ground_spawns_roadbase=tgo_generator.ground_spawns_roadbase,
ground_spawns=tgo_generator.ground_spawns,
) )
aircraft_generator.clear_parking_slots() aircraft_generator.clear_parking_slots()

View File

@ -9,13 +9,16 @@ from __future__ import annotations
import logging import logging
import random import random
from typing import Any, Dict, Iterator, List, Optional, TYPE_CHECKING, Type from collections import defaultdict
from typing import Any, Dict, Iterator, List, Optional, TYPE_CHECKING, Type, Tuple
import dcs.vehicles import dcs.vehicles
from dcs import Mission, Point from dcs import Mission, Point, unitgroup
from dcs.action import DoScript, SceneryDestructionZone from dcs.action import DoScript, SceneryDestructionZone
from dcs.condition import MapObjectIsDead from dcs.condition import MapObjectIsDead
from dcs.countries import *
from dcs.country import Country from dcs.country import Country
from dcs.point import StaticPoint, PointAction
from dcs.ships import ( from dcs.ships import (
CVN_71, CVN_71,
CVN_72, CVN_72,
@ -37,16 +40,17 @@ from dcs.task import (
) )
from dcs.translation import String from dcs.translation import String
from dcs.triggers import Event, TriggerOnce, TriggerStart, TriggerZone from dcs.triggers import Event, TriggerOnce, TriggerStart, TriggerZone
from dcs.unit import Unit, InvisibleFARP, BaseFARP from dcs.unit import Unit, InvisibleFARP, BaseFARP, SingleHeliPad, FARP
from dcs.unitgroup import MovingGroup, ShipGroup, StaticGroup, VehicleGroup from dcs.unitgroup import MovingGroup, ShipGroup, StaticGroup, VehicleGroup
from dcs.unittype import ShipType, VehicleType from dcs.unittype import ShipType, VehicleType
from dcs.vehicles import vehicle_map from dcs.vehicles import vehicle_map, Unarmed
from game.missiongenerator.groundforcepainter import ( from game.missiongenerator.groundforcepainter import (
NavalForcePainter, NavalForcePainter,
GroundForcePainter, GroundForcePainter,
) )
from game.missiongenerator.missiondata import CarrierInfo, MissionData from game.missiongenerator.missiondata import CarrierInfo, MissionData
from game.point_with_heading import PointWithHeading
from game.radio.RadioFrequencyContainer import RadioFrequencyContainer from game.radio.RadioFrequencyContainer import RadioFrequencyContainer
from game.radio.radios import RadioFrequency, RadioRegistry from game.radio.radios import RadioFrequency, RadioRegistry
from game.radio.tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage from game.radio.tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage
@ -74,6 +78,161 @@ FARP_FRONTLINE_DISTANCE = 10000
AA_CP_MIN_DISTANCE = 40000 AA_CP_MIN_DISTANCE = 40000
def farp_truck_types_for_country(
country_id: int,
) -> Tuple[Type[VehicleType], Type[VehicleType]]:
soviet_tankers: List[Type[VehicleType]] = [
Unarmed.ATMZ_5,
Unarmed.ATZ_10,
Unarmed.ATZ_5,
Unarmed.ATZ_60_Maz,
Unarmed.TZ_22_KrAZ,
]
soviet_trucks: List[Type[VehicleType]] = [
Unarmed.S_75_ZIL,
Unarmed.GAZ_3308,
Unarmed.GAZ_66,
Unarmed.KAMAZ_Truck,
Unarmed.KrAZ6322,
Unarmed.Ural_375,
Unarmed.Ural_375_PBU,
Unarmed.Ural_4320_31,
Unarmed.Ural_4320T,
Unarmed.ZIL_135,
]
axis_trucks: List[Type[VehicleType]] = [Unarmed.Blitz_36_6700A]
us_tankers: List[Type[VehicleType]] = [Unarmed.M978_HEMTT_Tanker]
us_trucks: List[Type[VehicleType]] = [Unarmed.M_818]
uk_trucks: List[Type[VehicleType]] = [Unarmed.Bedford_MWD]
if country_id in [
Abkhazia.id,
Algeria.id,
Bahrain.id,
Belarus.id,
Belgium.id,
Bulgaria.id,
China.id,
Croatia.id,
Cuba.id,
Cyprus.id,
CzechRepublic.id,
Egypt.id,
Ethiopia.id,
Finland.id,
GDR.id,
Georgia.id,
Ghana.id,
Greece.id,
Hungary.id,
India.id,
Insurgents.id,
Iraq.id,
Jordan.id,
Kazakhstan.id,
Lebanon.id,
Libya.id,
Morocco.id,
Nigeria.id,
NorthKorea.id,
Poland.id,
Romania.id,
Russia.id,
Serbia.id,
Slovakia.id,
Slovenia.id,
SouthAfrica.id,
SouthOssetia.id,
Sudan.id,
Syria.id,
Tunisia.id,
USSR.id,
Ukraine.id,
Venezuela.id,
Vietnam.id,
Yemen.id,
Yugoslavia.id,
]:
tanker_type = random.choice(soviet_tankers)
ammo_truck_type = random.choice(soviet_trucks)
elif country_id in [ItalianSocialRepublic.id, ThirdReich.id]:
tanker_type = random.choice(soviet_tankers)
ammo_truck_type = random.choice(axis_trucks)
elif country_id in [
Argentina.id,
Australia.id,
Austria.id,
Bolivia.id,
Brazil.id,
Canada.id,
Chile.id,
Denmark.id,
Ecuador.id,
France.id,
Germany.id,
Honduras.id,
Indonesia.id,
Iran.id,
Israel.id,
Italy.id,
Japan.id,
Kuwait.id,
Malaysia.id,
Mexico.id,
Norway.id,
Oman.id,
Pakistan.id,
Peru.id,
Philippines.id,
Portugal.id,
Qatar.id,
SaudiArabia.id,
SouthKorea.id,
Spain.id,
Sweden.id,
Switzerland.id,
Thailand.id,
TheNetherlands.id,
Turkey.id,
USA.id,
USAFAggressors.id,
UnitedArabEmirates.id,
]:
tanker_type = random.choice(us_tankers)
ammo_truck_type = random.choice(us_trucks)
elif country_id in [UK.id]:
tanker_type = random.choice(us_tankers)
ammo_truck_type = random.choice(uk_trucks)
elif country_id in [CombinedJointTaskForcesBlue.id]:
tanker_types = us_tankers
truck_types = us_trucks + uk_trucks
tanker_type = random.choice(tanker_types)
ammo_truck_type = random.choice(truck_types)
elif country_id in [CombinedJointTaskForcesRed.id]:
tanker_types = us_tankers
truck_types = us_trucks + uk_trucks
tanker_type = random.choice(tanker_types)
ammo_truck_type = random.choice(truck_types)
elif country_id in [UnitedNationsPeacekeepers.id]:
tanker_types = soviet_tankers + us_tankers
truck_types = soviet_trucks + us_trucks + uk_trucks
tanker_type = random.choice(tanker_types)
ammo_truck_type = random.choice(truck_types)
else:
tanker_types = soviet_tankers + us_tankers
truck_types = soviet_trucks + us_trucks + uk_trucks + axis_trucks
tanker_type = random.choice(tanker_types)
ammo_truck_type = random.choice(truck_types)
return tanker_type, ammo_truck_type
class GroundObjectGenerator: class GroundObjectGenerator:
"""generates the DCS groups and units from the TheaterGroundObject""" """generates the DCS groups and units from the TheaterGroundObject"""
@ -598,66 +757,321 @@ class HelipadGenerator:
self.game = game self.game = game
self.radio_registry = radio_registry self.radio_registry = radio_registry
self.tacan_registry = tacan_registry self.tacan_registry = tacan_registry
self.helipads: Optional[StaticGroup] = None self.helipads: list[StaticGroup] = []
def create_helipad(
self, i: int, helipad: PointWithHeading, helipad_type: str
) -> None:
# Note: Helipad are generated as neutral object in order not to interfere with
# capture triggers
pad: BaseFARP
neutral_country = self.m.country(self.game.neutral_country.name)
country = self.m.country(
self.game.coalition_for(self.cp.captured).faction.country.name
)
name = f"{self.cp.name} {helipad_type} {i}"
logging.info("Generating helipad static : " + name)
terrain = self.m.terrain
if helipad_type == "SINGLE_HELIPAD":
pad = SingleHeliPad(
unit_id=self.m.next_unit_id(), name=name, terrain=terrain
)
number_of_pads = 1
elif helipad_type == "FARP":
pad = FARP(unit_id=self.m.next_unit_id(), name=name, terrain=terrain)
number_of_pads = 4
else:
pad = InvisibleFARP(
unit_id=self.m.next_unit_id(), name=name, terrain=terrain
)
number_of_pads = 1
pad.position = Point(helipad.x, helipad.y, terrain=terrain)
pad.heading = helipad.heading.degrees
# Set FREQ
if isinstance(self.cp, RadioFrequencyContainer) and self.cp.frequency:
if isinstance(pad, BaseFARP):
pad.heliport_frequency = self.cp.frequency.mhz
sg = unitgroup.StaticGroup(self.m.next_group_id(), name)
sg.add_unit(pad)
sp = StaticPoint(pad.position)
sg.add_point(sp)
neutral_country.add_static_group(sg)
if number_of_pads > 1:
self.append_helipad(pad, name, helipad.heading.degrees, 60, 0, 0)
self.append_helipad(pad, name, helipad.heading.degrees + 180, 20, 0, 0)
self.append_helipad(
pad, name, helipad.heading.degrees + 90, 60, helipad.heading.degrees, 20
)
self.append_helipad(
pad,
name,
helipad.heading.degrees + 90,
60,
helipad.heading.degrees + 180,
60,
)
else:
self.helipads.append(sg)
# Generate a FARP Ammo and Fuel stack for each pad
self.m.static_group(
country=country,
name=(name + "_fuel"),
_type=Fortification.FARP_Fuel_Depot,
position=pad.position.point_from_heading(helipad.heading.degrees, 35),
heading=pad.heading + 180,
)
self.m.static_group(
country=country,
name=(name + "_ammo"),
_type=Fortification.FARP_Ammo_Dump_Coating,
position=pad.position.point_from_heading(
helipad.heading.degrees, 35
).point_from_heading(helipad.heading.degrees + 90, 10),
heading=pad.heading + 90,
)
self.m.static_group(
country=country,
name=(name + "_ws"),
_type=Fortification.Windsock,
position=helipad.point_from_heading(helipad.heading.degrees + 45, 35),
heading=pad.heading,
)
def append_helipad(
self,
pad: BaseFARP,
name: str,
heading_1: int,
distance_1: int,
heading_2: int,
distance_2: int,
) -> None:
new_pad = InvisibleFARP(pad._terrain)
new_pad.position = pad.position.point_from_heading(heading_1, distance_1)
new_pad.position = new_pad.position.point_from_heading(heading_2, distance_2)
sg = unitgroup.StaticGroup(self.m.next_group_id(), name)
sg.add_unit(new_pad)
self.helipads.append(sg)
def generate(self) -> None: def generate(self) -> None:
# This gets called for every control point, so we don't want to add an empty group (causes DCS mission editor to crash)
if len(self.cp.helipads) == 0:
return
# Note: Helipad are generated as neutral object in order not to interfer with
# capture triggers
country = self.m.country(self.cp.coalition.faction.country.name)
for i, helipad in enumerate(self.cp.helipads): for i, helipad in enumerate(self.cp.helipads):
heading = helipad.heading.degrees self.create_helipad(i, helipad, "SINGLE_HELIPAD")
name_i = self.cp.name + "_helipad" + "_" + str(i) for i, helipad in enumerate(self.cp.helipads_quad):
if self.helipads is None: self.create_helipad(i, helipad, "FARP")
self.helipads = self.m.farp( for i, helipad in enumerate(self.cp.helipads_invisible):
self.m.country(self.game.neutral_country.name), self.create_helipad(i, helipad, "Invisible FARP")
name_i,
helipad,
farp_type="InvisibleFARP",
)
else:
# Create a new Helipad Unit
self.helipads.add_unit(
InvisibleFARP(self.m.terrain, self.m.next_unit_id(), name_i)
)
# Set FREQ
if isinstance(self.cp, RadioFrequencyContainer) and self.cp.frequency:
for hp in self.helipads.units:
if isinstance(hp, BaseFARP):
hp.heliport_frequency = self.cp.frequency.mhz
pad = self.helipads.units[-1] class GroundSpawnRoadbaseGenerator:
pad.position = helipad """
pad.heading = heading Generates Highway strip starting positions for given control point
# Generate a FARP Ammo and Fuel stack for each pad """
self.m.static_group(
def __init__(
self,
mission: Mission,
cp: ControlPoint,
game: Game,
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,
):
self.m = mission
self.cp = cp
self.game = game
self.radio_registry = radio_registry
self.tacan_registry = tacan_registry
self.ground_spawns_roadbase: list[Tuple[StaticGroup, Point]] = []
def create_ground_spawn_roadbase(
self, i: int, ground_spawn: Tuple[PointWithHeading, Point]
) -> None:
# Note: FARPs are generated as neutral object in order not to interfere with
# capture triggers
neutral_country = self.m.country(self.game.neutral_country.name)
country = self.m.country(
self.game.coalition_for(self.cp.captured).faction.country.name
)
terrain = self.cp.coalition.game.theater.terrain
name = f"{self.cp.name} roadbase spawn {i}"
logging.info("Generating Roadbase Spawn static : " + name)
pad = InvisibleFARP(unit_id=self.m.next_unit_id(), name=name, terrain=terrain)
pad.position = Point(ground_spawn[0].x, ground_spawn[0].y, terrain=terrain)
pad.heading = ground_spawn[0].heading.degrees
sg = unitgroup.StaticGroup(self.m.next_group_id(), name)
sg.add_unit(pad)
sp = StaticPoint(pad.position)
sg.add_point(sp)
neutral_country.add_static_group(sg)
self.ground_spawns_roadbase.append((sg, ground_spawn[1]))
# tanker_type: Type[VehicleType]
# ammo_truck_type: Type[VehicleType]
tanker_type, ammo_truck_type = farp_truck_types_for_country(country.id)
# Generate ammo truck/farp and fuel truck/stack for each pad
if self.game.settings.ground_start_trucks_roadbase:
self.m.vehicle_group(
country=country, country=country,
name=(name_i + "_fuel"), name=(name + "_fuel"),
_type=Fortification.FARP_Fuel_Depot, _type=tanker_type,
position=helipad.point_from_heading(heading, 35), position=pad.position.point_from_heading(
heading=heading, ground_spawn[0].heading.degrees + 90, 35
)
self.m.static_group(
country=country,
name=(name_i + "_ammo"),
_type=Fortification.FARP_Ammo_Dump_Coating,
position=helipad.point_from_heading(heading, 35).point_from_heading(
heading + 90, 10
), ),
heading=heading, group_size=1,
heading=pad.heading + 315,
move_formation=PointAction.OffRoad,
)
self.m.vehicle_group(
country=country,
name=(name + "_ammo"),
_type=ammo_truck_type,
position=pad.position.point_from_heading(
ground_spawn[0].heading.degrees + 90, 35
).point_from_heading(ground_spawn[0].heading.degrees + 180, 10),
group_size=1,
heading=pad.heading + 315,
move_formation=PointAction.OffRoad,
)
else:
self.m.static_group(
country=country,
name=(name + "_fuel"),
_type=Fortification.FARP_Fuel_Depot,
position=pad.position.point_from_heading(
ground_spawn[0].heading.degrees + 90, 35
),
heading=pad.heading + 270,
) )
self.m.static_group( self.m.static_group(
country=country, country=country,
name=(name_i + "_ws"), name=(name + "_ammo"),
_type=Fortification.Windsock, _type=Fortification.FARP_Ammo_Dump_Coating,
position=helipad.point_from_heading(heading + 45, 35), position=pad.position.point_from_heading(
heading=heading, ground_spawn[0].heading.degrees + 90, 35
).point_from_heading(ground_spawn[0].heading.degrees + 180, 10),
heading=pad.heading + 180,
) )
def generate(self) -> None:
try:
for i, ground_spawn in enumerate(self.cp.ground_spawns_roadbase):
self.create_ground_spawn_roadbase(i, ground_spawn)
except AttributeError:
self.ground_spawns_roadbase = []
class GroundSpawnGenerator:
"""
Generates STOL aircraft starting positions for given control point
"""
def __init__(
self,
mission: Mission,
cp: ControlPoint,
game: Game,
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,
):
self.m = mission
self.cp = cp
self.game = game
self.radio_registry = radio_registry
self.tacan_registry = tacan_registry
self.ground_spawns: list[Tuple[StaticGroup, Point]] = []
def create_ground_spawn(
self, i: int, vtol_pad: Tuple[PointWithHeading, Point]
) -> None:
# Note: FARPs are generated as neutral object in order not to interfere with
# capture triggers
neutral_country = self.m.country(self.game.neutral_country.name)
country = self.m.country(
self.game.coalition_for(self.cp.captured).faction.country.name
)
terrain = self.cp.coalition.game.theater.terrain
name = f"{self.cp.name} ground spawn {i}"
logging.info("Generating Ground Spawn static : " + name)
pad = InvisibleFARP(unit_id=self.m.next_unit_id(), name=name, terrain=terrain)
pad.position = Point(vtol_pad[0].x, vtol_pad[0].y, terrain=terrain)
pad.heading = vtol_pad[0].heading.degrees
sg = unitgroup.StaticGroup(self.m.next_group_id(), name)
sg.add_unit(pad)
sp = StaticPoint(pad.position)
sg.add_point(sp)
neutral_country.add_static_group(sg)
self.ground_spawns.append((sg, vtol_pad[1]))
# tanker_type: Type[VehicleType]
# ammo_truck_type: Type[VehicleType]
tanker_type, ammo_truck_type = farp_truck_types_for_country(country.id)
# Generate a FARP Ammo and Fuel stack for each pad
if self.game.settings.ground_start_trucks:
self.m.vehicle_group(
country=country,
name=(name + "_fuel"),
_type=tanker_type,
position=pad.position.point_from_heading(
vtol_pad[0].heading.degrees - 175, 35
),
group_size=1,
heading=pad.heading + 45,
move_formation=PointAction.OffRoad,
)
self.m.vehicle_group(
country=country,
name=(name + "_ammo"),
_type=ammo_truck_type,
position=pad.position.point_from_heading(
vtol_pad[0].heading.degrees - 185, 35
),
group_size=1,
heading=pad.heading + 45,
move_formation=PointAction.OffRoad,
)
else:
self.m.static_group(
country=country,
name=(name + "_fuel"),
_type=Fortification.FARP_Fuel_Depot,
position=pad.position.point_from_heading(
vtol_pad[0].heading.degrees - 180, 45
),
heading=pad.heading,
)
self.m.static_group(
country=country,
name=(name + "_ammo"),
_type=Fortification.FARP_Ammo_Dump_Coating,
position=pad.position.point_from_heading(
vtol_pad[0].heading.degrees - 180, 35
),
heading=pad.heading + 270,
)
def generate(self) -> None:
try:
for i, vtol_pad in enumerate(self.cp.ground_spawns):
self.create_ground_spawn(i, vtol_pad)
except AttributeError:
self.ground_spawns = []
class TgoGenerator: class TgoGenerator:
"""Creates DCS groups and statics for the theater during mission generation. """Creates DCS groups and statics for the theater during mission generation.
@ -684,7 +1098,13 @@ class TgoGenerator:
self.unit_map = unit_map self.unit_map = unit_map
self.icls_alloc = iter(range(1, 21)) self.icls_alloc = iter(range(1, 21))
self.runways: Dict[str, RunwayData] = {} self.runways: Dict[str, RunwayData] = {}
self.helipads: dict[ControlPoint, StaticGroup] = {} self.helipads: dict[ControlPoint, list[StaticGroup]] = defaultdict(list)
self.ground_spawns_roadbase: dict[
ControlPoint, list[Tuple[StaticGroup, Point]]
] = defaultdict(list)
self.ground_spawns: dict[
ControlPoint, list[Tuple[StaticGroup, Point]]
] = defaultdict(list)
self.mission_data = mission_data self.mission_data = mission_data
def generate(self) -> None: def generate(self) -> None:
@ -696,8 +1116,25 @@ class TgoGenerator:
self.m, cp, self.game, self.radio_registry, self.tacan_registry self.m, cp, self.game, self.radio_registry, self.tacan_registry
) )
helipad_gen.generate() helipad_gen.generate()
if helipad_gen.helipads is not None: self.helipads[cp] = helipad_gen.helipads
self.helipads[cp] = helipad_gen.helipads
# Generate Highway Strip slots
ground_spawn_roadbase_gen = GroundSpawnRoadbaseGenerator(
self.m, cp, self.game, self.radio_registry, self.tacan_registry
)
ground_spawn_roadbase_gen.generate()
self.ground_spawns_roadbase[
cp
] = ground_spawn_roadbase_gen.ground_spawns_roadbase
random.shuffle(self.ground_spawns_roadbase[cp])
# Generate STOL pads
ground_spawn_gen = GroundSpawnGenerator(
self.m, cp, self.game, self.radio_registry, self.tacan_registry
)
ground_spawn_gen.generate()
self.ground_spawns[cp] = ground_spawn_gen.ground_spawns
random.shuffle(self.ground_spawns[cp])
for ground_object in cp.ground_objects: for ground_object in cp.ground_objects:
generator: GroundObjectGenerator generator: GroundObjectGenerator

View File

@ -1,14 +1,26 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING import logging
from typing import TYPE_CHECKING, List
from dcs.action import ClearFlag, DoScript, MarkToAll, SetFlag from dcs import Point
from dcs.action import (
ClearFlag,
DoScript,
MarkToAll,
SetFlag,
RemoveSceneObjects,
RemoveSceneObjectsMask,
SceneryDestructionZone,
Smoke,
)
from dcs.condition import ( from dcs.condition import (
AllOfCoalitionOutsideZone, AllOfCoalitionOutsideZone,
FlagIsFalse, FlagIsFalse,
FlagIsTrue, FlagIsTrue,
PartOfCoalitionInZone, PartOfCoalitionInZone,
TimeAfter, TimeAfter,
TimeSinceFlag,
) )
from dcs.mission import Mission from dcs.mission import Mission
from dcs.task import Option from dcs.task import Option
@ -17,7 +29,7 @@ from dcs.triggers import Event, TriggerCondition, TriggerOnce
from dcs.unit import Skill from dcs.unit import Skill
from game.theater import Airfield from game.theater import Airfield
from game.theater.controlpoint import Fob from game.theater.controlpoint import Fob, TRIGGER_RADIUS_CAPTURE
if TYPE_CHECKING: if TYPE_CHECKING:
from game.game import Game from game.game import Game
@ -37,6 +49,7 @@ TRIGGER_RADIUS_SMALL = 50000
TRIGGER_RADIUS_MEDIUM = 100000 TRIGGER_RADIUS_MEDIUM = 100000
TRIGGER_RADIUS_LARGE = 150000 TRIGGER_RADIUS_LARGE = 150000
TRIGGER_RADIUS_ALL_MAP = 3000000 TRIGGER_RADIUS_ALL_MAP = 3000000
TRIGGER_RADIUS_CLEAR_SCENERY = 1000
class Silence(Option): class Silence(Option):
@ -133,6 +146,37 @@ class TriggerGenerator:
v += 1 v += 1
self.mission.triggerrules.triggers.append(mark_trigger) self.mission.triggerrules.triggers.append(mark_trigger)
def _generate_clear_statics_trigger(self, scenery_clear_zones: List[Point]) -> None:
for zone_center in scenery_clear_zones:
trigger_zone = self.mission.triggers.add_triggerzone(
zone_center,
radius=TRIGGER_RADIUS_CLEAR_SCENERY,
hidden=False,
name="CLEAR",
)
clear_trigger = TriggerCondition(Event.NoEvent, "Clear Trigger")
clear_flag = self.get_capture_zone_flag()
clear_trigger.add_condition(TimeSinceFlag(clear_flag, 30))
clear_trigger.add_action(ClearFlag(clear_flag))
clear_trigger.add_action(SetFlag(clear_flag))
clear_trigger.add_action(
RemoveSceneObjects(
objects_mask=RemoveSceneObjectsMask.OBJECTS_ONLY,
zone=trigger_zone.id,
)
)
clear_trigger.add_action(
SceneryDestructionZone(destruction_level=100, zone=trigger_zone.id)
)
self.mission.triggerrules.triggers.append(clear_trigger)
enable_clear_trigger = TriggerOnce(Event.NoEvent, "Enable Clear Trigger")
enable_clear_trigger.add_condition(TimeAfter(30))
enable_clear_trigger.add_action(ClearFlag(clear_flag))
enable_clear_trigger.add_action(SetFlag(clear_flag))
# clear_trigger.add_action(MessageToAll(text=String("Enable clear trigger"),))
self.mission.triggerrules.triggers.append(enable_clear_trigger)
def _generate_capture_triggers( def _generate_capture_triggers(
self, player_coalition: str, enemy_coalition: str self, player_coalition: str, enemy_coalition: str
) -> None: ) -> None:
@ -154,7 +198,10 @@ class TriggerGenerator:
defend_coalition_int = 1 defend_coalition_int = 1
trigger_zone = self.mission.triggers.add_triggerzone( trigger_zone = self.mission.triggers.add_triggerzone(
cp.position, radius=3000, hidden=False, name="CAPTURE" cp.position,
radius=TRIGGER_RADIUS_CAPTURE,
hidden=False,
name="CAPTURE",
) )
flag = self.get_capture_zone_flag() flag = self.get_capture_zone_flag()
capture_trigger = TriggerCondition(Event.NoEvent, "Capture Trigger") capture_trigger = TriggerCondition(Event.NoEvent, "Capture Trigger")
@ -203,6 +250,11 @@ class TriggerGenerator:
self._set_allegiances(player_coalition, enemy_coalition) self._set_allegiances(player_coalition, enemy_coalition)
self._gen_markers() self._gen_markers()
self._generate_capture_triggers(player_coalition, enemy_coalition) self._generate_capture_triggers(player_coalition, enemy_coalition)
try:
self._generate_clear_statics_trigger(self.game.scenery_clear_zones)
self.game.scenery_clear_zones.clear()
except AttributeError:
logging.info(f"Unable to create Clear Statics triggers")
@classmethod @classmethod
def get_capture_zone_flag(cls) -> int: def get_capture_zone_flag(cls) -> int:

View File

@ -8,7 +8,7 @@ from typing import Iterator, List, Optional, TYPE_CHECKING, Tuple
from game.config import RUNWAY_REPAIR_COST from game.config import RUNWAY_REPAIR_COST
from game.data.units import UnitClass from game.data.units import UnitClass
from game.dcs.groundunittype import GroundUnitType from game.dcs.groundunittype import GroundUnitType
from game.theater import ControlPoint, MissionTarget from game.theater import ControlPoint, MissionTarget, ParkingType
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
@ -63,12 +63,16 @@ class ProcurementAi:
if len(self.faction.aircrafts) == 0 or len(self.air_wing.squadrons) == 0: if len(self.faction.aircrafts) == 0 or len(self.air_wing.squadrons) == 0:
return 1 return 1
parking_type = ParkingType(
fixed_wing=True, fixed_wing_stol=True, rotary_wing=True
)
for cp in self.owned_points: for cp in self.owned_points:
cp_ground_units = cp.allocated_ground_units( cp_ground_units = cp.allocated_ground_units(
self.game.coalition_for(self.is_player).transfers self.game.coalition_for(self.is_player).transfers
) )
armor_investment += cp_ground_units.total_value armor_investment += cp_ground_units.total_value
cp_aircraft = cp.allocated_aircraft() cp_aircraft = cp.allocated_aircraft(parking_type)
aircraft_investment += cp_aircraft.total_value aircraft_investment += cp_aircraft.total_value
air = self.game.settings.auto_procurement_balance / 100.0 air = self.game.settings.auto_procurement_balance / 100.0
@ -232,11 +236,13 @@ class ProcurementAi:
for squadron in self.air_wing.best_squadrons_for( for squadron in self.air_wing.best_squadrons_for(
request.near, request.task_capability, request.number, this_turn=False request.near, request.task_capability, request.number, this_turn=False
): ):
parking_type = ParkingType().from_squadron(squadron)
if not squadron.can_provide_pilots(request.number): if not squadron.can_provide_pilots(request.number):
continue continue
if not squadron.has_aircraft_capacity_for(request.number): if squadron.location.unclaimed_parking(parking_type) < request.number:
continue continue
if squadron.location.unclaimed_parking() < request.number: if not squadron.has_aircraft_capacity_for(request.number):
continue continue
if self.threat_zones.threatened(squadron.location.position): if self.threat_zones.threatened(squadron.location.position):
threatened.append(squadron) threatened.append(squadron)

View File

@ -7,7 +7,7 @@ from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType from game.dcs.groundunittype import GroundUnitType
from game.dcs.unittype import UnitType from game.dcs.unittype import UnitType
from game.squadrons import Squadron from game.squadrons import Squadron
from game.theater import ControlPoint from game.theater import ControlPoint, ParkingType
ItemType = TypeVar("ItemType") ItemType = TypeVar("ItemType")
@ -109,9 +109,11 @@ class AircraftPurchaseAdapter(PurchaseAdapter[Squadron]):
return item.owned_aircraft return item.owned_aircraft
def can_buy(self, item: Squadron) -> bool: def can_buy(self, item: Squadron) -> bool:
parking_type = ParkingType().from_squadron(item)
unclaimed_parking = self.control_point.unclaimed_parking(parking_type)
return ( return (
super().can_buy(item) super().can_buy(item)
and self.control_point.unclaimed_parking() > 0 and unclaimed_parking > 0
and item.has_aircraft_capacity_for(1) and item.has_aircraft_capacity_for(1)
) )

View File

@ -565,6 +565,30 @@ class Settings:
'Use this to allow spectators when disabling "Allow external views".' 'Use this to allow spectators when disabling "Allow external views".'
), ),
) )
ground_start_ai_planes: bool = boolean_option(
"AI fixed-wing aircraft can use roadbases / bases with only ground spawns",
MISSION_GENERATOR_PAGE,
GAMEPLAY_SECTION,
default=False,
detail=(
"If enabled, AI can use roadbases or airbases which only have ground spawns."
"AI will always air-start from these bases (due to DCS limitation)."
),
)
ground_start_trucks: bool = boolean_option(
"Spawn trucks at ground spawns in airbases instead of FARP statics",
MISSION_GENERATOR_PAGE,
GAMEPLAY_SECTION,
default=False,
detail=("Might have a negative performance impact."),
)
ground_start_trucks_roadbase: bool = boolean_option(
"Spawn trucks at ground spawns in roadbases instead of FARP statics",
MISSION_GENERATOR_PAGE,
GAMEPLAY_SECTION,
default=False,
detail=("Might have a negative performance impact."),
)
# Performance # Performance
perf_smoke_gen: bool = boolean_option( perf_smoke_gen: bool = boolean_option(

View File

@ -20,7 +20,7 @@ if TYPE_CHECKING:
from game import Game from game import Game
from game.coalition import Coalition from game.coalition import Coalition
from game.dcs.aircrafttype import AircraftType from game.dcs.aircrafttype import AircraftType
from game.theater import ControlPoint, MissionTarget from game.theater import ControlPoint, MissionTarget, ParkingType
from .operatingbases import OperatingBases from .operatingbases import OperatingBases
from .squadrondef import SquadronDef from .squadrondef import SquadronDef
@ -172,7 +172,10 @@ class Squadron:
raise ValueError("Squadrons can only be created with active pilots.") raise ValueError("Squadrons can only be created with active pilots.")
self._recruit_pilots(self.settings.squadron_pilot_limit) self._recruit_pilots(self.settings.squadron_pilot_limit)
if squadrons_start_full: if squadrons_start_full:
self.owned_aircraft = min(self.max_size, self.location.unclaimed_parking()) parking_type = ParkingType().from_squadron(self)
self.owned_aircraft = min(
self.max_size, self.location.unclaimed_parking(parking_type)
)
def end_turn(self) -> None: def end_turn(self) -> None:
if self.destination is not None: if self.destination is not None:
@ -326,9 +329,14 @@ class Squadron:
self.destination = None self.destination = None
def cancel_overflow_orders(self) -> None: def cancel_overflow_orders(self) -> None:
from game.theater import ParkingType
if self.pending_deliveries <= 0: if self.pending_deliveries <= 0:
return return
overflow = -self.location.unclaimed_parking() parking_type = ParkingType().from_aircraft(
self.aircraft, self.coalition.game.settings.ground_start_ai_planes
)
overflow = -self.location.unclaimed_parking(parking_type)
if overflow > 0: if overflow > 0:
sell_count = min(overflow, self.pending_deliveries) sell_count = min(overflow, self.pending_deliveries)
logging.debug( logging.debug(
@ -356,6 +364,8 @@ class Squadron:
return self.location if self.destination is None else self.destination return self.location if self.destination is None else self.destination
def plan_relocation(self, destination: ControlPoint) -> None: def plan_relocation(self, destination: ControlPoint) -> None:
from game.theater import ParkingType
if destination == self.location: if destination == self.location:
logging.warning( logging.warning(
f"Attempted to plan relocation of {self} to current location " f"Attempted to plan relocation of {self} to current location "
@ -369,7 +379,8 @@ class Squadron:
) )
return return
if self.expected_size_next_turn > destination.unclaimed_parking(): parking_type = ParkingType().from_squadron(self)
if self.expected_size_next_turn > destination.unclaimed_parking(parking_type):
raise RuntimeError(f"Not enough parking for {self} at {destination}.") raise RuntimeError(f"Not enough parking for {self} at {destination}.")
if not destination.can_operate(self.aircraft): if not destination.can_operate(self.aircraft):
raise RuntimeError(f"{self} cannot operate at {destination}.") raise RuntimeError(f"{self} cannot operate at {destination}.")
@ -377,6 +388,8 @@ class Squadron:
self.replan_ferry_flights() self.replan_ferry_flights()
def cancel_relocation(self) -> None: def cancel_relocation(self) -> None:
from game.theater import ParkingType
if self.destination is None: if self.destination is None:
logging.warning( logging.warning(
f"Attempted to cancel relocation of squadron with no transfer order. " f"Attempted to cancel relocation of squadron with no transfer order. "
@ -384,7 +397,10 @@ class Squadron:
) )
return return
if self.expected_size_next_turn >= self.location.unclaimed_parking(): parking_type = ParkingType().from_squadron(self)
if self.expected_size_next_turn >= self.location.unclaimed_parking(
parking_type
):
raise RuntimeError(f"Not enough parking for {self} at {self.location}.") raise RuntimeError(f"Not enough parking for {self} at {self.location}.")
self.destination = None self.destination = None
self.cancel_ferry_flights() self.cancel_ferry_flights()
@ -399,6 +415,7 @@ class Squadron:
for flight in list(package.flights): for flight in list(package.flights):
if flight.squadron == self and flight.flight_type is FlightType.FERRY: if flight.squadron == self and flight.flight_type is FlightType.FERRY:
package.remove_flight(flight) package.remove_flight(flight)
flight.return_pilots_and_aircraft()
if not package.flights: if not package.flights:
self.coalition.ato.remove_package(package) self.coalition.ato.remove_package(package)

View File

@ -94,6 +94,7 @@ if TYPE_CHECKING:
FREE_FRONTLINE_UNIT_SUPPLY: int = 15 FREE_FRONTLINE_UNIT_SUPPLY: int = 15
AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION: int = 12 AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION: int = 12
TRIGGER_RADIUS_CAPTURE = 3000
class ControlPointType(Enum): class ControlPointType(Enum):
@ -315,6 +316,47 @@ class ControlPointStatus(IntEnum):
StartingPosition = Union[ShipGroup, StaticGroup, Airport, Point] StartingPosition = Union[ShipGroup, StaticGroup, Airport, Point]
class ParkingType:
def __init__(
self,
fixed_wing: bool = False,
fixed_wing_stol: bool = False,
rotary_wing: bool = False,
) -> None:
self.include_fixed_wing = fixed_wing
self.include_fixed_wing_stol = fixed_wing_stol
self.include_rotary_wing = rotary_wing
def from_squadron(self, squadron: Squadron) -> ParkingType:
return self.from_aircraft(
squadron.aircraft, squadron.coalition.game.settings.ground_start_ai_planes
)
def from_aircraft(
self, aircraft: AircraftType, ground_start_ai_planes: bool
) -> ParkingType:
if aircraft.helicopter or aircraft.lha_capable:
self.include_rotary_wing = True
self.include_fixed_wing = True
self.include_fixed_wing_stol = True
elif aircraft.flyable or ground_start_ai_planes:
self.include_rotary_wing = False
self.include_fixed_wing = True
self.include_fixed_wing_stol = True
else:
self.include_rotary_wing = False
self.include_fixed_wing = True
self.include_fixed_wing_stol = False
return self
#: Fixed wing aircraft with no STOL or VTOL capability
include_fixed_wing: bool
#: Fixed wing aircraft with STOL capability
include_fixed_wing_stol: bool
#: Helicopters and VTOL aircraft
include_rotary_wing: bool
class ControlPoint(MissionTarget, SidcDescribable, ABC): class ControlPoint(MissionTarget, SidcDescribable, ABC):
# Not sure what distance DCS uses, but assuming it's about 2NM since that's roughly # Not sure what distance DCS uses, but assuming it's about 2NM since that's roughly
# the distance of the circle on the map. # the distance of the circle on the map.
@ -340,6 +382,10 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
self.connected_objectives: List[TheaterGroundObject] = [] self.connected_objectives: List[TheaterGroundObject] = []
self.preset_locations = PresetLocations() self.preset_locations = PresetLocations()
self.helipads: List[PointWithHeading] = [] self.helipads: List[PointWithHeading] = []
self.helipads_quad: List[PointWithHeading] = []
self.helipads_invisible: List[PointWithHeading] = []
self.ground_spawns_roadbase: List[Tuple[PointWithHeading, Point]] = []
self.ground_spawns: List[Tuple[PointWithHeading, Point]] = []
self._coalition: Optional[Coalition] = None self._coalition: Optional[Coalition] = None
self.captured_invert = False self.captured_invert = False
@ -525,7 +571,17 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
""" """
Returns true if cp has helipads Returns true if cp has helipads
""" """
return len(self.helipads) > 0 return (
len(self.helipads) + len(self.helipads_quad) + len(self.helipads_invisible)
> 0
)
@property
def has_ground_spawns(self) -> bool:
"""
Returns true if cp can operate STOL aircraft
"""
return len(self.ground_spawns_roadbase) + len(self.ground_spawns) > 0
def can_recruit_ground_units(self, game: Game) -> bool: def can_recruit_ground_units(self, game: Game) -> bool:
"""Returns True if this control point is capable of recruiting ground units.""" """Returns True if this control point is capable of recruiting ground units."""
@ -590,9 +646,8 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
def can_deploy_ground_units(self) -> bool: def can_deploy_ground_units(self) -> bool:
... ...
@property
@abstractmethod @abstractmethod
def total_aircraft_parking(self) -> int: def total_aircraft_parking(self, parking_type: ParkingType) -> int:
""" """
:return: The maximum number of aircraft that can be stored in this :return: The maximum number of aircraft that can be stored in this
control point control point
@ -761,17 +816,22 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
self, squadron: Squadron self, squadron: Squadron
) -> Optional[ControlPoint]: ) -> Optional[ControlPoint]:
closest = ObjectiveDistanceCache.get_closest_airfields(self) closest = ObjectiveDistanceCache.get_closest_airfields(self)
max_retreat_distance = squadron.aircraft.max_mission_range # Multiply the max mission range by two when evaluating retreats,
# since you only need to fly one way in that case
max_retreat_distance = squadron.aircraft.max_mission_range * 2
# Skip the first airbase because that's the airbase we're retreating # Skip the first airbase because that's the airbase we're retreating
# from. # from.
airfields = list(closest.operational_airfields_within(max_retreat_distance))[1:] airfields = list(closest.operational_airfields_within(max_retreat_distance))[1:]
not_preferred: Optional[ControlPoint] = None not_preferred: Optional[ControlPoint] = None
overfull: list[ControlPoint] = [] overfull: list[ControlPoint] = []
parking_type = ParkingType().from_squadron(squadron)
for airbase in airfields: for airbase in airfields:
if airbase.captured != self.captured: if airbase.captured != self.captured:
continue continue
if airbase.unclaimed_parking() < squadron.owned_aircraft: if airbase.unclaimed_parking(parking_type) < squadron.owned_aircraft:
if airbase.can_operate(squadron.aircraft): if airbase.can_operate(squadron.aircraft):
overfull.append(airbase) overfull.append(airbase)
continue continue
@ -798,7 +858,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
loss_count = math.inf loss_count = math.inf
for airbase in overfull: for airbase in overfull:
overflow = -( overflow = -(
airbase.unclaimed_parking() airbase.unclaimed_parking(parking_type)
- squadron.owned_aircraft - squadron.owned_aircraft
- squadron.pending_deliveries - squadron.pending_deliveries
) )
@ -813,10 +873,13 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
squadron.refund_orders() squadron.refund_orders()
self.capture_aircraft(game, squadron.aircraft, squadron.owned_aircraft) self.capture_aircraft(game, squadron.aircraft, squadron.owned_aircraft)
return return
parking_type = ParkingType().from_squadron(squadron)
logging.debug(f"{squadron} retreating to {destination} from {self}") logging.debug(f"{squadron} retreating to {destination} from {self}")
squadron.relocate_to(destination) squadron.relocate_to(destination)
squadron.cancel_overflow_orders() squadron.cancel_overflow_orders()
overflow = -destination.unclaimed_parking() overflow = -destination.unclaimed_parking(parking_type)
if overflow > 0: if overflow > 0:
logging.debug( logging.debug(
f"Not enough room for {squadron} at {destination}. Capturing " f"Not enough room for {squadron} at {destination}. Capturing "
@ -869,8 +932,11 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
def can_operate(self, aircraft: AircraftType) -> bool: def can_operate(self, aircraft: AircraftType) -> bool:
... ...
def unclaimed_parking(self) -> int: def unclaimed_parking(self, parking_type: ParkingType) -> int:
return self.total_aircraft_parking - self.allocated_aircraft().total return (
self.total_aircraft_parking(parking_type)
- self.allocated_aircraft(parking_type).total
)
@abstractmethod @abstractmethod
def active_runway( def active_runway(
@ -932,17 +998,33 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
u.position.x = u.position.x + delta.x u.position.x = u.position.x + delta.x
u.position.y = u.position.y + delta.y u.position.y = u.position.y + delta.y
def allocated_aircraft(self) -> AircraftAllocations: def allocated_aircraft(self, parking_type: ParkingType) -> AircraftAllocations:
present: dict[AircraftType, int] = defaultdict(int) present: dict[AircraftType, int] = defaultdict(int)
on_order: dict[AircraftType, int] = defaultdict(int) on_order: dict[AircraftType, int] = defaultdict(int)
transferring: dict[AircraftType, int] = defaultdict(int) transferring: dict[AircraftType, int] = defaultdict(int)
for squadron in self.squadrons: for squadron in self.squadrons:
if not parking_type.include_rotary_wing and (
squadron.aircraft.helicopter or squadron.aircraft.lha_capable
):
continue
elif not parking_type.include_fixed_wing and (
not squadron.aircraft.helicopter or squadron.aircraft.lha_capable
):
continue
present[squadron.aircraft] += squadron.owned_aircraft present[squadron.aircraft] += squadron.owned_aircraft
if squadron.destination is None: if squadron.destination is None:
on_order[squadron.aircraft] += squadron.pending_deliveries on_order[squadron.aircraft] += squadron.pending_deliveries
else: else:
transferring[squadron.aircraft] -= squadron.owned_aircraft transferring[squadron.aircraft] -= squadron.owned_aircraft
for squadron in self.coalition.air_wing.iter_squadrons(): for squadron in self.coalition.air_wing.iter_squadrons():
if not parking_type.include_rotary_wing and (
squadron.aircraft.helicopter or squadron.aircraft.lha_capable
):
continue
elif not parking_type.include_fixed_wing and (
not squadron.aircraft.helicopter or squadron.aircraft.lha_capable
):
continue
if squadron.destination == self: if squadron.destination == self:
on_order[squadron.aircraft] += squadron.pending_deliveries on_order[squadron.aircraft] += squadron.pending_deliveries
transferring[squadron.aircraft] += squadron.owned_aircraft transferring[squadron.aircraft] += squadron.owned_aircraft
@ -1093,11 +1175,14 @@ class Airfield(ControlPoint, CTLD):
# Needs ground spawns just like helos do, but also need to be able to # Needs ground spawns just like helos do, but also need to be able to
# limit takeoff weight to ~20500 lbs or it won't be able to take off. # limit takeoff weight to ~20500 lbs or it won't be able to take off.
# return false if aircraft is fixed wing and airport has no runways parking_type = ParkingType().from_aircraft(
if not aircraft.helicopter and not self.airport.runways: aircraft, self.coalition.game.settings.ground_start_ai_planes
return False )
else: if parking_type.include_rotary_wing and self.has_helipads:
return self.runway_is_operational() return True
if parking_type.include_fixed_wing_stol and self.has_ground_spawns:
return True
return self.runway_is_operational()
def mission_types(self, for_player: bool) -> Iterator[FlightType]: def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from game.ato import FlightType from game.ato import FlightType
@ -1120,14 +1205,25 @@ class Airfield(ControlPoint, CTLD):
yield FlightType.REFUELING yield FlightType.REFUELING
@property def total_aircraft_parking(self, parking_type: ParkingType) -> int:
def total_aircraft_parking(self) -> int:
""" """
Return total aircraft parking slots available Return total aircraft parking slots available
Note : additional helipads shouldn't contribute to this score as it could allow airfield Note : additional helipads shouldn't contribute to this score as it could allow airfield
to buy more planes than what they are able to host to buy more planes than what they are able to host
""" """
return len(self.airport.parking_slots) parking_slots = 0
if parking_type.include_rotary_wing:
parking_slots += (
len(self.helipads)
+ 4 * len(self.helipads_quad)
+ len(self.helipads_invisible)
)
if parking_type.include_fixed_wing_stol:
parking_slots += len(self.ground_spawns)
parking_slots += len(self.ground_spawns_roadbase)
if parking_type.include_fixed_wing:
parking_slots += len(self.airport.parking_slots)
return parking_slots
@property @property
def heading(self) -> Heading: def heading(self) -> Heading:
@ -1317,8 +1413,7 @@ class Carrier(NavalControlPoint):
def can_operate(self, aircraft: AircraftType) -> bool: def can_operate(self, aircraft: AircraftType) -> bool:
return aircraft.carrier_capable return aircraft.carrier_capable
@property def total_aircraft_parking(self, parking_type: ParkingType) -> int:
def total_aircraft_parking(self) -> int:
return 90 return 90
@property @property
@ -1348,8 +1443,7 @@ class Lha(NavalControlPoint):
def can_operate(self, aircraft: AircraftType) -> bool: def can_operate(self, aircraft: AircraftType) -> bool:
return aircraft.lha_capable return aircraft.lha_capable
@property def total_aircraft_parking(self, parking_type: ParkingType) -> int:
def total_aircraft_parking(self) -> int:
return 20 return 20
@property @property
@ -1383,8 +1477,7 @@ class OffMapSpawn(ControlPoint):
def mission_types(self, for_player: bool) -> Iterator[FlightType]: def mission_types(self, for_player: bool) -> Iterator[FlightType]:
yield from [] yield from []
@property def total_aircraft_parking(self, parking_type: ParkingType) -> int:
def total_aircraft_parking(self) -> int:
return 1000 return 1000
def can_operate(self, aircraft: AircraftType) -> bool: def can_operate(self, aircraft: AircraftType) -> bool:
@ -1444,7 +1537,7 @@ class Fob(ControlPoint, RadioFrequencyContainer, CTLD):
return SymbolSet.LAND_INSTALLATIONS, LandInstallationEntity.MILITARY_BASE return SymbolSet.LAND_INSTALLATIONS, LandInstallationEntity.MILITARY_BASE
def runway_is_operational(self) -> bool: def runway_is_operational(self) -> bool:
return self.has_helipads return self.has_helipads or self.has_ground_spawns
def active_runway( def active_runway(
self, self,
@ -1470,17 +1563,33 @@ class Fob(ControlPoint, RadioFrequencyContainer, CTLD):
yield from super().mission_types(for_player) yield from super().mission_types(for_player)
@property def total_aircraft_parking(self, parking_type: ParkingType) -> int:
def total_aircraft_parking(self) -> int: parking_slots = 0
return len(self.helipads) if parking_type.include_rotary_wing:
parking_slots += (
len(self.helipads)
+ 4 * len(self.helipads_quad)
+ len(self.helipads_invisible)
)
try:
if parking_type.include_fixed_wing_stol:
parking_slots += len(self.ground_spawns)
parking_slots += len(self.ground_spawns_roadbase)
except AttributeError:
self.ground_spawns_roadbase = []
self.ground_spawns = []
return parking_slots
def can_operate(self, aircraft: AircraftType) -> bool: def can_operate(self, aircraft: AircraftType) -> bool:
# FOBs and FARPs are the same class, distinguished only by non-FARP FOBs having parking_type = ParkingType().from_aircraft(
# zero parking. aircraft, self.coalition.game.settings.ground_start_ai_planes
# https://github.com/dcs-liberation/dcs_liberation/issues/2378 )
return ( if parking_type.include_rotary_wing and self.has_helipads:
aircraft.helicopter or aircraft.lha_capable return True
) and self.total_aircraft_parking > 0 if parking_type.include_fixed_wing_stol and self.has_ground_spawns:
return True
return False
@property @property
def heading(self) -> Heading: def heading(self) -> Heading:

View File

@ -48,7 +48,7 @@ from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType from game.dcs.groundunittype import GroundUnitType
from game.naming import namegen from game.naming import namegen
from game.procurement import AircraftProcurementRequest from game.procurement import AircraftProcurementRequest
from game.theater import ControlPoint, MissionTarget from game.theater import ControlPoint, MissionTarget, ParkingType, Carrier, Airfield
from game.theater.transitnetwork import ( from game.theater.transitnetwork import (
TransitConnection, TransitConnection,
TransitNetwork, TransitNetwork,
@ -728,8 +728,17 @@ class PendingTransfers:
self.order_airlift_assets_at(control_point) self.order_airlift_assets_at(control_point)
def desired_airlift_capacity(self, control_point: ControlPoint) -> int: def desired_airlift_capacity(self, control_point: ControlPoint) -> int:
parking_type = ParkingType()
parking_type.include_rotary_wing = True
if isinstance(control_point, Airfield) or isinstance(control_point, Carrier):
parking_type.include_fixed_wing = True
parking_type.include_fixed_wing_stol = True
else:
parking_type.include_fixed_wing = False
parking_type.include_fixed_wing_stol = False
if control_point.has_factory: if control_point.has_factory:
is_major_hub = control_point.total_aircraft_parking > 0 is_major_hub = control_point.total_aircraft_parking(parking_type) > 0
# Check if there is a CP which is only reachable via Airlift # Check if there is a CP which is only reachable via Airlift
transit_network = self.network_for(control_point) transit_network = self.network_for(control_point)
for cp in self.game.theater.control_points_for(self.player): for cp in self.game.theater.control_points_for(self.player):
@ -750,7 +759,8 @@ class PendingTransfers:
if ( if (
is_major_hub is_major_hub
and cp.has_factory and cp.has_factory
and cp.total_aircraft_parking > control_point.total_aircraft_parking and cp.total_aircraft_parking(parking_type)
> control_point.total_aircraft_parking(parking_type)
): ):
is_major_hub = False is_major_hub = False
@ -769,7 +779,16 @@ class PendingTransfers:
) )
def order_airlift_assets_at(self, control_point: ControlPoint) -> None: def order_airlift_assets_at(self, control_point: ControlPoint) -> None:
unclaimed_parking = control_point.unclaimed_parking() parking_type = ParkingType()
parking_type.include_rotary_wing = True
if isinstance(control_point, Airfield) or isinstance(control_point, Carrier):
parking_type.include_fixed_wing = True
parking_type.include_fixed_wing_stol = True
else:
parking_type.include_fixed_wing = False
parking_type.include_fixed_wing_stol = False
unclaimed_parking = control_point.unclaimed_parking(parking_type)
# Buy a maximum of unclaimed_parking only to prevent that aircraft procurement # Buy a maximum of unclaimed_parking only to prevent that aircraft procurement
# take place at another base # take place at another base
gap = min( gap = min(

View File

@ -22,10 +22,11 @@ from dcs.unittype import FlyingType
from game.ato.flightplans.custom import CustomFlightPlan from game.ato.flightplans.custom import CustomFlightPlan
from game.ato.flighttype import FlightType from game.ato.flighttype import FlightType
from game.ato.flightwaypointtype import FlightWaypointType from game.ato.flightwaypointtype import FlightWaypointType
from game.dcs.aircrafttype import AircraftType
from game.server import EventStream from game.server import EventStream
from game.sim import GameUpdateEvents from game.sim import GameUpdateEvents
from game.squadrons import Pilot, Squadron from game.squadrons import Pilot, Squadron
from game.theater import ConflictTheater, ControlPoint from game.theater import ConflictTheater, ControlPoint, ParkingType
from qt_ui.delegates import TwoColumnRowDelegate from qt_ui.delegates import TwoColumnRowDelegate
from qt_ui.errorreporter import report_errors from qt_ui.errorreporter import report_errors
from qt_ui.models import AtoModel, SquadronModel from qt_ui.models import AtoModel, SquadronModel
@ -105,7 +106,8 @@ class SquadronDestinationComboBox(QComboBox):
self.squadron = squadron self.squadron = squadron
self.theater = theater self.theater = theater
room = squadron.location.unclaimed_parking() parking_type = ParkingType().from_squadron(squadron)
room = squadron.location.unclaimed_parking(parking_type)
self.addItem( self.addItem(
f"Remain at {squadron.location} (room for {room} more aircraft)", f"Remain at {squadron.location} (room for {room} more aircraft)",
squadron.location, squadron.location,
@ -142,11 +144,20 @@ class SquadronDestinationComboBox(QComboBox):
self.setCurrentIndex(selected_index) self.setCurrentIndex(selected_index)
def iter_destinations(self) -> Iterator[ControlPoint]: def iter_destinations(self) -> Iterator[ControlPoint]:
size = self.squadron.expected_size_next_turn
parking_type = ParkingType().from_squadron(self.squadron)
for control_point in self.theater.control_points_for(self.squadron.player): for control_point in self.theater.control_points_for(self.squadron.player):
if control_point == self.squadron.location: if control_point == self.squadron.location:
continue continue
if not control_point.can_operate(self.squadron.aircraft): if not control_point.can_operate(self.squadron.aircraft):
continue continue
ac_type = self.squadron.aircraft.dcs_unit_type
if (
self.squadron.destination is not control_point
and control_point.unclaimed_parking(parking_type) < size
and self.calculate_parking_slots(control_point, ac_type) < size
):
continue
yield control_point yield control_point
@staticmethod @staticmethod
@ -156,14 +167,39 @@ class SquadronDestinationComboBox(QComboBox):
if cp.dcs_airport: if cp.dcs_airport:
ap = deepcopy(cp.dcs_airport) ap = deepcopy(cp.dcs_airport)
overflow = [] overflow = []
parking_type = ParkingType(
fixed_wing=False, fixed_wing_stol=False, rotary_wing=True
)
free_helicopter_slots = cp.total_aircraft_parking(parking_type)
parking_type = ParkingType(
fixed_wing=False, fixed_wing_stol=True, rotary_wing=False
)
free_ground_spawns = cp.total_aircraft_parking(parking_type)
for s in cp.squadrons: for s in cp.squadrons:
for count in range(s.owned_aircraft): for count in range(s.owned_aircraft):
slot = ap.free_parking_slot(s.aircraft.dcs_unit_type) is_heli = s.aircraft.helicopter
if slot: is_vtol = not is_heli and s.aircraft.lha_capable
slot.unit_id = id(s) + count count_ground_spawns = (
s.aircraft.flyable
or cp.coalition.game.settings.ground_start_ai_planes
)
if free_helicopter_slots > 0 and (is_heli or is_vtol):
free_helicopter_slots = -1
elif free_ground_spawns > 0 and (
is_heli or is_vtol or count_ground_spawns
):
free_ground_spawns = -1
else: else:
overflow.append(s) slot = ap.free_parking_slot(s.aircraft.dcs_unit_type)
break if slot:
slot.unit_id = id(s) + count
else:
overflow.append(s)
break
if overflow: if overflow:
overflow_msg = "" overflow_msg = ""
for s in overflow: for s in overflow:
@ -178,7 +214,11 @@ class SquadronDestinationComboBox(QComboBox):
) )
return len(ap.free_parking_slots(dcs_unit_type)) return len(ap.free_parking_slots(dcs_unit_type))
else: else:
return cp.unclaimed_parking() parking_type = ParkingType().from_aircraft(
next(AircraftType.for_dcs_type(dcs_unit_type)),
cp.coalition.game.settings.ground_start_ai_planes,
)
return cp.unclaimed_parking(parking_type)
class SquadronDialog(QDialog): class SquadronDialog(QDialog):

View File

@ -25,6 +25,7 @@ from game.theater import (
ControlPointType, ControlPointType,
FREE_FRONTLINE_UNIT_SUPPLY, FREE_FRONTLINE_UNIT_SUPPLY,
NavalControlPoint, NavalControlPoint,
ParkingType,
) )
from qt_ui.dialogs import Dialog from qt_ui.dialogs import Dialog
from qt_ui.models import GameModel from qt_ui.models import GameModel
@ -237,8 +238,26 @@ class QBaseMenu2(QDialog):
self.repair_button.setDisabled(True) self.repair_button.setDisabled(True)
def update_intel_summary(self) -> None: def update_intel_summary(self) -> None:
aircraft = self.cp.allocated_aircraft().total_present parking_type_all = ParkingType(
parking = self.cp.total_aircraft_parking fixed_wing=True, fixed_wing_stol=True, rotary_wing=True
)
aircraft = self.cp.allocated_aircraft(parking_type_all).total_present
parking = self.cp.total_aircraft_parking(parking_type_all)
parking_type_fixed_wing = ParkingType(
fixed_wing=True, fixed_wing_stol=False, rotary_wing=False
)
parking_type_stol = ParkingType(
fixed_wing=False, fixed_wing_stol=True, rotary_wing=False
)
parking_type_rotary_wing = ParkingType(
fixed_wing=False, fixed_wing_stol=False, rotary_wing=True
)
fixed_wing_parking = self.cp.total_aircraft_parking(parking_type_fixed_wing)
ground_spawn_parking = self.cp.total_aircraft_parking(parking_type_stol)
rotary_wing_parking = self.cp.total_aircraft_parking(parking_type_rotary_wing)
ground_unit_limit = self.cp.frontline_unit_count_limit ground_unit_limit = self.cp.frontline_unit_count_limit
deployable_unit_info = "" deployable_unit_info = ""
@ -257,6 +276,9 @@ class QBaseMenu2(QDialog):
"\n".join( "\n".join(
[ [
f"{aircraft}/{parking} aircraft", f"{aircraft}/{parking} aircraft",
f"{fixed_wing_parking} fixed wing parking",
f"{ground_spawn_parking} ground spawns",
f"{rotary_wing_parking} rotary wing parking",
f"{self.cp.base.total_armor} ground units" + deployable_unit_info, f"{self.cp.base.total_armor} ground units" + deployable_unit_info,
f"{allocated.total_transferring} more ground units en route, {allocated.total_ordered} ordered", f"{allocated.total_transferring} more ground units en route, {allocated.total_ordered} ordered",
str(self.cp.runway_status), str(self.cp.runway_status),

View File

@ -23,7 +23,10 @@ class QBaseMenuTabs(QTabWidget):
if isinstance(cp, Fob): if isinstance(cp, Fob):
self.ground_forces_hq = QGroundForcesHQ(cp, game_model) self.ground_forces_hq = QGroundForcesHQ(cp, game_model)
self.addTab(self.ground_forces_hq, "Ground Forces HQ") self.addTab(self.ground_forces_hq, "Ground Forces HQ")
if cp.helipads: if cp.has_ground_spawns:
self.airfield_command = QAirfieldCommand(cp, game_model)
self.addTab(self.airfield_command, "Airfield Command")
elif cp.has_helipads:
self.airfield_command = QAirfieldCommand(cp, game_model) self.airfield_command = QAirfieldCommand(cp, game_model)
self.addTab(self.airfield_command, "Heliport") self.addTab(self.airfield_command, "Heliport")
else: else:

View File

@ -13,7 +13,7 @@ from PySide2.QtWidgets import (
from game.dcs.aircrafttype import AircraftType from game.dcs.aircrafttype import AircraftType
from game.purchaseadapter import AircraftPurchaseAdapter from game.purchaseadapter import AircraftPurchaseAdapter
from game.squadrons import Squadron from game.squadrons import Squadron
from game.theater import ControlPoint from game.theater import ControlPoint, ParkingType
from qt_ui.models import GameModel from qt_ui.models import GameModel
from qt_ui.uiconstants import ICONS from qt_ui.uiconstants import ICONS
from qt_ui.windows.basemenu.UnitTransactionFrame import UnitTransactionFrame from qt_ui.windows.basemenu.UnitTransactionFrame import UnitTransactionFrame
@ -91,8 +91,12 @@ class QHangarStatus(QHBoxLayout):
self.setAlignment(Qt.AlignLeft) self.setAlignment(Qt.AlignLeft)
def update_label(self) -> None: def update_label(self) -> None:
next_turn = self.control_point.allocated_aircraft() parking_type = ParkingType(
max_amount = self.control_point.total_aircraft_parking fixed_wing=True, fixed_wing_stol=True, rotary_wing=True
)
next_turn = self.control_point.allocated_aircraft(parking_type)
max_amount = self.control_point.total_aircraft_parking(parking_type)
components = [f"{next_turn.total_present} present"] components = [f"{next_turn.total_present} present"]
if next_turn.total_ordered > 0: if next_turn.total_ordered > 0:

View File

@ -11,7 +11,7 @@ from PySide2.QtWidgets import (
QWidget, QWidget,
) )
from game.theater import ControlPoint from game.theater import ControlPoint, ParkingType
class QIntelInfo(QFrame): class QIntelInfo(QFrame):
@ -24,7 +24,12 @@ class QIntelInfo(QFrame):
intel_layout = QVBoxLayout() intel_layout = QVBoxLayout()
units_by_task: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int)) units_by_task: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
for unit_type, count in self.cp.allocated_aircraft().present.items(): parking_type = ParkingType(
fixed_wing=True, fixed_wing_stol=True, rotary_wing=True
)
for unit_type, count in self.cp.allocated_aircraft(
parking_type
).present.items():
if count: if count:
task_type = unit_type.dcs_unit_type.task_default.name task_type = unit_type.dcs_unit_type.task_default.name
units_by_task[task_type][unit_type.name] += count units_by_task[task_type][unit_type.name] += count

View File

@ -17,6 +17,7 @@ from PySide2.QtWidgets import (
) )
from game.game import Game from game.game import Game
from game.theater import ParkingType
from qt_ui.uiconstants import ICONS from qt_ui.uiconstants import ICONS
from qt_ui.windows.finances.QFinancesMenu import FinancesLayout from qt_ui.windows.finances.QFinancesMenu import FinancesLayout
@ -77,7 +78,10 @@ class AircraftIntelLayout(IntelTableLayout):
total = 0 total = 0
for control_point in game.theater.control_points_for(player): for control_point in game.theater.control_points_for(player):
allocation = control_point.allocated_aircraft() parking_type = ParkingType(
fixed_wing=True, fixed_wing_stol=True, rotary_wing=True
)
allocation = control_point.allocated_aircraft(parking_type)
base_total = allocation.total_present base_total = allocation.total_present
total += base_total total += base_total
if not base_total: if not base_total:

View File

@ -1,9 +1,24 @@
import pytest import pytest
from typing import Any from typing import Any
from dcs import Point
from dcs.planes import AJS37
from dcs.terrain.terrain import Airport from dcs.terrain.terrain import Airport
from game.ato.flighttype import FlightType from game.ato.flighttype import FlightType
from game.theater.controlpoint import Airfield, Carrier, Lha, OffMapSpawn, Fob from game.dcs.aircrafttype import AircraftType
from game.dcs.countries import country_with_name
from game.point_with_heading import PointWithHeading
from game.squadrons import Squadron
from game.squadrons.operatingbases import OperatingBases
from game.theater.controlpoint import (
Airfield,
Carrier,
Lha,
OffMapSpawn,
Fob,
ParkingType,
)
from game.utils import Heading
@pytest.fixture @pytest.fixture
@ -117,3 +132,89 @@ def test_mission_types_enemy(mocker: Any) -> None:
off_map_spawn = OffMapSpawn(name="test", position=None, theater=None, starts_blue=True) # type: ignore off_map_spawn = OffMapSpawn(name="test", position=None, theater=None, starts_blue=True) # type: ignore
mission_types = list(off_map_spawn.mission_types(for_player=True)) mission_types = list(off_map_spawn.mission_types(for_player=True))
assert len(mission_types) == 0 assert len(mission_types) == 0
@pytest.fixture
def test_control_point_parking(mocker: Any) -> None:
"""
Test correct number of parking slots are returned for control point
"""
# Airfield
mocker.patch("game.theater.controlpoint.unclaimed_parking", return_value=10)
airport = Airport(None, None) # type: ignore
airport.name = "test" # required for Airfield.__init__
point = Point(0, 0, None) # type: ignore
control_point = Airfield(airport, theater=None, starts_blue=True) # type: ignore
parking_type_ground_start = ParkingType(
fixed_wing=False, fixed_wing_stol=True, rotary_wing=False
)
parking_type_rotary = ParkingType(
fixed_wing=False, fixed_wing_stol=False, rotary_wing=True
)
for x in range(10):
control_point.ground_spawns.append(
(
PointWithHeading.from_point(
point,
Heading.from_degrees(0),
),
point,
)
)
for x in range(20):
control_point.helipads.append(
PointWithHeading.from_point(
point,
Heading.from_degrees(0),
)
)
assert control_point.unclaimed_parking(parking_type_ground_start) == 10
assert control_point.unclaimed_parking(parking_type_rotary) == 20
@pytest.fixture
def test_parking_type_from_squadron(mocker: Any) -> None:
"""
Test correct ParkingType object returned for a squadron of Viggens
"""
mocker.patch(
"game.theater.controlpoint.parking_type.include_fixed_wing_stol",
return_value=True,
)
aircraft = next(AircraftType.for_dcs_type(AJS37))
squadron = Squadron(
name="test",
nickname=None,
country=country_with_name("Sweden"),
role="test",
aircraft=aircraft,
max_size=16,
livery=None,
primary_task=FlightType.STRIKE,
auto_assignable_mission_types=set(aircraft.iter_task_capabilities()),
operating_bases=OperatingBases.default_for_aircraft(aircraft),
female_pilot_percentage=0,
) # type: ignore
parking_type = ParkingType().from_squadron(squadron)
assert parking_type.include_rotary_wing == False
assert parking_type.include_fixed_wing == True
assert parking_type.include_fixed_wing_stol == True
@pytest.fixture
def test_parking_type_from_aircraft(mocker: Any) -> None:
"""
Test correct ParkingType object returned for Viggen aircraft type
"""
mocker.patch(
"game.theater.controlpoint.parking_type.include_fixed_wing_stol",
return_value=True,
)
aircraft = next(AircraftType.for_dcs_type(AJS37))
parking_type = ParkingType().from_aircraft(aircraft, False)
assert parking_type.include_rotary_wing == False
assert parking_type.include_fixed_wing == True
assert parking_type.include_fixed_wing_stol == True