Merge pull request #1183 from dcs-liberation/helipads

[WIP] Add possibility to add helipads to FOB control points
This commit is contained in:
C. Perreau 2021-09-08 22:54:05 +02:00 committed by GitHub
commit 82f5287282
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 281 additions and 28 deletions

View File

@ -7,7 +7,8 @@ Saves from 4.x are not compatible with 5.0.
* **[Campaign]** Weather! Theaters now experience weather that is more realistic for the region and its current season. For example, Persian Gulf will have very hot, sunny summers and Marianas will experience lots of rain during fall. These changes affect pressure, temperature, clouds and precipitation. Additionally, temperature will drop during the night, by an amount that is somewhat realistic for the region.
* **[Campaign]** Weapon data such as fallbacks and introduction years is now moddable. Due to the new architecture to support this, the old data was not automatically migrated.
* **[Campaign]** Era-restricted loadouts will now skip LGBs when no TGP is available in the loadout. This only applies to default loadouts; buddy-lasing can be coordinated with custom loadouts.
* **[Campaign]** Squadrons now have a home base and will not operate out of other bases. See https://github.com/dcs-liberation/dcs_liberation/discussions/1550 for details.
* **[Campaign]** FOBs control point can have FARP/helipad slot and host helicopters. To enable this feature on a FOB, add "Invisible FARP" statics objects near the FOB location in the campaign definition file.
* **[Campaign]** Squadrons now have a home base and will not operate out of other bases. See https://github.com/dcs-liberation/dcs_liberation/issues/1145 for status.
* **[Campaign]** Aircraft now belong to squadrons rather than bases to support squadron location transfers.
* **[Campaign]** Skipped turns are no longer counted as defeats on front lines.
* **[Campaign AI]** Overhauled campaign AI target prioritization. This currently only affects the ordering of DEAD missions.

View File

@ -45,7 +45,7 @@ class MizCampaignLoader:
SHIPPING_LANE_UNIT_TYPE = HandyWind.id
FOB_UNIT_TYPE = Unarmed.SKP_11.id
FARP_HELIPAD = "SINGLE_HELIPAD"
FARP_HELIPADS_TYPE = ["Invisible FARP", "SINGLE_HELIPAD"]
OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id
SHIP_UNIT_TYPE = USS_Arleigh_Burke_IIa.id
@ -212,7 +212,7 @@ class MizCampaignLoader:
@property
def helipads(self) -> Iterator[StaticGroup]:
for group in self.blue.static_group:
if group.units[0].type == self.FARP_HELIPAD:
if group.units[0].type in self.FARP_HELIPADS_TYPE:
yield group
@property

View File

@ -184,6 +184,10 @@ class AircraftType(UnitType[Type[FlyingType]]):
def flyable(self) -> bool:
return self.dcs_unit_type.flyable
@property
def helicopter(self) -> bool:
return self.dcs_unit_type.helicopter
@cached_property
def max_speed(self) -> Speed:
return kph(self.dcs_unit_type.max_speed)

View File

@ -8,6 +8,8 @@ from datetime import date, datetime, timedelta
from enum import Enum
from typing import Any, List, Type, Union, cast, TYPE_CHECKING
from dcs.countries import Switzerland, UnitedNationsPeacekeepers, USAFAggressors
from dcs.country import Country
from dcs.mapping import Point
from dcs.task import CAP, CAS, PinpointStrike
from dcs.vehicles import AirDefence
@ -195,6 +197,17 @@ class Game:
)
)
@property
def neutral_country(self) -> Type[Country]:
"""Return the best fitting country that can be used as neutral faction in the generated mission"""
countries_in_use = [self.red.country_name, self.blue.country_name]
if UnitedNationsPeacekeepers not in countries_in_use:
return UnitedNationsPeacekeepers
elif Switzerland.name not in countries_in_use:
return Switzerland
else:
return USAFAggressors
def _generate_events(self) -> None:
for front_line in self.theater.conflicts():
self._generate_player_event(

View File

@ -39,6 +39,7 @@ from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator
from gen.visualgen import VisualGenerator
from .. import db
from ..theater import Airfield, FrontLine
from ..theater.bullseye import Bullseye
from ..unitmap import UnitMap
if TYPE_CHECKING:
@ -105,6 +106,9 @@ class Operation:
cls.current_mission.coalition["red"] = Coalition(
"red", bullseye=cls.game.red.bullseye.to_pydcs()
)
cls.current_mission.coalition["neutrals"] = Coalition(
"neutrals", bullseye=Bullseye(Point(0, 0)).to_pydcs()
)
p_country = cls.game.blue.country_name
e_country = cls.game.red.country_name
@ -115,6 +119,16 @@ class Operation:
country_dict[db.country_id_from_name(e_country)]()
)
belligerents = [
db.country_id_from_name(p_country),
db.country_id_from_name(e_country),
]
for country in country_dict.keys():
if country not in belligerents:
cls.current_mission.coalition["neutrals"].add_country(
country_dict[country]()
)
@classmethod
def inject_lua_trigger(cls, contents: str, comment: str) -> None:
trigger = TriggerStart(comment=comment)
@ -365,6 +379,7 @@ class Operation:
cls.laser_code_registry,
cls.unit_map,
air_support=cls.airsupportgen.air_support,
helipads=cls.groundobjectgen.helipads,
)
cls.airgen.clear_parking_slots()

