diff --git a/README.md b/README.md index 1da6ca5f..1556d48d 100644 --- a/README.md +++ b/README.md @@ -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 ! diff --git a/changelog.md b/changelog.md index 9e34c673..df7f38cc 100644 --- a/changelog.md +++ b/changelog.md @@ -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) diff --git a/client/package-lock.json b/client/package-lock.json index 54f411d8..f9cbcf88 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -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" } diff --git a/game/ato/flightplans/waypointbuilder.py b/game/ato/flightplans/waypointbuilder.py index 7d78c9f2..a9be6ee5 100644 --- a/game/ato/flightplans/waypointbuilder.py +++ b/game/ato/flightplans/waypointbuilder.py @@ -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", diff --git a/game/campaignloader/mizcampaignloader.py b/game/campaignloader/mizcampaignloader.py index 1dcc028c..76737a33 100644 --- a/game/campaignloader/mizcampaignloader.py +++ b/game/campaignloader/mizcampaignloader.py @@ -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) diff --git a/game/dcs/aircrafttype.py b/game/dcs/aircrafttype.py index 20f967ae..b26198fb 100644 --- a/game/dcs/aircrafttype.py +++ b/game/dcs/aircrafttype.py @@ -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, diff --git a/game/factions/faction.py b/game/factions/faction.py index 9faee69e..22a1d115 100644 --- a/game/factions/faction.py +++ b/game/factions/faction.py @@ -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") diff --git a/game/migrator.py b/game/migrator.py index 3b67cd94..c96d62a1 100644 --- a/game/migrator.py +++ b/game/migrator.py @@ -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 diff --git a/game/missiongenerator/aircraft/aircraftgenerator.py b/game/missiongenerator/aircraft/aircraftgenerator.py index 63179163..0370b9bb 100644 --- a/game/missiongenerator/aircraft/aircraftgenerator.py +++ b/game/missiongenerator/aircraft/aircraftgenerator.py @@ -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() diff --git a/game/missiongenerator/aircraft/flightgroupspawner.py b/game/missiongenerator/aircraft/flightgroupspawner.py index fa868138..7bca76b2 100644 --- a/game/missiongenerator/aircraft/flightgroupspawner.py +++ b/game/missiongenerator/aircraft/flightgroupspawner.py @@ -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 ) diff --git a/game/missiongenerator/missiongenerator.py b/game/missiongenerator/missiongenerator.py index 2a7eebd8..ce13a695 100644 --- a/game/missiongenerator/missiongenerator.py +++ b/game/missiongenerator/missiongenerator.py @@ -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, ) diff --git a/game/missiongenerator/tgogenerator.py b/game/missiongenerator/tgogenerator.py index 2ed7547e..f2863459 100644 --- a/game/missiongenerator/tgogenerator.py +++ b/game/missiongenerator/tgogenerator.py @@ -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 ) diff --git a/game/sim/missionresultsprocessor.py b/game/sim/missionresultsprocessor.py index 4fc3f808..79f2d681 100644 --- a/game/sim/missionresultsprocessor.py +++ b/game/sim/missionresultsprocessor.py @@ -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) diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index 7add8807..c5fe8801 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -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: diff --git a/game/theater/start_generator.py b/game/theater/start_generator.py index 8e722d78..9a0b2ef4 100644 --- a/game/theater/start_generator.py +++ b/game/theater/start_generator.py @@ -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 diff --git a/game/theater/theatergroundobject.py b/game/theater/theatergroundobject.py index 70467c34..2f8d3bbf 100644 --- a/game/theater/theatergroundobject.py +++ b/game/theater/theatergroundobject.py @@ -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): diff --git a/game/theater/theatergroup.py b/game/theater/theatergroup.py index 2965f146..206112d3 100644 --- a/game/theater/theatergroup.py +++ b/game/theater/theatergroup.py @@ -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: diff --git a/pydcs_extensions/__init__.py b/pydcs_extensions/__init__.py index ccdd9e01..c5c4071e 100644 --- a/pydcs_extensions/__init__.py +++ b/pydcs_extensions/__init__.py @@ -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 * diff --git a/pydcs_extensions/ea6b/__init__.py b/pydcs_extensions/ea6b/__init__.py new file mode 100644 index 00000000..46638b52 --- /dev/null +++ b/pydcs_extensions/ea6b/__init__.py @@ -0,0 +1 @@ +from .ea6b import * diff --git a/pydcs_extensions/ea6b/ea6b.py b/pydcs_extensions/ea6b/ea6b.py new file mode 100644 index 00000000..9c028ec6 --- /dev/null +++ b/pydcs_extensions/ea6b/ea6b.py @@ -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 + + 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 + + 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 + + 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 + + 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 + + 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 diff --git a/pydcs_extensions/f106/f106.py b/pydcs_extensions/f106/f106.py index e0253b22..c11b1c94 100644 --- a/pydcs_extensions/f106/f106.py +++ b/pydcs_extensions/f106/f106.py @@ -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 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 class Pylon8: - Weapons.L005_Sorbtsiya_ECM_pod__left_ = ( + L005_Sorbtsiya_ECM_pod__left_ = ( 8, Weapons.L005_Sorbtsiya_ECM_pod__left_, ) diff --git a/pydcs_extensions/su15/__init__.py b/pydcs_extensions/su15/__init__.py new file mode 100644 index 00000000..f70df111 --- /dev/null +++ b/pydcs_extensions/su15/__init__.py @@ -0,0 +1 @@ +from .su15 import * diff --git a/pydcs_extensions/su15/su15.py b/pydcs_extensions/su15/su15.py new file mode 100644 index 00000000..aa74087f --- /dev/null +++ b/pydcs_extensions/su15/su15.py @@ -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 diff --git a/qt_ui/main.py b/qt_ui/main.py index fb24e21f..d7063af6 100644 --- a/qt_ui/main.py +++ b/qt_ui/main.py @@ -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, diff --git a/qt_ui/widgets/QConditionsDialog.py b/qt_ui/widgets/QConditionsDialog.py new file mode 100644 index 00000000..959574a1 --- /dev/null +++ b/qt_ui/widgets/QConditionsDialog.py @@ -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() diff --git a/qt_ui/widgets/QConditionsWidget.py b/qt_ui/widgets/QConditionsWidget.py index 37c26d65..69762377 100644 --- a/qt_ui/widgets/QConditionsWidget.py +++ b/qt_ui/widgets/QConditionsWidget.py @@ -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. diff --git a/qt_ui/widgets/conditions/DcsCloudBaseSelector.py b/qt_ui/widgets/conditions/DcsCloudBaseSelector.py new file mode 100644 index 00000000..5510732c --- /dev/null +++ b/qt_ui/widgets/conditions/DcsCloudBaseSelector.py @@ -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) diff --git a/qt_ui/widgets/conditions/DcsCloudDensitySelector.py b/qt_ui/widgets/conditions/DcsCloudDensitySelector.py new file mode 100644 index 00000000..f2c93445 --- /dev/null +++ b/qt_ui/widgets/conditions/DcsCloudDensitySelector.py @@ -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) diff --git a/qt_ui/widgets/conditions/DcsCloudThicknessSelector.py b/qt_ui/widgets/conditions/DcsCloudThicknessSelector.py new file mode 100644 index 00000000..5aa3c40c --- /dev/null +++ b/qt_ui/widgets/conditions/DcsCloudThicknessSelector.py @@ -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) diff --git a/qt_ui/widgets/conditions/DcsPrecipitationSelector.py b/qt_ui/widgets/conditions/DcsPrecipitationSelector.py new file mode 100644 index 00000000..b188dfeb --- /dev/null +++ b/qt_ui/widgets/conditions/DcsPrecipitationSelector.py @@ -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") diff --git a/qt_ui/widgets/conditions/QTimeAdjustmentWidget.py b/qt_ui/widgets/conditions/QTimeAdjustmentWidget.py new file mode 100644 index 00000000..8c116d0e --- /dev/null +++ b/qt_ui/widgets/conditions/QTimeAdjustmentWidget.py @@ -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("

Time & Date:

")) + vbox.addWidget( + QLabel( + '

WARNING: CHANGING TIME/DATE WILL RE-INITIALIZE THE TURN

' + ) + ) + + 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) diff --git a/qt_ui/widgets/conditions/QTimeTurnWidget.py b/qt_ui/widgets/conditions/QTimeTurnWidget.py new file mode 100644 index 00000000..b1149818 --- /dev/null +++ b/qt_ui/widgets/conditions/QTimeTurnWidget.py @@ -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")) diff --git a/qt_ui/widgets/conditions/QWeatherAdjustmentWidget.py b/qt_ui/widgets/conditions/QWeatherAdjustmentWidget.py new file mode 100644 index 00000000..d5ce9498 --- /dev/null +++ b/qt_ui/widgets/conditions/QWeatherAdjustmentWidget.py @@ -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("

Weather:

") + 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("

Clouds:

") + 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) diff --git a/qt_ui/widgets/conditions/QWeatherWidget.py b/qt_ui/widgets/conditions/QWeatherWidget.py new file mode 100644 index 00000000..dabf13cc --- /dev/null +++ b/qt_ui/widgets/conditions/QWeatherWidget.py @@ -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) diff --git a/qt_ui/windows/basemenu/QBaseMenu2.py b/qt_ui/windows/basemenu/QBaseMenu2.py index ebaaea83..1e2ad458 100644 --- a/qt_ui/windows/basemenu/QBaseMenu2.py +++ b/qt_ui/windows/basemenu/QBaseMenu2.py @@ -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) diff --git a/qt_ui/windows/newgame/QNewGameWizard.py b/qt_ui/windows/newgame/QNewGameWizard.py index 696463bf..0948079c 100644 --- a/qt_ui/windows/newgame/QNewGameWizard.py +++ b/qt_ui/windows/newgame/QNewGameWizard.py @@ -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"), diff --git a/qt_ui/windows/newgame/WizardPages/QGeneratorSettings.py b/qt_ui/windows/newgame/WizardPages/QGeneratorSettings.py index 8184b6bd..cf153da8 100644 --- a/qt_ui/windows/newgame/WizardPages/QGeneratorSettings.py +++ b/qt_ui/windows/newgame/WizardPages/QGeneratorSettings.py @@ -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)) diff --git a/requirements.txt b/requirements.txt index 2f83aba1..5d2df385 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/resources/campaigns/grabthars_hammer.miz b/resources/campaigns/grabthars_hammer.miz index 82e68ccb..708023a3 100644 Binary files a/resources/campaigns/grabthars_hammer.miz and b/resources/campaigns/grabthars_hammer.miz differ diff --git a/resources/campaigns/grabthars_hammer.yaml b/resources/campaigns/grabthars_hammer.yaml index 6118953f..cf58c725 100644 --- a/resources/campaigns/grabthars_hammer.yaml +++ b/resources/campaigns/grabthars_hammer.yaml @@ -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: diff --git a/resources/campaigns/operation_gazelle.miz b/resources/campaigns/operation_gazelle.miz index 1787bb8f..a47f4bac 100644 Binary files a/resources/campaigns/operation_gazelle.miz and b/resources/campaigns/operation_gazelle.miz differ diff --git a/resources/campaigns/operation_gazelle.yaml b/resources/campaigns/operation_gazelle.yaml index 0dcbbc83..3ab539de 100644 --- a/resources/campaigns/operation_gazelle.yaml +++ b/resources/campaigns/operation_gazelle.yaml @@ -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 diff --git a/resources/campaigns/operation_peace_spring.miz b/resources/campaigns/operation_peace_spring.miz index cedd7dff..c4d3555d 100644 Binary files a/resources/campaigns/operation_peace_spring.miz and b/resources/campaigns/operation_peace_spring.miz differ diff --git a/resources/campaigns/operation_peace_spring.yaml b/resources/campaigns/operation_peace_spring.yaml index 34ca8212..3cb90a0b 100644 --- a/resources/campaigns/operation_peace_spring.yaml +++ b/resources/campaigns/operation_peace_spring.yaml @@ -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 diff --git a/resources/customized_payloads/EA_6B.lua b/resources/customized_payloads/EA_6B.lua new file mode 100644 index 00000000..ba87fc1e --- /dev/null +++ b/resources/customized_payloads/EA_6B.lua @@ -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 diff --git a/resources/customized_payloads/Su_15.lua b/resources/customized_payloads/Su_15.lua new file mode 100644 index 00000000..cc7628e6 --- /dev/null +++ b/resources/customized_payloads/Su_15.lua @@ -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 diff --git a/resources/customized_payloads/Su_15TM.lua b/resources/customized_payloads/Su_15TM.lua new file mode 100644 index 00000000..acec4d15 --- /dev/null +++ b/resources/customized_payloads/Su_15TM.lua @@ -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 diff --git a/resources/factions/russia_1965.json b/resources/factions/russia_1965.json index e1f5c8dd..cc308298 100644 --- a/resources/factions/russia_1965.json +++ b/resources/factions/russia_1965.json @@ -11,6 +11,7 @@ "MiG-15bis Fagot", "MiG-19P Farmer-B", "MiG-21bis Fishbed-N", + "Su-15 Flagon-A", "Tu-95MS Bear-H" ], "awacs": [ diff --git a/resources/factions/russia_1975 (Mi-24P).json b/resources/factions/russia_1975 (Mi-24P).json index 52966176..bcc1da4e 100644 --- a/resources/factions/russia_1975 (Mi-24P).json +++ b/resources/factions/russia_1975 (Mi-24P).json @@ -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", diff --git a/resources/factions/russia_1975.json b/resources/factions/russia_1975.json index e38ef47f..7f7ed2cd 100644 --- a/resources/factions/russia_1975.json +++ b/resources/factions/russia_1975.json @@ -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", diff --git a/resources/factions/russia_1980.json b/resources/factions/russia_1980.json index 526b232b..bf81f1ad 100644 --- a/resources/factions/russia_1980.json +++ b/resources/factions/russia_1980.json @@ -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", diff --git a/resources/factions/usa_1975.json b/resources/factions/usa_1975.json index ef601ae8..d60fb74a 100644 --- a/resources/factions/usa_1975.json +++ b/resources/factions/usa_1975.json @@ -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", diff --git a/resources/factions/usa_1990.json b/resources/factions/usa_1990.json index e23ab278..c0cddf0c 100644 --- a/resources/factions/usa_1990.json +++ b/resources/factions/usa_1990.json @@ -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)", diff --git a/resources/factions/usa_2005.json b/resources/factions/usa_2005.json index b678f3ec..cd0a3a19 100644 --- a/resources/factions/usa_2005.json +++ b/resources/factions/usa_2005.json @@ -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", diff --git a/resources/factions/usn_1985.json b/resources/factions/usn_1985.json index 7b53c2c0..e0610384 100644 --- a/resources/factions/usn_1985.json +++ b/resources/factions/usn_1985.json @@ -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)", diff --git a/resources/factions/usn_2005.json b/resources/factions/usn_2005.json index 1f463129..8fca7220 100644 --- a/resources/factions/usn_2005.json +++ b/resources/factions/usn_2005.json @@ -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", diff --git a/resources/factions/usn_2009.json b/resources/factions/usn_2009.json index 9d35841a..004fb9d8 100644 --- a/resources/factions/usn_2009.json +++ b/resources/factions/usn_2009.json @@ -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", diff --git a/resources/plugins/arty/CG_ArtySpotter_V1_2a_MP.lua b/resources/plugins/arty/CG_ArtySpotter_V1_2a_MP.lua new file mode 100644 index 00000000..880af5ec --- /dev/null +++ b/resources/plugins/arty/CG_ArtySpotter_V1_2a_MP.lua @@ -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) + + + + diff --git a/resources/plugins/arty/arty-config.lua b/resources/plugins/arty/arty-config.lua new file mode 100644 index 00000000..4297ea0c --- /dev/null +++ b/resources/plugins/arty/arty-config.lua @@ -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 diff --git a/resources/plugins/arty/plugin.json b/resources/plugins/arty/plugin.json new file mode 100644 index 00000000..8b34ec1f --- /dev/null +++ b/resources/plugins/arty/plugin.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/resources/plugins/plugins.json b/resources/plugins/plugins.json index 31e55c1f..aeafbac8 100644 --- a/resources/plugins/plugins.json +++ b/resources/plugins/plugins.json @@ -1,6 +1,7 @@ [ "base", "ctld", + "arty", "dismounts", "ewrj", "ewrs", diff --git a/resources/squadrons/OH58D/AUS Army.yaml b/resources/squadrons/OH58D/AUS Army.yaml index 81419f07..058019df 100644 --- a/resources/squadrons/OH58D/AUS Army.yaml +++ b/resources/squadrons/OH58D/AUS Army.yaml @@ -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 diff --git a/resources/squadrons/OH58D/DE Army.yaml b/resources/squadrons/OH58D/DE Army.yaml index 8bf42d01..c14386d4 100644 --- a/resources/squadrons/OH58D/DE Army.yaml +++ b/resources/squadrons/OH58D/DE Army.yaml @@ -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 diff --git a/resources/squadrons/OH58D/ES Army.yaml b/resources/squadrons/OH58D/ES Army.yaml index c9d469aa..1f4ad17d 100644 --- a/resources/squadrons/OH58D/ES Army.yaml +++ b/resources/squadrons/OH58D/ES Army.yaml @@ -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 diff --git a/resources/squadrons/OH58D/FR Army.yaml b/resources/squadrons/OH58D/FR Army.yaml index 9f5df872..6242711e 100644 --- a/resources/squadrons/OH58D/FR Army.yaml +++ b/resources/squadrons/OH58D/FR Army.yaml @@ -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 diff --git a/resources/squadrons/OH58D/GR Army.yaml b/resources/squadrons/OH58D/GR Army.yaml index 8dc89f39..9d2151a8 100644 --- a/resources/squadrons/OH58D/GR Army.yaml +++ b/resources/squadrons/OH58D/GR Army.yaml @@ -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 diff --git a/resources/squadrons/OH58D/ISR Army.yaml b/resources/squadrons/OH58D/ISR Army.yaml index 911c1e63..79ac2914 100644 --- a/resources/squadrons/OH58D/ISR Army.yaml +++ b/resources/squadrons/OH58D/ISR Army.yaml @@ -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 diff --git a/resources/squadrons/OH58D/JPN Army.yaml b/resources/squadrons/OH58D/JPN Army.yaml index fff41212..bbefc604 100644 --- a/resources/squadrons/OH58D/JPN Army.yaml +++ b/resources/squadrons/OH58D/JPN Army.yaml @@ -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 diff --git a/resources/squadrons/OH58D/NL Army.yaml b/resources/squadrons/OH58D/NL Army.yaml index 0a342576..9b3a5fcc 100644 --- a/resources/squadrons/OH58D/NL Army.yaml +++ b/resources/squadrons/OH58D/NL Army.yaml @@ -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 diff --git a/resources/squadrons/OH58D/PL Army.yaml b/resources/squadrons/OH58D/PL Army.yaml index d9c713a5..1ae1f319 100644 --- a/resources/squadrons/OH58D/PL Army.yaml +++ b/resources/squadrons/OH58D/PL Army.yaml @@ -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 diff --git a/resources/squadrons/OH58D/RU Army.yaml b/resources/squadrons/OH58D/RU Army.yaml index 6f81909a..ca060c61 100644 --- a/resources/squadrons/OH58D/RU Army.yaml +++ b/resources/squadrons/OH58D/RU Army.yaml @@ -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 diff --git a/resources/squadrons/OH58D/TUN Army.yaml b/resources/squadrons/OH58D/TUN Army.yaml index a39ed6a2..a631c84b 100644 --- a/resources/squadrons/OH58D/TUN Army.yaml +++ b/resources/squadrons/OH58D/TUN Army.yaml @@ -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 diff --git a/resources/squadrons/OH58D/TWN Army.yaml b/resources/squadrons/OH58D/TWN Army.yaml index f02dd1a2..82fde046 100644 --- a/resources/squadrons/OH58D/TWN Army.yaml +++ b/resources/squadrons/OH58D/TWN Army.yaml @@ -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 diff --git a/resources/squadrons/OH58D/UK Army Desert.yaml b/resources/squadrons/OH58D/UK Army Desert.yaml index 2caef78d..163d2651 100644 --- a/resources/squadrons/OH58D/UK Army Desert.yaml +++ b/resources/squadrons/OH58D/UK Army Desert.yaml @@ -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 diff --git a/resources/squadrons/OH58D/UK Army.yaml b/resources/squadrons/OH58D/UK Army.yaml index 3139ee77..948e565a 100644 --- a/resources/squadrons/OH58D/UK Army.yaml +++ b/resources/squadrons/OH58D/UK Army.yaml @@ -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 diff --git a/resources/squadrons/OH58D/US 3-17 B.yaml b/resources/squadrons/OH58D/US 3-17 B.yaml index fdd02416..fd827137 100644 --- a/resources/squadrons/OH58D/US 3-17 B.yaml +++ b/resources/squadrons/OH58D/US 3-17 B.yaml @@ -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 diff --git a/resources/stylesheets/style-dcs.css b/resources/stylesheets/style-dcs.css index 601dd897..065c62d3 100644 --- a/resources/stylesheets/style-dcs.css +++ b/resources/stylesheets/style-dcs.css @@ -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; +} diff --git a/resources/ui/units/aircrafts/banners/EA_6B.jpg b/resources/ui/units/aircrafts/banners/EA_6B.jpg new file mode 100644 index 00000000..bbd5c992 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/EA_6B.jpg differ diff --git a/resources/ui/units/aircrafts/icons/EA_6B_24.jpg b/resources/ui/units/aircrafts/icons/EA_6B_24.jpg new file mode 100644 index 00000000..1b4f7347 Binary files /dev/null and b/resources/ui/units/aircrafts/icons/EA_6B_24.jpg differ diff --git a/resources/ui/units/aircrafts/icons/Su_15TM_24.jpg b/resources/ui/units/aircrafts/icons/Su_15TM_24.jpg new file mode 100644 index 00000000..6e96d9b4 Binary files /dev/null and b/resources/ui/units/aircrafts/icons/Su_15TM_24.jpg differ diff --git a/resources/ui/units/aircrafts/icons/Su_15_24.jpg b/resources/ui/units/aircrafts/icons/Su_15_24.jpg new file mode 100644 index 00000000..c744e798 Binary files /dev/null and b/resources/ui/units/aircrafts/icons/Su_15_24.jpg differ diff --git a/resources/units/aircraft/EA_6B.yaml b/resources/units/aircraft/EA_6B.yaml new file mode 100644 index 00000000..ddf3ad31 --- /dev/null +++ b/resources/units/aircraft/EA_6B.yaml @@ -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 diff --git a/resources/units/aircraft/OH58D.yaml b/resources/units/aircraft/OH58D.yaml index b5502bc5..34f73457 100644 --- a/resources/units/aircraft/OH58D.yaml +++ b/resources/units/aircraft/OH58D.yaml @@ -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, diff --git a/resources/units/aircraft/Su_15.yaml b/resources/units/aircraft/Su_15.yaml new file mode 100644 index 00000000..92b6d0d3 --- /dev/null +++ b/resources/units/aircraft/Su_15.yaml @@ -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 diff --git a/resources/units/aircraft/Su_15TM.yaml b/resources/units/aircraft/Su_15TM.yaml new file mode 100644 index 00000000..966ce444 --- /dev/null +++ b/resources/units/aircraft/Su_15TM.yaml @@ -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 diff --git a/resources/units/aircraft/VSN_A6A.yaml b/resources/units/aircraft/VSN_A6A.yaml index aa3bdf80..dd35468c 100644 --- a/resources/units/aircraft/VSN_A6A.yaml +++ b/resources/units/aircraft/VSN_A6A.yaml @@ -15,7 +15,6 @@ tasks: BAI: 350 CAS: 450 DEAD: 450 - Escort: 90 OCA/Aircraft: 400 OCA/Runway: 400 SEAD: 450 diff --git a/resources/units/aircraft/VSN_F104S.yaml b/resources/units/aircraft/VSN_F104S.yaml index 7095ba88..da708fc0 100644 --- a/resources/units/aircraft/VSN_F104S.yaml +++ b/resources/units/aircraft/VSN_F104S.yaml @@ -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 diff --git a/resources/units/aircraft/VSN_F104S_AG.yaml b/resources/units/aircraft/VSN_F104S_AG.yaml index ff5c2c39..36784fd8 100644 --- a/resources/units/aircraft/VSN_F104S_AG.yaml +++ b/resources/units/aircraft/VSN_F104S_AG.yaml @@ -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 diff --git a/resources/weapons/a2a-missiles/AIM-7E-2.yaml b/resources/weapons/a2a-missiles/AIM-7E-2.yaml index a8c18f6b..94005435 100644 --- a/resources/weapons/a2a-missiles/AIM-7E-2.yaml +++ b/resources/weapons/a2a-missiles/AIM-7E-2.yaml @@ -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}" \ No newline at end of file + - "{AIM-7E}" + - "{AIM-7E-2}" + - "{HB_F4E_AIM-7E-2}" diff --git a/resources/weapons/a2a-missiles/AIM-7E.yaml b/resources/weapons/a2a-missiles/AIM-7E.yaml index e426f1ff..958231b3 100644 --- a/resources/weapons/a2a-missiles/AIM-7E.yaml +++ b/resources/weapons/a2a-missiles/AIM-7E.yaml @@ -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}" \ No newline at end of file + - "{BELLY AIM-7E}" + - "{HB_F4E_AIM-7E}" diff --git a/resources/weapons/a2a-missiles/AIM-7F.yaml b/resources/weapons/a2a-missiles/AIM-7F.yaml index 3714716d..07abb802 100644 --- a/resources/weapons/a2a-missiles/AIM-7F.yaml +++ b/resources/weapons/a2a-missiles/AIM-7F.yaml @@ -5,4 +5,5 @@ clsids: - "{SHOULDER AIM-7F}" - "{BELLY AIM-7F}" - "{AIM-7F}" - - "{LAU-115 - AIM-7F}" \ No newline at end of file + - "{LAU-115 - AIM-7F}" + - "{HB_F4E_AIM-7F}" diff --git a/resources/weapons/a2a-missiles/AIM-7M.yaml b/resources/weapons/a2a-missiles/AIM-7M.yaml index 9128c8ed..76f5ac5e 100644 --- a/resources/weapons/a2a-missiles/AIM-7M.yaml +++ b/resources/weapons/a2a-missiles/AIM-7M.yaml @@ -6,3 +6,4 @@ clsids: - "{BELLY AIM-7M}" - "{8D399DDA-FF81-4F14-904D-099B34FE7918}" - "{LAU-115 - AIM-7M}" + - "{HB_F4E_AIM-7M}" diff --git a/resources/weapons/a2a-missiles/AIM-7P.yaml b/resources/weapons/a2a-missiles/AIM-7P.yaml new file mode 100644 index 00000000..669ca5ac --- /dev/null +++ b/resources/weapons/a2a-missiles/AIM-7P.yaml @@ -0,0 +1,7 @@ +name: AIM-7P +year: 1987 +fallback: AIM-7M +clsids: + - "{SHOULDER AIM-7P}" + - "{BELLY AIM-7P}" + - "{AIM-7P}" diff --git a/resources/weapons/a2a-missiles/AIM-9L.yaml b/resources/weapons/a2a-missiles/AIM-9L.yaml index ad51cf5d..b1ac2bd1 100644 --- a/resources/weapons/a2a-missiles/AIM-9L.yaml +++ b/resources/weapons/a2a-missiles/AIM-9L.yaml @@ -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}" diff --git a/resources/weapons/a2a-missiles/AIM-9M.yaml b/resources/weapons/a2a-missiles/AIM-9M.yaml index 67eb6c3a..f6fe582b 100644 --- a/resources/weapons/a2a-missiles/AIM-9M.yaml +++ b/resources/weapons/a2a-missiles/AIM-9M.yaml @@ -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}" diff --git a/resources/weapons/standoff/AGM-12A.yaml b/resources/weapons/standoff/AGM-12A.yaml new file mode 100644 index 00000000..80bd5dc1 --- /dev/null +++ b/resources/weapons/standoff/AGM-12A.yaml @@ -0,0 +1,6 @@ +name: AGM-12A +year: 1959 +fallback: Mk 84 +clsids: + - "{AGM_12A}" + - "{AGM_12A_SWA}" diff --git a/resources/weapons/standoff/AGM-12B.yaml b/resources/weapons/standoff/AGM-12B.yaml new file mode 100644 index 00000000..908af1b9 --- /dev/null +++ b/resources/weapons/standoff/AGM-12B.yaml @@ -0,0 +1,6 @@ +name: AGM-12B +year: 1960 +fallback: AGM-12A +clsids: + - "{AGM_12B}" + - "{AGM_12B_SWA}" diff --git a/resources/weapons/standoff/AGM-12C.yaml b/resources/weapons/standoff/AGM-12C.yaml new file mode 100644 index 00000000..7e107a67 --- /dev/null +++ b/resources/weapons/standoff/AGM-12C.yaml @@ -0,0 +1,6 @@ +name: AGM-12C +year: 1965 +fallback: AGM-12B +clsids: + - "{AGM_12C}" + - "{AGM_12C_SWA}" diff --git a/resources/weapons/standoff/AGM-45A.yaml b/resources/weapons/standoff/AGM-45A.yaml index d69a5df0..61828864 100644 --- a/resources/weapons/standoff/AGM-45A.yaml +++ b/resources/weapons/standoff/AGM-45A.yaml @@ -3,3 +3,6 @@ year: 1965 type: ARM clsids: - "{AGM_45A}" + - "{LAU118_AGM_45A}" + - "{LAU_34_AGM_45A}" + - "{LAU_34_AGM_45A_SWA}" diff --git a/resources/weapons/standoff/AGM-62-I.yaml b/resources/weapons/standoff/AGM-62-I.yaml new file mode 100644 index 00000000..ddf2dc2d --- /dev/null +++ b/resources/weapons/standoff/AGM-62-I.yaml @@ -0,0 +1,5 @@ +name: AGM-62 Walleye I +year: 1967 +fallback: AGM-12C +clsids: + - "{AGM_62_I}" diff --git a/resources/weapons/standoff/AGM-62.yaml b/resources/weapons/standoff/AGM-62.yaml index d1f72a12..b41c2a16 100644 --- a/resources/weapons/standoff/AGM-62.yaml +++ b/resources/weapons/standoff/AGM-62.yaml @@ -1,5 +1,5 @@ name: AGM-62 Walleye II year: 1972 -fallback: Mk 84 +fallback: AGM-62 Walleye I clsids: - "{C40A1E3A-DD05-40D9-85A4-217729E37FAE}" diff --git a/resources/weapons/standoff/AGM-65A-2X.yaml b/resources/weapons/standoff/AGM-65A-2X.yaml new file mode 100644 index 00000000..9489389a --- /dev/null +++ b/resources/weapons/standoff/AGM-65A-2X.yaml @@ -0,0 +1,5 @@ +name: 2xAGM-65A +year: 1972 +clsids: + - "{HB_F4EAGM-65A_LAU88_2x_Right}" + - "{HB_F4EAGM-65A_LAU88_2x_Left}" diff --git a/resources/weapons/standoff/AGM-65A-3X.yaml b/resources/weapons/standoff/AGM-65A-3X.yaml new file mode 100644 index 00000000..337fefe4 --- /dev/null +++ b/resources/weapons/standoff/AGM-65A-3X.yaml @@ -0,0 +1,5 @@ +name: 3xAGM-65A +year: 1972 +clsids: + - "{HB_F4EAGM-65A_LAU88_3x_Right}" + - "{HB_F4EAGM-65A_LAU88_3x_Left}" diff --git a/resources/weapons/standoff/AGM-65A.yaml b/resources/weapons/standoff/AGM-65A.yaml index 78b0e54e..f5b0f196 100644 --- a/resources/weapons/standoff/AGM-65A.yaml +++ b/resources/weapons/standoff/AGM-65A.yaml @@ -3,3 +3,5 @@ year: 1972 fallback: AGM-62 Walleye II clsids: - "LAU_117_AGM_65A" + - "{HB_F4E_AGM-65A_LAU117}" + - "{HB_F4E_AGM-65A_LAU117_SWA}" diff --git a/resources/weapons/standoff/AGM-65B-2X.yaml b/resources/weapons/standoff/AGM-65B-2X.yaml new file mode 100644 index 00000000..0ce640bc --- /dev/null +++ b/resources/weapons/standoff/AGM-65B-2X.yaml @@ -0,0 +1,5 @@ +name: 2xAGM-65B +year: 1975 +clsids: + - "{HB_F4EAGM-65B_LAU88_2x_Right}" + - "{HB_F4EAGM-65B_LAU88_2x_Left}" diff --git a/resources/weapons/standoff/AGM-65B-3X.yaml b/resources/weapons/standoff/AGM-65B-3X.yaml new file mode 100644 index 00000000..0534361a --- /dev/null +++ b/resources/weapons/standoff/AGM-65B-3X.yaml @@ -0,0 +1,5 @@ +name: 3xAGM-65B +year: 1975 +clsids: + - "{HB_F4EAGM-65B_LAU88_3x_Right}" + - "{HB_F4EAGM-65B_LAU88_3x_Left}" diff --git a/resources/weapons/standoff/AGM-65B.yaml b/resources/weapons/standoff/AGM-65B.yaml index 8a2f5998..205f724c 100644 --- a/resources/weapons/standoff/AGM-65B.yaml +++ b/resources/weapons/standoff/AGM-65B.yaml @@ -3,3 +3,5 @@ year: 1975 fallback: AGM-65A clsids: - "LAU_117_AGM_65B" + - "{HB_F4E_AGM-65B_LAU117}" + - "{HB_F4E_AGM-65B_LAU117_SWA}" diff --git a/resources/weapons/standoff/AGM-65D-2X.yaml b/resources/weapons/standoff/AGM-65D-2X.yaml index 3911443f..9e7a717c 100644 --- a/resources/weapons/standoff/AGM-65D-2X.yaml +++ b/resources/weapons/standoff/AGM-65D-2X.yaml @@ -4,3 +4,5 @@ fallback: AGM-65D clsids: - "{E6A6262A-CA08-4B3D-B030-E1A993B98452}" - "{E6A6262A-CA08-4B3D-B030-E1A993B98453}" + - "{HB_F4EAGM-65D_LAU88_2x_Right}" + - "{HB_F4EAGM-65D_LAU88_2x_Left}" diff --git a/resources/weapons/standoff/AGM-65D-3X.yaml b/resources/weapons/standoff/AGM-65D-3X.yaml index e3b26152..df1c4299 100644 --- a/resources/weapons/standoff/AGM-65D-3X.yaml +++ b/resources/weapons/standoff/AGM-65D-3X.yaml @@ -3,3 +3,5 @@ year: 1986 fallback: 2xAGM-65D clsids: - "{DAC53A2F-79CA-42FF-A77A-F5649B601308}" + - "{HB_F4EAGM-65D_LAU88_3x_Right}" + - "{HB_F4EAGM-65D_LAU88_3x_Left}" diff --git a/resources/weapons/standoff/AGM-65D.yaml b/resources/weapons/standoff/AGM-65D.yaml index 42c11666..f843c7b5 100644 --- a/resources/weapons/standoff/AGM-65D.yaml +++ b/resources/weapons/standoff/AGM-65D.yaml @@ -5,3 +5,5 @@ clsids: - "{444BA8AE-82A7-4345-842E-76154EFCCA47}" - "{444BA8AE-82A7-4345-842E-76154EFCCA46}" - "LAU_88_AGM_65D_ONE" + - "{HB_F4E_AGM-65D_LAU117}" + - "{HB_F4E_AGM-65D_LAU117_SWA}" diff --git a/resources/weapons/standoff/AGM-65G.yaml b/resources/weapons/standoff/AGM-65G.yaml index 2c2d9d2b..ab2ca14b 100644 --- a/resources/weapons/standoff/AGM-65G.yaml +++ b/resources/weapons/standoff/AGM-65G.yaml @@ -3,3 +3,5 @@ year: 1989 fallback: AGM-65F clsids: - "LAU_117_AGM_65G" + - "{LAU_117A_AGM_65G}" + - "{HB_F4E_AGM-65G_LAU117}"