Merge remote-tracking branch 'remotes/dcs-retribution/dcs-retribution/dev' into pretense-generator

This commit is contained in:
MetalStormGhost 2024-07-05 12:06:53 +03:00
commit 1a6d73f055
111 changed files with 2645 additions and 500 deletions

View File

@ -90,5 +90,6 @@ Excellent lua scripts DCS Liberation/Retribution uses as plugins:
* For the JTAC feature, DCS Retribution embeds Ciribob's JTAC Autolase [script](https://github.com/ciribob/DCS-JTACAutoLaze).
* Walder's [Skynet-IADS](https://github.com/walder/Skynet-IADS) is used for Integrated Air Defense System.
* Carstens Arty Spotter https://www.digitalcombatsimulator.com/en/files/3339128/ is an amazing force multiplyer to drop the hammer on enemies.
Please also show some support to these projects !

View File

@ -18,12 +18,17 @@
* **[Campaign Design]** Support for Kola map by Orbx
* **[UI]** Zoom level retained when switching campaigns
* **[UX]** Allow changing squadrons in flight's edit dialog
* **[Cheats]** Sink/Resurrect carriers instead of showing an error during cheat-capture (use AWCD-cheat to add squadrons upon resurrection)
* **[UI/UX]** Allow changing conditions such as Time, Date & Weather
* **[Modding]** Added support for Su-15 Flagon mod (v1.0)
* **[Plugins]** Support for Carsten's Arty Spotter script
## Fixes
* **[UI/UX]** A-10A flights can be edited again
* **[Mission Generation]** IADS bug sometimes triggering "no skynet usable units" error during mission generation
* **[New Game Wizard]** Campaign errors show a dialog again and avoid CTDs
* **[UI]** Landmap wasn't updating when switching to a different theater
* **[Mission Results Processor]** Squadrons of a sunken carrier are now disbanded
# Retribution v1.3.1
#### Note: Re-save your missions in DCS' Mission Editor to avoid possible crashes due to datalink (usually the case when F-16C blk50s are used) when hosting missions on a dedicated server.
@ -75,6 +80,7 @@
* **[Mission Generator]** Set F-14's IP waypoint according to the flight-plan's ingress point
* **[Mission Generator]** Automatically de-spawn aircraft when arrival/divert is an off-map spawn
* **[Options]** Option to de-spawn AI flights in the air if their start-type was manually set to In-Flight
* **[Campaign Design]** Ability to add separate ground spawns for C-130 and other large aircraft to campaigns.
* **[Config]** Preference setting to use custom Liberation payloads instead of prioritizing Retribution's default
* **[Config]** Preference setting to configure the server-port on which Retribution's back-end will run
* **[Options]** Made AI jettisoning empty fuel tanks optional (disabled by default)

View File

@ -6971,11 +6971,11 @@
}
},
"node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dependencies": {
"fill-range": "^7.0.1"
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
@ -8727,9 +8727,9 @@
"dev": true
},
"node_modules/ejs": {
"version": "3.1.9",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz",
"integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==",
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
"integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
"dev": true,
"dependencies": {
"jake": "^10.8.5"
@ -10162,9 +10162,9 @@
}
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dependencies": {
"to-regex-range": "^5.0.1"
},
@ -27236,11 +27236,11 @@
}
},
"braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"requires": {
"fill-range": "^7.0.1"
"fill-range": "^7.1.1"
}
},
"browser-process-hrtime": {
@ -28504,9 +28504,9 @@
"dev": true
},
"ejs": {
"version": "3.1.9",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz",
"integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==",
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
"integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
"dev": true,
"requires": {
"jake": "^10.8.5"
@ -29614,9 +29614,9 @@
"dev": true
},
"fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"requires": {
"to-regex-range": "^5.0.1"
}

View File

@ -249,6 +249,22 @@ class WaypointBuilder:
objective: MissionTarget,
) -> FlightWaypoint:
alt = self.get_combat_altitude
if ingress_type in [
FlightWaypointType.INGRESS_CAS,
FlightWaypointType.INGRESS_OCA_AIRCRAFT,
]:
weather = self.flight.coalition.game.conditions.weather
max_alt = feet(30000)
if weather.clouds and (
weather.clouds.preset
and "overcast" in weather.clouds.preset.description.lower()
or weather.clouds.density > 5
):
max_alt = meters(
max(feet(500).meters, weather.clouds.base - feet(500).meters)
)
alt = min(alt, max_alt)
alt_type: AltitudeReference = "BARO"
if self.is_helo or self.flight.is_hercules:
alt_type = "RADIO"
@ -381,13 +397,23 @@ class WaypointBuilder:
return waypoint
def cas(self, position: Point) -> FlightWaypoint:
weather = self.flight.coalition.game.conditions.weather
max_alt = feet(30000)
if weather.clouds and (
weather.clouds.preset
and "overcast" in weather.clouds.preset.description.lower()
or weather.clouds.density > 5
):
max_alt = meters(
max(feet(500).meters, weather.clouds.base - feet(500).meters)
)
return FlightWaypoint(
"CAS",
FlightWaypointType.CAS,
position,
feet(self.flight.coalition.game.settings.heli_combat_alt_agl)
if self.is_helo
else meters(1000),
else min(meters(1000), max_alt),
"RADIO",
description="Provide CAS",
pretty_name="CAS",

View File

@ -9,7 +9,7 @@ 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, A_10A, AJS37
from dcs.planes import F_15C, A_10A, AJS37, C_130
from dcs.ships import HandyWind, LHA_Tarawa, Stennis, USS_Arleigh_Burke_IIa
from dcs.statics import Fortification, Warehouse
from dcs.terrain import Airport
@ -43,6 +43,7 @@ class MizCampaignLoader:
OFF_MAP_UNIT_TYPE = F_15C.id
GROUND_SPAWN_UNIT_TYPE = A_10A.id
GROUND_SPAWN_ROADBASE_UNIT_TYPE = AJS37.id
GROUND_SPAWN_LARGE_UNIT_TYPE = C_130.id
CV_UNIT_TYPE = Stennis.id
LHA_UNIT_TYPE = LHA_Tarawa.id
@ -237,6 +238,12 @@ class MizCampaignLoader:
if group.units[0].type == self.GROUND_SPAWN_ROADBASE_UNIT_TYPE:
yield group
@property
def ground_spawns_large(self) -> Iterator[PlaneGroup]:
for group in itertools.chain(self.blue.plane_group, self.red.plane_group):
if group.units[0].type == self.GROUND_SPAWN_LARGE_UNIT_TYPE:
yield group
@property
def ground_spawns(self) -> Iterator[PlaneGroup]:
for group in itertools.chain(self.blue.plane_group, self.red.plane_group):
@ -536,6 +543,10 @@ class MizCampaignLoader:
closest, distance = self.objective_info(plane_group)
self._add_ground_spawn(closest.ground_spawns_roadbase, plane_group)
for plane_group in self.ground_spawns_large:
closest, distance = self.objective_info(plane_group)
self._add_ground_spawn(closest.ground_spawns_large, 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)

View File

@ -532,6 +532,15 @@ class AircraftType(UnitType[Type[FlyingType]]):
for task_name, priority in data.get("tasks", {}).items():
task_priorities[FlightType(task_name)] = priority
if (
FlightType.SEAD_SWEEP not in task_priorities
and FlightType.SEAD in task_priorities
):
task_priorities[FlightType.SEAD_SWEEP] = task_priorities[FlightType.SEAD]
cls._custom_weapon_injections(aircraft, data)
cls._user_weapon_injections(aircraft)
display_name = data.get("display_name", variant_id)
return AircraftType(
dcs_unit_type=aircraft,

View File

@ -402,12 +402,17 @@ class Faction:
self.remove_aircraft("VSN_F106B")
if not mod_settings.a6a_intruder:
self.remove_aircraft("VSN_A6A")
if not mod_settings.ea6b_prowler:
self.remove_aircraft("EA_6B")
if not mod_settings.jas39_gripen:
self.remove_aircraft("JAS39Gripen")
self.remove_aircraft("JAS39Gripen_BVR")
self.remove_aircraft("JAS39Gripen_AG")
if not mod_settings.super_etendard:
self.remove_aircraft("VSN_SEM")
if not mod_settings.su15_flagon:
self.remove_aircraft("Su_15")
self.remove_aircraft("Su_15TM")
if not mod_settings.su30_flanker_h:
self.remove_aircraft("Su-30MKA")
self.remove_aircraft("Su-30MKI")

View File

@ -96,6 +96,7 @@ class Migrator:
try_set_attr(cp, "ground_spawns_roadbase", [])
try_set_attr(cp, "helipads_quad", [])
try_set_attr(cp, "helipads_invisible", [])
try_set_attr(cp, "ground_spawns_large", [])
if (
cp.dcs_airport and is_sinai and cp.dcs_airport.id == 20
): # fix for Hatzor

View File

@ -56,6 +56,7 @@ class AircraftGenerator:
mission_data: MissionData,
helipads: dict[ControlPoint, list[StaticGroup]],
ground_spawns_roadbase: dict[ControlPoint, list[Tuple[StaticGroup, Point]]],
ground_spawns_large: dict[ControlPoint, list[Tuple[StaticGroup, Point]]],
ground_spawns: dict[ControlPoint, list[Tuple[StaticGroup, Point]]],
) -> None:
self.mission = mission
@ -69,6 +70,7 @@ class AircraftGenerator:
self.mission_data = mission_data
self.helipads = helipads
self.ground_spawns_roadbase = ground_spawns_roadbase
self.ground_spawns_large = ground_spawns_large
self.ground_spawns = ground_spawns
self.ewrj_package_dict: Dict[int, List[FlyingGroup[Any]]] = {}
@ -208,6 +210,7 @@ class AircraftGenerator:
self.mission,
self.helipads,
self.ground_spawns_roadbase,
self.ground_spawns_large,
self.ground_spawns,
self.mission_data,
).create_idle_aircraft()
@ -239,6 +242,7 @@ class AircraftGenerator:
self.mission,
self.helipads,
self.ground_spawns_roadbase,
self.ground_spawns_large,
self.ground_spawns,
self.mission_data,
).create_flight_group()

View File

