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
* **[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 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.
* **[UI]** Added fuel selector in flight's edit window.
* **[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.countries import CombinedJointTaskForcesBlue, CombinedJointTaskForcesRed
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.statics import Fortification, Warehouse
from dcs.terrain import Airport
from dcs.unitgroup import PlaneGroup, ShipGroup, StaticGroup, VehicleGroup
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
from game.point_with_heading import PointWithHeading
from game.positioned import Positioned
from game.profiling import logged_duration
from game.scenery_group import SceneryGroup
@ -28,6 +29,7 @@ from game.theater.controlpoint import (
OffMapSpawn,
)
from game.theater.presetlocation import PresetLocation
from game.utils import Distance, meters, feet, Heading
from game.utils import Distance, meters, feet
if TYPE_CHECKING:
@ -40,6 +42,8 @@ class MizCampaignLoader:
RED_COUNTRY = CombinedJointTaskForcesRed()
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
LHA_UNIT_TYPE = LHA_Tarawa.id
@ -48,7 +52,7 @@ class MizCampaignLoader:
CP_CONVOY_SPAWN_TYPE = Armor.M1043_HMMWV_Armament.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
SHIP_UNIT_TYPE = USS_Arleigh_Burke_IIa.id
@ -96,6 +100,8 @@ class MizCampaignLoader:
STRIKE_TARGET_UNIT_TYPE = Fortification.Tech_combine.id
GROUND_SPAWN_WAYPOINT_DISTANCE = 1000
def __init__(self, miz: Path, theater: ConflictTheater) -> None:
self.theater = theater
self.mission = Mission()
@ -224,6 +230,18 @@ class MizCampaignLoader:
if group.units[0].type in self.FARP_HELIPADS_TYPE:
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
def factories(self) -> Iterator[StaticGroup]:
for group in self.blue.static_group:
@ -362,6 +380,37 @@ class MizCampaignLoader:
closest = spawn
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:
for group in self.front_line_path_groups:
# 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:
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:
closest, distance = self.objective_info(static)

View File

@ -13,6 +13,7 @@ from game.theater import (
FrontLine,
MissionTarget,
OffMapSpawn,
ParkingType,
)
from game.theater.theatergroundobject import (
BuildingGroundObject,
@ -161,11 +162,21 @@ class ObjectiveFinder:
break
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 = []
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
if control_point.allocated_aircraft().total_present >= min_aircraft:
if (
control_point.allocated_aircraft(parking_type).total_present
>= min_aircraft
):
airfields.append(control_point)
return self._targets_by_range(airfields)

View File

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

View File

@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Any
from dcs.countries import countries_by_name
from game.ato.packagewaypoints import PackageWaypoints
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:
from game import Game
@ -110,9 +110,13 @@ class Migrator:
s.country = countries_by_name[c]()
# 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(
0, s.location.unclaimed_parking() + s.owned_aircraft
0, s.location.unclaimed_parking(parking_type) + s.owned_aircraft
)
def _update_factions(self) -> None:

View File

@ -3,8 +3,9 @@ from __future__ import annotations
import logging
from datetime import datetime
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.condition import FlagIsTrue, GroupDead, Or, FlagIsFalse
from dcs.country import Country
@ -54,7 +55,9 @@ class AircraftGenerator:
laser_code_registry: LaserCodeRegistry,
unit_map: UnitMap,
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:
self.mission = mission
self.settings = settings
@ -67,6 +70,8 @@ class AircraftGenerator:
self.flights: List[FlightData] = []
self.mission_data = mission_data
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 = settings.plugins.get("ewrj")
@ -186,7 +191,13 @@ class AircraftGenerator:
flight.state = Completed(flight, self.game.settings)
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()
AircraftPainter(flight, group).apply_livery()
self.unit_map.add_aircraft(group, flight)
@ -196,7 +207,13 @@ class AircraftGenerator:
) -> FlyingGroup[Any]:
"""Creates and configures the flight group in the mission."""
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()
self.flights.append(
FlightGroupConfigurator(
@ -216,10 +233,10 @@ class AircraftGenerator:
wpt = group.waypoint("LANDING")
if flight.is_helo and isinstance(flight.arrival, Fob) and wpt:
hpad = self.helipads[flight.arrival].units.pop(0)
wpt.helipad_id = hpad.id
wpt.link_unit = hpad.id
self.helipads[flight.arrival].units.append(hpad)
hpad = self.helipads[flight.arrival].pop(0)
wpt.helipad_id = hpad.units[0].id
wpt.link_unit = hpad.units[0].id
self.helipads[flight.arrival].append(hpad)
if self.ewrj:
self._track_ewrj_flight(flight, group)

View File

@ -29,6 +29,8 @@ from .flightdata import FlightData
from .waypoints import WaypointGenerator
from ...ato.flightplans.aewc import AewcFlightPlan
from ...ato.flightplans.theaterrefueling import TheaterRefuelingFlightPlan
from ...ato.flightwaypointtype import FlightWaypointType
from ...theater import Fob
if TYPE_CHECKING:
from game import Game
@ -103,6 +105,19 @@ class FlightGroupConfigurator:
self.mission_data,
).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(
package=self.flight.package,
aircraft_type=self.flight.unit_type,

View File

@ -1,10 +1,10 @@
import logging
import random
from typing import Any, Union
from typing import Any, Union, Tuple, Optional
from dcs import Mission
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.planes import F_14A, Su_33
from dcs.point import PointAction
@ -47,13 +47,17 @@ class FlightGroupSpawner:
flight: Flight,
country: Country,
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,
) -> None:
self.flight = flight
self.country = country
self.mission = mission
self.helipads = helipads
self.ground_spawns_roadbase = ground_spawns_roadbase
self.ground_spawns = ground_spawns
self.mission_data = mission_data
def create_flight_group(self) -> FlyingGroup[Any]:
@ -88,11 +92,11 @@ class FlightGroupSpawner:
return grp
def create_idle_aircraft(self) -> FlyingGroup[Any]:
airport = self.flight.squadron.location.dcs_airport
assert airport is not None
group = self._generate_at_airport(
assert isinstance(self.flight.squadron.location, Airfield)
airfield = self.flight.squadron.location
group = self._generate_at_airfield(
name=namegen.next_aircraft_name(self.country, self.flight),
airport=airport,
airfield=airfield,
)
group.uncontrolled = True
@ -121,18 +125,47 @@ class FlightGroupSpawner:
elif isinstance(cp, Fob):
is_heli = self.flight.squadron.aircraft.helicopter
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(
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)
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(
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):
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:
raise NotImplementedError(
f"Aircraft spawn behavior not implemented for {cp} ({cp.__class__})"
@ -185,7 +218,7 @@ class FlightGroupSpawner:
group.points[0].alt_type = alt_type
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.
# 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
@ -193,13 +226,14 @@ class FlightGroupSpawner:
# 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
# option).
self.flight.unit_type.dcs_unit_type.load_payloads()
return self.mission.flight_group_from_airport(
country=self.country,
name=name,
aircraft_type=self.flight.unit_type.dcs_unit_type,
airport=airport,
airport=airfield.airport,
maintask=None,
start_type=self.dcs_start_type(),
start_type=self._start_type_at_airfield(airfield),
group_size=self.flight.count,
parking_slots=None,
)
@ -253,26 +287,84 @@ class FlightGroupSpawner:
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:
helipad = self.helipads[cp]
except IndexError:
raise NoParkingSlotError()
helipad = self.helipads[cp].pop()
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
# raise RuntimeError(f"Not enough helipads available at {cp}") from ex
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
if self.start_type is StartType.WARM:
group.points[0].type = "TakeOffGroundHot"
group.points[0].type = "TakeOffGround"
group.units[0].heading = helipad.units[0].heading
if self.start_type is not StartType.COLD:
group.points[0].action = PointAction.FromGroundAreaHot
hpad = helipad.units[0]
for i in range(self.flight.count):
pos = cp.helipads.pop(0)
group.units[i].position = pos
group.units[i].heading = hpad.heading
cp.helipads.append(pos)
group.points[0].type = "TakeOffGroundHot"
for i in range(self.flight.count - 1):
try:
helipad = self.helipads[cp].pop()
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
def dcs_start_type(self) -> DcsStartType:
@ -284,6 +376,12 @@ class FlightGroupSpawner:
return DcsStartType.Warm
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(
self,
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)
from game.theater import TRIGGER_RADIUS_CAPTURE
FRONTLINE_COLORS = Rgba(255, 0, 0, 255)
WHITE = Rgba(255, 255, 255, 255)
CP_RED = Rgba(255, 0, 0, 80)
CP_BLUE = Rgba(0, 0, 255, 80)
CP_CIRCLE_RADIUS = 2500
BLUE_PATH_COLOR = Rgba(0, 0, 255, 100)
RED_PATH_COLOR = Rgba(255, 0, 0, 100)
ACTIVE_PATH_COLOR = Rgba(255, 80, 80, 100)
@ -40,7 +41,7 @@ class DrawingsGenerator:
color = CP_RED
shape = self.player_layer.add_circle(
cp.position,
CP_CIRCLE_RADIUS,
TRIGGER_RADIUS_CAPTURE,
line_thickness=2,
color=WHITE,
fill=color,

View File

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

View File

@ -9,13 +9,16 @@ from __future__ import annotations
import logging
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
from dcs import Mission, Point
from dcs import Mission, Point, unitgroup
from dcs.action import DoScript, SceneryDestructionZone
from dcs.condition import MapObjectIsDead
from dcs.countries import *
from dcs.country import Country
from dcs.point import StaticPoint, PointAction
from dcs.ships import (
CVN_71,
CVN_72,
@ -37,16 +40,17 @@ from dcs.task import (
)
from dcs.translation import String
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.unittype import ShipType, VehicleType
from dcs.vehicles import vehicle_map
from dcs.vehicles import vehicle_map, Unarmed
from game.missiongenerator.groundforcepainter import (
NavalForcePainter,
GroundForcePainter,
)
from game.missiongenerator.missiondata import CarrierInfo, MissionData
from game.point_with_heading import PointWithHeading
from game.radio.RadioFrequencyContainer import RadioFrequencyContainer
from game.radio.radios import RadioFrequency, RadioRegistry
from game.radio.tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage
@ -74,6 +78,161 @@ FARP_FRONTLINE_DISTANCE = 10000
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:
"""generates the DCS groups and units from the TheaterGroundObject"""
@ -598,66 +757,321 @@ class HelipadGenerator:
self.game = game
self.radio_registry = radio_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:
# 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):
heading = helipad.heading.degrees
name_i = self.cp.name + "_helipad" + "_" + str(i)
if self.helipads is None:
self.helipads = self.m.farp(
self.m.country(self.game.neutral_country.name),
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)
)
self.create_helipad(i, helipad, "SINGLE_HELIPAD")
for i, helipad in enumerate(self.cp.helipads_quad):
self.create_helipad(i, helipad, "FARP")
for i, helipad in enumerate(self.cp.helipads_invisible):
self.create_helipad(i, helipad, "Invisible FARP")
# 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]
pad.position = helipad
pad.heading = heading
# Generate a FARP Ammo and Fuel stack for each pad
self.m.static_group(
class GroundSpawnRoadbaseGenerator:
"""
Generates Highway strip 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_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,
name=(name_i + "_fuel"),
_type=Fortification.FARP_Fuel_Depot,
position=helipad.point_from_heading(heading, 35),
heading=heading,
)
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
name=(name + "_fuel"),
_type=tanker_type,
position=pad.position.point_from_heading(
ground_spawn[0].heading.degrees + 90, 35
),
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(
country=country,
name=(name_i + "_ws"),
_type=Fortification.Windsock,
position=helipad.point_from_heading(heading + 45, 35),
heading=heading,
name=(name + "_ammo"),
_type=Fortification.FARP_Ammo_Dump_Coating,
position=pad.position.point_from_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:
"""Creates DCS groups and statics for the theater during mission generation.
@ -684,7 +1098,13 @@ class TgoGenerator:
self.unit_map = unit_map
self.icls_alloc = iter(range(1, 21))
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
def generate(self) -> None:
@ -696,8 +1116,25 @@ class TgoGenerator:
self.m, cp, self.game, self.radio_registry, self.tacan_registry
)
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:
generator: GroundObjectGenerator

View File

@ -1,14 +1,26 @@
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 (
AllOfCoalitionOutsideZone,
FlagIsFalse,
FlagIsTrue,
PartOfCoalitionInZone,
TimeAfter,
TimeSinceFlag,
)
from dcs.mission import Mission
from dcs.task import Option
@ -17,7 +29,7 @@ from dcs.triggers import Event, TriggerCondition, TriggerOnce
from dcs.unit import Skill
from game.theater import Airfield
from game.theater.controlpoint import Fob
from game.theater.controlpoint import Fob, TRIGGER_RADIUS_CAPTURE
if TYPE_CHECKING:
from game.game import Game
@ -37,6 +49,7 @@ TRIGGER_RADIUS_SMALL = 50000
TRIGGER_RADIUS_MEDIUM = 100000
TRIGGER_RADIUS_LARGE = 150000
TRIGGER_RADIUS_ALL_MAP = 3000000
TRIGGER_RADIUS_CLEAR_SCENERY = 1000
class Silence(Option):
@ -133,6 +146,37 @@ class TriggerGenerator:
v += 1
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(
self, player_coalition: str, enemy_coalition: str
) -> None:
@ -154,7 +198,10 @@ class TriggerGenerator:
defend_coalition_int = 1
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()
capture_trigger = TriggerCondition(Event.NoEvent, "Capture Trigger")
@ -203,6 +250,11 @@ class TriggerGenerator:
self._set_allegiances(player_coalition, enemy_coalition)
self._gen_markers()
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
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.data.units import UnitClass
from game.dcs.groundunittype import GroundUnitType
from game.theater import ControlPoint, MissionTarget
from game.theater import ControlPoint, MissionTarget, ParkingType
if TYPE_CHECKING:
from game import Game
@ -63,12 +63,16 @@ class ProcurementAi:
if len(self.faction.aircrafts) == 0 or len(self.air_wing.squadrons) == 0:
return 1
parking_type = ParkingType(
fixed_wing=True, fixed_wing_stol=True, rotary_wing=True
)
for cp in self.owned_points:
cp_ground_units = cp.allocated_ground_units(
self.game.coalition_for(self.is_player).transfers
)
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
air = self.game.settings.auto_procurement_balance / 100.0
@ -232,11 +236,13 @@ class ProcurementAi:
for squadron in self.air_wing.best_squadrons_for(
request.near, request.task_capability, request.number, this_turn=False
):
parking_type = ParkingType().from_squadron(squadron)
if not squadron.can_provide_pilots(request.number):
continue
if not squadron.has_aircraft_capacity_for(request.number):
if squadron.location.unclaimed_parking(parking_type) < request.number:
continue
if squadron.location.unclaimed_parking() < request.number:
if not squadron.has_aircraft_capacity_for(request.number):
continue
if self.threat_zones.threatened(squadron.location.position):
threatened.append(squadron)

View File

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

View File

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

View File

@ -94,6 +94,7 @@ if TYPE_CHECKING:
FREE_FRONTLINE_UNIT_SUPPLY: int = 15
AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION: int = 12
TRIGGER_RADIUS_CAPTURE = 3000
class ControlPointType(Enum):
@ -315,6 +316,47 @@ class ControlPointStatus(IntEnum):
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):
# Not sure what distance DCS uses, but assuming it's about 2NM since that's roughly
# the distance of the circle on the map.
@ -340,6 +382,10 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
self.connected_objectives: List[TheaterGroundObject] = []
self.preset_locations = PresetLocations()
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.captured_invert = False
@ -525,7 +571,17 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
"""
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:
"""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:
...
@property
@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
control point
@ -761,17 +816,22 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
self, squadron: Squadron
) -> Optional[ControlPoint]:
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
# from.
airfields = list(closest.operational_airfields_within(max_retreat_distance))[1:]
not_preferred: Optional[ControlPoint] = None
overfull: list[ControlPoint] = []
parking_type = ParkingType().from_squadron(squadron)
for airbase in airfields:
if airbase.captured != self.captured:
continue
if airbase.unclaimed_parking() < squadron.owned_aircraft:
if airbase.unclaimed_parking(parking_type) < squadron.owned_aircraft:
if airbase.can_operate(squadron.aircraft):
overfull.append(airbase)
continue
@ -798,7 +858,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
loss_count = math.inf
for airbase in overfull:
overflow = -(
airbase.unclaimed_parking()
airbase.unclaimed_parking(parking_type)
- squadron.owned_aircraft
- squadron.pending_deliveries
)
@ -813,10 +873,13 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
squadron.refund_orders()
self.capture_aircraft(game, squadron.aircraft, squadron.owned_aircraft)
return
parking_type = ParkingType().from_squadron(squadron)
logging.debug(f"{squadron} retreating to {destination} from {self}")
squadron.relocate_to(destination)
squadron.cancel_overflow_orders()
overflow = -destination.unclaimed_parking()
overflow = -destination.unclaimed_parking(parking_type)
if overflow > 0:
logging.debug(
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 unclaimed_parking(self) -> int:
return self.total_aircraft_parking - self.allocated_aircraft().total
def unclaimed_parking(self, parking_type: ParkingType) -> int:
return (
self.total_aircraft_parking(parking_type)
- self.allocated_aircraft(parking_type).total
)
@abstractmethod
def active_runway(
@ -932,17 +998,33 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
u.position.x = u.position.x + delta.x
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)
on_order: dict[AircraftType, int] = defaultdict(int)
transferring: dict[AircraftType, int] = defaultdict(int)
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
if squadron.destination is None:
on_order[squadron.aircraft] += squadron.pending_deliveries
else:
transferring[squadron.aircraft] -= squadron.owned_aircraft
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:
on_order[squadron.aircraft] += squadron.pending_deliveries
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
# 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
if not aircraft.helicopter and not self.airport.runways:
return False
else:
return self.runway_is_operational()
parking_type = ParkingType().from_aircraft(
aircraft, self.coalition.game.settings.ground_start_ai_planes
)
if parking_type.include_rotary_wing and self.has_helipads:
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]:
from game.ato import FlightType
@ -1120,14 +1205,25 @@ class Airfield(ControlPoint, CTLD):
yield FlightType.REFUELING
@property
def total_aircraft_parking(self) -> int:
def total_aircraft_parking(self, parking_type: ParkingType) -> int:
"""
Return total aircraft parking slots available
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
"""
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
def heading(self) -> Heading:
@ -1317,8 +1413,7 @@ class Carrier(NavalControlPoint):
def can_operate(self, aircraft: AircraftType) -> bool:
return aircraft.carrier_capable
@property
def total_aircraft_parking(self) -> int:
def total_aircraft_parking(self, parking_type: ParkingType) -> int:
return 90
@property
@ -1348,8 +1443,7 @@ class Lha(NavalControlPoint):
def can_operate(self, aircraft: AircraftType) -> bool:
return aircraft.lha_capable
@property
def total_aircraft_parking(self) -> int:
def total_aircraft_parking(self, parking_type: ParkingType) -> int:
return 20
@property
@ -1383,8 +1477,7 @@ class OffMapSpawn(ControlPoint):
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
yield from []
@property
def total_aircraft_parking(self) -> int:
def total_aircraft_parking(self, parking_type: ParkingType) -> int:
return 1000
def can_operate(self, aircraft: AircraftType) -> bool:
@ -1444,7 +1537,7 @@ class Fob(ControlPoint, RadioFrequencyContainer, CTLD):
return SymbolSet.LAND_INSTALLATIONS, LandInstallationEntity.MILITARY_BASE
def runway_is_operational(self) -> bool:
return self.has_helipads
return self.has_helipads or self.has_ground_spawns
def active_runway(
self,
@ -1470,17 +1563,33 @@ class Fob(ControlPoint, RadioFrequencyContainer, CTLD):
yield from super().mission_types(for_player)
@property
def total_aircraft_parking(self) -> int:
return len(self.helipads)
def total_aircraft_parking(self, parking_type: ParkingType) -> int:
parking_slots = 0
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:
# FOBs and FARPs are the same class, distinguished only by non-FARP FOBs having
# zero parking.
# https://github.com/dcs-liberation/dcs_liberation/issues/2378
return (
aircraft.helicopter or aircraft.lha_capable
) and self.total_aircraft_parking > 0
parking_type = ParkingType().from_aircraft(
aircraft, self.coalition.game.settings.ground_start_ai_planes
)
if parking_type.include_rotary_wing and self.has_helipads:
return True
if parking_type.include_fixed_wing_stol and self.has_ground_spawns:
return True
return False
@property
def heading(self) -> Heading:

View File

@ -48,7 +48,7 @@ from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
from game.naming import namegen
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 (
TransitConnection,
TransitNetwork,
@ -728,8 +728,17 @@ class PendingTransfers:
self.order_airlift_assets_at(control_point)
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:
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
transit_network = self.network_for(control_point)
for cp in self.game.theater.control_points_for(self.player):
@ -750,7 +759,8 @@ class PendingTransfers:
if (
is_major_hub
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
@ -769,7 +779,16 @@ class PendingTransfers:
)
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
# take place at another base
gap = min(

View File

@ -22,10 +22,11 @@ from dcs.unittype import FlyingType
from game.ato.flightplans.custom import CustomFlightPlan
from game.ato.flighttype import FlightType
from game.ato.flightwaypointtype import FlightWaypointType
from game.dcs.aircrafttype import AircraftType
from game.server import EventStream
from game.sim import GameUpdateEvents
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.errorreporter import report_errors
from qt_ui.models import AtoModel, SquadronModel
@ -105,7 +106,8 @@ class SquadronDestinationComboBox(QComboBox):
self.squadron = squadron
self.theater = theater
room = squadron.location.unclaimed_parking()
parking_type = ParkingType().from_squadron(squadron)
room = squadron.location.unclaimed_parking(parking_type)
self.addItem(
f"Remain at {squadron.location} (room for {room} more aircraft)",
squadron.location,
@ -142,11 +144,20 @@ class SquadronDestinationComboBox(QComboBox):
self.setCurrentIndex(selected_index)
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):
if control_point == self.squadron.location:
continue
if not control_point.can_operate(self.squadron.aircraft):
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
@staticmethod
@ -156,14 +167,39 @@ class SquadronDestinationComboBox(QComboBox):
if cp.dcs_airport:
ap = deepcopy(cp.dcs_airport)
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 count in range(s.owned_aircraft):
slot = ap.free_parking_slot(s.aircraft.dcs_unit_type)
if slot:
slot.unit_id = id(s) + count
is_heli = s.aircraft.helicopter
is_vtol = not is_heli and s.aircraft.lha_capable
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:
overflow.append(s)
break
slot = ap.free_parking_slot(s.aircraft.dcs_unit_type)
if slot:
slot.unit_id = id(s) + count
else:
overflow.append(s)
break
if overflow:
overflow_msg = ""
for s in overflow:
@ -178,7 +214,11 @@ class SquadronDestinationComboBox(QComboBox):
)
return len(ap.free_parking_slots(dcs_unit_type))
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):

View File

@ -25,6 +25,7 @@ from game.theater import (
ControlPointType,
FREE_FRONTLINE_UNIT_SUPPLY,
NavalControlPoint,
ParkingType,
)
from qt_ui.dialogs import Dialog
from qt_ui.models import GameModel
@ -237,8 +238,26 @@ class QBaseMenu2(QDialog):
self.repair_button.setDisabled(True)
def update_intel_summary(self) -> None:
aircraft = self.cp.allocated_aircraft().total_present
parking = self.cp.total_aircraft_parking
parking_type_all = ParkingType(
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
deployable_unit_info = ""
@ -257,6 +276,9 @@ class QBaseMenu2(QDialog):
"\n".join(
[
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"{allocated.total_transferring} more ground units en route, {allocated.total_ordered} ordered",
str(self.cp.runway_status),

View File

@ -23,7 +23,10 @@ class QBaseMenuTabs(QTabWidget):
if isinstance(cp, Fob):
self.ground_forces_hq = QGroundForcesHQ(cp, game_model)
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.addTab(self.airfield_command, "Heliport")
else:

View File

@ -13,7 +13,7 @@ from PySide2.QtWidgets import (
from game.dcs.aircrafttype import AircraftType
from game.purchaseadapter import AircraftPurchaseAdapter
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.uiconstants import ICONS
from qt_ui.windows.basemenu.UnitTransactionFrame import UnitTransactionFrame
@ -91,8 +91,12 @@ class QHangarStatus(QHBoxLayout):
self.setAlignment(Qt.AlignLeft)
def update_label(self) -> None:
next_turn = self.control_point.allocated_aircraft()
max_amount = self.control_point.total_aircraft_parking
parking_type = ParkingType(
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"]
if next_turn.total_ordered > 0:

View File

@ -11,7 +11,7 @@ from PySide2.QtWidgets import (
QWidget,
)
from game.theater import ControlPoint
from game.theater import ControlPoint, ParkingType
class QIntelInfo(QFrame):
@ -24,7 +24,12 @@ class QIntelInfo(QFrame):
intel_layout = QVBoxLayout()
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:
task_type = unit_type.dcs_unit_type.task_default.name
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.theater import ParkingType
from qt_ui.uiconstants import ICONS
from qt_ui.windows.finances.QFinancesMenu import FinancesLayout
@ -77,7 +78,10 @@ class AircraftIntelLayout(IntelTableLayout):
total = 0
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
total += base_total
if not base_total:

View File

@ -1,9 +1,24 @@
import pytest
from typing import Any
from dcs import Point
from dcs.planes import AJS37
from dcs.terrain.terrain import Airport
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
@ -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
mission_types = list(off_map_spawn.mission_types(for_player=True))
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