diff --git a/game/helipad.py b/game/helipad.py new file mode 100644 index 00000000..8660ad4f --- /dev/null +++ b/game/helipad.py @@ -0,0 +1,13 @@ +from typing import Optional + +from dcs.unitgroup import StaticGroup + +from game.point_with_heading import PointWithHeading + + +class Helipad(PointWithHeading): + def __init__(self): + super(Helipad, self).__init__() + self.heading = 0 + self.occupied = False + self.static_unit: Optional[StaticGroup] = None diff --git a/game/theater/conflicttheater.py b/game/theater/conflicttheater.py index 7e9fa0fe..f4b52de2 100644 --- a/game/theater/conflicttheater.py +++ b/game/theater/conflicttheater.py @@ -41,6 +41,7 @@ from dcs.unitgroup import ( ) from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed +from ..helipad import Helipad from ..scenery_group import SceneryGroup from pyproj import CRS, Transformer from shapely import geometry, ops @@ -549,7 +550,7 @@ class MizCampaignLoader: for group in self.helipads: closest, distance = self.objective_info(group) closest.helipads.append( - PointWithHeading.from_point(group.position, group.units[0].heading) + Helipad.from_point(group.position, group.units[0].heading) ) for group in self.factories: diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index f2e96a88..0da38c26 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -10,6 +10,7 @@ from enum import Enum from functools import total_ordering from typing import Any, Dict, Iterator, List, Optional, Set, TYPE_CHECKING, Type, Union +from dcs import helicopters from dcs.mapping import Point from dcs.ships import ( CVN_74_John_C__Stennis, @@ -39,6 +40,7 @@ from .theatergroundobject import ( VehicleGroupGroundObject, ) from ..db import PRICES +from ..helipad import Helipad from ..utils import nautical_miles from ..weather import Conditions @@ -296,7 +298,7 @@ class ControlPoint(MissionTarget, ABC): self.connected_objectives: List[TheaterGroundObject] = [] self.base_defenses: List[BaseDefenseGroundObject] = [] self.preset_locations = PresetLocations() - self.helipads: List[PointWithHeading] = [] + self.helipads: List[Helipad] = [] # TODO: Should be Airbase specific. self.size = size @@ -378,6 +380,29 @@ class ControlPoint(MissionTarget, ABC): return True return False + @property + def has_helipads(self) -> bool: + """ + Returns true if cp has helipads + """ + return len(self.helipads) > 0 + + @property + def has_free_helipad(self) -> bool: + """ + Returns true if cp has a free helipad + """ + return False in [h.occupied for h in self.helipads] + + def get_free_helipad(self) -> Optional[Helipad]: + """ + Returns the first free additional helipad + """ + for h in self.helipads: + if not h.occupied: + return h + return None + def can_recruit_ground_units(self, game: Game) -> bool: """Returns True if this control point is capable of recruiting ground units.""" if not self.can_deploy_ground_units: @@ -1084,10 +1109,13 @@ class Fob(ControlPoint): @property def total_aircraft_parking(self) -> int: - return 0 + return len(self.helipads) def can_operate(self, aircraft: FlyingType) -> bool: - return False + if aircraft in helicopters.helicopter_map.values(): + return True + else: + return False @property def heading(self) -> int: diff --git a/game/theater/missiontarget.py b/game/theater/missiontarget.py index 8d026625..197de586 100644 --- a/game/theater/missiontarget.py +++ b/game/theater/missiontarget.py @@ -46,4 +46,4 @@ class MissionTarget: @property def strike_targets(self) -> List[Union[MissionTarget, Unit]]: - raise NotImplementedError + return [] diff --git a/gen/aircraft.py b/gen/aircraft.py index 92cead03..19391835 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -1110,18 +1110,43 @@ class AircraftConflictGenerator: at=self.m.find_group(group_name), ) else: - if not isinstance(cp, Airfield): - raise RuntimeError( - f"Attempted to spawn at airfield for non-airfield {cp}" + + # If the flight is an helicopter flight, then prioritize dedicated helipads + group = None + if flight.unit_type in helicopters.helicopter_map.values(): + helipad = cp.get_free_helipad() + if helipad is not None: + group = self._generate_at_group( + name=name, + side=country, + unit_type=flight.unit_type, + count=flight.count, + start_type=flight.start_type, + at=helipad.static_unit, + ) + group.points[0].action = PointAction.FromGroundArea + group.points[0].type = "From Ground Area" + helipad.occupied = True + + for i in range(flight.count - 1): + helipad = cp.get_free_helipad() + if helipad is not None: + helipad.occupied = True + group.units[1 + i].position = Point(helipad.x, helipad.y) + + if group is None: + if not isinstance(cp, Airfield): + raise RuntimeError( + f"Attempted to spawn at airfield for non-airfield {cp}" + ) + group = self._generate_at_airport( + name=name, + side=country, + unit_type=flight.unit_type, + count=flight.count, + start_type=flight.start_type, + airport=cp.airport, ) - group = self._generate_at_airport( - name=name, - side=country, - unit_type=flight.unit_type, - count=flight.count, - start_type=flight.start_type, - airport=cp.airport, - ) except Exception as e: # Generated when there is no place on Runway or on Parking Slots logging.error(e) diff --git a/gen/flights/closestairfields.py b/gen/flights/closestairfields.py index 4d6bc4fb..6c5c3bfc 100644 --- a/gen/flights/closestairfields.py +++ b/gen/flights/closestairfields.py @@ -25,7 +25,11 @@ class ClosestAirfields: @property def operational_airfields(self) -> Iterator[ControlPoint]: - return (c for c in self.closest_airfields if c.runway_is_operational()) + return ( + c + for c in self.closest_airfields + if c.runway_is_operational() or c.has_helipads + ) def airfields_within(self, distance: Distance) -> Iterator[ControlPoint]: """Iterates over all airfields within the given range of the target. diff --git a/gen/flights/flight.py b/gen/flights/flight.py index 397399ed..6f843c40 100644 --- a/gen/flights/flight.py +++ b/gen/flights/flight.py @@ -142,6 +142,8 @@ class FlightWaypoint: PointAction.FromParkingArea: FlightWaypointType.TAKEOFF, PointAction.FromParkingAreaHot: FlightWaypointType.TAKEOFF, PointAction.FromRunway: FlightWaypointType.TAKEOFF, + PointAction.FromGroundArea: FlightWaypointType.TAKEOFF, + PointAction.FromGroundAreaHot: FlightWaypointType.TAKEOFF, }[point.action] if waypoint.waypoint_type == FlightWaypointType.NAV: waypoint.name = "NAV" diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index e648e4bb..b1b0cc70 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -1772,4 +1772,5 @@ class FlightPlanBuilder: for flight in self.package.flights: if flight.departure == airfield: return airfield + raise RuntimeError("Could not find any airfield assigned to this package") diff --git a/gen/groundobjectsgen.py b/gen/groundobjectsgen.py index d6349c8e..62f289e5 100644 --- a/gen/groundobjectsgen.py +++ b/gen/groundobjectsgen.py @@ -587,6 +587,8 @@ class HelipadGenerator: sp.position = pad.position sg.add_point(sp) country.add_static_group(sg) + helipad.static_unit = sg + helipad.occupied = False class GroundObjectsGenerator: diff --git a/resources/campaigns/golan_heights_lite.miz b/resources/campaigns/golan_heights_lite.miz index 3113fd56..c05eb89d 100644 Binary files a/resources/campaigns/golan_heights_lite.miz and b/resources/campaigns/golan_heights_lite.miz differ