View File

@ -32,6 +32,7 @@ from dcs.ships import (
)
from dcs.terrain.terrain import Airport, ParkingSlot
from dcs.unit import Unit
from dcs.unittype import FlyingType
from game import db
from game.point_with_heading import PointWithHeading
@ -410,6 +411,13 @@ 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
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:
@ -828,6 +836,22 @@ class ControlPoint(MissionTarget, ABC):
"""Return the number of ammo depots, including dead ones"""
return len(list(self.all_ammo_depots))
@property
def active_fuel_depots_count(self) -> int:
"""Return the number of available fuel depots"""
return len(
[
obj
for obj in self.connected_objectives
if obj.category == "fuel" and not obj.is_dead
]
)
@property
def total_fuel_depots_count(self) -> int:
"""Return the number of fuel depots, including dead ones"""
return len([obj for obj in self.connected_objectives if obj.category == "fuel"])
@property
def strike_targets(self) -> Sequence[Union[MissionTarget, Unit]]:
return []
@ -886,6 +910,11 @@ class Airfield(ControlPoint):
@property
def total_aircraft_parking(self) -> 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)
@property
@ -1165,7 +1194,7 @@ class Fob(ControlPoint):
self.name = name
def runway_is_operational(self) -> bool:
return False
return self.has_helipads
def active_runway(
self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData]
@ -1189,10 +1218,10 @@ class Fob(ControlPoint):
@property
def total_aircraft_parking(self) -> int:
return 0
return len(self.helipads)
def can_operate(self, aircraft: AircraftType) -> bool:
return False
return aircraft.helicopter
@property
def heading(self) -> Heading:

View File

@ -111,6 +111,9 @@ VERSION = _build_version_string()
#: * DCS 2.7.4.9632 changed scenery target IDs. Any mission using map buildings as
#: strike targets must check and potentially recreate all those objectives.
#:
#: Version 8.1
#: * You can now add "Invisible FARP" static to FOB to add helicopter slots
#:
#: Version 9.0
#: * Campaign files now define the initial squadron layouts. See TODO.
#: * CV and LHA control points now get their names from the group name in the campaign

View File

