mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Merge remote-tracking branch 'remotes/dcs-retribution/dcs-retribution/dev' into pretense-generator
This commit is contained in:
commit
1a6d73f055
@ -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 !
|
||||
|
||||
@ -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)
|
||||
|
||||
40
client/package-lock.json
generated
40
client/package-lock.json
generated
@ -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"
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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 *
|
||||
|
||||
1
pydcs_extensions/ea6b/__init__.py
Normal file
1
pydcs_extensions/ea6b/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .ea6b import *
|
||||
105
pydcs_extensions/ea6b/ea6b.py
Normal file
105
pydcs_extensions/ea6b/ea6b.py
Normal 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
|
||||
@ -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_,
|
||||
)
|
||||
|
||||
1
pydcs_extensions/su15/__init__.py
Normal file
1
pydcs_extensions/su15/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .su15 import *
|
||||
203
pydcs_extensions/su15/su15.py
Normal file
203
pydcs_extensions/su15/su15.py
Normal 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
|
||||
@ -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,
|
||||
|
||||
85
qt_ui/widgets/QConditionsDialog.py
Normal file
85
qt_ui/widgets/QConditionsDialog.py
Normal 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()
|
||||
@ -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("0º")
|
||||
windsLayout.addWidget(self.windGLSpeedLabel, 0, 2)
|
||||
windsLayout.addWidget(self.windGLDirLabel, 0, 3)
|
||||
|
||||
self.windFL08SpeedLabel = self.makeLabel("0kts")
|
||||
self.windFL08DirLabel = self.makeLabel("0º")
|
||||
windsLayout.addWidget(self.windFL08SpeedLabel, 1, 2)
|
||||
windsLayout.addWidget(self.windFL08DirLabel, 1, 3)
|
||||
|
||||
self.windFL26SpeedLabel = self.makeLabel("0kts")
|
||||
self.windFL26DirLabel = self.makeLabel("0º")
|
||||
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.
|
||||
|
||||
|
||||
95
qt_ui/widgets/conditions/DcsCloudBaseSelector.py
Normal file
95
qt_ui/widgets/conditions/DcsCloudBaseSelector.py
Normal 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)
|
||||
43
qt_ui/widgets/conditions/DcsCloudDensitySelector.py
Normal file
43
qt_ui/widgets/conditions/DcsCloudDensitySelector.py
Normal 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)
|
||||
88
qt_ui/widgets/conditions/DcsCloudThicknessSelector.py
Normal file
88
qt_ui/widgets/conditions/DcsCloudThicknessSelector.py
Normal 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)
|
||||
29
qt_ui/widgets/conditions/DcsPrecipitationSelector.py
Normal file
29
qt_ui/widgets/conditions/DcsPrecipitationSelector.py
Normal 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")
|
||||
38
qt_ui/widgets/conditions/QTimeAdjustmentWidget.py
Normal file
38
qt_ui/widgets/conditions/QTimeAdjustmentWidget.py
Normal 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)
|
||||
70
qt_ui/widgets/conditions/QTimeTurnWidget.py
Normal file
70
qt_ui/widgets/conditions/QTimeTurnWidget.py
Normal 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"))
|
||||
92
qt_ui/widgets/conditions/QWeatherAdjustmentWidget.py
Normal file
92
qt_ui/widgets/conditions/QWeatherAdjustmentWidget.py
Normal 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)
|
||||
207
qt_ui/widgets/conditions/QWeatherWidget.py
Normal file
207
qt_ui/widgets/conditions/QWeatherWidget.py
Normal 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("0º")
|
||||
windsLayout.addWidget(self.windGLSpeedLabel, 0, 2)
|
||||
windsLayout.addWidget(self.windGLDirLabel, 0, 3)
|
||||
|
||||
self.windFL08SpeedLabel = self.makeLabel("0kts")
|
||||
self.windFL08DirLabel = self.makeLabel("0º")
|
||||
windsLayout.addWidget(self.windFL08SpeedLabel, 1, 2)
|
||||
windsLayout.addWidget(self.windFL08DirLabel, 1, 3)
|
||||
|
||||
self.windFL26SpeedLabel = self.makeLabel("0kts")
|
||||
self.windFL26DirLabel = self.makeLabel("0º")
|
||||
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)
|
||||
@ -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)
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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
|
||||
|
||||
Binary file not shown.
@ -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:
|
||||
|
||||
Binary file not shown.
@ -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
|
||||
|
||||
Binary file not shown.
@ -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
|
||||
|
||||
36
resources/customized_payloads/EA_6B.lua
Normal file
36
resources/customized_payloads/EA_6B.lua
Normal 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
|
||||
33
resources/customized_payloads/Su_15.lua
Normal file
33
resources/customized_payloads/Su_15.lua
Normal 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
|
||||
43
resources/customized_payloads/Su_15TM.lua
Normal file
43
resources/customized_payloads/Su_15TM.lua
Normal 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
|
||||
@ -11,6 +11,7 @@
|
||||
"MiG-15bis Fagot",
|
||||
"MiG-19P Farmer-B",
|
||||
"MiG-21bis Fishbed-N",
|
||||
"Su-15 Flagon-A",
|
||||
"Tu-95MS Bear-H"
|
||||
],
|
||||
"awacs": [
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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)",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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)",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
723
resources/plugins/arty/CG_ArtySpotter_V1_2a_MP.lua
Normal file
723
resources/plugins/arty/CG_ArtySpotter_V1_2a_MP.lua
Normal 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)
|
||||
|
||||
|
||||
|
||||
|
||||
22
resources/plugins/arty/arty-config.lua
Normal file
22
resources/plugins/arty/arty-config.lua
Normal 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
|
||||
38
resources/plugins/arty/plugin.json
Normal file
38
resources/plugins/arty/plugin.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
[
|
||||
"base",
|
||||
"ctld",
|
||||
"arty",
|
||||
"dismounts",
|
||||
"ewrj",
|
||||
"ewrs",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
BIN
resources/ui/units/aircrafts/banners/EA_6B.jpg
Normal file
BIN
resources/ui/units/aircrafts/banners/EA_6B.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 132 KiB |
BIN
resources/ui/units/aircrafts/icons/EA_6B_24.jpg
Normal file
BIN
resources/ui/units/aircrafts/icons/EA_6B_24.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
resources/ui/units/aircrafts/icons/Su_15TM_24.jpg
Normal file
BIN
resources/ui/units/aircrafts/icons/Su_15TM_24.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.1 KiB |
BIN
resources/ui/units/aircrafts/icons/Su_15_24.jpg
Normal file
BIN
resources/ui/units/aircrafts/icons/Su_15_24.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.9 KiB |
17
resources/units/aircraft/EA_6B.yaml
Normal file
17
resources/units/aircraft/EA_6B.yaml
Normal 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
|
||||
@ -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,
|
||||
|
||||
20
resources/units/aircraft/Su_15.yaml
Normal file
20
resources/units/aircraft/Su_15.yaml
Normal 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
|
||||
23
resources/units/aircraft/Su_15TM.yaml
Normal file
23
resources/units/aircraft/Su_15TM.yaml
Normal 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
|
||||
@ -15,7 +15,6 @@ tasks:
|
||||
BAI: 350
|
||||
CAS: 450
|
||||
DEAD: 450
|
||||
Escort: 90
|
||||
OCA/Aircraft: 400
|
||||
OCA/Runway: 400
|
||||
SEAD: 450
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -4,4 +4,6 @@ fallback: AIM-7E
|
||||
#Ignore how the CLSIDs look, these are not normal 7Es they are 7E-2 DOGFIGHT MISSILES
|
||||
clsids:
|
||||
- "{LAU-115 - AIM-7E}"
|
||||
- "{AIM-7E}"
|
||||
- "{AIM-7E}"
|
||||
- "{AIM-7E-2}"
|
||||
- "{HB_F4E_AIM-7E-2}"
|
||||
|
||||
@ -4,4 +4,5 @@ fallback: AIM-9X
|
||||
# Do not add the other 7E looking names, they are actually AIM-7E-2 6 years newer
|
||||
clsids:
|
||||
- "{SHOULDER AIM-7E}"
|
||||
- "{BELLY AIM-7E}"
|
||||
- "{BELLY AIM-7E}"
|
||||
- "{HB_F4E_AIM-7E}"
|
||||
|
||||
@ -5,4 +5,5 @@ clsids:
|
||||
- "{SHOULDER AIM-7F}"
|
||||
- "{BELLY AIM-7F}"
|
||||
- "{AIM-7F}"
|
||||
- "{LAU-115 - AIM-7F}"
|
||||
- "{LAU-115 - AIM-7F}"
|
||||
- "{HB_F4E_AIM-7F}"
|
||||
|
||||
@ -6,3 +6,4 @@ clsids:
|
||||
- "{BELLY AIM-7M}"
|
||||
- "{8D399DDA-FF81-4F14-904D-099B34FE7918}"
|
||||
- "{LAU-115 - AIM-7M}"
|
||||
- "{HB_F4E_AIM-7M}"
|
||||
|
||||
7
resources/weapons/a2a-missiles/AIM-7P.yaml
Normal file
7
resources/weapons/a2a-missiles/AIM-7P.yaml
Normal file
@ -0,0 +1,7 @@
|
||||
name: AIM-7P
|
||||
year: 1987
|
||||
fallback: AIM-7M
|
||||
clsids:
|
||||
- "{SHOULDER AIM-7P}"
|
||||
- "{BELLY AIM-7P}"
|
||||
- "{AIM-7P}"
|
||||
@ -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}"
|
||||
|
||||
@ -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}"
|
||||
|
||||
6
resources/weapons/standoff/AGM-12A.yaml
Normal file
6
resources/weapons/standoff/AGM-12A.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
name: AGM-12A
|
||||
year: 1959
|
||||
fallback: Mk 84
|
||||
clsids:
|
||||
- "{AGM_12A}"
|
||||
- "{AGM_12A_SWA}"
|
||||
6
resources/weapons/standoff/AGM-12B.yaml
Normal file
6
resources/weapons/standoff/AGM-12B.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
name: AGM-12B
|
||||
year: 1960
|
||||
fallback: AGM-12A
|
||||
clsids:
|
||||
- "{AGM_12B}"
|
||||
- "{AGM_12B_SWA}"
|
||||
6
resources/weapons/standoff/AGM-12C.yaml
Normal file
6
resources/weapons/standoff/AGM-12C.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
name: AGM-12C
|
||||
year: 1965
|
||||
fallback: AGM-12B
|
||||
clsids:
|
||||
- "{AGM_12C}"
|
||||
- "{AGM_12C_SWA}"
|
||||
@ -3,3 +3,6 @@ year: 1965
|
||||
type: ARM
|
||||
clsids:
|
||||
- "{AGM_45A}"
|
||||
- "{LAU118_AGM_45A}"
|
||||
- "{LAU_34_AGM_45A}"
|
||||
- "{LAU_34_AGM_45A_SWA}"
|
||||
|
||||
5
resources/weapons/standoff/AGM-62-I.yaml
Normal file
5
resources/weapons/standoff/AGM-62-I.yaml
Normal 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
Loading…
x
Reference in New Issue
Block a user