mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
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:
parent
5a71806651
commit
e273e93012
@ -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.
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -90,6 +90,8 @@ class TurnState(Enum):
|
||||
|
||||
|
||||
class Game:
|
||||
scenery_clear_zones: List[Point]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
player_faction: Faction,
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
)
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user