@ -229,6 +229,7 @@ class AircraftConflictGenerator:
laser_code_registry: LaserCodeRegistry,
unit_map: UnitMap,
air_support: AirSupport,
helipads: dict[ControlPoint, list[StaticGroup]],
) -> None:
self.m = mission
self.game = game
@ -239,6 +240,7 @@ class AircraftConflictGenerator:
self.unit_map = unit_map
self.flights: List[FlightData] = []
self.air_support = air_support
self.helipads = helipads
@cached_property
def use_client(self) -> bool:
@ -534,6 +536,54 @@ class AircraftConflictGenerator:
group_size=count,
)
def _generate_at_cp_helipad(
self,
name: str,
side: Country,
unit_type: Type[FlyingType],
count: int,
start_type: str,
cp: ControlPoint,
) -> FlyingGroup[Any]:
assert count > 0
logging.info(
"airgen at cp's helipads : {} for {} at {}".format(
unit_type, side.id, cp.name
)
)
try:
helipad = self.helipads[cp].pop()
except IndexError as ex:
raise RuntimeError(f"Not enough helipads available at {cp}") from ex
group = self._generate_at_group(
name=name,
side=side,
unit_type=unit_type,
count=count,
start_type=start_type,
at=helipad,
)
# Note : A bit dirty, need better support in pydcs
group.points[0].action = PointAction.FromGroundArea
group.points[0].type = "TakeOffGround"
group.units[0].heading = helipad.units[0].heading
if start_type != "Cold":
group.points[0].action = PointAction.FromGroundAreaHot
group.points[0].type = "TakeOffGroundHot"
for i in range(count - 1):
try:
helipad = self.helipads[cp].pop()
group.units[1 + i].position = Point(helipad.x, helipad.y)
group.units[1 + i].heading = helipad.units[0].heading
except IndexError as ex:
raise RuntimeError(f"Not enough helipads available at {cp}") from ex
return group
def _add_radio_waypoint(
self,
group: FlyingGroup[Any],
@ -692,11 +742,13 @@ class AircraftConflictGenerator:
self, cp: ControlPoint, country: Country, flight: Flight
) -> FlyingGroup[Any]:
name = namegen.next_aircraft_name(country, cp.id, flight)
group: FlyingGroup[Any]
try:
if flight.start_type == "In Flight":
group = self._generate_inflight(
name=name, side=country, flight=flight, origin=cp
)
return group
elif isinstance(cp, NavalControlPoint):
group_name = cp.get_carrier_group_name()
carrier_group = self.m.find_group(group_name)
@ -705,7 +757,7 @@ class AircraftConflictGenerator:
f"Carrier group {carrier_group} is a "
"{carrier_group.__class__.__name__}, expected a ShipGroup"
)
group = self._generate_at_group(
return self._generate_at_group(
name=name,
side=country,
unit_type=flight.unit_type.dcs_unit_type,
@ -714,11 +766,22 @@ class AircraftConflictGenerator:
at=carrier_group,
)
else:
# If the flight is an helicopter flight, then prioritize dedicated helipads
if flight.unit_type.helicopter:
return self._generate_at_cp_helipad(
name=name,
side=country,
unit_type=flight.unit_type.dcs_unit_type,
count=flight.count,
start_type=flight.start_type,
cp=cp,
)
if not isinstance(cp, Airfield):
raise RuntimeError(
f"Attempted to spawn at airfield for non-airfield {cp}"
)
group = self._generate_at_airport(
return self._generate_at_airport(
name=name,
side=country,
unit_type=flight.unit_type.dcs_unit_type,
@ -737,8 +800,7 @@ class AircraftConflictGenerator:
name=name, side=country, flight=flight, origin=cp
)
group.points[0].alt = 1500
return group
return group
@staticmethod
def set_reduced_fuel(

View File

@ -220,6 +220,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"

View File

@ -9,6 +9,7 @@ from __future__ import annotations
import logging
import random
from collections import defaultdict
from typing import (
Dict,
Iterator,
@ -35,7 +36,7 @@ from dcs.task import (
FireAtPoint,
)
from dcs.triggers import TriggerStart, TriggerZone
from dcs.unit import Ship, Unit, Vehicle, SingleHeliPad
from dcs.unit import Ship, Unit, Vehicle, InvisibleFARP
from dcs.unitgroup import ShipGroup, StaticGroup, VehicleGroup
from dcs.unittype import StaticType, ShipType, VehicleType
from dcs.vehicles import vehicle_map
@ -589,22 +590,45 @@ class HelipadGenerator:
self.game = game
self.radio_registry = radio_registry
self.tacan_registry = tacan_registry
self.helipads: list[StaticGroup] = []
def generate(self) -> None:
# Note : Helipad are generated as neutral object in order not to interfer with capture triggers
neutral_country = self.m.country(self.game.neutral_country.name)
country = self.m.country(self.game.coalition_for(self.cp.captured).country_name)
for i, helipad in enumerate(self.cp.helipads):
name = self.cp.name + "_helipad_" + str(i)
logging.info("Generating helipad : " + name)
pad = SingleHeliPad(name=(name + "_unit"))
logging.info("Generating helipad static : " + name)
pad = InvisibleFARP(name=name)
pad.position = Point(helipad.x, helipad.y)
pad.heading = helipad.heading.degrees
# pad.heliport_frequency = self.radio_registry.alloc_uhf() TODO : alloc radio & callsign
sg = unitgroup.StaticGroup(self.m.next_group_id(), name)
sg.add_unit(pad)
sp = StaticPoint()
sp.position = pad.position
sg.add_point(sp)
country.add_static_group(sg)
neutral_country.add_static_group(sg)
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,
)
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,
)
class GroundObjectsGenerator:
@ -631,13 +655,18 @@ class GroundObjectsGenerator:
self.unit_map = unit_map
self.icls_alloc = iter(range(1, 21))
self.runways: Dict[str, RunwayData] = {}
self.helipads: dict[ControlPoint, list[StaticGroup]] = defaultdict(list)
def generate(self) -> None:
for cp in self.game.theater.controlpoints:
country = self.m.country(self.game.coalition_for(cp.captured).country_name)
HelipadGenerator(
# Generate helipads
helipad_gen = HelipadGenerator(
self.m, cp, self.game, self.radio_registry, self.tacan_registry
).generate()
)
helipad_gen.generate()
self.helipads[cp] = helipad_gen.helipads
for ground_object in cp.ground_objects:
generator: GenericGroundObjectGenerator[Any]

View File

@ -40,12 +40,14 @@ class QAircraftRecruitmentMenu(UnitTransactionFrame[Squadron]):
row = 0
unit_types: Set[AircraftType] = set()
for squadron in cp.squadrons:
unit_types.add(squadron.aircraft)
sorted_squadrons = sorted(cp.squadrons, key=lambda s: (s.aircraft.name, s.name))
for row, squadron in enumerate(sorted_squadrons):
self.add_purchase_row(squadron, task_box_layout, row)
stretch = QVBoxLayout()
stretch.addStretch()
task_box_layout.addLayout(stretch, row, 0)

View File

@ -1,11 +0,0 @@
{
"name": "Syria - Battle for Golan Heights",
"theater": "Syria",
"authors": "Khopa",
"recommended_player_faction": "Israel 2000",
"recommended_enemy_faction": "Syria 2011",
"description": "<p>In this scenario, you start in Israel and the conflict is focused around the golan heights, an historically disputed territory.<br/><br/>This scenario is designed to be performance friendly.</p>",
"miz": "golan_heights_lite.miz",
"performance": 1,
"version": "8.0"
}

View File

@ -0,0 +1,104 @@
---
name: Syria - Battle for Golan Heights
theater: Syria
authors: Khopa
recommended_player_faction: Israel 2000
recommended_enemy_faction: Syria 2011
description: <p>In this scenario, you start in Israel and the conflict is focused around the golan heights, an historically disputed territory.<br/><br/>This scenario is designed to be performance and helicopter friendly.</p>
miz: golan_heights_lite.miz
performance: 1
version: "9.0"
squadrons:
# Ramat-David
30:
- primary: AEW&C
aircraft:
- E-3A
- primary: Refueling
aircraft:
- KC-135 Stratotanker
- primary: Transport
aircraft:
- C-130
- primary: BARCAP
secondary: any
aircraft:
- F-15C Eagle
- primary: BARCAP
secondary: any
aircraft:
- F-4E Phantom II
- primary: Strike
secondary: air-to-ground
aircraft:
- F-15E Strike Eagle
- primary: SEAD
secondary: any
aircraft:
- F-16CM Fighting Falcon (Block 50)
# Golan South
Golan South:
- primary: CAS
secondary: air-to-ground
aircraft:
- AH-1W SuperCobra
- primary: CAS
secondary: air-to-ground
aircraft:
- AH-64D Apache Longbow
- primary: Transport
aircraft:
- UH-1H Iroquois
# Golan North
Golan North:
- primary: CAS
secondary: air-to-ground
aircraft:
- Mi-24P Hind-F
- primary: CAS
aircraft:
- SA 342M Gazelle
- primary: Transport
secondary: air-to-ground
aircraft:
- Mi-8MTV2 Hip
# Marj Ruhayyil
23:
- primary: BARCAP
secondary: any
aircraft:
- MiG-21bis Fishbed-N
- primary: BARCAP
secondary: any
aircraft:
- MiG-23MLD Flogger-K
- primary: SEAD
secondary: air-to-ground
aircraft:
- Su-17M4 Fitter-K
- primary: Strike
secondary: air-to-ground
aircraft:
- Su-17M4 Fitter-K
# Damascus
7:
- primary: BARCAP
secondary: any
aircraft:
- MiG-29S Fulcrum-C
- primary: BARCAP
secondary: any
aircraft:
- MiG-21bis Fishbed-N
- primary: BARCAP
secondary: any
aircraft:
- MiG-25PD Foxbat-E
- primary: SEAD
secondary: air-to-ground
aircraft:
- Su-24M Fencer-D
- primary: Strike
secondary: air-to-ground
aircraft:
- Su-17M4 Fitter-K