@ -67,6 +67,7 @@ class FlightGroupSpawner:
mission: Mission,
helipads: dict[ControlPoint, list[StaticGroup]],
ground_spawns_roadbase: dict[ControlPoint, list[Tuple[StaticGroup, Point]]],
ground_spawns_large: dict[ControlPoint, list[Tuple[StaticGroup, Point]]],
ground_spawns: dict[ControlPoint, list[Tuple[StaticGroup, Point]]],
mission_data: MissionData,
) -> None:
@ -75,6 +76,7 @@ class FlightGroupSpawner:
self.mission = mission
self.helipads = helipads
self.ground_spawns_roadbase = ground_spawns_roadbase
self.ground_spawns_large = ground_spawns_large
self.ground_spawns = ground_spawns
self.mission_data = mission_data
@ -178,6 +180,8 @@ class FlightGroupSpawner:
raise RuntimeError(
f"Cannot spawn fixed-wing aircraft at {cp} because of insufficient ground spawn slots."
)
is_large = self.flight.unit_type.dcs_unit_type.width > 40
pilot_count = len(self.flight.roster.members)
if (
not is_heli
@ -193,10 +197,18 @@ class FlightGroupSpawner:
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 and is_large:
pad_group = self._generate_at_cp_ground_spawn(name, cp, is_large)
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
else:
pad_group = self._generate_at_cp_ground_spawn(name, cp, True)
if pad_group is not None:
return pad_group
return self._generate_over_departure(name, cp)
elif isinstance(cp, Airfield):
is_heli = self.flight.squadron.aircraft.helicopter
@ -204,6 +216,35 @@ class FlightGroupSpawner:
pad_group = self._generate_at_cp_helipad(name, cp)
if pad_group is not None:
return pad_group
# Large planes (wingspan more than 40 meters, looking at you, C-130)
# First try spawning on large ground spawns
# Then try the regular airfield ramp spawns
is_large = self.flight.unit_type.dcs_unit_type.width > 40
if (
cp.has_ground_spawns
and is_large
and len(self.ground_spawns_large[cp]) >= self.flight.count
and (self.flight.client_count > 0)
):
pad_group = self._generate_at_cp_ground_spawn(name, cp, is_large)
if pad_group is not None:
return pad_group
# Below 40 meter wingspan aircraft
# First try spawning on regular or roadbase ground spawns
# Then try the regular airfield ramp spawns
# Then, if both of the above fail, use the large ground spawns
if (
cp.has_ground_spawns
and len(self.ground_spawns[cp])
+ len(self.ground_spawns_roadbase[cp])
+ len(self.ground_spawns_large[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
if (
cp.has_ground_spawns
and len(self.ground_spawns[cp])
@ -214,33 +255,45 @@ class FlightGroupSpawner:
pad_group = self._generate_at_cp_ground_spawn(name, cp)
if pad_group is not None:
return pad_group
# TODO: get rid of the nevatim hack once fixed in DCS...
if self._check_nevatim_hack(cp):
slots = [
slot
for slot in cp.dcs_airport.free_parking_slots(
self.flight.squadron.aircraft.dcs_unit_type
)
if slot.slot_name in [str(n) for n in range(55, 66)]
]
return self._generate_at_airfield(name, cp, slots)
elif self._check_ramon_airbase_hack(cp):
# TODO: get rid of the ramon airbase hack once fixed in DCS...
slots = [
slot
for slot in cp.dcs_airport.free_parking_slots(
self.flight.squadron.aircraft.dcs_unit_type
)
if slot.slot_name
not in [
str(n)
for n in [1, 2, 3, 4, 5, 6, 13, 14, 15, 16, 17, 18, 61]
try:
# TODO: get rid of the nevatim hack once fixed in DCS...
if self._check_nevatim_hack(cp):
slots = [
slot
for slot in cp.dcs_airport.free_parking_slots(
self.flight.squadron.aircraft.dcs_unit_type
)
if slot.slot_name in [str(n) for n in range(55, 66)]
]
]
return self._generate_at_airfield(name, cp, slots)
else:
return self._generate_at_airfield(name, cp)
return self._generate_at_airfield(name, cp, slots)
elif self._check_ramon_airbase_hack(cp):
# TODO: get rid of the ramon airbase hack once fixed in DCS...
slots = [
slot
for slot in cp.dcs_airport.free_parking_slots(
self.flight.squadron.aircraft.dcs_unit_type
)
if slot.slot_name
not in [
str(n)
for n in [1, 2, 3, 4, 5, 6, 13, 14, 15, 16, 17, 18, 61]
]
]
return self._generate_at_airfield(name, cp, slots)
else:
return self._generate_at_airfield(name, cp)
except NoParkingSlotError:
if (
cp.has_ground_spawns
and len(self.ground_spawns_large[cp]) >= self.flight.count
and (self.flight.client_count > 0 or is_heli)
):
pad_group = self._generate_at_cp_ground_spawn(name, cp, True)
if pad_group is not None:
return pad_group
else:
raise NoParkingSlotError
return self._generate_at_airfield(name, cp)
else:
raise NotImplementedError(
f"Aircraft spawn behavior not implemented for {cp} ({cp.__class__})"
@ -440,22 +493,26 @@ class FlightGroupSpawner:
return group
def _generate_at_cp_ground_spawn(
self, name: str, cp: ControlPoint
self, name: str, cp: ControlPoint, is_large: bool = False
) -> Optional[FlyingGroup[Any]]:
is_airbase = False
is_roadbase = False
try:
if len(self.ground_spawns_roadbase[cp]) > 0:
ground_spawn = self.ground_spawns_roadbase[cp].pop()
is_roadbase = True
if is_large:
if len(self.ground_spawns_large[cp]) > 0:
ground_spawn = self.ground_spawns_large[cp].pop()
is_airbase = True
else:
ground_spawn = self.ground_spawns[cp].pop()
is_airbase = True
if len(self.ground_spawns_roadbase[cp]) > 0:
ground_spawn = self.ground_spawns_roadbase[cp].pop()
is_roadbase = True
if len(self.ground_spawns[cp]) > 0:
ground_spawn = self.ground_spawns[cp].pop()
is_airbase = True
except IndexError as ex:
logging.warning("Not enough STOL slots available at " + str(ex))
logging.warning("Not enough ground spawn 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])
@ -524,10 +581,14 @@ class FlightGroupSpawner:
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()
if is_large:
if len(self.ground_spawns_large[cp]) > 0:
ground_spawn = self.ground_spawns_large[cp].pop()
else:
ground_spawn = self.ground_spawns[cp].pop()
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
)

View File

@ -249,6 +249,7 @@ class MissionGenerator:
mission_data=self.mission_data,
helipads=tgo_generator.helipads,
ground_spawns_roadbase=tgo_generator.ground_spawns_roadbase,
ground_spawns_large=tgo_generator.ground_spawns_large,
ground_spawns=tgo_generator.ground_spawns,
)

View File

@ -1042,6 +1042,123 @@ class GroundSpawnRoadbaseGenerator:
self.ground_spawns_roadbase = []
class GroundSpawnLargeGenerator:
"""
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_large: list[Tuple[StaticGroup, Point]] = []
def create_ground_spawn_large(
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} large ground spawn {i}"
logging.info("Generating Large 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_large.append((sg, vtol_pad[1]))
# tanker_type: Type[VehicleType]
# ammo_truck_type: Type[VehicleType]
tanker_type, ammo_truck_type, power_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, 45
),
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, 45
),
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, 55
),
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, 45
),
heading=pad.heading + 270,
)
if self.game.settings.ground_start_ground_power_trucks:
self.m.vehicle_group(
country=country,
name=(name + "_power"),
_type=power_truck_type,
position=pad.position.point_from_heading(
vtol_pad[0].heading.degrees - 185, 45
),
group_size=1,
heading=pad.heading + 45,
move_formation=PointAction.OffRoad,
)
def generate(self) -> None:
try:
for i, vtol_pad in enumerate(self.cp.ground_spawns_large):
self.create_ground_spawn_large(i, vtol_pad)
except AttributeError:
self.ground_spawns_large = []
class GroundSpawnGenerator:
"""
Generates STOL aircraft starting positions for given control point
@ -1207,6 +1324,9 @@ class TgoGenerator:
self.ground_spawns_roadbase: dict[
ControlPoint, list[Tuple[StaticGroup, Point]]
] = defaultdict(list)
self.ground_spawns_large: dict[
ControlPoint, list[Tuple[StaticGroup, Point]]
] = defaultdict(list)
self.ground_spawns: dict[
ControlPoint, list[Tuple[StaticGroup, Point]]
] = defaultdict(list)
@ -1233,7 +1353,15 @@ class TgoGenerator:
] = ground_spawn_roadbase_gen.ground_spawns_roadbase
random.shuffle(self.ground_spawns_roadbase[cp])
# Generate STOL pads
# Generate Large Ground Spawn slots
ground_large_spawn_gen = GroundSpawnLargeGenerator(
self.m, cp, self.game, self.radio_registry, self.tacan_registry
)
ground_large_spawn_gen.generate()
self.ground_spawns_large[cp] = ground_large_spawn_gen.ground_spawns_large
random.shuffle(self.ground_spawns_large[cp])
# Generate Ground Spawn slots
ground_spawn_gen = GroundSpawnGenerator(
self.m, cp, self.game, self.radio_registry, self.tacan_registry
)

View File

@ -159,11 +159,13 @@ class MissionResultsProcessor:
captured.control_point.capture(
self.game, events, captured.captured_by_player
)
logging.info(f"Will run redeploy for {captured.control_point}")
self.redeploy_units(captured.control_point)
except Exception:
logging.exception(f"Could not process base capture {captured}")
for captured in debriefing.base_captures:
logging.info(f"Will run redeploy for {captured.control_point}")
self.redeploy_units(captured.control_point)
def record_carcasses(self, debriefing: Debriefing) -> None:
for destroyed_unit in debriefing.state_data.destroyed_statics:
self.game.add_destroyed_units(destroyed_unit)
@ -301,10 +303,6 @@ class MissionResultsProcessor:
""" "
Auto redeploy units to newly captured base
"""
ally_connected_cps = [
ocp for ocp in cp.connected_points if cp.captured == ocp.captured
]
enemy_connected_cps = [
ocp for ocp in cp.connected_points if cp.captured != ocp.captured
]
@ -314,28 +312,54 @@ class MissionResultsProcessor:
if len(enemy_connected_cps) == 0:
return
ally_connected_cps = [
ocp
for ocp in cp.transitive_connected_friendly_destinations()
if cp.captured == ocp.captured and ocp.base.total_armor
]
settings = cp.coalition.game.settings
factor = (
settings.frontline_reserves_factor
if cp.captured
else settings.frontline_reserves_factor_red
)
# From each ally cp, send reinforcements
for ally_cp in ally_connected_cps:
for ally_cp in sorted(
ally_connected_cps,
key=lambda x: len(
[cp for cp in x.connected_points if x.captured != cp.captured]
),
):
self.redeploy_between(cp, ally_cp)
if cp.base.total_armor > factor * cp.deployable_front_line_units:
break
def redeploy_between(self, destination: ControlPoint, source: ControlPoint) -> None:
total_units_redeployed = 0
moved_units = {}
if source.has_active_frontline or not destination.captured:
# If there are still active front lines to defend at the
# transferring CP we should not transfer all units.
#
# Opfor also does not transfer all of their units.
# TODO: Balance the CPs rather than moving half from everywhere.
move_factor = 0.5
else:
# Otherwise we can move everything.
move_factor = 1
settings = source.coalition.game.settings
reserves = max(
1,
settings.reserves_procurement_target
if source.captured
else settings.reserves_procurement_target_red,
)
total_units = source.base.total_armor
reserves_factor = (reserves - 1) / total_units # slight underestimation
source_frontline_count = len(
[cp for cp in source.connected_points if not source.is_friendly_to(cp)]
)
move_factor = max(0.0, 1 / (source_frontline_count + 1) - reserves_factor)
for frontline_unit, count in source.base.armor.items():
moved_units[frontline_unit] = int(count * move_factor)
total_units_redeployed = total_units_redeployed + int(count * move_factor)
moved_count = int(count * move_factor)
moved_units[frontline_unit] = moved_count
total_units_redeployed += moved_count
destination.base.commission_units(moved_units)
source.base.commit_losses(moved_units)

View File

@ -390,6 +390,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
self.helipads_quad: List[PointWithHeading] = []
self.helipads_invisible: List[PointWithHeading] = []
self.ground_spawns_roadbase: List[Tuple[PointWithHeading, Point]] = []
self.ground_spawns_large: List[Tuple[PointWithHeading, Point]] = []
self.ground_spawns: List[Tuple[PointWithHeading, Point]] = []
self._coalition: Optional[Coalition] = None
@ -572,6 +573,23 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
connected.extend(cp.transitive_friendly_shipping_destinations(seen))
return connected
def transitive_connected_friendly_destinations(
self, seen: Optional[Set[ControlPoint]] = None
) -> List[ControlPoint]:
if seen is None:
seen = {self}
connected = []
for cp in set(self.connected_points + list(self.shipping_lanes.keys())):
if cp.captured != self.captured:
continue
if cp in seen:
continue
seen.add(cp)
connected.append(cp)
connected.extend(cp.transitive_connected_friendly_destinations(seen))
return connected
@property
def has_factory(self) -> bool:
for tgo in self.connected_objectives:
@ -594,7 +612,12 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
"""
Returns true if cp can operate STOL aircraft
"""
return len(self.ground_spawns_roadbase) + len(self.ground_spawns) > 0
return (
len(self.ground_spawns_roadbase)
+ len(self.ground_spawns_large)
+ 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."""
@ -1266,6 +1289,7 @@ class Airfield(ControlPoint, CTLD):
if parking_type.include_fixed_wing_stol:
parking_slots += len(self.ground_spawns)
parking_slots += len(self.ground_spawns_roadbase)
parking_slots += len(self.ground_spawns_large)
if parking_type.include_fixed_wing:
parking_slots += len(self.airport.parking_slots)
return parking_slots
@ -1655,13 +1679,19 @@ class Fob(ControlPoint, RadioFrequencyContainer, CTLD):
+ len(self.helipads_invisible)
)
try:
if parking_type.include_fixed_wing_stol:
if parking_type.include_fixed_wing_stol:
try:
parking_slots += len(self.ground_spawns)
except AttributeError:
self.ground_spawns_roadbase = []
try:
parking_slots += len(self.ground_spawns_roadbase)
except AttributeError:
self.ground_spawns_roadbase = []
self.ground_spawns = []
except AttributeError:
self.ground_spawns_large = []
try:
parking_slots += len(self.ground_spawns_large)
except AttributeError:
self.ground_spawns = []
return parking_slots
def can_operate(self, aircraft: AircraftType) -> bool:

View File

@ -68,6 +68,7 @@ class ModSettings:
a4_skyhawk: bool = False
a6a_intruder: bool = False
a7e_corsair2: bool = False
ea6b_prowler: bool = False
f4bc_phantom: bool = False
f9f_panther: bool = False
f15d_baz: bool = False
@ -86,6 +87,7 @@ class ModSettings:
uh_60l: bool = False
jas39_gripen: bool = False
super_etendard: bool = False
su15_flagon: bool = False
su30_flanker_h: bool = False
su57_felon: bool = False
frenchpack: bool = False

View File

@ -283,6 +283,10 @@ class TheaterGroundObject(MissionTarget, SidcDescribable, ABC):
def coalition(self) -> Coalition:
return self.control_point.coalition
@property
def is_naval_control_point(self) -> bool:
return False
class BuildingGroundObject(TheaterGroundObject):
def __init__(
@ -384,6 +388,10 @@ class GenericCarrierGroundObject(NavalGroundObject, ABC):
def is_control_point(self) -> bool:
return True
@property
def is_naval_control_point(self) -> bool:
return True
# TODO: Why is this both a CP and a TGO?
class CarrierGroundObject(GenericCarrierGroundObject):

View File

@ -66,6 +66,18 @@ class TheaterUnit:
if self.ground_object.is_iads:
iads = self.ground_object.control_point.coalition.game.theater.iads_network
iads.update_tgo(self.ground_object, events)
if self.ground_object.is_naval_control_point:
cp = self.ground_object.control_point
for squadron in cp.squadrons:
cp.coalition.air_wing.squadrons[squadron.aircraft].remove(squadron)
def revive(self, events: GameUpdateEvents) -> None:
self.alive = True
self.ground_object.threat_poly()
events.update_tgo(self.ground_object)
if self.ground_object.is_iads:
iads = self.ground_object.control_point.coalition.game.theater.iads_network
iads.update_tgo(self.ground_object, events)
@property
def unit_name(self) -> str:

View File

@ -2,6 +2,7 @@ from .SWPack import *
from .a4ec import *
from .a7e import *
from .a6a import *
from .ea6b import *
from .f9f import *
from .f100 import *
from .f104 import *
@ -23,6 +24,7 @@ from .jas39 import *
from .ov10a import *
from .spanishnavypack import *
from .super_etendard import *
from .su15 import *
from .su30 import *
from .su57 import *
from .swedishmilitaryassetspack import *

View File

@ -0,0 +1 @@
from .ea6b import *

View File

@ -0,0 +1,105 @@
from dcs import task
from dcs.planes import PlaneType
from dcs.weapons_data import Weapons
from game.modsupport import planemod
from pydcs_extensions.weapon_injector import inject_weapons
class WeaponsEA6B:
EA6B_AN_ALQ_99 = {
"clsid": "{EA6B_ANALQ991}",
"name": "EA6B AN-ALQ-99",
"weight": 435,
}
EA6B_AN_ALQ_99_ = {
"clsid": "{EA6B_ANALQ992}",
"name": "EA6B AN-ALQ-99",
"weight": 435,
}
inject_weapons(WeaponsEA6B)
@planemod
class EA_6B(PlaneType):
id = "EA_6B"
height = 4.57
width = 10.15
length = 17.98
fuel_max = 6994
max_speed = 1047.96
chaff = 30
flare = 30
charge_total = 60
chaff_charge_size = 1
flare_charge_size = 1
eplrs = True
radio_frequency = 250.5
livery_name = "EA_6B" # from type
class Pylon1:
LAU_118A___AGM_45B_Shrike_ARM = (1, Weapons.LAU_118A___AGM_45B_Shrike_ARM)
AGM_88C_HARM___High_Speed_Anti_Radiation_Missile_ = (
1,
Weapons.AGM_88C_HARM___High_Speed_Anti_Radiation_Missile_,
)
F_5_275Gal_Fuel_tank = (1, Weapons.F_5_275Gal_Fuel_tank)
EA6B_AN_ALQ_99 = (1, Weapons.EA6B_AN_ALQ_99)
# ERRR <CLEAN>
class Pylon2:
LAU_118A___AGM_45B_Shrike_ARM = (2, Weapons.LAU_118A___AGM_45B_Shrike_ARM)
AGM_88C_HARM___High_Speed_Anti_Radiation_Missile_ = (
2,
Weapons.AGM_88C_HARM___High_Speed_Anti_Radiation_Missile_,
)
F_5_275Gal_Fuel_tank = (2, Weapons.F_5_275Gal_Fuel_tank)
EA6B_AN_ALQ_99 = (2, Weapons.EA6B_AN_ALQ_99)
# ERRR <CLEAN>
class Pylon3:
F_5_275Gal_Fuel_tank = (3, Weapons.F_5_275Gal_Fuel_tank)
EA6B_AN_ALQ_99_ = (3, Weapons.EA6B_AN_ALQ_99_)
# ERRR <CLEAN>
class Pylon4:
LAU_118A___AGM_45B_Shrike_ARM = (4, Weapons.LAU_118A___AGM_45B_Shrike_ARM)
AGM_88C_HARM___High_Speed_Anti_Radiation_Missile_ = (
4,
Weapons.AGM_88C_HARM___High_Speed_Anti_Radiation_Missile_,
)
F_5_275Gal_Fuel_tank = (4, Weapons.F_5_275Gal_Fuel_tank)
EA6B_AN_ALQ_99 = (4, Weapons.EA6B_AN_ALQ_99)
# ERRR <CLEAN>
class Pylon5:
LAU_118A___AGM_45B_Shrike_ARM = (5, Weapons.LAU_118A___AGM_45B_Shrike_ARM)
AGM_88C_HARM___High_Speed_Anti_Radiation_Missile_ = (
5,
Weapons.AGM_88C_HARM___High_Speed_Anti_Radiation_Missile_,
)
F_5_275Gal_Fuel_tank = (5, Weapons.F_5_275Gal_Fuel_tank)
EA6B_AN_ALQ_99 = (5, Weapons.EA6B_AN_ALQ_99)
# ERRR <CLEAN>
pylons = {1, 2, 3, 4, 5}
tasks = [
task.Escort,
task.Reconnaissance,
task.GroundAttack,
task.CAS,
task.AFAC,
task.RunwayAttack,
task.AntishipStrike,
task.SEAD,
]
task_default = task.GroundAttack

View File

@ -31,16 +31,6 @@ class WeaponsF106:
"name": "AIR-2A Genie Nuclear air-to-air unguided rocket.",
"weight": 66,
}
L005_Sorbtsiya_ECM_pod__left_ = {
"clsid": "{44EE8698-89F9-48EE-AF36-5FD31896A82F}",
"name": "L005 Sorbtsiya ECM pod (left)",
"weight": 150,
}
L005_Sorbtsiya_ECM_pod__right_ = {
"clsid": "{44EE8698-89F9-48EE-AF36-5FD31896A82A}",
"name": "L005 Sorbtsiya ECM pod (right)",
"weight": 150,
}
inject_weapons(WeaponsF106)
@ -150,7 +140,7 @@ class VSN_F106A(PlaneType):
# ERRR <CLEAN>
class Pylon8:
WeaponsF106.L005_Sorbtsiya_ECM_pod__left_ = (
L005_Sorbtsiya_ECM_pod__left_ = (
8,
Weapons.L005_Sorbtsiya_ECM_pod__left_,
)
@ -279,7 +269,7 @@ class VSN_F106B(PlaneType):
# ERRR <CLEAN>
class Pylon8:
Weapons.L005_Sorbtsiya_ECM_pod__left_ = (
L005_Sorbtsiya_ECM_pod__left_ = (
8,
Weapons.L005_Sorbtsiya_ECM_pod__left_,
)

View File

@ -0,0 +1 @@
from .su15 import *

View File

@ -0,0 +1,203 @@
from typing import Any, Dict, Set
from dcs import task
from dcs.planes import PlaneType
from dcs.weapons_data import Weapons
from game.modsupport import planemod
from pydcs_extensions.weapon_injector import inject_weapons
class WeaponsSu15:
PTB_600 = {"clsid": "{Su_15_PTB-600}", "name": "PTB-600", "weight": 535}
R_8M1R = {"clsid": "{R-8M1R}", "name": "R-8M1R", "weight": 285}
R_8M1T = {"clsid": "{R-8M1T}", "name": "R-8M1T", "weight": 265}
R_8R_Inert = {"clsid": "{R-8RInert}", "name": "R-8R Inert", "weight": 285}
R_8T_Inert = {"clsid": "{R-8TInert}", "name": "R-8T Inert", "weight": 265}
R_98MR = {"clsid": "{R-98MR}", "name": "R-98MR", "weight": 292}
R_98MT = {"clsid": "{R-98MT}", "name": "R-98MT", "weight": 272}
inject_weapons(WeaponsSu15)
@planemod
class Su_15TM(PlaneType):
id = "Su_15TM"
height = 5
width = 9.34
length = 21.41
fuel_max = 5550
max_speed = 2229.984
chaff = 30
flare = 15
charge_total = 60
chaff_charge_size = 1
flare_charge_size = 2
category = "Interceptor" # {78EFB7A2-FD52-4b57-A6A6-3BF0E1D6555F}
radio_frequency = 127.5
livery_name = "SU_15TM" # from type
class Pylon1:
R_98MR = (1, WeaponsSu15.R_98MR)
R_98MT = (1, WeaponsSu15.R_98MT)
R_8M1R = (1, WeaponsSu15.R_8M1R)
R_8M1T = (1, WeaponsSu15.R_8M1T)
UB_32A_pod___32_x_S_5KO__57mm_UnGd_Rkts__HEAT_Frag = (
1,
Weapons.UB_32A_pod___32_x_S_5KO__57mm_UnGd_Rkts__HEAT_Frag,
)
UB_16UM_pod___16_x_S_5KO__57mm_UnGd_Rkts__HEAT_Frag = (
1,
Weapons.UB_16UM_pod___16_x_S_5KO__57mm_UnGd_Rkts__HEAT_Frag,
)
S_24B___240mm_UnGd_Rkt__235kg__HE_Frag___Low_Smk_ = (
1,
Weapons.S_24B___240mm_UnGd_Rkt__235kg__HE_Frag___Low_Smk_,
)
FAB_100___100kg_GP_Bomb_LD = (1, Weapons.FAB_100___100kg_GP_Bomb_LD)
FAB_250___250kg_GP_Bomb_LD = (1, Weapons.FAB_250___250kg_GP_Bomb_LD)
FAB_500_M_62___500kg_GP_Bomb_LD = (1, Weapons.FAB_500_M_62___500kg_GP_Bomb_LD)
class Pylon2:
APU_60_1M_with_R_60__AA_8_Aphid____IR_AAM = (
2,
Weapons.APU_60_1M_with_R_60__AA_8_Aphid____IR_AAM,
)
APU_60_1M_with_R_60M__AA_8_Aphid_B____IR_AAM = (
2,
Weapons.APU_60_1M_with_R_60M__AA_8_Aphid_B____IR_AAM,
)
class Pylon3:
SPPU_22_1___2_x_23mm__GSh_23L_Autocannon_Pod = (
3,
Weapons.SPPU_22_1___2_x_23mm__GSh_23L_Autocannon_Pod,
)
PTB_600 = (3, WeaponsSu15.PTB_600)
UB_32A_pod___32_x_S_5KO__57mm_UnGd_Rkts__HEAT_Frag = (
3,
Weapons.UB_32A_pod___32_x_S_5KO__57mm_UnGd_Rkts__HEAT_Frag,
)
UB_16UM_pod___16_x_S_5KO__57mm_UnGd_Rkts__HEAT_Frag = (
3,
Weapons.UB_16UM_pod___16_x_S_5KO__57mm_UnGd_Rkts__HEAT_Frag,
)
S_24B___240mm_UnGd_Rkt__235kg__HE_Frag___Low_Smk_ = (
3,
Weapons.S_24B___240mm_UnGd_Rkt__235kg__HE_Frag___Low_Smk_,
)
FAB_100___100kg_GP_Bomb_LD = (3, Weapons.FAB_100___100kg_GP_Bomb_LD)
FAB_250___250kg_GP_Bomb_LD = (3, Weapons.FAB_250___250kg_GP_Bomb_LD)
FAB_500_M_62___500kg_GP_Bomb_LD = (3, Weapons.FAB_500_M_62___500kg_GP_Bomb_LD)
class Pylon4:
SPPU_22_1___2_x_23mm__GSh_23L_Autocannon_Pod = (
4,
Weapons.SPPU_22_1___2_x_23mm__GSh_23L_Autocannon_Pod,
)
PTB_600 = (4, WeaponsSu15.PTB_600)
UB_32A_pod___32_x_S_5KO__57mm_UnGd_Rkts__HEAT_Frag = (
4,
Weapons.UB_32A_pod___32_x_S_5KO__57mm_UnGd_Rkts__HEAT_Frag,
)
UB_16UM_pod___16_x_S_5KO__57mm_UnGd_Rkts__HEAT_Frag = (
4,
Weapons.UB_16UM_pod___16_x_S_5KO__57mm_UnGd_Rkts__HEAT_Frag,
)
S_24B___240mm_UnGd_Rkt__235kg__HE_Frag___Low_Smk_ = (
4,
Weapons.S_24B___240mm_UnGd_Rkt__235kg__HE_Frag___Low_Smk_,
)
FAB_100___100kg_GP_Bomb_LD = (4, Weapons.FAB_100___100kg_GP_Bomb_LD)
FAB_250___250kg_GP_Bomb_LD = (4, Weapons.FAB_250___250kg_GP_Bomb_LD)
FAB_500_M_62___500kg_GP_Bomb_LD = (4, Weapons.FAB_500_M_62___500kg_GP_Bomb_LD)
class Pylon5:
APU_60_1M_with_R_60__AA_8_Aphid____IR_AAM = (
5,
Weapons.APU_60_1M_with_R_60__AA_8_Aphid____IR_AAM,
)
APU_60_1M_with_R_60M__AA_8_Aphid_B____IR_AAM = (
5,
Weapons.APU_60_1M_with_R_60M__AA_8_Aphid_B____IR_AAM,
)
class Pylon6:
R_98MR = (6, WeaponsSu15.R_98MR)
R_98MT = (6, WeaponsSu15.R_98MT)
R_8M1R = (6, WeaponsSu15.R_8M1R)
R_8M1T = (6, WeaponsSu15.R_8M1T)
UB_32A_pod___32_x_S_5KO__57mm_UnGd_Rkts__HEAT_Frag = (
6,
Weapons.UB_32A_pod___32_x_S_5KO__57mm_UnGd_Rkts__HEAT_Frag,
)
UB_16UM_pod___16_x_S_5KO__57mm_UnGd_Rkts__HEAT_Frag = (
6,
Weapons.UB_16UM_pod___16_x_S_5KO__57mm_UnGd_Rkts__HEAT_Frag,
)
S_24B___240mm_UnGd_Rkt__235kg__HE_Frag___Low_Smk_ = (
6,
Weapons.S_24B___240mm_UnGd_Rkt__235kg__HE_Frag___Low_Smk_,
)
FAB_100___100kg_GP_Bomb_LD = (6, Weapons.FAB_100___100kg_GP_Bomb_LD)
FAB_250___250kg_GP_Bomb_LD = (6, Weapons.FAB_250___250kg_GP_Bomb_LD)
FAB_500_M_62___500kg_GP_Bomb_LD = (6, Weapons.FAB_500_M_62___500kg_GP_Bomb_LD)
pylons: Set[int] = {1, 2, 3, 4, 5, 6}
tasks = [
task.GroundAttack,
task.CAS,
task.CAP,
task.Escort,
task.FighterSweep,
task.Intercept,
]
task_default = task.Intercept
@planemod
class Su_15(PlaneType):
id = "Su_15"
height = 5
width = 9.34
length = 21.41
fuel_max = 5600
max_speed = 2229.984
category = "Interceptor" # {78EFB7A2-FD52-4b57-A6A6-3BF0E1D6555F}
radio_frequency = 127.5
livery_name = "SU_15" # from type
class Pylon1:
R_8M1R = (1, WeaponsSu15.R_8M1R)
R_8R_Inert = (1, WeaponsSu15.R_8R_Inert)
R_8M1T = (1, WeaponsSu15.R_8M1T)
R_8T_Inert = (1, WeaponsSu15.R_8T_Inert)
class Pylon2:
SPPU_22_1___2_x_23mm__GSh_23L_Autocannon_Pod = (
2,
Weapons.SPPU_22_1___2_x_23mm__GSh_23L_Autocannon_Pod,
)
PTB_600 = (2, WeaponsSu15.PTB_600)
class Pylon3:
SPPU_22_1___2_x_23mm__GSh_23L_Autocannon_Pod = (
3,
Weapons.SPPU_22_1___2_x_23mm__GSh_23L_Autocannon_Pod,
)
PTB_600 = (3, WeaponsSu15.PTB_600)
class Pylon4:
R_8M1R = (4, WeaponsSu15.R_8M1R)
R_8R_Inert = (4, WeaponsSu15.R_8R_Inert)
R_8M1T = (4, WeaponsSu15.R_8M1T)
R_8T_Inert = (4, WeaponsSu15.R_8T_Inert)
pylons: Set[int] = {1, 2, 3, 4}
tasks = [task.CAP, task.Escort, task.FighterSweep, task.Intercept]
task_default = task.Intercept

View File

@ -324,6 +324,7 @@ def create_game(
a4_skyhawk=False,
a6a_intruder=False,
a7e_corsair2=False,
ea6b_prowler=False,
fa_18efg=False,
fa18ef_tanker=False,
f4bc_phantom=False,
@ -335,6 +336,7 @@ def create_game(
f105_thunderchief=False,
hercules=False,
jas39_gripen=False,
su15_flagon=False,
su30_flanker_h=False,
su57_felon=False,
frenchpack=False,

View File

@ -0,0 +1,85 @@
from copy import deepcopy
from datetime import datetime, timedelta
from PySide6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QPushButton
from game.sim import GameUpdateEvents
from game.weather.clouds import Clouds
from qt_ui.widgets.conditions.QTimeAdjustmentWidget import QTimeAdjustmentWidget
from qt_ui.widgets.conditions.QTimeTurnWidget import QTimeTurnWidget
from qt_ui.widgets.conditions.QWeatherAdjustmentWidget import QWeatherAdjustmentWidget
from qt_ui.widgets.conditions.QWeatherWidget import QWeatherWidget
class QConditionsDialog(QDialog):
def __init__(self, time_turn: QTimeTurnWidget, weather: QWeatherWidget):
super().__init__()
self.time_turn = time_turn
self.weather = weather
self.init_ui()
def init_ui(self):
self.setWindowTitle("Time & Weather Conditions")
self.setMinimumSize(360, 380)
vbox = QVBoxLayout()
self.time_adjuster = QTimeAdjustmentWidget(self.time_turn)
vbox.addWidget(self.time_adjuster, 1)
self.weather_adjuster = QWeatherAdjustmentWidget(self.weather)
vbox.addWidget(self.weather_adjuster, 8)
hbox = QHBoxLayout()
reject_btn = QPushButton("REJECT")
reject_btn.setProperty("style", "btn-danger")
reject_btn.clicked.connect(self.close)
hbox.addWidget(reject_btn)
accept_btn = QPushButton("ACCEPT")
accept_btn.setProperty("style", "btn-success")
accept_btn.clicked.connect(self.apply_conditions)
hbox.addWidget(accept_btn)
vbox.addLayout(hbox, 1)
self.setLayout(vbox)
def apply_conditions(self) -> None:
qdt: datetime = self.time_adjuster.datetime_edit.dateTime().toPython()
sim = self.time_turn.sim_controller
current_time = sim.current_time_in_sim_if_game_loaded
if current_time:
current_time = deepcopy(current_time)
sim.game_loop.sim.time = qdt
game = sim.game_loop.game
game.date = qdt.date() - timedelta(days=game.turn // 4)
game.conditions.start_time = qdt
self.time_turn.set_current_turn(game.turn, game.conditions)
# TODO: create new weather object
new_weather_type = self.weather_adjuster.type_selector.currentData()
new_weather = new_weather_type(
seasonal_conditions=game.theater.seasonal_conditions,
day=qdt.date(),
time_of_day=game.current_turn_time_of_day,
)
# self.weather.conditions.weather = WeatherType()
preset = self.weather_adjuster.preset_selector.currentData()
new_weather.clouds = Clouds(
base=self.weather_adjuster.cloud_base.base.value(),
density=self.weather_adjuster.cloud_density.density.value(),
thickness=self.weather_adjuster.cloud_thickness.thickness.value(),
precipitation=self.weather_adjuster.precipitation.selector.currentData(),
preset=preset,
)
self.weather.conditions.weather = new_weather
self.weather.update_forecast()
if game.turn > 0 and current_time != qdt:
events = GameUpdateEvents()
game.initialize_turn(events, for_blue=True, for_red=True)
sim.sim_update.emit(events)
self.accept()

View File

@ -1,282 +1,15 @@
from datetime import datetime
from PySide6.QtGui import QPixmap
from PySide6 import QtCore, QtGui
from PySide6.QtGui import QCursor
from PySide6.QtWidgets import (
QFrame,
QGridLayout,
QGroupBox,
QHBoxLayout,
QLabel,
QVBoxLayout,
)
from dcs.weather import CloudPreset, Weather as PydcsWeather
import qt_ui.uiconstants as CONST
from game.sim.gameupdateevents import GameUpdateEvents
from game.timeofday import TimeOfDay
from game.utils import mps
from game.weather.conditions import Conditions
from qt_ui.simcontroller import SimController
class QTimeTurnWidget(QGroupBox):
"""
UI Component to display current turn and time info
"""
def __init__(self, sim_controller: SimController) -> None:
super(QTimeTurnWidget, self).__init__("Turn")
self.sim_controller = sim_controller
self.setStyleSheet(
"padding: 0px; margin-left: 5px; margin-right: 0px; margin-top: 1ex; margin-bottom: 5px; border-right: 0px"
)
self.icons = {
TimeOfDay.Dawn: CONST.ICONS["Dawn"],
TimeOfDay.Day: CONST.ICONS["Day"],
TimeOfDay.Dusk: CONST.ICONS["Dusk"],
TimeOfDay.Night: CONST.ICONS["Night"],
}
# self.setProperty('style', 'conditions__widget--turn')
self.layout = QHBoxLayout()
self.setLayout(self.layout)
self.daytime_icon = QLabel()
self.daytime_icon.setPixmap(self.icons[TimeOfDay.Dawn])
self.layout.addWidget(self.daytime_icon)
self.time_column = QVBoxLayout()
self.layout.addLayout(self.time_column)
self.date_display = QLabel()
self.time_column.addWidget(self.date_display)
self.time_display = QLabel()
self.time_column.addWidget(self.time_display)
sim_controller.sim_update.connect(self.on_sim_update)
def on_sim_update(self, _events: GameUpdateEvents) -> None:
time = self.sim_controller.current_time_in_sim_if_game_loaded
if time is None:
self.date_display.setText("")
self.time_display.setText("")
else:
self.set_date_and_time(time)
def set_current_turn(self, turn: int, conditions: Conditions) -> None:
"""Sets the turn information display.
:arg turn Current turn number.
:arg conditions Current time and weather conditions.
"""
self.daytime_icon.setPixmap(self.icons[conditions.time_of_day])
self.set_date_and_time(conditions.start_time)
self.setTitle(f"Turn {turn}")
def set_date_and_time(self, time: datetime) -> None:
self.date_display.setText(time.strftime("%d %b %Y"))
self.time_display.setText(time.strftime("%H:%M:%S Local"))
class QWeatherWidget(QGroupBox):
"""
UI Component to display current weather forecast
"""
turn = None
conditions = None
def __init__(self):
super(QWeatherWidget, self).__init__("")
self.setProperty("style", "QWeatherWidget")
self.icons = {
TimeOfDay.Dawn: CONST.ICONS["Dawn"],
TimeOfDay.Day: CONST.ICONS["Day"],
TimeOfDay.Dusk: CONST.ICONS["Dusk"],
TimeOfDay.Night: CONST.ICONS["Night"],
}
self.layout = QHBoxLayout()
self.setLayout(self.layout)
self.makeWeatherIcon()
self.makeCloudRainFogWidget()
self.makeWindsWidget()
def makeWeatherIcon(self):
"""Makes the Weather Icon Widget"""
self.weather_icon = QLabel()
self.weather_icon.setPixmap(self.icons[TimeOfDay.Dawn])
self.layout.addWidget(self.weather_icon)
def makeCloudRainFogWidget(self):
"""Makes the Cloud, Rain, Fog Widget"""
self.textLayout = QVBoxLayout()
self.layout.addLayout(self.textLayout)
self.forecastClouds = self.makeLabel()
self.textLayout.addWidget(self.forecastClouds)
self.forecastRain = self.makeLabel()
self.textLayout.addWidget(self.forecastRain)
self.forecastFog = self.makeLabel()
self.textLayout.addWidget(self.forecastFog)
def makeWindsWidget(self):
"""Factory for the winds widget."""
windsLayout = QGridLayout()
self.layout.addLayout(windsLayout)
windsLayout.addWidget(self.makeIcon(CONST.ICONS["Weather_winds"]), 0, 0, 3, 1)
windsLayout.addWidget(self.makeLabel("At GL"), 0, 1)
windsLayout.addWidget(self.makeLabel("At FL08"), 1, 1)
windsLayout.addWidget(self.makeLabel("At FL26"), 2, 1)
self.windGLSpeedLabel = self.makeLabel("0kts")
self.windGLDirLabel = self.makeLabel("")
windsLayout.addWidget(self.windGLSpeedLabel, 0, 2)
windsLayout.addWidget(self.windGLDirLabel, 0, 3)
self.windFL08SpeedLabel = self.makeLabel("0kts")
self.windFL08DirLabel = self.makeLabel("")
windsLayout.addWidget(self.windFL08SpeedLabel, 1, 2)
windsLayout.addWidget(self.windFL08DirLabel, 1, 3)
self.windFL26SpeedLabel = self.makeLabel("0kts")
self.windFL26DirLabel = self.makeLabel("")
windsLayout.addWidget(self.windFL26SpeedLabel, 2, 2)
windsLayout.addWidget(self.windFL26DirLabel, 2, 3)
def makeLabel(self, text: str = "") -> QLabel:
"""Shorthand to generate a QLabel with widget standard style
:arg pixmap QPixmap for the icon.
"""
label = QLabel(text)
label.setProperty("style", "text-sm")
return label
def makeIcon(self, pixmap: QPixmap) -> QLabel:
"""Shorthand to generate a QIcon with pixmap.
:arg pixmap QPixmap for the icon.
"""
icon = QLabel()
icon.setPixmap(pixmap)
return icon
def setCurrentTurn(self, turn: int, conditions: Conditions) -> None:
"""Sets the turn information display.
:arg turn Current turn number.
:arg conditions Current time and weather conditions.
"""
self.turn = turn
self.conditions = conditions
self.update_forecast()
self.updateWinds()
def updateWinds(self):
"""Updates the UI with the current conditions wind info."""
windGlSpeed = mps(self.conditions.weather.wind.at_0m.speed or 0)
windGlDir = str(self.conditions.weather.wind.at_0m.direction or 0).rjust(3, "0")
self.windGLSpeedLabel.setText(f"{int(windGlSpeed.knots)}kts")
self.windGLDirLabel.setText(f"{windGlDir}º")
windFL08Speed = mps(self.conditions.weather.wind.at_2000m.speed or 0)
windFL08Dir = str(self.conditions.weather.wind.at_2000m.direction or 0).rjust(
3, "0"
)
self.windFL08SpeedLabel.setText(f"{int(windFL08Speed.knots)}kts")
self.windFL08DirLabel.setText(f"{windFL08Dir}º")
windFL26Speed = mps(self.conditions.weather.wind.at_8000m.speed or 0)
windFL26Dir = str(self.conditions.weather.wind.at_8000m.direction or 0).rjust(
3, "0"
)
self.windFL26SpeedLabel.setText(f"{int(windFL26Speed.knots)}kts")
self.windFL26DirLabel.setText(f"{windFL26Dir}º")
def update_forecast_from_preset(self, preset: CloudPreset) -> None:
self.forecastFog.setText("No fog")
if "Rain" in preset.name:
self.forecastRain.setText("Rain")
self.update_forecast_icons("rain")
else:
self.forecastRain.setText("No rain")
self.update_forecast_icons("partly-cloudy")
# We get a description like the following for the cloud preset.
#
# 09 ##Two Layer Broken/Scattered \nMETAR:BKN 7.5/10 SCT 20/22 FEW41
#
# The second line is probably interesting but doesn't fit into the widget
# currently, so for now just extract the first line.
self.forecastClouds.setText(preset.description.splitlines()[0].split("##")[1])
def update_forecast(self):
"""Updates the Forecast Text and icon with the current conditions wind info."""
if (
self.conditions.weather.clouds
and self.conditions.weather.clouds.preset is not None
):
self.update_forecast_from_preset(self.conditions.weather.clouds.preset)
return
if self.conditions.weather.clouds is None:
cloud_density = 0
precipitation = None
else:
cloud_density = self.conditions.weather.clouds.density
precipitation = self.conditions.weather.clouds.precipitation
if not cloud_density:
self.forecastClouds.setText("Clear")
weather_type = "clear"
elif cloud_density < 3:
self.forecastClouds.setText("Partly Cloudy")
weather_type = "partly-cloudy"
elif cloud_density < 5:
self.forecastClouds.setText("Mostly Cloudy")
weather_type = "partly-cloudy"
else:
self.forecastClouds.setText("Totally Cloudy")
weather_type = "partly-cloudy"
if precipitation == PydcsWeather.Preceptions.Rain:
self.forecastRain.setText("Rain")
weather_type = "rain"
elif precipitation == PydcsWeather.Preceptions.Thunderstorm:
self.forecastRain.setText("Thunderstorm")
weather_type = "thunderstorm"
else:
self.forecastRain.setText("No rain")
if not self.conditions.weather.fog is not None:
self.forecastFog.setText("No fog")
else:
visibility = round(self.conditions.weather.fog.visibility.nautical_miles, 1)
self.forecastFog.setText(f"Fog vis: {visibility}nm")
if cloud_density > 1:
weather_type = "cloudy-fog"
else:
weather_type = "fog"
self.update_forecast_icons(weather_type)
def update_forecast_icons(self, weather_type: str) -> None:
time = "night" if self.conditions.time_of_day == TimeOfDay.Night else "day"
icon_key = f"Weather_{time}-{weather_type}"
icon = CONST.ICONS.get(icon_key) or CONST.ICONS["Weather_night-partly-cloudy"]
self.weather_icon.setPixmap(icon)
from qt_ui.widgets.QConditionsDialog import QConditionsDialog
from qt_ui.widgets.conditions.QTimeTurnWidget import QTimeTurnWidget
from qt_ui.widgets.conditions.QWeatherWidget import QWeatherWidget
class QConditionsWidget(QFrame):
@ -287,6 +20,7 @@ class QConditionsWidget(QFrame):
def __init__(self, sim_controller: SimController) -> None:
super(QConditionsWidget, self).__init__()
self.setProperty("style", "QConditionsWidget")
self.setCursor(QCursor(QtCore.Qt.CursorShape.PointingHandCursor))
self.layout = QGridLayout()
self.layout.setContentsMargins(0, 0, 0, 0)
@ -305,6 +39,9 @@ class QConditionsWidget(QFrame):
self.weather_widget.hide()
self.layout.addWidget(self.weather_widget, 0, 1)
def mouseDoubleClickEvent(self, event: QtGui.QMouseEvent) -> None:
QConditionsDialog(self.time_turn_widget, self.weather_widget).exec()
def setCurrentTurn(self, turn: int, conditions: Conditions) -> None:
"""Sets the turn information display.

View File

@ -0,0 +1,95 @@
from typing import Optional
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QHBoxLayout, QLabel, QSlider, QSpinBox, QComboBox
from game.weather.clouds import Clouds
class DcsCloudBaseSelector(QHBoxLayout):
M2FT_FACTOR = 3.2808399
def __init__(self, clouds: Optional[Clouds]) -> None:
super().__init__()
self.preset = clouds.preset if clouds else None
self.unit_changing = False
self.label = QLabel("Cloud Base: ")
self.addWidget(self.label)
self.base = QSlider(Qt.Orientation.Horizontal)
self.base.setRange(self.min_base, self.max_base)
self.base.setValue(
clouds.base
if clouds
else round(self.max_base - (self.max_base - self.min_base) / 2)
)
self.base.valueChanged.connect(self.on_slider_change)
self.addWidget(self.base, 1)
self.base_spinner = QSpinBox()
self.base_spinner.setValue(self.base.value())
self.base_spinner.setFixedWidth(75)
self.base_spinner.setSingleStep(100)
self.base_spinner.valueChanged.connect(self.update_slider)
self.addWidget(self.base_spinner, 1)
self.unit = QComboBox()
self.unit.insertItems(0, ["m", "ft"])
self.unit.currentIndexChanged.connect(self.on_unit_change)
self.unit.setCurrentIndex(1)
self.addWidget(self.unit)
self.update_bounds()
@property
def min_base(self) -> int:
return self.preset.min_base if self.preset else 300
@property
def max_base(self) -> int:
return self.preset.max_base if self.preset else 5000
def update_bounds(self) -> None:
self.base.setRange(self.min_base, self.max_base)
index = self.unit.currentIndex()
if index == 0:
self.base_spinner.setRange(self.min_base, self.max_base)
elif index == 1:
self.base_spinner.setRange(
self.m2ft(self.min_base), self.m2ft(self.max_base)
)
def on_slider_change(self, value: int) -> None:
if self.unit.currentIndex() == 0:
self.base_spinner.setValue(value)
elif self.unit.currentIndex() == 1 and not self.unit_changing:
self.base_spinner.setValue(self.m2ft(value))
def update_slider(self, value: int) -> None:
if self.unit_changing:
return
if self.unit.currentIndex() == 0:
self.base.setValue(value)
elif self.unit.currentIndex() == 1:
self.unit_changing = True
self.base.setValue(self.ft2m(value))
self.unit_changing = False
def on_unit_change(self, index: int) -> None:
self.unit_changing = True
if index == 0:
self.base_spinner.setRange(self.min_base, self.max_base)
self.base_spinner.setValue(self.base.value())
elif index == 1:
self.base_spinner.setRange(
self.m2ft(self.min_base), self.m2ft(self.max_base)
)
self.base_spinner.setValue(self.m2ft(self.base.value()))
self.unit_changing = False
def m2ft(self, value: int) -> int:
return round(value * self.M2FT_FACTOR)
def ft2m(self, value: int) -> int:
return round(value / self.M2FT_FACTOR)

View File

@ -0,0 +1,43 @@
from typing import Optional
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QHBoxLayout, QLabel, QSlider, QSpinBox
from dcs.weather import CloudPreset
from game.weather.clouds import Clouds
class DcsCloudDensitySelector(QHBoxLayout):
def __init__(self, clouds: Optional[Clouds]) -> None:
super().__init__()
self.unit_changing = False
self.label = QLabel("Density : ")
self.addWidget(self.label)
self.density = QSlider(Qt.Orientation.Horizontal)
self.density.setRange(0, 10)
if clouds:
self.density.setValue(clouds.density)
self.density.valueChanged.connect(self.on_slider_change)
self.addWidget(self.density, 1)
self.density_spinner = QSpinBox()
self.density_spinner.setValue(self.density.value())
self.density_spinner.setFixedWidth(75)
self.density_spinner.valueChanged.connect(self.update_slider)
self.addWidget(self.density_spinner, 1)
def on_slider_change(self, value: int) -> None:
self.density_spinner.setValue(value)
def update_slider(self, value: int) -> None:
self.density.setValue(value)
def update_ui(self, preset: Optional[CloudPreset]) -> None:
self.label.setVisible(preset is None)
self.density.setVisible(preset is None)
self.density_spinner.setVisible(preset is None)
if preset:
self.density.setValue(0)

View File

@ -0,0 +1,88 @@
from typing import Optional
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QHBoxLayout, QLabel, QSlider, QSpinBox, QComboBox
from dcs.weather import CloudPreset
from game.weather.clouds import Clouds
class DcsCloudThicknessSelector(QHBoxLayout):
M2FT_FACTOR = 3.2808399
def __init__(self, clouds: Optional[Clouds]) -> None:
super().__init__()
self.unit_changing = False
self.label = QLabel("Thickness : ")
self.addWidget(self.label)
self.thickness = QSlider(Qt.Orientation.Horizontal)
self.thickness.setRange(200, 2000)
if clouds:
self.thickness.setValue(clouds.thickness)
self.thickness.valueChanged.connect(self.on_slider_change)
self.addWidget(self.thickness, 1)
self.thickness_spinner = QSpinBox()
self.thickness_spinner.setValue(self.thickness.value())
self.thickness_spinner.setFixedWidth(75)
self.thickness_spinner.setSingleStep(100)
self.thickness_spinner.valueChanged.connect(self.update_slider)
self.addWidget(self.thickness_spinner, 1)
self.unit = QComboBox()
self.unit.insertItems(0, ["m", "ft"])
self.unit.currentIndexChanged.connect(self.on_unit_change)
self.unit.setCurrentIndex(1)
self.addWidget(self.unit)
def update_ui(self, preset: Optional[CloudPreset]) -> None:
self.label.setVisible(preset is None)
self.thickness.setVisible(preset is None)
self.thickness_spinner.setVisible(preset is None)
self.unit.setVisible(preset is None)
if preset:
self.thickness.setValue(0)
def on_slider_change(self, value: int) -> None:
if self.unit.currentIndex() == 0:
self.thickness_spinner.setValue(value)
elif self.unit.currentIndex() == 1 and not self.unit_changing:
self.thickness_spinner.setValue(self.m2ft(value))
def update_slider(self, value: int) -> None:
if self.unit_changing:
return
if self.unit.currentIndex() == 0:
self.thickness.setValue(value)
elif self.unit.currentIndex() == 1:
self.unit_changing = True
self.thickness.setValue(self.ft2m(value))
self.unit_changing = False
def on_unit_change(self, index: int) -> None:
self.unit_changing = True
mini = (
self.thickness.minimum()
if index == 0
else self.m2ft(self.thickness.minimum())
)
maxi = (
self.thickness.maximum()
if index == 0
else self.m2ft(self.thickness.maximum())
)
value = (
self.thickness.value() if index == 0 else self.m2ft(self.thickness.value())
)
self.thickness_spinner.setRange(mini, maxi)
self.thickness_spinner.setValue(value)
self.unit_changing = False
def m2ft(self, value: int) -> int:
return round(value * self.M2FT_FACTOR)
def ft2m(self, value: int) -> int:
return round(value / self.M2FT_FACTOR)

View File

@ -0,0 +1,29 @@
from typing import Optional
from PySide6.QtWidgets import QHBoxLayout, QLabel, QComboBox
from dcs.weather import Weather as PydcsWeather, CloudPreset
from game.weather.clouds import Clouds
class DcsPrecipitationSelector(QHBoxLayout):
def __init__(self, clouds: Clouds) -> None:
super().__init__()
self.unit_changing = False
self.label = QLabel("Precipitation : ")
self.addWidget(self.label)
self.selector = QComboBox()
for p in PydcsWeather.Preceptions:
self.selector.addItem(p.name.replace("_", ""), p)
if clouds:
self.selector.setCurrentText(clouds.precipitation.name.replace("_", ""))
self.addWidget(self.selector, 1)
def update_ui(self, preset: Optional[CloudPreset]) -> None:
self.selector.setEnabled(preset is None)
if preset:
self.selector.setCurrentText("None")

View File

@ -0,0 +1,38 @@
from typing import Optional
from PySide6.QtCore import QDateTime
from PySide6.QtWidgets import QVBoxLayout, QWidget, QLabel, QHBoxLayout, QDateTimeEdit
from qt_ui.widgets.conditions.QTimeTurnWidget import QTimeTurnWidget
class QTimeAdjustmentWidget(QWidget):
def __init__(
self, time_turn: QTimeTurnWidget, parent: Optional[QWidget] = None
) -> None:
super().__init__(parent)
self.current_datetime = time_turn.sim_controller.current_time_in_sim
self.init_ui()
def init_ui(self) -> None:
vbox = QVBoxLayout()
vbox.addWidget(QLabel("<h2><b>Time & Date:</b></h2>"))
vbox.addWidget(
QLabel(
'<h4 style="color:orange"><b>WARNING: CHANGING TIME/DATE WILL RE-INITIALIZE THE TURN</b></h4>'
)
)
hbox = QHBoxLayout()
t = self.current_datetime.time()
d = self.current_datetime.date()
self.datetime_edit = QDateTimeEdit(
QDateTime(d.year, d.month, d.day, t.hour, t.minute, t.second)
)
hbox.addWidget(self.datetime_edit)
vbox.addLayout(hbox)
self.setLayout(vbox)

View File

@ -0,0 +1,70 @@
from datetime import datetime
from PySide6.QtWidgets import QGroupBox, QHBoxLayout, QLabel, QVBoxLayout
from game.sim import GameUpdateEvents
from game.timeofday import TimeOfDay
from game.weather.conditions import Conditions
from qt_ui import uiconstants as CONST
from qt_ui.simcontroller import SimController
class QTimeTurnWidget(QGroupBox):
"""
UI Component to display current turn and time info
"""
def __init__(self, sim_controller: SimController) -> None:
super(QTimeTurnWidget, self).__init__("Turn")
self.sim_controller = sim_controller
self.setStyleSheet(
"padding: 0px; margin-left: 5px; margin-right: 0px; margin-top: 1ex; margin-bottom: 5px; border-right: 0px"
)
self.icons = {
TimeOfDay.Dawn: CONST.ICONS["Dawn"],
TimeOfDay.Day: CONST.ICONS["Day"],
TimeOfDay.Dusk: CONST.ICONS["Dusk"],
TimeOfDay.Night: CONST.ICONS["Night"],
}
# self.setProperty('style', 'conditions__widget--turn')
self.layout = QHBoxLayout()
self.setLayout(self.layout)
self.daytime_icon = QLabel()
self.daytime_icon.setPixmap(self.icons[TimeOfDay.Dawn])
self.layout.addWidget(self.daytime_icon)
self.time_column = QVBoxLayout()
self.layout.addLayout(self.time_column)
self.date_display = QLabel()
self.time_column.addWidget(self.date_display)
self.time_display = QLabel()
self.time_column.addWidget(self.time_display)
sim_controller.sim_update.connect(self.on_sim_update)
def on_sim_update(self, _events: GameUpdateEvents) -> None:
time = self.sim_controller.current_time_in_sim_if_game_loaded
if time is None:
self.date_display.setText("")
self.time_display.setText("")
else:
self.set_date_and_time(time)
def set_current_turn(self, turn: int, conditions: Conditions) -> None:
"""Sets the turn information display.
:arg turn Current turn number.
:arg conditions Current time and weather conditions.
"""
self.daytime_icon.setPixmap(self.icons[conditions.time_of_day])
self.set_date_and_time(conditions.start_time)
self.setTitle(f"Turn {turn}")
def set_date_and_time(self, time: datetime) -> None:
self.date_display.setText(time.strftime("%d %b %Y"))
self.time_display.setText(time.strftime("%H:%M:%S Local"))

View File

@ -0,0 +1,92 @@
from PySide6.QtWidgets import QLabel, QHBoxLayout, QComboBox, QWidget, QVBoxLayout
from dcs.cloud_presets import CLOUD_PRESETS
from game.weather.weather import ClearSkies, Cloudy, Raining, Thunderstorm
from qt_ui.widgets.conditions.DcsCloudBaseSelector import DcsCloudBaseSelector
from qt_ui.widgets.conditions.DcsCloudDensitySelector import DcsCloudDensitySelector
from qt_ui.widgets.conditions.DcsCloudThicknessSelector import DcsCloudThicknessSelector
from qt_ui.widgets.conditions.DcsPrecipitationSelector import DcsPrecipitationSelector
from qt_ui.widgets.conditions.QWeatherWidget import QWeatherWidget
class QWeatherAdjustmentWidget(QWidget):
def __init__(self, weather: QWeatherWidget) -> None:
super().__init__()
self.weather = weather
self.init_ui()
def init_ui(self) -> None:
weather = self.weather.conditions.weather
vbox = QVBoxLayout()
label = QLabel("<h2><b>Weather:</b></h2>")
label.setMaximumHeight(75)
vbox.addWidget(label)
hbox = QHBoxLayout()
hbox.addWidget(QLabel("Type"))
self.type_selector = QComboBox()
for text, w_type in [
("Clear", ClearSkies),
("Clouds", Cloudy),
("Rain", Raining),
("Thunderstorm", Thunderstorm),
]:
self.type_selector.addItem(text, w_type)
if isinstance(weather, w_type):
self.type_selector.setCurrentText(text)
self.type_selector.currentIndexChanged.connect(self.update_ui_for_type)
hbox.addWidget(self.type_selector)
vbox.addLayout(hbox)
label = QLabel("<h3><b>Clouds:</b></h3>")
label.setMaximumHeight(50)
vbox.addWidget(label)
clouds = weather.clouds
hbox = QHBoxLayout()
hbox.addWidget(QLabel("Preset"))
self.preset_selector = QComboBox()
for _, preset in CLOUD_PRESETS.items():
self.preset_selector.addItem(preset.value.ui_name, preset.value)
self.preset_selector.addItem("Custom", None)
self.preset_selector.setCurrentText(
clouds.preset.ui_name if clouds and clouds.preset else "Custom"
)
self.preset_selector.currentIndexChanged.connect(self.update_ui)
hbox.addWidget(self.preset_selector)
vbox.addLayout(hbox)
self.cloud_base = DcsCloudBaseSelector(clouds)
vbox.addLayout(self.cloud_base)
self.cloud_thickness = DcsCloudThicknessSelector(clouds)
vbox.addLayout(self.cloud_thickness)
self.cloud_density = DcsCloudDensitySelector(clouds)
vbox.addLayout(self.cloud_density)
self.precipitation = DcsPrecipitationSelector(clouds)
vbox.addLayout(self.precipitation)
self.setLayout(vbox)
self.update_ui_for_type()
def update_ui_for_type(self) -> None:
if self.type_selector.currentData() in [ClearSkies, Thunderstorm]:
self.preset_selector.setCurrentText("Custom")
self.preset_selector.setDisabled(True)
else:
self.preset_selector.setDisabled(False)
self.update_ui()
def update_ui(self) -> None:
preset = self.preset_selector.currentData()
self.cloud_base.preset = preset
self.cloud_base.update_bounds()
self.cloud_thickness.update_ui(preset)
self.cloud_density.update_ui(preset)
self.precipitation.update_ui(preset)

View File

@ -0,0 +1,207 @@
from PySide6.QtGui import QPixmap
from PySide6.QtWidgets import QGroupBox, QHBoxLayout, QLabel, QVBoxLayout, QGridLayout
from dcs.weather import CloudPreset, Weather as PydcsWeather
from game.timeofday import TimeOfDay
from game.utils import mps
from game.weather.conditions import Conditions
from qt_ui import uiconstants as CONST
class QWeatherWidget(QGroupBox):
"""
UI Component to display current weather forecast
"""
turn = None
conditions = None
def __init__(self):
super(QWeatherWidget, self).__init__("")
self.setProperty("style", "QWeatherWidget")
self.icons = {
TimeOfDay.Dawn: CONST.ICONS["Dawn"],
TimeOfDay.Day: CONST.ICONS["Day"],
TimeOfDay.Dusk: CONST.ICONS["Dusk"],
TimeOfDay.Night: CONST.ICONS["Night"],
}
self.layout = QHBoxLayout()
self.setLayout(self.layout)
self.makeWeatherIcon()
self.makeCloudRainFogWidget()
self.makeWindsWidget()
def makeWeatherIcon(self):
"""Makes the Weather Icon Widget"""
self.weather_icon = QLabel()
self.weather_icon.setPixmap(self.icons[TimeOfDay.Dawn])
self.layout.addWidget(self.weather_icon)
def makeCloudRainFogWidget(self):
"""Makes the Cloud, Rain, Fog Widget"""
self.textLayout = QVBoxLayout()
self.layout.addLayout(self.textLayout)
self.forecastClouds = self.makeLabel()
self.textLayout.addWidget(self.forecastClouds)
self.forecastRain = self.makeLabel()
self.textLayout.addWidget(self.forecastRain)
self.forecastFog = self.makeLabel()
self.textLayout.addWidget(self.forecastFog)
def makeWindsWidget(self):
"""Factory for the winds widget."""
windsLayout = QGridLayout()
self.layout.addLayout(windsLayout)
windsLayout.addWidget(self.makeIcon(CONST.ICONS["Weather_winds"]), 0, 0, 3, 1)
windsLayout.addWidget(self.makeLabel("At GL"), 0, 1)
windsLayout.addWidget(self.makeLabel("At FL08"), 1, 1)
windsLayout.addWidget(self.makeLabel("At FL26"), 2, 1)
self.windGLSpeedLabel = self.makeLabel("0kts")
self.windGLDirLabel = self.makeLabel("")
windsLayout.addWidget(self.windGLSpeedLabel, 0, 2)
windsLayout.addWidget(self.windGLDirLabel, 0, 3)
self.windFL08SpeedLabel = self.makeLabel("0kts")
self.windFL08DirLabel = self.makeLabel("")
windsLayout.addWidget(self.windFL08SpeedLabel, 1, 2)
windsLayout.addWidget(self.windFL08DirLabel, 1, 3)
self.windFL26SpeedLabel = self.makeLabel("0kts")
self.windFL26DirLabel = self.makeLabel("")
windsLayout.addWidget(self.windFL26SpeedLabel, 2, 2)
windsLayout.addWidget(self.windFL26DirLabel, 2, 3)
def makeLabel(self, text: str = "") -> QLabel:
"""Shorthand to generate a QLabel with widget standard style
:arg pixmap QPixmap for the icon.
"""
label = QLabel(text)
label.setProperty("style", "text-sm")
return label
def makeIcon(self, pixmap: QPixmap) -> QLabel:
"""Shorthand to generate a QIcon with pixmap.
:arg pixmap QPixmap for the icon.
"""
icon = QLabel()
icon.setPixmap(pixmap)
return icon
def setCurrentTurn(self, turn: int, conditions: Conditions) -> None:
"""Sets the turn information display.
:arg turn Current turn number.
:arg conditions Current time and weather conditions.
"""
self.turn = turn
self.conditions = conditions
self.update_forecast()
self.updateWinds()
def updateWinds(self):
"""Updates the UI with the current conditions wind info."""
windGlSpeed = mps(self.conditions.weather.wind.at_0m.speed or 0)
windGlDir = str(self.conditions.weather.wind.at_0m.direction or 0).rjust(3, "0")
self.windGLSpeedLabel.setText(f"{int(windGlSpeed.knots)}kts")
self.windGLDirLabel.setText(f"{windGlDir}º")
windFL08Speed = mps(self.conditions.weather.wind.at_2000m.speed or 0)
windFL08Dir = str(self.conditions.weather.wind.at_2000m.direction or 0).rjust(
3, "0"
)
self.windFL08SpeedLabel.setText(f"{int(windFL08Speed.knots)}kts")
self.windFL08DirLabel.setText(f"{windFL08Dir}º")
windFL26Speed = mps(self.conditions.weather.wind.at_8000m.speed or 0)
windFL26Dir = str(self.conditions.weather.wind.at_8000m.direction or 0).rjust(
3, "0"
)
self.windFL26SpeedLabel.setText(f"{int(windFL26Speed.knots)}kts")
self.windFL26DirLabel.setText(f"{windFL26Dir}º")
def update_forecast_from_preset(self, preset: CloudPreset) -> None:
self.forecastFog.setText("No fog")
if "Rain" in preset.name:
self.forecastRain.setText("Rain")
self.update_forecast_icons("rain")
else:
self.forecastRain.setText("No rain")
self.update_forecast_icons("partly-cloudy")
# We get a description like the following for the cloud preset.
#
# 09 ##Two Layer Broken/Scattered \nMETAR:BKN 7.5/10 SCT 20/22 FEW41
#
# The second line is probably interesting but doesn't fit into the widget
# currently, so for now just extract the first line.
self.forecastClouds.setText(preset.description.splitlines()[0].split("##")[1])
def update_forecast(self):
"""Updates the Forecast Text and icon with the current conditions wind info."""
if (
self.conditions.weather.clouds
and self.conditions.weather.clouds.preset is not None
):
self.update_forecast_from_preset(self.conditions.weather.clouds.preset)
return
if self.conditions.weather.clouds is None:
cloud_density = 0
precipitation = None
else:
cloud_density = self.conditions.weather.clouds.density
precipitation = self.conditions.weather.clouds.precipitation
if not cloud_density:
self.forecastClouds.setText("Clear")
weather_type = "clear"
elif cloud_density < 3:
self.forecastClouds.setText("Partly Cloudy")
weather_type = "partly-cloudy"
elif cloud_density < 5:
self.forecastClouds.setText("Mostly Cloudy")
weather_type = "partly-cloudy"
else:
self.forecastClouds.setText("Totally Cloudy")
weather_type = "partly-cloudy"
if precipitation == PydcsWeather.Preceptions.Rain:
self.forecastRain.setText("Rain")
weather_type = "rain"
elif precipitation == PydcsWeather.Preceptions.Thunderstorm:
self.forecastRain.setText("Thunderstorm")
weather_type = "thunderstorm"
else:
self.forecastRain.setText("No rain")
if not self.conditions.weather.fog is not None:
self.forecastFog.setText("No fog")
else:
visibility = round(self.conditions.weather.fog.visibility.nautical_miles, 1)
self.forecastFog.setText(f"Fog vis: {visibility}nm")
if cloud_density > 1:
weather_type = "cloudy-fog"
else:
weather_type = "fog"
self.update_forecast_icons(weather_type)
def update_forecast_icons(self, weather_type: str) -> None:
time = "night" if self.conditions.time_of_day == TimeOfDay.Night else "day"
icon_key = f"Weather_{time}-{weather_type}"
icon = CONST.ICONS.get(icon_key) or CONST.ICONS["Weather_night-partly-cloudy"]
self.weather_icon.setPixmap(icon)

View File

@ -19,6 +19,7 @@ from game.radio.RadioFrequencyContainer import RadioFrequencyContainer
from game.radio.TacanContainer import TacanContainer
from game.server import EventStream
from game.sim import GameUpdateEvents
from game.sim.missionresultsprocessor import MissionResultsProcessor
from game.theater import (
AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION,
ControlPoint,
@ -158,7 +159,8 @@ class QBaseMenu2(QDialog):
transfer_button.clicked.connect(self.open_transfer_dialog)
if self.cheat_capturable:
capture_button = QPushButton("CHEAT: Capture")
label = "Sink/Resurrect" if self.cp.is_fleet else "Capture"
capture_button = QPushButton(f"CHEAT: {label}")
capture_button.setProperty("style", "btn-danger")
bottom_row.addWidget(capture_button)
capture_button.clicked.connect(self.cheat_capture)
@ -180,9 +182,23 @@ class QBaseMenu2(QDialog):
def cheat_capture(self) -> None:
events = GameUpdateEvents()
self.cp.capture(self.game_model.game, events, for_player=not self.cp.captured)
if self.cp.is_fleet:
for go in self.cp.ground_objects:
if go.is_naval_control_point:
if go.alive_unit_count > 0:
for u in go.units:
u.kill(events)
else:
for u in go.units:
u.revive(events)
else:
self.cp.capture(
self.game_model.game, events, for_player=not self.cp.captured
)
mrp = MissionResultsProcessor(self.game_model.game)
mrp.redeploy_units(self.cp)
# Reinitialized ground planners and the like. The ATO needs to be reset because
# missions planned against the flipped base are no longer valid.
# missions planned against the flipped base (or killed carrier) are no longer valid.
self.game_model.game.initialize_turn(events)
EventStream.put_nowait(events)
GameUpdateSignal.get_instance().updateGame(self.game_model.game)

View File

@ -92,6 +92,7 @@ class NewGameWizard(QtWidgets.QWizard):
a4_skyhawk=self.field("a4_skyhawk"),
a6a_intruder=self.field("a6a_intruder"),
a7e_corsair2=self.field("a7e_corsair2"),
ea6b_prowler=self.field("ea6b_prowler"),
f4bc_phantom=self.field("f4bc_phantom"),
f15d_baz=self.field("f15d_baz"),
f_15_idf=self.field("f_15_idf"),
@ -109,6 +110,7 @@ class NewGameWizard(QtWidgets.QWizard):
uh_60l=self.field("uh_60l"),
jas39_gripen=self.field("jas39_gripen"),
super_etendard=self.field("super_etendard"),
su15_flagon=self.field("su15_flagon"),
su30_flanker_h=self.field("su30_flanker_h"),
su57_felon=self.field("su57_felon"),
ov10a_bronco=self.field("ov10a_bronco"),

View File

@ -94,6 +94,8 @@ class GeneratorOptions(QtWidgets.QWizardPage):
self.registerField("a6a_intruder", self.a6a_intruder)
self.a7e_corsair2 = QtWidgets.QCheckBox()
self.registerField("a7e_corsair2", self.a7e_corsair2)
self.ea6b_prowler = QtWidgets.QCheckBox()
self.registerField("ea6b_prowler", self.ea6b_prowler)
self.hercules = QtWidgets.QCheckBox()
self.registerField("hercules", self.hercules)
self.uh_60l = QtWidgets.QCheckBox()
@ -128,6 +130,8 @@ class GeneratorOptions(QtWidgets.QWizardPage):
self.registerField("jas39_gripen", self.jas39_gripen)
self.super_etendard = QtWidgets.QCheckBox()
self.registerField("super_etendard", self.super_etendard)
self.su15_flagon = QtWidgets.QCheckBox()
self.registerField("su15_flagon", self.su15_flagon)
self.su30_flanker_h = QtWidgets.QCheckBox()
self.registerField("su30_flanker_h", self.su30_flanker_h)
self.su57_felon = QtWidgets.QCheckBox()
@ -160,6 +164,7 @@ class GeneratorOptions(QtWidgets.QWizardPage):
("A-6A Intruder (v2.7.5.01)", self.a6a_intruder),
("A-7E Corsair II", self.a7e_corsair2),
("C-130J-30 Super Hercules (v6.8.2)", self.hercules),
("EA-6B Prowler (v2.9.4.102)", self.ea6b_prowler),
("F-100 Super Sabre (v2.7.18.30765 patch 20.10.22)", self.f100_supersabre),
("F-104 Starfighter (v2.7.11.222.01)", self.f104_starfighter),
("F-105 Thunderchief (v2.7.12.23x)", self.f105_thunderchief),
@ -180,6 +185,7 @@ class GeneratorOptions(QtWidgets.QWizardPage):
("OV-10A Bronco", self.ov10a_bronco),
("Spanish Naval Assets pack (desdemicabina 3.2.0)", self.spanishnavypack),
("Star Wars Modpack 2.54+", self.SWPack),
("Su-15 Flagon (v1.0)", self.su15_flagon),
("Su-30 Flanker-H (V2.7.3 beta)", self.su30_flanker_h),
("Su-57 Felon (build-04)", self.su57_felon),
("Super Étendard (v2.5.5)", self.super_etendard),
@ -225,6 +231,7 @@ class GeneratorOptions(QtWidgets.QWizardPage):
self.a4_skyhawk.setChecked(s.get("a4_skyhawk", False))
self.a6a_intruder.setChecked(s.get("a6a_intruder", False))
self.a7e_corsair2.setChecked(s.get("a7e_corsair2", False))
self.ea6b_prowler.setChecked(s.get("ea6b_prowler", False))
self.hercules.setChecked(s.get("hercules", False))
self.uh_60l.setChecked(s.get("uh_60l", False))
self.f4bc_phantom.setChecked(s.get("f4bc_phantom", False))

View File

@ -4,34 +4,35 @@ asgiref==3.8.1
atomicwrites==1.4.1
attrs==23.2.0
black==23.9.1
certifi==2024.2.2
certifi==2024.6.2
cfgv==3.4.0
click==8.1.7
colorama==0.4.6
distlib==0.3.8
Faker==24.14.0
fastapi==0.110.2
filelock==3.13.4
Faker==25.8.0
fastapi==0.111.0
filelock==3.15.1
h11==0.14.0
httptools==0.6.1
identify==2.5.36
idna==3.7
iniconfig==2.0.0
Jinja2==3.1.3
lupa==2.1
Jinja2==3.1.4
lupa==2.2
MarkupSafe==2.1.5
mypy==1.10.0
mypy-extensions==1.0.0
nodeenv==1.8.0
packaging==24.0
nodeenv==1.9.1
numpy==1.26.4
packaging==24.1
pathspec==0.12.1
pefile==2023.2.7
Pillow==10.3.0
platformdirs==4.2.1
platformdirs==4.2.2
pluggy==1.5.0
pre-commit==3.7.0
pydantic==2.7.1
pydantic-settings==2.2.1
pre-commit==3.7.1
pydantic==2.7.4
pydantic-settings==2.3.3
pydcs @ git+https://github.com/dcs-retribution/pydcs@4f4d3fd51dc14ad8e16e3bf6b130e8efc18dcabd
pyinstaller==5.13.2
pyinstaller-hooks-contrib==2024.0
@ -41,7 +42,7 @@ pyshp==2.3.1
PySide6==6.4.2
PySide6-Addons==6.4.2
PySide6-Essentials==6.4.2
pytest==8.1.2
pytest==8.2.2
pytest-cov==5.0.0
python-dateutil==2.9.0.post0
python-dotenv==1.0.1
@ -59,11 +60,11 @@ toml==0.10.2
tomli==2.0.1
types-Jinja2==2.11.9
types-MarkupSafe==1.1.10
types-Pillow==10.2.0.20240423
types-Pillow==10.2.0.20240520
types-PyYAML==6.0.12.20240311
types-tabulate==0.9.0.20240106
typing_extensions==4.11.0
uvicorn==0.29.0
virtualenv==20.26.0
typing_extensions==4.12.2
uvicorn==0.30.1
virtualenv==20.26.2
watchgod==0.8.2
websockets==12.0

View File

@ -43,11 +43,6 @@ squadrons:
aircraft:
- AH-64D Apache Longbow
size: 4
- primary: CAS
secondary: any
aircraft:
- OH-58D(R) Kiowa Warrior
size: 4
- primary: Refueling
aircraft:
- KC-135 Stratotanker
@ -101,7 +96,12 @@ squadrons:
secondary: air-to-ground
aircraft:
- AV-8B Harrier II Night Attack
size: 18
size: 10
- primary: CAS
secondary: any
aircraft:
- OH-58D(R) Kiowa Warrior
size: 8
- primary: Air Assault
secondary: any
aircraft:

View File

@ -32,7 +32,7 @@ squadrons:
aircraft:
- C-130J-30 Super Hercules
- C-130
size: 8
size: 4
# Tel Nof
23:
- primary: SEAD
@ -53,20 +53,20 @@ squadrons:
secondary: any
aircraft:
- UH-1H Iroquois
size: 8
size: 4
# Hatzor
20:
- primary: BAI
secondary: any
aircraft:
- A-4E Skyhawk
- F-4E Phantom II
- F-4E-45MC Phantom II
size: 20
- primary: Strike
secondary: any
aircraft:
- A-4E Skyhawk
- F-4E Phantom II
- F-4E-45MC Phantom II
size: 20
# El Arish
29:
@ -83,7 +83,7 @@ squadrons:
secondary: any
aircraft:
- MiG-15bis Fagot
size: 8
size: 16
- primary: Air Assault
secondary: any
aircraft:
@ -91,50 +91,31 @@ squadrons:
size: 4
# Al Mansurah
14:
- primary: Escort
secondary:
- BAI
- BARCAP
- Escort
- Fighter sweep
- Intercept
- TARCAP
- primary: BARCAP
secondary: any
aircraft:
- MiG-21bis Fishbed-N
size: 20
size: 16
- primary: BAI
secondary: any
aircraft:
- MiG-19P Farmer-B
size: 20
size: 16
# Cairo West
18:
- primary: Strike
secondary: any
aircraft:
- Tu-16 Badger
size: 14
- primary: Escort
secondary: air-to-air
aircraft:
- MiG-21bis Fishbed-N
size: 16
# FARP
Port Tewfik Staging Area:
- primary: Transport
secondary: any
aircraft:
- Mi-8MTV2 Hip
size: 4
# Cairo West
18:
- primary: Strike
secondary:
- DEAD
- OCA/Runway
aircraft:
- Tu-16 Badger
size: 15
- primary: BARCAP
secondary:
- BAI
- BARCAP
- Escort
- Fighter sweep
- Intercept
- TARCAP
aircraft:
- MiG-21bis Fishbed-N
size: 20
# FARP
Port Tewfik Staging Area:
- primary: Air Assault
secondary: any
aircraft:
- Mi-8MTV2 Hip

View File

@ -36,6 +36,11 @@ squadrons:
aircraft:
- S-3B Tanker
size: 4
- primary: Air Assault
secondary: any
aircraft:
- UH-60A
size: 4
# Akrotiri
44:
- primary: Strike
@ -85,11 +90,6 @@ squadrons:
aircraft:
- OH-58D(R) Kiowa Warrior
size: 4
- primary: Air Assault
secondary: any
aircraft:
- UH-60A
size: 4
# Damascus
7:
- primary: Strike

View File

@ -0,0 +1,36 @@
local unitPayloads = {
["name"] = "EA_6B",
["payloads"] = {
[1] = {
["displayName"] = "Liberation SEAD",
["name"] = "Liberation SEAD",
["pylons"] = {
[1] = {
["CLSID"] = "{B06DD79A-F21E-4EB9-BD9D-AB3844618C93}",
["num"] = 1,
},
[2] = {
["CLSID"] = "{0395076D-2F77-4420-9D33-087A4398130B}",
["num"] = 2,
},
[3] = {
["CLSID"] = "{EA6B_ANALQ992}",
["num"] = 3,
},
[4] = {
["CLSID"] = "{0395076D-2F77-4420-9D33-087A4398130B}",
["num"] = 4,
},
[5] = {
["CLSID"] = "{B06DD79A-F21E-4EB9-BD9D-AB3844618C93}",
["num"] = 5,
},
},
["tasks"] = {
[1] = 11,
},
},
},
["unitType"] = "EA_6B",
}
return unitPayloads

View File

@ -0,0 +1,33 @@
local unitPayloads = {
["name"] = "Su_15",
["payloads"] = {
[1] = {
["name"] = "BARCAP",
["pylons"] = {
[1] = {
["CLSID"] = "{R-8M1R}",
["num"] = 4,
},
[2] = {
["CLSID"] = "{R-8M1T}",
["num"] = 1,
},
[3] = {
["CLSID"] = "{E92CBFE5-C153-11d8-9897-000476191836}",
["num"] = 3,
},
[4] = {
["CLSID"] = "{E92CBFE5-C153-11d8-9897-000476191836}",
["num"] = 2,
},
},
["tasks"] = {
[1] = 10,
},
},
},
["tasks"] = {
},
["unitType"] = "Su_15",
}
return unitPayloads

View File

@ -0,0 +1,43 @@
local unitPayloads = {
["name"] = "Su_15TM",
["payloads"] = {
[1] = {
["displayName"] = "BARCAP",
["name"] = "BARCAP",
["pylons"] = {
[1] = {
["CLSID"] = "{R-98MR}",
["num"] = 6,
},
[2] = {
["CLSID"] = "{R-98MT}",
["num"] = 1,
},
[3] = {
["CLSID"] = "{R-60M}",
["num"] = 5,
},
[4] = {
["CLSID"] = "{R-60M}",
["num"] = 2,
},
[5] = {
["CLSID"] = "{E92CBFE5-C153-11d8-9897-000476191836}",
["num"] = 4,
},
[6] = {
["CLSID"] = "{E92CBFE5-C153-11d8-9897-000476191836}",
["num"] = 3,
},
},
["tasks"] = {
[1] = 11,
[2] = 10,
},
},
},
["tasks"] = {
},
["unitType"] = "Su_15TM",
}
return unitPayloads

View File

@ -11,6 +11,7 @@
"MiG-15bis Fagot",
"MiG-19P Farmer-B",
"MiG-21bis Fishbed-N",
"Su-15 Flagon-A",
"Tu-95MS Bear-H"
],
"awacs": [

View File

@ -14,6 +14,7 @@
"MiG-23MLD Flogger-K",
"MiG-25PD Foxbat-E",
"MiG-29A Fulcrum-A",
"Su-15TM Flagon-E",
"Su-17M4 Fitter-K",
"Su-24M Fencer-D",
"Su-25 Frogfoot",

View File

@ -14,6 +14,7 @@
"MiG-23MLD Flogger-K",
"MiG-25PD Foxbat-E",
"MiG-29A Fulcrum-A",
"Su-15TM Flagon-E",
"Su-17M4 Fitter-K",
"Su-24M Fencer-D",
"Su-25 Frogfoot",

View File

@ -17,6 +17,7 @@
"MiG-27K Flogger-J2",
"MiG-29A Fulcrum-A",
"MiG-31 Foxhound",
"Su-15TM Flagon-E",
"Su-17M4 Fitter-K",
"Su-24M Fencer-D",
"Su-25 Frogfoot",

View File

@ -13,6 +13,7 @@
"CH-53E",
"A-4E Skyhawk",
"A-7E Corsair II",
"EA-6B Prowler",
"F-14A Tomcat (AI)",
"F-14A Tomcat (Block 135-GR Late)",
"F-4B Phantom II",

View File

@ -16,6 +16,7 @@
"C-130J-30 Super Hercules",
"CH-47D",
"CH-53E",
"EA-6B Prowler",
"F-117A Nighthawk",
"F-14A Tomcat (AI)",
"F-14A Tomcat (Block 135-GR Late)",

View File

@ -26,21 +26,22 @@
"F-15E Strike Eagle (AI)",
"F-15E Strike Eagle (Suite 4+)",
"F-16CM Fighting Falcon (Block 50)",
"F-16D Fighting Falcon (Block 52+)",
"F-16D Fighting Falcon (Block 52)",
"F-16D Fighting Falcon (Block 50+)",
"F-16D Fighting Falcon (Block 50)",
"F-16D Fighting Falcon (Block 52+)",
"F-16D Fighting Falcon (Block 52)",
"F-16D Fighting Falcon (Block 50+)",
"F-16D Fighting Falcon (Block 50)",
"F-22A Raptor",
"F/A-18C Hornet (Lot 20)",
"F/A-18E Super Hornet",
"F/A-18F Super Hornet",
"EA-18G Growler",
"F/A-18E Super Hornet",
"F/A-18F Super Hornet",
"EA-6B Prowler",
"EA-18G Growler",
"OH-58D(R) Kiowa Warrior",
"S-3B Viking",
"SH-60B Seahawk",
"UH-1H Iroquois",
"UH-60A",
"UH-60L"
"UH-60A",
"UH-60L"
],
"awacs": [
"E-2C Hawkeye",

View File

@ -10,6 +10,7 @@
"AH-1W SuperCobra",
"A-4E Skyhawk",
"A-6A Intruder",
"EA-6B Prowler",
"A-7E Corsair II",
"F-14A Tomcat (AI)",
"F-14A Tomcat (Block 135-GR Late)",

View File

@ -8,6 +8,7 @@
],
"aircrafts": [
"F-14B Tomcat",
"EA-6B Prowler",
"F/A-18C Hornet (Lot 20)",
"F/A-18E Super Hornet",
"F/A-18F Super Hornet",

View File

@ -11,7 +11,8 @@
"F/A-18C Hornet (Lot 20)",
"F/A-18E Super Hornet",
"F/A-18F Super Hornet",
"EA-18G Growler",
"EA-18G Growler",
"EA-6B Prowler",
"AV-8B Harrier II Night Attack",
"AH-1W SuperCobra",
"S-3B Viking",

View File

@ -0,0 +1,723 @@
-- Artillery Spotter script - Multiplayer version
-- by Carsten Gurk aka Don Rudi
-- Map for passing settings from Retribution
cg_arty_options = {
["user_fireDelay"] = 10,
["user_quantity"] = 20,
["user_spread"] = 50,
["user_spottingDistance"] = 15,
}
local version = "MP 1.2"
-- User configurable variables
local user_fireDelay = cg_arty_options.user_fireDelay -- time to impcat of the rounds
local user_quantity = cg_arty_options.user_quantity -- how many rounds will be fired in a fire for effect task
local user_spread = cg_arty_options.user_spread -- impact radius of the rounds during fire for effect
local user_spottingDistance = cg_arty_options.user_spottingDistance -- max allowable distance from player to target to prevent cheating. In kilometers.
local user_restrictByType = "" -- Restriction by type ("", "helo", etc.)
local user_restrictByUnitName = "" -- Restriction by unit name ("", "spotter", etc.), not case sensitive
local user_markerPrefix = "" -- Prefix for marker text, for instance "#arty"
-- end of user block
-- Script variables
local SINGLE_ROUND = false -- pilot called single round on marker (from F10 menu)
local artyCall = 0 -- pilot called arty (from F10 menu)
local artyRadius = user_spread -- Artillery Radius
local adjustRadius = 20 -- fire adjustment
local quantity = 1 -- Rounds expanded
local quantity_effect = user_quantity -- Rounds expanded during fire for effect
local tntEquivalent = 12 -- TNT equivalent for explosion
local fireDelay = user_fireDelay -- delay til artillery fires in seconds
local firstShotFired = true
local markerSet = false
local pos = { x = 0, y = 0, z = 0 }
local playerPos = { x = 0, y = 0, z = 0 }
local target = {}
local adjustX = 0
local adjustZ = 0
local adjustDistance = 0 -- Adjust fire (from F10 menu)
local adjustDirection = 0 -- Adjust fire (from F10 menu)
local position = ""
local markerText = ""
local artyTasks = {}
local menuItems = false
-- optional arty enabled user flag, for use in triggers, if the player wants to
trigger.action.setUserFlag( "artyEnabled", 1 )
-- select format of target coordinates MGRS or LAT/LONG
local outputFormat = "MGRS"
--local outputFormat = "LL"
-- set values selected by player through F10 menu
local function setValue( _valueType, _value, _initiatorName )
if _valueType == "arty" then
artyCall = _value
end
if _valueType == "dist" then
adjustDistance = _value
trigger.action.outText("Fire adjusted by "..adjustDistance.." meters", 10)
end
if _valueType == "dir" then
adjustDirection = _value
end
artyAction( _initiatorName )
end
-- Function to add F10 menu items for a specific group and store references
local function addMenuItems(groupId, initiatorName)
menuItems = true
local artyTask = artyTasks[initiatorName]
artyTask.ArtyMenu = missionCommands.addSubMenuForGroup(groupId, 'Artillery Commands')
artyTask.AdjustDistance = missionCommands.addSubMenuForGroup(groupId, 'Adjust distance', artyTask.ArtyMenu)
artyTask.AdjustDirection = missionCommands.addSubMenuForGroup(groupId, 'Adjust direction', artyTask.ArtyMenu)
artyTask.commands = {}
artyTask.commands[#artyTask.commands + 1] = missionCommands.addCommandForGroup(groupId, 'request single round', artyTask.ArtyMenu, function() setValue("arty", 1, initiatorName) end)
artyTask.commands[#artyTask.commands + 1] = missionCommands.addCommandForGroup(groupId, 'request fire for effect', artyTask.ArtyMenu, function() setValue("arty", 2, initiatorName) end)
artyTask.commands[#artyTask.commands + 1] = missionCommands.addCommandForGroup(groupId, 'adjust fire by 20m', artyTask.AdjustDistance, function() setValue("dist", 20, initiatorName) end)
artyTask.commands[#artyTask.commands + 1] = missionCommands.addCommandForGroup(groupId, 'adjust fire by 50m', artyTask.AdjustDistance, function() setValue("dist", 50, initiatorName) end)
artyTask.commands[#artyTask.commands + 1] = missionCommands.addCommandForGroup(groupId, 'adjust fire by 100m', artyTask.AdjustDistance, function() setValue("dist", 100, initiatorName) end)
artyTask.commands[#artyTask.commands + 1] = missionCommands.addCommandForGroup(groupId, 'adjust fire by 200m', artyTask.AdjustDistance, function() setValue("dist", 200, initiatorName) end)
artyTask.commands[#artyTask.commands + 1] = missionCommands.addCommandForGroup(groupId, 'adjust fire by 500m', artyTask.AdjustDistance, function() setValue("dist", 500, initiatorName) end)
artyTask.commands[#artyTask.commands + 1] = missionCommands.addCommandForGroup(groupId, 'adjust fire North', artyTask.AdjustDirection, function() setValue("dir", 360, initiatorName) end)
artyTask.commands[#artyTask.commands + 1] = missionCommands.addCommandForGroup(groupId, 'adjust fire North-East', artyTask.AdjustDirection, function() setValue("dir", 45, initiatorName) end)
artyTask.commands[#artyTask.commands + 1] = missionCommands.addCommandForGroup(groupId, 'adjust fire East', artyTask.AdjustDirection, function() setValue("dir", 90, initiatorName) end)
artyTask.commands[#artyTask.commands + 1] = missionCommands.addCommandForGroup(groupId, 'adjust fire South-East', artyTask.AdjustDirection, function() setValue("dir", 135, initiatorName) end)
artyTask.commands[#artyTask.commands + 1] = missionCommands.addCommandForGroup(groupId, 'adjust fire South', artyTask.AdjustDirection, function() setValue("dir", 180, initiatorName) end)
artyTask.commands[#artyTask.commands + 1] = missionCommands.addCommandForGroup(groupId, 'adjust fire South-West', artyTask.AdjustDirection, function() setValue("dir", 225, initiatorName) end)
artyTask.commands[#artyTask.commands + 1] = missionCommands.addCommandForGroup(groupId, 'adjust fire West', artyTask.AdjustDirection, function() setValue("dir", 270, initiatorName) end)
artyTask.commands[#artyTask.commands + 1] = missionCommands.addCommandForGroup(groupId, 'adjust fire North-West', artyTask.AdjustDirection, function() setValue("dir", 315, initiatorName) end)
end
-- Function to remove F10 menu items for a specific group
local function removeMenuItems(initiatorName)
local artyTask = artyTasks[initiatorName]
if artyTask then
for _, command in ipairs(artyTask.commands) do
missionCommands.removeItemForGroup(artyTasks[initiatorName].groupID, command)
end
missionCommands.removeItemForGroup(artyTasks[initiatorName].groupID, artyTask.AdjustDistance)
missionCommands.removeItemForGroup(artyTasks[initiatorName].groupID, artyTask.AdjustDirection)
missionCommands.removeItemForGroup(artyTasks[initiatorName].groupID, artyTask.ArtyMenu)
end
menuItems = false
end
-- Calculate distance
local function getDist(_point1, _point2)
local xUnit = _point1.x
local yUnit = _point1.z
local xZone = _point2.x
local yZone = _point2.z
local xDiff = xUnit - xZone
local yDiff = yUnit - yZone
return math.sqrt(xDiff * xDiff + yDiff * yDiff)
end
-- Shelling Zone
local function shellZone ( _initiatorName )
trigger.action.outTextForUnit( artyTasks[_initiatorName].unitID, "Arty Task Created - fire incoming "..quantity.." rounds", 10)
if artyCall == 1 then
artyRadius = 5
else
artyRadius = 50
end
local _shellPos = artyTasks[_initiatorName].pos
if firstShotFired == true then
_shellPos.x = _shellPos.x + adjustX
_shellPos.y = _shellPos.y
_shellPos.z = _shellPos.z + adjustZ
end
for i = 1, quantity do
-- Create a random offset within the given radius
local randomX = math.random(-artyRadius, artyRadius)
local randomZ = math.random(-artyRadius, artyRadius)
local strikePos = {
x = _shellPos.x + randomX,
y = _shellPos.y,
z = _shellPos.z + randomZ
}
-- Delay the shelling by 1 second for each shell
timer.scheduleFunction(function()
trigger.action.explosion(strikePos, tntEquivalent) -- Create an explosion at the target position with a predefined power
end, {}, timer.getTime() + i)
end
--[[
if firstShotFired == false then
addMenuItems ()
firstShotFired = true
end
]]--
end
-- MGRS conversion to LL to x,z
local function convertMGRStoPos ( _mrgs )
local lat, lon = coord.MGRStoLL( _mgrs )
local markerPos = coord.LLtoLO( lat, lon, 0 )
return markerPos
end
-- x,z coordinates conversion to LAT/LONG and MGRS
local function convertPos2Coord ( _pos, _reply )
local lat, lon, alt = coord.LOtoLL (_pos)
local lat_degrees = math.floor (lat)
local lat_minutes = (60 * (lat - lat_degrees))
local lat_seconds = math.floor(60 * (lat_minutes - math.floor(lat_minutes)))
lat_minutes = math.floor(lat_minutes)
local lon_degrees = math.floor (lon)
local lon_minutes = (60 * (lon - lon_degrees))
local lon_seconds = math.floor (60 * (lon_minutes - math.floor(lon_minutes)))
lon_minutes = math.floor(lon_minutes)
local coordStringLL = "N" .. lat_degrees .. " " .. lat_minutes .. " " ..lat_seconds.. " E".. lon_degrees .. " " .. lon_minutes .. " ".. lon_seconds
local targetMGRS = coord.LLtoMGRS(lat, lon)
targetMGRS.Easting = math.floor (( targetMGRS.Easting /10 ) + 0.5 )
targetMGRS.Northing = math.floor (( targetMGRS.Northing / 10 ) + 0.5 )
--local coordStringMGRS = targetMGRS.UTMZone.." "..targetMGRS.MGRSDigraph.." "..string.sub(targetMGRS.Easting, 1, -2).." "..string.sub(targetMGRS.Northing, 1, -2)
local coordStringMGRS = targetMGRS.UTMZone.." "..targetMGRS.MGRSDigraph.." "..targetMGRS.Easting.." "..targetMGRS.Northing
if outputFormat == "MGRS" then
coordString = coordStringMGRS
else
coordString = coordStringLL
end
-- return either formated string or MGRS coordinate
if _reply == "string" then
return coordString
elseif _reply == "pos" then
return targetMGRS
end
end
-- Who is the player
-- Function to determine which unit is controlled by the player
--[[
local function getPlayerControlledUnit()
local playerUnit = nil
-- Iterate through all coalitions and their respective player units
for coalitionID = 1, 2 do -- 1 = Red, 2 = Blue
local playerUnits = coalition.getPlayers(coalitionID)
for _, unit in ipairs(playerUnits) do
if unit and unit:getPlayerName() then
playerUnit = unit
break
end
end
if playerUnit then
break
end
end
return playerUnit
end
]]--
-- Check if user has created F10 map marker
artyAction = function ( _initiatorName )
-- Check Call for arty - 1 = single round, 2 = fire for effect
if artyCall == 1 or artyCall == 2 then
if MARKER_FOUND == true and artyTasks[_initiatorName] then
-- check if target is within 15km from player
--trigger.action.outTextForUnit( artyTasks[_initiatorName].unitID, "Arty action marker found.", 10)
--local _player = _initiator
local _playerPos = artyTasks[_initiatorName].playerPos
local _targetPos = artyTasks[_initiatorName].pos
local _dist = math.floor( getDist ( _targetPos, _playerPos ) / 10 ) / 100
if trigger.misc.getUserFlag( "artyEnabled" ) == 1 and _dist <= user_spottingDistance then
position = convertPos2Coord ( _targetPos, "string" )
if artyCall == 1 then
trigger.action.outTextForUnit( artyTasks[_initiatorName].unitID, "Arty single round requested on "..position, 10)
quantity = 1
elseif artyCall == 2 then
trigger.action.outTextForUnit( artyTasks[_initiatorName].unitID, "Arty fire for effect requested on "..position, 10)
quantity = quantity_effect
end
timer.scheduleFunction(shellZone, _initiatorName, timer.getTime() + fireDelay)
trigger.action.setUserFlag( "artyFired", 1 )
else
trigger.action.outTextForUnit( artyTasks[_initiatorName].unitID, "Artillery not available", 10)
end
else
trigger.action.outTextForUnit( artyTasks[_initiatorName].unitID, "Arty Requested Without Marker", 10)
end
artyCall = 0
end
-- Check Call for arty direction correction
if adjustDirection == 360 then
adjustX = adjustDistance
adjustZ = 0
artyRadius = 5
trigger.action.outTextForUnit( artyTasks[_initiatorName].unitID, "Fire adjusted to the North", 10)
adjustDirection = 0
end
if adjustDirection == 45 then
adjustX = adjustDistance
adjustZ = adjustDistance
artyRadius = 5
adjustDirection = 0
end
if adjustDirection == 90 then
adjustX = 0
adjustZ = adjustDistance
artyRadius = 5
trigger.action.outTextForUnit( artyTasks[_initiatorName].unitID, "Fire adjusted to the East", 10)
adjustDirection = 0
end
if adjustDirection == 135 then
adjustX = -adjustDistance
adjustZ = adjustDistance
artyRadius = 5
trigger.action.outTextForUnit( artyTasks[_initiatorName].unitID, "Fire adjusted to the South-East", 10)
adjustDirection = 0
end
if adjustDirection == 180 then
adjustX = -adjustDistance
adjustZ = 0
artyRadius = 5
trigger.action.outTextForUnit( artyTasks[_initiatorName].unitID, "Fire adjusted to the South", 10)
adjustDirection = 0
end
if adjustDirection == 225 then
adjustX = -adjustDistance
adjustZ = -adjustDistance
artyRadius = 5
trigger.action.outTextForUnit( artyTasks[_initiatorName].unitID, "Fire adjusted to the South-West", 10)
adjustDirection = 0
end
if adjustDirection == 270 then
adjustX = 0
adjustZ = -adjustDistance
artyRadius = 5
trigger.action.outTextForUnit( artyTasks[_initiatorName].unitID, "Fire adjusted meters to the West", 10)
adjustDirection = 0
end
if adjustDirection == 315 then
adjustX = adjustDistance
adjustZ = -adjustDistance
artyRadius = 5
trigger.action.outTextForUnit( artyTasks[_initiatorName].unitID, "Fire adjusted meters to the North-West", 10)
adjustDirection = 0
end
end
-- Main
trigger.action.outText("Arty spotter script "..version.." loaded", 10)
-- Map Marker Text - read and process
-- Function to remove spaces from a string
local function removeSpaces( _text )
_text = _text:gsub( " ", "" )
_text = _text:gsub( "-", "" )
return _text
end
-- Function to validate the structure of the MGRS coordinate
local function checkValidMGRS( _mgrs, len)
if len == 13 then
-- Pattern: 2 digits 1 letter UTM Zone, 2 letters MGRS Digraph, 4 digits Easting, 4 digits Northing
return _mgrs:match("^%d%d%u%u%u%d%d%d%d%d%d%d%d$")
elseif len == 10 then
-- Pattern: 2 letters MGRS Digraph, 4 digits Easting, 4 digits Northing
return _mgrs:match("^%u%u%d%d%d%d%d%d%d%d$")
elseif len == 8 then
-- Pattern: 4 digits Easting, 4 digits Northing
return _mgrs:match("^%d%d%d%d%d%d%d%d$")
else
return false
end
end
-- Function to validate and complete MGRS coordinates
local function processMGRS( _text, _playerPos, initiatorName )
local _cleanedText = string.upper( removeSpaces( _text ) )
local len = #_cleanedText
local _isValidMGRS = checkValidMGRS( _cleanedText, len)
if _isValidMGRS then
trigger.action.outTextForUnit( artyTasks[initiatorName].unitID, "Processing MGRS: " .. _cleanedText, 10)
if len == 13 then
-- Complete MGRS coordinate
return _cleanedText
elseif len == 10 then
-- Add UTM Zone based on player position
local _utmZone = coord.LLtoMGRS(_playerPos.Lat, _playerPos.Lon).UTMZone
return _utmZone .. _cleanedText
elseif len == 8 then
-- Add UTM Zone and MGRS Digraph based on player position
local _mgrs = coord.LLtoMGRS( _playerPos.Lat, _playerPos.Lon )
return _mgrs.UTMZone .. _mgrs.MGRSDigraph .. _cleanedText
else
-- Invalid MGRS coordinate
return nil
end
else
trigger.action.outTextForUnit( artyTasks[initiatorName].unitID, "Invalid text input: " .. _cleanedText, 10)
return nil
end
end
-- Function to convert a valid MGRS to vec3
local function MGRStoVec3( _mgrs )
local lat, lon = coord.MGRStoLL( _mgrs )
local vec3 = coord.LLtoLO( lat, lon, 0 )
return vec3
end
-- Function to check if the initiator is valid based on restrictions
local function isValidInitiator(initiator)
if not initiator then return false end
-- Check type restriction
if user_restrictByType == "helo" then
if not initiator:getDesc().category == Unit.Category.Helicopter then
return false
end
end
-- Check name restriction
if user_restrictByUnitName ~= "" then
local name = initiator:getName():lower()
if not name:find(user_restrictByUnitName:lower()) then
return false
end
end
return true
end
-- Function to check if the marker text has the required prefix and remove it
local function checkAndRemovePrefix(text)
if user_markerPrefix ~= "" and text:sub(1, #user_markerPrefix) == user_markerPrefix then
return true, text:sub(#user_markerPrefix + 1)
elseif user_markerPrefix == "" then
return true, text
else
return false, text
end
end
-- Event handler for map marker creation
local function onPlayerAddMarker(event)
if event.id == world.event.S_EVENT_MARK_ADDED and user_markerPrefix == "" then
if isValidInitiator(event.initiator) then
--local hasPrefix, cleanedText = checkAndRemovePrefix(event.text)
hasPrefix = true
if hasPrefix then
MARKER_FOUND = true
pos = event.pos
if event.initiator then
local initiatorName = event.initiator:getName()
local playerUnit = event.initiator
local playerPos = playerUnit:getPoint()
-- Store position
if not artyTasks[initiatorName] then
artyTasks[initiatorName] = {}
end
trigger.action.outTextForUnit( event.initiator:getID(), "Marker added", 5)
artyTasks[initiatorName].playerPos = playerPos
artyTasks[initiatorName].pos = pos
artyTasks[initiatorName].unitID = event.initiator:getID()
-- Add menu items for the initiator's group
local groupId = event.initiator:getGroup():getID()
artyTasks[initiatorName].groupID = groupId
if menuItems == false then
addMenuItems(groupId, initiatorName)
end
end
end
else
--trigger.action.outText("You do not have permission to add a marker.", 5)
end
elseif event.id == world.event.S_EVENT_MARK_CHANGE then
if isValidInitiator(event.initiator) then
local hasPrefix, cleanedText = checkAndRemovePrefix(event.text)
if hasPrefix then
MARKER_FOUND = true
local markText = cleanedText
trigger.action.outText("Text: "..markText, 10)
if markText and event.initiator then
local initiatorName = event.initiator:getName()
local playerUnit = event.initiator
if not artyTasks[initiatorName] then
artyTasks[initiatorName] = {}
end
artyTasks[initiatorName].initiator = event.initiator
artyTasks[initiatorName].unitID = event.initiator:getID()
trigger.action.outTextForUnit( event.initiator:getID(), "Marker changed", 5)
if playerUnit then
local playerPos = playerUnit:getPoint()
local lat, lon = coord.LOtoLL(playerPos)
local playerPosition = { Lat = lat, Lon = lon }
local validMGRS = processMGRS(markText, playerPosition, initiatorName)
if validMGRS then
trigger.action.outTextForUnit( artyTasks[initiatorName].unitID, "Valid MGRS: " .. validMGRS, 10)
local tmpMGRS = {
UTMZone = string.sub(validMGRS, 1, 3),
MGRSDigraph = string.sub(validMGRS, 4, 5),
Easting = tonumber(string.sub(validMGRS, 6, 9)) * 10,
Northing = tonumber(string.sub(validMGRS, 10, 13)) * 10
}
local targetPoint = MGRStoVec3(tmpMGRS)
targetPoint.y = land.getHeight({ x = targetPoint.x, y = targetPoint.z })
artyTasks[initiatorName].pos = targetPoint
artyTasks[initiatorName].playerPos = playerPos
else
trigger.action.outTextForUnit( artyTasks[initiatorName].unitID, "Invalid MGRS coordinate entered.", 10)
end
local groupId = event.initiator:getGroup():getID()
artyTasks[initiatorName].groupID = groupId
if menuItems == false then
addMenuItems(groupId, initiatorName)
end
end
end
end
else
--trigger.action.outText("You do not have permission to change this marker.", 5)
end
elseif event.id == world.event.S_EVENT_MARK_REMOVED then
trigger.action.outText("Marker removed", 5)
if event.initiator then
local initiatorName = event.initiator:getName()
if artyTasks[initiatorName] then
removeMenuItems(initiatorName)
artyTasks[initiatorName] = nil
end
end
end
end
-- Register the event handler
local eventHandler = { f = onPlayerAddMarker }
function eventHandler:onEvent(e)
self.f(e)
end
world.addEventHandler(eventHandler)

View File

@ -0,0 +1,22 @@
-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- configuration file for Carsten's Arty Spotter Plugin
--
-- This configuration is tailored for a mission generated by DCS Retribution
-- see https://github.com/dcs-retribution/dcs-retribution
-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- arty plugin - configuration
if dcsRetribution then
-- retrieve specific options values
if dcsRetribution.plugins then
if dcsRetribution.plugins.arty then
env.info("DCSRetribution|Carsten's Arty Spotter plugin - Setting Up")
cg_arty_options.user_fireDelay = dcsRetribution.plugins.arty.user_fireDelay
cg_arty_options.user_quantity = dcsRetribution.plugins.arty.user_quantity
cg_arty_options.user_spread = dcsRetribution.plugins.arty.user_spread
cg_arty_options.user_spottingDistance = dcsRetribution.plugins.arty.user_spottingDistance
end
end
end

View File

@ -0,0 +1,38 @@
{
"nameInUI": "Carsten's Arty Spotter",
"defaultValue": false,
"specificOptions": [
{
"nameInUI": "Time to impcat of the rounds",
"mnemonic": "user_fireDelay",
"defaultValue": 15
},
{
"nameInUI": "Salvo quantity",
"mnemonic": "user_quantity",
"defaultValue": 5
},
{
"nameInUI": "Impact radius",
"mnemonic": "user_spread",
"defaultValue": 150
},
{
"nameInUI": "Max spotting distance. In kilometers.",
"mnemonic": "user_spottingDistance",
"defaultValue": 15
}
],
"scriptsWorkOrders": [
{
"file": "CG_ArtySpotter_V1_2a_MP.lua",
"mnemonic": "arty"
}
],
"configurationWorkOrders": [
{
"file": "arty-config.lua",
"mnemonic": "arty-config"
}
]
}

View File

@ -1,6 +1,7 @@
[
"base",
"ctld",
"arty",
"dismounts",
"ewrj",
"ewrs",

View File

@ -3,8 +3,7 @@ name: Australian Army
country: Australia
role: Light Attack and Scout Helicopter
aircraft: OH-58D(R) Kiowa Warrior
livery:
- AUS Army Fictional
livery: AUS Army Fictional
mission_types:
- BAI
- CAS

View File

@ -3,8 +3,7 @@ name: German Army
country: Germany
role: Light Attack and Scout Helicopter
aircraft: OH-58D(R) Kiowa Warrior
livery:
- DE Army Fictional
livery: DE Army Fictional
mission_types:
- BAI
- CAS

View File

@ -3,8 +3,7 @@ name: Spanish Army
country: Spain
role: Light Attack and Scout Helicopter
aircraft: OH-58D(R) Kiowa Warrior
livery:
- ES Army Fictional
livery: ES Army Fictional
mission_types:
- BAI
- CAS

View File

@ -3,8 +3,7 @@ name: French Army
country: France
role: Light Attack and Scout Helicopter
aircraft: OH-58D(R) Kiowa Warrior
livery:
- FR Army Fictional
livery: FR Army Fictional
mission_types:
- BAI
- CAS

View File

@ -3,8 +3,7 @@ name: Greek Army
country: Greece
role: Light Attack and Scout Helicopter
aircraft: OH-58D(R) Kiowa Warrior
livery:
- GR Army Fictional
livery: GR Army Fictional
mission_types:
- BAI
- CAS

View File

@ -3,8 +3,7 @@ name: Israeli Army
country: Israel
role: Light Attack and Scout Helicopter
aircraft: OH-58D(R) Kiowa Warrior
livery:
- ISR Army Fictional
livery: ISR Army Fictional
mission_types:
- BAI
- CAS

View File

@ -3,8 +3,7 @@ name: Japanese Army
country: Japan
role: Light Attack and Scout Helicopter
aircraft: OH-58D(R) Kiowa Warrior
livery:
- JPN Army Fictional
livery: JPN Army Fictional
mission_types:
- BAI
- CAS

View File

@ -3,8 +3,7 @@ name: Dutch Army
country: The Netherlands
role: Light Attack and Scout Helicopter
aircraft: OH-58D(R) Kiowa Warrior
livery:
- NL Army Fictional
livery: NL Army Fictional
mission_types:
- BAI
- CAS

View File

@ -3,8 +3,7 @@ name: Polish Army
country: Poland
role: Light Attack and Scout Helicopter
aircraft: OH-58D(R) Kiowa Warrior
livery:
- PL Army Fictional
livery: PL Army Fictional
mission_types:
- BAI
- CAS

View File

@ -3,8 +3,7 @@ name: Russian Army
country: Russia
role: Light Attack and Scout Helicopter
aircraft: OH-58D(R) Kiowa Warrior
livery:
- RU Army Fictional
livery: RU Army Fictional
mission_types:
- BAI
- CAS

View File

@ -3,8 +3,7 @@ name: Tunisian Army
country: Tunisia
role: Light Attack and Scout Helicopter
aircraft: OH-58D(R) Kiowa Warrior
livery:
- TUN Army
livery: TUN Army
mission_types:
- BAI
- CAS

View File

@ -3,8 +3,7 @@ name: Taiwanese Army
country: Combined Joint Task Forces Blue
role: Light Attack and Scout Helicopter
aircraft: OH-58D(R) Kiowa Warrior
livery:
- TWN Army Fictional
livery: TWN Army Fictional
mission_types:
- BAI
- CAS

View File

@ -3,8 +3,7 @@ name: British Army Air Corps Desert
country: UK
role: Light Attack and Scout Helicopter
aircraft: OH-58D(R) Kiowa Warrior
livery:
- UK Army Fictional Desert
livery: UK Army Fictional Desert
mission_types:
- BAI
- CAS

View File

@ -3,8 +3,7 @@ name: British Army Air Corps
country: UK
role: Light Attack and Scout Helicopter
aircraft: OH-58D(R) Kiowa Warrior
livery:
- UK Army Fictional
livery: UK Army Fictional
mission_types:
- BAI
- CAS

View File

@ -5,8 +5,7 @@ female_pilot_percentage: 10
country: USA
role: Light Attack and Scout Helicopter
aircraft: OH-58D(R) Kiowa Warrior
livery:
- US 3-17 B 937 Iraq
livery: US 3-17 B 937 Iraq
mission_types:
- BAI
- CAS

View File

@ -590,6 +590,11 @@ QFrame[style="QConditionsWidget"] {
background: transparent;
}
QFrame[style="QConditionsWidget"]:hover {
cursor: pointer;
background: #43A6C6;
}
QGroupBox[style="QWeatherWidget"] {
padding: 0px;
margin-left: 0px;
@ -664,3 +669,7 @@ QCalendarWidget QTableView{
.comms {
padding: 2px;
}
.hidden {
visibility: hidden;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@ -0,0 +1,17 @@
carrier_capable: true
description:
"The Northrop Grumman EA-6B Prowler is a twin-engine, four-seat, mid-wing electronic-warfare \
\ aircraft derived from the A-6 Intruder airframe. The EA-6A was the initial electronic warfare \
\ version of the A-6 used by the United States Marine Corps and United States Navy. \
\ It was capable of carrying and firing anti-radiation missiles (ARMs), such as the AGM-88 HARM."
introduced: 1971
manufacturer: Northrop Grumman
origin: USA
price: 11
role: Carrier-based Electronic-warfare Aircraft
gunfighter: false
variants:
EA-6B Prowler: {}
tasks:
SEAD: 460
SEAD Escort: 460

View File

@ -29,7 +29,7 @@ radios:
default_overrides:
#NetCrewControlPriority: 0,
#Remove_doors: true,
PDU: true,
#PDU: true,
#Rifles: true,
#MMS_removal: false,
#Rapid_Deployment_Gear: false,

View File

@ -0,0 +1,20 @@
description:
"The Sukhoi Su-15 (NATO reporting name: Flagon) is a twinjet supersonic interceptor
aircraft developed by the Soviet Union. It entered service in 1965 and remained one
of the front-line designs into the 1990s."
introduced: 1965
manufacturer: Sukhoi
origin: USSR/Russia
price: 12
role: Interceptor
max_range: 200
gunfighter: true
variants:
Su-15 Flagon-A: {}
kneeboard_units: "metric"
tasks:
BARCAP: 360
Escort: 360
Fighter sweep: 360
Intercept: 360
TARCAP: 360

View File

@ -0,0 +1,23 @@
description:
"The Sukhoi Su-15 (NATO reporting name: Flagon) is a twinjet supersonic interceptor
aircraft developed by the Soviet Union. It entered service in 1965 and remained one
of the front-line designs into the 1990s. The Flagon-A was followed in December 1971
by the Su-15TM (NATO Flagon-E), with the improved Taifun-M radar (NATO Twin Scan)
and provision for UPK-23-250 gun pod or R-60 (AA-8 Aphid) short-range air-to-air
missiles."
introduced: 1971
manufacturer: Sukhoi
origin: USSR/Russia
price: 12
role: Interceptor
max_range: 200
gunfighter: true
variants:
Su-15TM Flagon-E: {}
kneeboard_units: "metric"
tasks:
BARCAP: 370
Escort: 370
Fighter sweep: 370
Intercept: 370
TARCAP: 370

View File

@ -15,7 +15,6 @@ tasks:
BAI: 350
CAS: 450
DEAD: 450
Escort: 90
OCA/Aircraft: 400
OCA/Runway: 400
SEAD: 450

View File

@ -16,15 +16,8 @@ gunfighter: true
variants:
F-104S Starfighter: {}
tasks:
Anti-ship: 150
BAI: 140
BARCAP: 250
CAS: 140
DEAD: 140
Escort: 150
Fighter sweep: 200
Intercept: 300
OCA/Aircraft: 140
OCA/Runway: 200
Strike: 50
TARCAP: 250

View File

@ -18,13 +18,8 @@ variants:
tasks:
Anti-ship: 150
BAI: 140
BARCAP: 250
CAS: 140
DEAD: 140
Escort: 150
Fighter sweep: 200
Intercept: 300
OCA/Aircraft: 140
OCA/Runway: 200
Strike: 50
TARCAP: 250

View File

@ -5,3 +5,5 @@ fallback: AIM-7E
clsids:
- "{LAU-115 - AIM-7E}"
- "{AIM-7E}"
- "{AIM-7E-2}"
- "{HB_F4E_AIM-7E-2}"

View File

@ -5,3 +5,4 @@ fallback: AIM-9X
clsids:
- "{SHOULDER AIM-7E}"
- "{BELLY AIM-7E}"
- "{HB_F4E_AIM-7E}"

View File

@ -6,3 +6,4 @@ clsids:
- "{BELLY AIM-7F}"
- "{AIM-7F}"
- "{LAU-115 - AIM-7F}"
- "{HB_F4E_AIM-7F}"

View File

@ -6,3 +6,4 @@ clsids:
- "{BELLY AIM-7M}"
- "{8D399DDA-FF81-4F14-904D-099B34FE7918}"
- "{LAU-115 - AIM-7M}"
- "{HB_F4E_AIM-7M}"

View File

@ -0,0 +1,7 @@
name: AIM-7P
year: 1987
fallback: AIM-7M
clsids:
- "{SHOULDER AIM-7P}"
- "{BELLY AIM-7P}"
- "{AIM-7P}"

View File

@ -10,3 +10,5 @@ clsids:
- "LAU-127_AIM-9L"
- "{LAU-138 wtip - AIM-9L}"
- "{LAU-7 - AIM-9L}"
- "{LAU-7_AIM-9L_Left}"
- "{LAU-7_AIM-9L_Right}"

View File

@ -11,3 +11,5 @@ clsids:
- "{LAU-138 wtip - AIM-9M}"
- "{LAU-7 - AIM-9M}"
- "{AIM-9M-ON-ADAPTER}"
- "{LAU-7_AIM-9M_Left}"
- "{LAU-7_AIM-9M_Right}"

View File

@ -0,0 +1,6 @@
name: AGM-12A
year: 1959
fallback: Mk 84
clsids:
- "{AGM_12A}"
- "{AGM_12A_SWA}"

View File

@ -0,0 +1,6 @@
name: AGM-12B
year: 1960
fallback: AGM-12A
clsids:
- "{AGM_12B}"
- "{AGM_12B_SWA}"

View File

@ -0,0 +1,6 @@
name: AGM-12C
year: 1965
fallback: AGM-12B
clsids:
- "{AGM_12C}"
- "{AGM_12C_SWA}"

View File

@ -3,3 +3,6 @@ year: 1965
type: ARM
clsids:
- "{AGM_45A}"
- "{LAU118_AGM_45A}"
- "{LAU_34_AGM_45A}"
- "{LAU_34_AGM_45A_SWA}"

View File

@ -0,0 +1,5 @@
name: AGM-62 Walleye I
year: 1967
fallback: AGM-12C
clsids:
- "{AGM_62_I}"

Some files were not shown because too many files have changed in this diff Show More