mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Merge pull request #1183 from dcs-liberation/helipads
[WIP] Add possibility to add helipads to FOB control points
This commit is contained in:
commit
82f5287282
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
13
game/game.py
13
game/game.py
@ -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(
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,7 +800,6 @@ class AircraftConflictGenerator:
|
||||
name=name, side=country, flight=flight, origin=cp
|
||||
)
|
||||
group.points[0].alt = 1500
|
||||
|
||||
return group
|
||||
|
||||
@staticmethod
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"
|
||||
}
|
||||
Binary file not shown.
104
resources/campaigns/golan_heights_lite.yaml
Normal file
104
resources/campaigns/golan_heights_lite.yaml
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user