diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..70d466be --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,37 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +Before filing, please search the issue tracker to see if the issue has already been reported. + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Additional information** + +We will usually need more information for debugging. Include as much of the following as you are able: + +- DCS Liberation save file (the `.liberation` file you save from the DCS Liberation window). By default these are located in your DCS saved games directory (`%USERPROFILE%/Saved Games/DCS`). +- The generated mission file (the `.miz` file that you load in DCS to play the turn). By default these are located in your missions directory (`%USERPROFILE%/Saved Games/DCS/Missions`). +- A tacview track file, especially when demonstrating an issue with AI behavior. By default these are locaed in your Tacview tracks directory (`%USERPROFILE%/Documents/Tacview`). + +**Version information (please complete the following information):** + - DCS Liberation [e.g. 2.3.1]: + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..ddf2c8f3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +Before filing, please search the issue tracker to see if this feature has already been requested. + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.gitignore b/.gitignore index e53921ce..9f22b322 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ resources/payloads/*.lua venv logs.txt .DS_Store +.vscode/settings.json dist/** a.py resources/tools/a.miz diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..02bbb60b --- /dev/null +++ b/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. \ No newline at end of file diff --git a/README.md b/README.md index 82e950b4..863fd7d4 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,16 @@ It is an external program that generates full and complex DCS missions and manag Latest release is available here : https://github.com/Khopa/dcs_liberation/releases +To download preview builds of the next version of DCS Liberation, see https://github.com/Khopa/dcs_liberation/wiki/Preview-builds. + +## Bugs and feature requests + +If you need to report a bug or want to suggest a new feature, you can do this on our [bug tracker](https://github.com/Khopa/dcs_liberation/issues). In either case, please use the search bar at the top of the page to see if it has already been reported. Note that you may need to remove the filter for open bugs if it's something we've recently fixed. + +## Roadmap + +Our plans for future releases can be found on our [Projects page](https://github.com/Khopa/dcs_liberation/projects). Each planned release has a Project, and the page for that project has columns for to do, in progress, and done. Items in the Done column are in the [preview build](https://github.com/Khopa/dcs_liberation/wiki/Preview-builds) for that release. Items in the To do column are planned to be added to that release. + ## Resources Tutorials, contributors and developer's guides are available in the project's [Wiki](https://github.com/Khopa/dcs_liberation/wiki/) diff --git a/changelog.md b/changelog.md index 1d45037d..e3776b32 100644 --- a/changelog.md +++ b/changelog.md @@ -1,7 +1,86 @@ +# 2.4.0 + +Saves from 2.3 are not compatible with 2.4. + +## Highlights + +* Improved flight plan generation to avoid loitering in or traveling through threatened areas when practical. +* Improved AI aircraft purchasing behavior. +* Era-restricted weapons (work in progress). +* Tons of UI polish. +* Rebalanced economy to keep opfor competitive over the course of the game. + +## Features/Improvements + +* **[Flight Planner]** Air-to-air and SEAD escorts will no longer be automatically planned for packages that are not in range of threats. +* **[Flight Planner]** Non-custom flight plans will now navigate around threat areas en route to the target area when practical. +* **[Flight Planner]** Flight plans along front lines now ensure that the race track start is closer to the departure airfield than the race track end. +* **[Campaign AI]** Auto-purchase now prefers airfields that are not within range of the enemy. +* **[Campaign AI]** Auto-purchase now prefers the best aircraft for the task, but will attempt to maintain some variety. +* **[Campaign AI]** Opfor now sells off odd aircraft since they're unlikely to be used. +* **[Campaign AI]** Multiple rounds of CAP will be planned (roughly 90 minutes of coverage). Default starting budget has increased to account for the increased need for aircraft. +* **[Mission Generator]** Multiple groups are created for complex SAM sites (SAMs with additional point defense or SHORADS), improving Skynet behavior. +* **[Mission Generator]** Default start type can now be chosen in the settings. This replaces the non-functional "AI Parking Start" option. **Selecting any type other than cold will break OCA/Aircraft missions.** +* **[Cheat Menu]** Added ability to toggle base capture and frontline advance/retreat cheats. +* **[Skynet]** Updated to 2.0.1. +* **[Skynet]** Point defenses are now configured to remain on to protect the site they accompany. +* **[Hercules]** Updated the Hercules Cargo list file. +* **[Balance]** Opfor now gains income using the same rules as the player, significantly increasing their income relative to the player for most campaigns. +* **[Balance]** Units now retreat from captured bases when able. Units with no retreat path will be captured and sold. +* **[Economy]** FOBs generate only $10M per turn (previously $20M like airbases). +* **[Economy]** Carriers and off-map spawns generate no income (previously $20M like airbases). +* **[Economy]** Sales of aircraft and ground vehicles can now be cancelled before the next turn begins. +* **[UI]** Multi-SAM objectives now show threat and detection rings per group. +* **[UI]** New icon for AA sites with no active threat. +* **[UI]** Unit names are now prettier and more accurate, and can now be set per-country for added historical flavour. +* **[UI]** Default loadout is now shown for flights with no custom loadout selected. +* **[UI]** Aircraft for a new flight are now only selectable if they match the task type for that flight. +* **[UI]** WIP - There is now a unit info button for each unit in the recruitment list, that should help newer players learn what each unit does. +* **[UI]** Docs for time-on-target and creating new theaters/factions/loadouts are now linked in the UI at the appropriate places. +* **[UI]** ASAP is now a checkbox rather than a button. Enabling this will disable the TOT selector but changes to the package structure will automatically re-ASAP the package. +* **[UI]** Arrival airfield is now shown in the flight list if it differs from the departure airfield. +* **[UI]** Start type can now be selected when creating a flight. +* **[UI]** Arrival and divert airfields can be edited after the flight is created. +* **[Factions]** Added option for date-based loadout restriction. Active radar homing missiles are handled, patches welcome for the other thousand weapons. +* **[Factions]** Added Poland 2010 faction. +* **[Factions]** Added Greece 2005 faction. +* **[Factions]** Added Iran 1988 faction. +* **[Units]** Support for E-2 Hawkeye, SH-60B Seahawk, S-3B Viking (thanks to awinterquest) and SpGH Dana - these are now being used by appropriate factions. +* **[Culling]** Missile sites are no longer culled. +* **[Campaigns]** Added campaign "Black Sea Lite" by Starfire +* **[Campaigns]** Added campaign "Exercise Vegas Nerve" by Starfire +* **[New game Wizard]** The theater page is now the first page of the campaign wizard, recommended factions will be selected automatically on the faction selection page +* **[New game Wizard]** Added information text about the selected campaign performance. +* **[Mod Support]** Added support for High Digit SAMs mod 1.4.0 +* **[Mod Support]** Added SAMs sites generator : KS19Generator, SA10BGenerator, SA12Generator, SA17Generator, SA20Generator, SA20BGenerator, SA23Generator + +## Fixes + +* **[Hercules]** Updated the default Hercules radio frequency. +* **[Economy]** Pending unit orders at captured bases will be refunded. +* **[UI]** Carrier group SAM threat rings now move with the carrier. +* **[UI]** Base intel menu no longer compresses text, and is now scrollable. +* **[UI]** Edit Flight window is now dynamically sized to adapt to the width of waypoint names, so they no longer get truncated. +* **[UI]** Budget income display is now rounded to 2 decimal places. +* **[UI]** Fixed incorrect income per turn displayed for strike target tooltip. +* **[Factions]** USA with C-130 faction now links to the required mod. +* **[Campaign]** Fixed issue where destroyed buildings would sometimes not count as destroyed and thus respawn. +* **[Campaign]** Fixed issue where destroyed runways were not registered. +* **[Units]** J-11A is no longer spawned with empty loadout. +* **[Units]** F-14B is no longer spawned with empty loadout for fighter sweep tasks. +* **[Units]** Pyotr Velikiy cruiser has been removed for now as it's nearly unkillable. +* **[Units]** Submarines have been removed for now as they aren't wholly functional. +* **[Units]** Fixed "FACTION ERROR : Unable to find OliverHazardPerryGroupGenerator in pydcs" error at startup. +* **[Mission Generator]** Fixed a bug where units set to Aggressive stance sometimes did not move. +* **[Mission Generator]** Flyover points for OCA/Aircraft missions are now generated correctly. +* **[Flight Planner]** Fixed not being able to create custom waypoints for buildings. +* **[Flight Planner]** Strike missions will no longer be automatically planned against SAMs. +* **[Flight Planner]** Strike missions will no longer be automatically planned against FOB structures. + # 2.3.4 ## Fixes: -* **[Mission Generator]** Mission generator would crash when generating fire missions for destroyed SCUD sites - fixed +[Mission Generator] Mission generator would crash when generating fire missions for destroyed SCUD sites - fixed # 2.3.3 diff --git a/game/data/doctrine.py b/game/data/doctrine.py index fce67b1b..31b0a03b 100644 --- a/game/data/doctrine.py +++ b/game/data/doctrine.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from datetime import timedelta -from game.utils import nm_to_meter, feet_to_meter +from game.utils import Distance, feet, nautical_miles @dataclass(frozen=True) @@ -12,31 +12,43 @@ class Doctrine: strike: bool antiship: bool - strike_max_range: int - sead_max_range: int + rendezvous_altitude: Distance + hold_distance: Distance + push_distance: Distance + join_distance: Distance + split_distance: Distance + ingress_egress_distance: Distance + ingress_altitude: Distance + egress_altitude: Distance - rendezvous_altitude: int - hold_distance: int - push_distance: int - join_distance: int - split_distance: int - ingress_egress_distance: int - ingress_altitude: int - egress_altitude: int - - min_patrol_altitude: int - max_patrol_altitude: int - pattern_altitude: int + min_patrol_altitude: Distance + max_patrol_altitude: Distance + pattern_altitude: Distance + #: The duration that CAP flights will remain on-station. cap_duration: timedelta - cap_min_track_length: int - cap_max_track_length: int - cap_min_distance_from_cp: int - cap_max_distance_from_cp: int + + #: The minimum length of the CAP race track. + cap_min_track_length: Distance + + #: The maximum length of the CAP race track. + cap_max_track_length: Distance + + #: The minimum distance between the defended position and the *end* of the + #: CAP race track. + cap_min_distance_from_cp: Distance + + #: The maximum distance between the defended position and the *end* of the + #: CAP race track. + cap_max_distance_from_cp: Distance + + #: The engagement range of CAP flights. Any enemy aircraft within this range + #: of the CAP's current position will be engaged by the CAP. + cap_engagement_range: Distance cas_duration: timedelta - sweep_distance: int + sweep_distance: Distance MODERN_DOCTRINE = Doctrine( @@ -45,26 +57,25 @@ MODERN_DOCTRINE = Doctrine( sead=True, strike=True, antiship=True, - strike_max_range=1500000, - sead_max_range=1500000, - rendezvous_altitude=feet_to_meter(25000), - hold_distance=nm_to_meter(15), - push_distance=nm_to_meter(20), - join_distance=nm_to_meter(20), - split_distance=nm_to_meter(20), - ingress_egress_distance=nm_to_meter(45), - ingress_altitude=feet_to_meter(20000), - egress_altitude=feet_to_meter(20000), - min_patrol_altitude=feet_to_meter(15000), - max_patrol_altitude=feet_to_meter(33000), - pattern_altitude=feet_to_meter(5000), + rendezvous_altitude=feet(25000), + hold_distance=nautical_miles(15), + push_distance=nautical_miles(20), + join_distance=nautical_miles(20), + split_distance=nautical_miles(20), + ingress_egress_distance=nautical_miles(45), + ingress_altitude=feet(20000), + egress_altitude=feet(20000), + min_patrol_altitude=feet(15000), + max_patrol_altitude=feet(33000), + pattern_altitude=feet(5000), cap_duration=timedelta(minutes=30), - cap_min_track_length=nm_to_meter(15), - cap_max_track_length=nm_to_meter(40), - cap_min_distance_from_cp=nm_to_meter(10), - cap_max_distance_from_cp=nm_to_meter(40), + cap_min_track_length=nautical_miles(15), + cap_max_track_length=nautical_miles(40), + cap_min_distance_from_cp=nautical_miles(10), + cap_max_distance_from_cp=nautical_miles(40), + cap_engagement_range=nautical_miles(50), cas_duration=timedelta(minutes=30), - sweep_distance=nm_to_meter(60), + sweep_distance=nautical_miles(60), ) COLDWAR_DOCTRINE = Doctrine( @@ -73,26 +84,25 @@ COLDWAR_DOCTRINE = Doctrine( sead=True, strike=True, antiship=True, - strike_max_range=1500000, - sead_max_range=1500000, - rendezvous_altitude=feet_to_meter(22000), - hold_distance=nm_to_meter(10), - push_distance=nm_to_meter(10), - join_distance=nm_to_meter(10), - split_distance=nm_to_meter(10), - ingress_egress_distance=nm_to_meter(30), - ingress_altitude=feet_to_meter(18000), - egress_altitude=feet_to_meter(18000), - min_patrol_altitude=feet_to_meter(10000), - max_patrol_altitude=feet_to_meter(24000), - pattern_altitude=feet_to_meter(5000), + rendezvous_altitude=feet(22000), + hold_distance=nautical_miles(10), + push_distance=nautical_miles(10), + join_distance=nautical_miles(10), + split_distance=nautical_miles(10), + ingress_egress_distance=nautical_miles(30), + ingress_altitude=feet(18000), + egress_altitude=feet(18000), + min_patrol_altitude=feet(10000), + max_patrol_altitude=feet(24000), + pattern_altitude=feet(5000), cap_duration=timedelta(minutes=30), - cap_min_track_length=nm_to_meter(12), - cap_max_track_length=nm_to_meter(24), - cap_min_distance_from_cp=nm_to_meter(8), - cap_max_distance_from_cp=nm_to_meter(25), + cap_min_track_length=nautical_miles(12), + cap_max_track_length=nautical_miles(24), + cap_min_distance_from_cp=nautical_miles(8), + cap_max_distance_from_cp=nautical_miles(25), + cap_engagement_range=nautical_miles(35), cas_duration=timedelta(minutes=30), - sweep_distance=nm_to_meter(40), + sweep_distance=nautical_miles(40), ) WWII_DOCTRINE = Doctrine( @@ -101,24 +111,23 @@ WWII_DOCTRINE = Doctrine( sead=False, strike=True, antiship=True, - strike_max_range=1500000, - sead_max_range=1500000, - hold_distance=nm_to_meter(5), - push_distance=nm_to_meter(5), - join_distance=nm_to_meter(5), - split_distance=nm_to_meter(5), - rendezvous_altitude=feet_to_meter(10000), - ingress_egress_distance=nm_to_meter(7), - ingress_altitude=feet_to_meter(8000), - egress_altitude=feet_to_meter(8000), - min_patrol_altitude=feet_to_meter(4000), - max_patrol_altitude=feet_to_meter(15000), - pattern_altitude=feet_to_meter(5000), + hold_distance=nautical_miles(5), + push_distance=nautical_miles(5), + join_distance=nautical_miles(5), + split_distance=nautical_miles(5), + rendezvous_altitude=feet(10000), + ingress_egress_distance=nautical_miles(7), + ingress_altitude=feet(8000), + egress_altitude=feet(8000), + min_patrol_altitude=feet(4000), + max_patrol_altitude=feet(15000), + pattern_altitude=feet(5000), cap_duration=timedelta(minutes=30), - cap_min_track_length=nm_to_meter(8), - cap_max_track_length=nm_to_meter(18), - cap_min_distance_from_cp=nm_to_meter(0), - cap_max_distance_from_cp=nm_to_meter(5), + cap_min_track_length=nautical_miles(8), + cap_max_track_length=nautical_miles(18), + cap_min_distance_from_cp=nautical_miles(0), + cap_max_distance_from_cp=nautical_miles(5), + cap_engagement_range=nautical_miles(20), cas_duration=timedelta(minutes=30), - sweep_distance=nm_to_meter(10), + sweep_distance=nautical_miles(10), ) diff --git a/game/data/weapons.py b/game/data/weapons.py new file mode 100644 index 00000000..afe30035 --- /dev/null +++ b/game/data/weapons.py @@ -0,0 +1,326 @@ +from __future__ import annotations + +import datetime +import inspect +import logging +from collections import defaultdict +from dataclasses import dataclass +from typing import Dict, Iterator, Optional, Set, Tuple, Type, Union, cast + +from dcs.unitgroup import FlyingGroup +from dcs.unittype import FlyingType +from dcs.weapons_data import Weapons, weapon_ids + + +PydcsWeapon = Dict[str, Union[int, str]] +PydcsWeaponAssignment = Tuple[int, PydcsWeapon] + + +@dataclass(frozen=True) +class Weapon: + """Wraps a pydcs weapon dict in a hashable type.""" + + cls_id: str + name: str + weight: int + + def available_on(self, date: datetime.date) -> bool: + introduction_year = WEAPON_INTRODUCTION_YEARS.get(self) + if introduction_year is None: + logging.warning( + f"No introduction year for {self}, assuming always available") + return True + return date >= datetime.date(introduction_year, 1, 1) + + @property + def as_pydcs(self) -> PydcsWeapon: + return { + "clsid": self.cls_id, + "name": self.name, + "weight": self.weight, + } + + @property + def fallbacks(self) -> Iterator[Weapon]: + yield self + fallback = WEAPON_FALLBACK_MAP[self] + if fallback is not None: + yield from fallback.fallbacks + + @classmethod + def from_pydcs(cls, weapon_data: PydcsWeapon) -> Weapon: + return cls( + cast(str, weapon_data["clsid"]), + cast(str, weapon_data["name"]), + cast(int, weapon_data["weight"]) + ) + + @classmethod + def from_clsid(cls, clsid: str) -> Optional[Weapon]: + data = weapon_ids.get(clsid) + if data is None: + return None + return cls.from_pydcs(data) + + +@dataclass(frozen=True) +class Pylon: + number: int + allowed: Set[Weapon] + + def can_equip(self, weapon: Weapon) -> bool: + return weapon in self.allowed + + def equip(self, group: FlyingGroup, weapon: Weapon) -> None: + if not self.can_equip(weapon): + raise ValueError(f"Pylon {self.number} cannot equip {weapon.name}") + group.load_pylon(self.make_pydcs_assignment(weapon), self.number) + + def make_pydcs_assignment(self, weapon: Weapon) -> PydcsWeaponAssignment: + return self.number, weapon.as_pydcs + + def available_on(self, date: datetime.date) -> Iterator[Weapon]: + for weapon in self.allowed: + if weapon.available_on(date): + yield weapon + + @classmethod + def for_aircraft(cls, aircraft: Type[FlyingType], number: int) -> Pylon: + # In pydcs these are all arbitrary inner classes of the aircraft type. + # The only way to identify them is by their name. + pylons = [v for v in aircraft.__dict__.values() if + inspect.isclass(v) and v.__name__.startswith("Pylon")] + + # And that Pylon class has members with irrelevant names that have + # values of (pylon number, allowed weapon). + allowed = set() + for pylon in pylons: + for key, value in pylon.__dict__.items(): + if key.startswith("__"): + continue + pylon_number, weapon = value + if pylon_number != number: + continue + allowed.add(Weapon.from_pydcs(weapon)) + + return cls(number, allowed) + + @classmethod + def iter_pylons(cls, aircraft: Type[FlyingType]) -> Iterator[Pylon]: + for pylon in sorted(list(aircraft.pylons)): + yield cls.for_aircraft(aircraft, pylon) + + +_WEAPON_FALLBACKS = [ + # AIM-120C + (Weapons.AIM_120C, Weapons.AIM_120B), + (Weapons.LAU_115___AIM_120C, Weapons.LAU_115___AIM_120B), + (Weapons.LAU_115_2_LAU_127_AIM_120C, Weapons.LAU_115_2_LAU_127_AIM_120B), + + # AIM-120B + (Weapons.AIM_120B, Weapons.AIM_7MH), + (Weapons.LAU_115___AIM_120B, Weapons.LAU_115C_AIM_7MH), + (Weapons.LAU_115_2_LAU_127_AIM_120B, Weapons.LAU_115C_AIM_7MH), + + # AIM-7MH + (Weapons.AIM_7MH, Weapons.AIM_7M), + (Weapons.AIM_7MH_, Weapons.AIM_7M_), + (Weapons.AIM_7MH__, Weapons.AIM_7M__), + (Weapons.LAU_115C_AIM_7MH, Weapons.LAU_115___AIM_7M), + + # AIM-7M + (Weapons.AIM_7M, Weapons.AIM_7F), + (Weapons.AIM_7M_, None), + (Weapons.AIM_7M__, None), + (Weapons.LAU_115___AIM_7M, Weapons.LAU_115C_AIM_7F), + + # AIM-7F + (Weapons.AIM_7F, Weapons.AIM_7E), + (Weapons.AIM_7F_, Weapons.AIM_7E), + (Weapons.AIM_7F__, Weapons.AIM_7E), + (Weapons.LAU_115C_AIM_7F, Weapons.LAU_115C_AIM_7E), + + # AIM-7E + (Weapons.AIM_7E, Weapons.AIM_9X_Sidewinder_IR_AAM), + (Weapons.LAU_115C_AIM_7E, Weapons.LAU_115_LAU_127_AIM_9X), + + # AIM-9X + (Weapons.AIM_9X_Sidewinder_IR_AAM, Weapons.AIM_9P5_Sidewinder_IR_AAM), + (Weapons.LAU_7_AIM_9X_Sidewinder_IR_AAM, Weapons.LAU_7_AIM_9P5_Sidewinder_IR_AAM), + (Weapons.LAU_115_LAU_127_AIM_9X, Weapons.LAU_115_LAU_127_AIM_9M), + (Weapons.LAU_115_2_LAU_127_AIM_9X, Weapons.LAU_115_2_LAU_127_AIM_9M), + (Weapons.LAU_127_AIM_9X, Weapons.LAU_127_AIM_9M), + + # AIM-9P5 + (Weapons.AIM_9P5_Sidewinder_IR_AAM, Weapons.AIM_9P_Sidewinder_IR_AAM), + (Weapons.LAU_7_AIM_9P5_Sidewinder_IR_AAM, Weapons.LAU_7_AIM_9P_Sidewinder_IR_AAM), + + # AIM-9P + (Weapons.AIM_9P_Sidewinder_IR_AAM, Weapons.AIM_9M_Sidewinder_IR_AAM), + (Weapons.LAU_7_AIM_9P_Sidewinder_IR_AAM, Weapons.LAU_7_AIM_9M_Sidewinder_IR_AAM), + + # AIM-9M + (Weapons.AIM_9M_Sidewinder_IR_AAM, Weapons.AIM_9L_Sidewinder_IR_AAM), + (Weapons.LAU_7_AIM_9M_Sidewinder_IR_AAM, Weapons.LAU_7_AIM_9L), + + # AIM-9L + (Weapons.AIM_9L_Sidewinder_IR_AAM, None), + (Weapons.LAU_7_AIM_9L, None), + + # AIM-54C Mk47 + (Weapons.AIM_54C_Mk47, Weapons.AIM_54A_Mk60), + (Weapons.AIM_54C_Mk47_, Weapons.AIM_54A_Mk60_), + (Weapons.AIM_54C_Mk47__, Weapons.AIM_54A_Mk60__), + + # AIM-54A Mk60 + (Weapons.AIM_54A_Mk60, Weapons.AIM_54A_Mk47), + (Weapons.AIM_54A_Mk60_, Weapons.AIM_54A_Mk47_), + (Weapons.AIM_54A_Mk60__, Weapons.AIM_54A_Mk47__), + + # R-27 (AA-10 Alamo) + (Weapons.R_27ER, Weapons.R_27R), + (Weapons.R_27ET, Weapons.R_27T), + + # R-77 (AA-12) + (Weapons.R_77, Weapons.R_27ER), + (Weapons.R_77_, Weapons.R_27ER), + + # R-73 (AA-11) + (Weapons.R_73, Weapons.R_60M), + (Weapons.R_73_, Weapons.R_60M_), + + # GBU-38 (JDAM) + (Weapons.GBU_38, Weapons.GBU_12), + (Weapons.GBU_38_16, Weapons.MK_82_28), # B1-B only + (Weapons._2_GBU_38_, Weapons._2_GBU_12), + (Weapons._2_GBU_38, Weapons._2_GBU_12), + (Weapons._3_GBU_38, Weapons._3_GBU_12), + (Weapons.BRU_55___2_x_GBU_38, Weapons.BRU_33___2_x_GBU_12), + (Weapons.BRU_57___2_x_GBU_38, Weapons.BRU_33___2_x_GBU_12), + + # AGM-154A (JSOW) + (Weapons.AGM_154A, Weapons.GBU_12), + (Weapons.BRU_55___2_x_AGM_154A, Weapons.BRU_33___2_x_GBU_12), + (Weapons.BRU_57___2_x_AGM_154A, Weapons.BRU_33___2_x_GBU_12), + + # AGM-154C (JSOW) + (Weapons.AGM_154C, Weapons.GBU_12), + (Weapons.AGM_154C_4, Weapons.MK_82_28), # B1-B only + (Weapons.BRU_55___2_x_AGM_154C, Weapons.BRU_33___2_x_GBU_12), + + # AGM-84E (SLAM) + (Weapons.AGM_84E, Weapons.LAU_117_AGM_65F), + + # CBU-97 + (Weapons.CBU_97, Weapons.GBU_12), + (Weapons.TER_9A___2_x_CBU_97, Weapons.TER_9A___2_x_GBU_12), + (Weapons.TER_9A___2_x_CBU_97_, Weapons.TER_9A___2_x_GBU_12), + (Weapons.TER_9A___3_x_CBU_97, Weapons.TER_9A___2_x_GBU_12), + +] + +WEAPON_FALLBACK_MAP: Dict[Weapon, Optional[Weapon]] = defaultdict( + lambda: cast(Optional[Weapon], None), + ((Weapon.from_pydcs(a), b if b is None else Weapon.from_pydcs(b)) + for a, b in _WEAPON_FALLBACKS)) + + +WEAPON_INTRODUCTION_YEARS = { + # AIM-120C + Weapon.from_pydcs(Weapons.AIM_120C): 1996, + Weapon.from_pydcs(Weapons.LAU_115_2_LAU_127_AIM_120C): 1996, + Weapon.from_pydcs(Weapons.LAU_115___AIM_120C): 1996, + + # AIM-120B + Weapon.from_pydcs(Weapons.AIM_120B): 1994, + Weapon.from_pydcs(Weapons.LAU_115_2_LAU_127_AIM_120B): 1994, + Weapon.from_pydcs(Weapons.LAU_115___AIM_120B): 1994, + + # AIM-7MH + Weapon.from_pydcs(Weapons.AIM_7MH): 1987, + Weapon.from_pydcs(Weapons.AIM_7MH_): 1987, + Weapon.from_pydcs(Weapons.AIM_7MH__): 1987, + Weapon.from_pydcs(Weapons.LAU_115C_AIM_7MH): 1987, + + # AIM-7M + Weapon.from_pydcs(Weapons.AIM_7M): 1982, + Weapon.from_pydcs(Weapons.AIM_7M_): 1982, + Weapon.from_pydcs(Weapons.AIM_7M__): 1982, + Weapon.from_pydcs(Weapons.LAU_115___AIM_7M): 1982, + + # AIM-7F + Weapon.from_pydcs(Weapons.AIM_7F): 1976, + Weapon.from_pydcs(Weapons.AIM_7F_): 1976, + Weapon.from_pydcs(Weapons.AIM_7F__): 1976, + Weapon.from_pydcs(Weapons.LAU_115C_AIM_7F): 1976, + + # AIM-7E + Weapon.from_pydcs(Weapons.AIM_7E): 1963, + Weapon.from_pydcs(Weapons.LAU_115C_AIM_7E): 1963, + + # AIM-9X + Weapon.from_pydcs(Weapons.AIM_9X_Sidewinder_IR_AAM): 2003, + Weapon.from_pydcs(Weapons.LAU_7_AIM_9X_Sidewinder_IR_AAM): 2003, + Weapon.from_pydcs(Weapons.LAU_115_LAU_127_AIM_9X): 2003, + Weapon.from_pydcs(Weapons.LAU_115_2_LAU_127_AIM_9X): 2003, + Weapon.from_pydcs(Weapons.LAU_127_AIM_9X): 2003, + + # AIM-9P5 + Weapon.from_pydcs(Weapons.AIM_9P5_Sidewinder_IR_AAM): 1963, + Weapon.from_pydcs(Weapons.LAU_7_AIM_9P5_Sidewinder_IR_AAM): 1963, + + # AIM-9P + Weapon.from_pydcs(Weapons.AIM_9P_Sidewinder_IR_AAM): 1978, + Weapon.from_pydcs(Weapons.LAU_7_AIM_9P_Sidewinder_IR_AAM): 1978, + + # AIM-9M + Weapon.from_pydcs(Weapons.AIM_9M_Sidewinder_IR_AAM): 1983, + Weapon.from_pydcs(Weapons.LAU_7_AIM_9M_Sidewinder_IR_AAM): 1983, + + # AIM-9L + Weapon.from_pydcs(Weapons.AIM_9L_Sidewinder_IR_AAM): 1977, + Weapon.from_pydcs(Weapons.LAU_7_AIM_9L): 1977, + + # AIM-54C-Mk47 + Weapon.from_pydcs(Weapons.AIM_54C_Mk47): 1986, + Weapon.from_pydcs(Weapons.AIM_54C_Mk47_): 1986, + Weapon.from_pydcs(Weapons.AIM_54C_Mk47__): 1986, + Weapon.from_pydcs(Weapons.AIM_54C): 1986, # this weapon id is unused (legacy F-14A) + + # R-77 (AA-12) + Weapon.from_pydcs(Weapons.R_77): 2002, + Weapon.from_pydcs(Weapons.R_77_): 2002, + + # R-73 (AA-11) + Weapon.from_pydcs(Weapons.R_73): 1989, + Weapon.from_pydcs(Weapons.R_73_): 1989, + + # GBU-38 (JDAM) + Weapon.from_pydcs(Weapons.GBU_38): 1998, + Weapon.from_pydcs(Weapons.GBU_38_16): 1998, # B1-B only + Weapon.from_pydcs(Weapons._2_GBU_38_): 1998, + Weapon.from_pydcs(Weapons._2_GBU_38): 1998, + Weapon.from_pydcs(Weapons._3_GBU_38): 1998, + Weapon.from_pydcs(Weapons.BRU_55___2_x_GBU_38): 1998, + Weapon.from_pydcs(Weapons.BRU_57___2_x_GBU_38): 1998, + + # AGM-154A (JSOW) + Weapon.from_pydcs(Weapons.AGM_154A): 1999, + Weapon.from_pydcs(Weapons.BRU_55___2_x_AGM_154A): 1999, + Weapon.from_pydcs(Weapons.BRU_57___2_x_AGM_154A): 1999, + + # AGM-154C (JSOW) + Weapon.from_pydcs(Weapons.AGM_154C): 2005, + Weapon.from_pydcs(Weapons.AGM_154C_4): 2005, # B1-B only + Weapon.from_pydcs(Weapons.BRU_55___2_x_AGM_154C): 2005, + + # AGM-84E + Weapon.from_pydcs(Weapons.AGM_84E): 1990, + + # CBU-97 + Weapon.from_pydcs(Weapons.CBU_97): 1995, + Weapon.from_pydcs(Weapons.TER_9A___2_x_CBU_97): 1995, + Weapon.from_pydcs(Weapons.TER_9A___2_x_CBU_97_): 1995, + Weapon.from_pydcs(Weapons.TER_9A___3_x_CBU_97): 1995 + +} diff --git a/game/db.py b/game/db.py index 8453dbcf..08260e15 100644 --- a/game/db.py +++ b/game/db.py @@ -1,6 +1,8 @@ from datetime import datetime from enum import Enum from typing import Dict, List, Optional, Tuple, Type, Union +import json +from pathlib import Path from dcs.countries import country_dict from dcs.helicopters import ( @@ -17,6 +19,7 @@ from dcs.helicopters import ( SA342M, SA342Minigun, SA342Mistral, + SH_60B, UH_1H, UH_60A, helicopter_map, @@ -40,12 +43,14 @@ from dcs.planes import ( C_101CC, C_130, E_3A, + E_2C, FA_18C_hornet, FW_190A8, FW_190D9, F_117A, F_14A_135_GR, F_14B, + F_111F, F_15C, F_15E, F_16A, @@ -61,6 +66,7 @@ from dcs.planes import ( Ju_88A4, KC130, KC_135, + KC135MPRS, KJ_2000, L_39C, L_39ZA, @@ -84,6 +90,7 @@ from dcs.planes import ( P_51D_30_NA, PlaneType, RQ_1A_Predator, + S_3B, S_3B_Tanker, SpitfireLFMkIX, SpitfireLFMkIXCW, @@ -130,6 +137,7 @@ from dcs.task import ( CargoTransportation, Embarking, Escort, + FighterSweep, GroundAttack, Intercept, MainTask, @@ -157,6 +165,7 @@ from dcs.vehicles import ( ) import pydcs_extensions.frenchpack.frenchpack as frenchpack +import pydcs_extensions.highdigitsams.highdigitsams as highdigitsams # PATCH pydcs data with MODS from game.factions.faction_loader import FactionLoader from pydcs_extensions.a4ec.a4ec import A_4E_C @@ -166,6 +175,8 @@ from pydcs_extensions.mb339.mb339 import MB_339PAN from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_M, Rafale_B from pydcs_extensions.su57.su57 import Su_57 +UNITINFOTEXT_PATH = Path("./resources/units/unit_info_text.json") + plane_map["A-4E-C"] = A_4E_C plane_map["MB-339PAN"] = MB_339PAN plane_map["Rafale_M"] = Rafale_M @@ -209,6 +220,49 @@ vehicle_map["Toyota_vert"] = frenchpack.DIM__TOYOTA_GREEN vehicle_map["Toyota_desert"] = frenchpack.DIM__TOYOTA_DESERT vehicle_map["Kamikaze"] = frenchpack.DIM__KAMIKAZE +vehicle_map[highdigitsams.AAA_SON_9_Fire_Can.id] = highdigitsams.AAA_SON_9_Fire_Can +vehicle_map[highdigitsams.AAA_100mm_KS_19.id] = highdigitsams.AAA_100mm_KS_19 +vehicle_map[highdigitsams.SAM_SA_10B_S_300PS_54K6_CP.id] = highdigitsams.SAM_SA_10B_S_300PS_54K6_CP +vehicle_map[highdigitsams.SAM_SA_10B_S_300PS_5P85SE_LN.id] = highdigitsams.SAM_SA_10B_S_300PS_5P85SE_LN +vehicle_map[highdigitsams.SAM_SA_10B_S_300PS_5P85SU_LN.id] = highdigitsams.SAM_SA_10B_S_300PS_5P85SU_LN +vehicle_map[highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE.id] = highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE +vehicle_map[highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE.id] = highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE +vehicle_map[highdigitsams.SAM_SA_10B_S_300PS_30N6_TR.id] = highdigitsams.SAM_SA_10B_S_300PS_30N6_TR +vehicle_map[highdigitsams.SAM_SA_10B_S_300PS_40B6M_TR.id] = highdigitsams.SAM_SA_10B_S_300PS_40B6M_TR +vehicle_map[highdigitsams.SAM_SA_10B_S_300PS_40B6MD_SR.id] = highdigitsams.SAM_SA_10B_S_300PS_40B6MD_SR +vehicle_map[highdigitsams.SAM_SA_10B_S_300PS_64H6E_SR.id] = highdigitsams.SAM_SA_10B_S_300PS_64H6E_SR +vehicle_map[highdigitsams.SAM_SA_20_S_300PMU1_CP_54K6.id] = highdigitsams.SAM_SA_20_S_300PMU1_CP_54K6 +vehicle_map[highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E.id] = highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E +vehicle_map[highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E_truck.id] = highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E_truck +vehicle_map[highdigitsams.SAM_SA_20_S_300PMU1_SR_5N66E.id] = highdigitsams.SAM_SA_20_S_300PMU1_SR_5N66E +vehicle_map[highdigitsams.SAM_SA_20_S_300PMU1_SR_64N6E.id] = highdigitsams.SAM_SA_20_S_300PMU1_SR_64N6E +vehicle_map[highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85CE.id] = highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85CE +vehicle_map[highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85DE.id] = highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85DE +vehicle_map[highdigitsams.SAM_SA_20B_S_300PMU2_CP_54K6E2.id] = highdigitsams.SAM_SA_20B_S_300PMU2_CP_54K6E2 +vehicle_map[highdigitsams.SAM_SA_20B_S_300PMU2_TR_92H6E_truck.id] = highdigitsams.SAM_SA_20B_S_300PMU2_TR_92H6E_truck +vehicle_map[highdigitsams.SAM_SA_20B_S_300PMU2_SR_64N6E2.id] = highdigitsams.SAM_SA_20B_S_300PMU2_SR_64N6E2 +vehicle_map[highdigitsams.SAM_SA_20B_S_300PMU2_LN_5P85SE2.id] = highdigitsams.SAM_SA_20B_S_300PMU2_LN_5P85SE2 +vehicle_map[highdigitsams.SAM_SA_12_S_300V_9S457_CP.id] = highdigitsams.SAM_SA_12_S_300V_9S457_CP +vehicle_map[highdigitsams.SAM_SA_12_S_300V_9A82_LN.id] = highdigitsams.SAM_SA_12_S_300V_9A82_LN +vehicle_map[highdigitsams.SAM_SA_12_S_300V_9A83_LN.id] = highdigitsams.SAM_SA_12_S_300V_9A83_LN +vehicle_map[highdigitsams.SAM_SA_12_S_300V_9S15_SR.id] = highdigitsams.SAM_SA_12_S_300V_9S15_SR +vehicle_map[highdigitsams.SAM_SA_12_S_300V_9S19_SR.id] = highdigitsams.SAM_SA_12_S_300V_9S19_SR +vehicle_map[highdigitsams.SAM_SA_12_S_300V_9S32_TR.id] = highdigitsams.SAM_SA_12_S_300V_9S32_TR +vehicle_map[highdigitsams.SAM_SA_23_S_300VM_9S457ME_CP.id] = highdigitsams.SAM_SA_23_S_300VM_9S457ME_CP +vehicle_map[highdigitsams.SAM_SA_23_S_300VM_9S15M2_SR.id] = highdigitsams.SAM_SA_23_S_300VM_9S15M2_SR +vehicle_map[highdigitsams.SAM_SA_23_S_300VM_9S19M2_SR.id] = highdigitsams.SAM_SA_23_S_300VM_9S19M2_SR +vehicle_map[highdigitsams.SAM_SA_23_S_300VM_9S32ME_TR.id] = highdigitsams.SAM_SA_23_S_300VM_9S32ME_TR +vehicle_map[highdigitsams.SAM_SA_23_S_300VM_9A83ME_LN.id] = highdigitsams.SAM_SA_23_S_300VM_9A83ME_LN +vehicle_map[highdigitsams.SAM_SA_23_S_300VM_9A82ME_LN.id] = highdigitsams.SAM_SA_23_S_300VM_9A82ME_LN +vehicle_map[highdigitsams.SAM_SA_17_Buk_M1_2_LN_9A310M1_2.id] = highdigitsams.SAM_SA_17_Buk_M1_2_LN_9A310M1_2 +vehicle_map[highdigitsams.SAM_SA_2__V759__LN_SM_90.id] = highdigitsams.SAM_SA_2__V759__LN_SM_90 +vehicle_map[highdigitsams.SAM_HQ_2_LN_SM_90.id] = highdigitsams.SAM_HQ_2_LN_SM_90 +vehicle_map[highdigitsams.SAM_SA_3__V_601P__LN_5P73.id] = highdigitsams.SAM_SA_3__V_601P__LN_5P73 +vehicle_map[highdigitsams.SAM_SA_24_Igla_S_manpad.id] = highdigitsams.SAM_SA_24_Igla_S_manpad +vehicle_map[highdigitsams.SAM_SA_14_Strela_3_manpad.id] = highdigitsams.SAM_SA_14_Strela_3_manpad +vehicle_map[highdigitsams.Polyana_D4M1_C2_node.id] = highdigitsams.Polyana_D4M1_C2_node +vehicle_map[highdigitsams._34Ya6E_Gazetchik_E_decoy.id] = highdigitsams._34Ya6E_Gazetchik_E_decoy + """ ---------- BEGINNING OF CONFIGURATION SECTION """ @@ -302,6 +356,7 @@ PRICES = { A_10A: 16, A_10C: 22, A_10C_2: 24, + S_3B: 10, # heli Ka_50: 13, @@ -317,6 +372,7 @@ PRICES = { AH_64A: 24, AH_64D: 30, OH_58D: 6, + SH_60B: 6, # Bombers B_52H: 35, @@ -325,6 +381,7 @@ PRICES = { Tu_160: 50, Tu_22M3: 40, Tu_95MS: 35, + F_111F: 21, # special IL_76MD: 30, @@ -335,10 +392,12 @@ PRICES = { IL_78M: 25, KC_135: 25, KC130: 25, + KC135MPRS: 25, A_50: 50, KJ_2000: 50, E_3A: 50, + E_2C: 50, C_130: 25, Hercules: 25, @@ -410,6 +469,7 @@ PRICES = { Artillery.MLRS_9K57_Uragan_BM_27: 50, Artillery.MLRS_9A52_Smerch: 40, Artillery._2B11_mortar: 4, + Artillery.SpGH_Dana: 26, Unarmed.Transport_UAZ_469: 3, Unarmed.Transport_Ural_375: 3, @@ -459,14 +519,14 @@ PRICES = { AirDefence.SAM_SA_19_Tunguska_2S6: 30, AirDefence.SAM_SA_6_Kub_LN_2P25: 20, AirDefence.SAM_SA_3_S_125_LN_5P73: 6, - AirDefence.SAM_SA_10_S_300PS_LN_5P85C: 22, - AirDefence.SAM_SA_10_S_300PS_LN_5P85D: 22, + AirDefence.SAM_SA_11_Buk_LN_9A310M1: 30, + AirDefence.SAM_SA_11_Buk_CC_9S470M1: 25, + AirDefence.SAM_SA_11_Buk_SR_9S18M1: 28, AirDefence.SAM_SA_8_Osa_9A33: 28, AirDefence.SAM_SA_15_Tor_9A331: 40, AirDefence.SAM_SA_13_Strela_10M3_9A35M3: 16, AirDefence.SAM_SA_9_Strela_1_9P31: 12, - AirDefence.SAM_SA_11_Buk_CC_9S470M1: 25, AirDefence.SAM_SA_8_Osa_LD_9T217: 22, AirDefence.SAM_Patriot_AMG_AN_MRC_137: 35, AirDefence.SAM_Patriot_ECS_AN_MSQ_104: 30, @@ -481,7 +541,6 @@ PRICES = { AirDefence.SAM_Patriot_EPP_III: 15, AirDefence.SAM_Patriot_ICC: 18, AirDefence.SAM_Roland_ADS: 12, - AirDefence.SAM_SA_10_S_300PS_CP_54K6: 18, AirDefence.Stinger_MANPADS: 6, AirDefence.SAM_Stinger_comm_dsr: 4, AirDefence.SAM_Stinger_comm: 4, @@ -499,11 +558,8 @@ PRICES = { AirDefence.SAM_SA_18_Igla_S_comm: 8, AirDefence.EWR_1L13: 30, AirDefence.SAM_SA_6_Kub_STR_9S91: 22, - AirDefence.SAM_SA_10_S_300PS_TR_30N6: 24, - AirDefence.SAM_SA_10_S_300PS_SR_5N66M: 30, + AirDefence.EWR_55G6: 30, - AirDefence.SAM_SA_10_S_300PS_SR_64H6E: 30, - AirDefence.SAM_SA_11_Buk_SR_9S18M1: 28, AirDefence.CP_9S80M1_Sborka: 10, AirDefence.SAM_Hawk_TR_AN_MPQ_46: 14, AirDefence.SAM_Hawk_SR_AN_MPQ_50: 18, @@ -564,6 +620,57 @@ PRICES = { frenchpack.DIM__TOYOTA_DESERT: 2, frenchpack.DIM__KAMIKAZE: 6, + # SA-10 + AirDefence.SAM_SA_10_S_300PS_CP_54K6: 18, + AirDefence.SAM_SA_10_S_300PS_TR_30N6: 24, + AirDefence.SAM_SA_10_S_300PS_SR_5N66M: 30, + AirDefence.SAM_SA_10_S_300PS_SR_64H6E: 30, + AirDefence.SAM_SA_10_S_300PS_LN_5P85C: 22, + AirDefence.SAM_SA_10_S_300PS_LN_5P85D: 22, + + # High digit sams mod + highdigitsams.AAA_SON_9_Fire_Can: 8, + highdigitsams.AAA_100mm_KS_19: 10, + + highdigitsams.SAM_SA_10B_S_300PS_54K6_CP: 20, + highdigitsams.SAM_SA_10B_S_300PS_5P85SE_LN: 24, + highdigitsams.SAM_SA_10B_S_300PS_5P85SU_LN: 24, + highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE: 24, + highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE: 24, + highdigitsams.SAM_SA_10B_S_300PS_30N6_TR: 26, + highdigitsams.SAM_SA_10B_S_300PS_40B6M_TR: 26, + highdigitsams.SAM_SA_10B_S_300PS_40B6MD_SR: 32, + highdigitsams.SAM_SA_10B_S_300PS_64H6E_SR: 32, + + highdigitsams.SAM_SA_12_S_300V_9S457_CP: 22, + highdigitsams.SAM_SA_12_S_300V_9A82_LN: 26, + highdigitsams.SAM_SA_12_S_300V_9A83_LN: 26, + highdigitsams.SAM_SA_12_S_300V_9S15_SR: 34, + highdigitsams.SAM_SA_12_S_300V_9S19_SR: 34, + highdigitsams.SAM_SA_12_S_300V_9S32_TR: 28, + + highdigitsams.SAM_SA_20_S_300PMU1_CP_54K6: 26, + highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E: 30, + highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E_truck: 32, + highdigitsams.SAM_SA_20_S_300PMU1_SR_5N66E: 38, + highdigitsams.SAM_SA_20_S_300PMU1_SR_64N6E: 38, + highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85CE: 28, + highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85DE: 28, + + highdigitsams.SAM_SA_20B_S_300PMU2_CP_54K6E2: 27, + highdigitsams.SAM_SA_20B_S_300PMU2_TR_92H6E_truck: 33, + highdigitsams.SAM_SA_20B_S_300PMU2_SR_64N6E2: 40, + highdigitsams.SAM_SA_20B_S_300PMU2_LN_5P85SE2: 30, + + highdigitsams.SAM_SA_23_S_300VM_9S457ME_CP: 30, + highdigitsams.SAM_SA_23_S_300VM_9S15M2_SR: 45, + highdigitsams.SAM_SA_23_S_300VM_9S19M2_SR: 45, + highdigitsams.SAM_SA_23_S_300VM_9S32ME_TR: 35, + highdigitsams.SAM_SA_23_S_300VM_9A83ME_LN: 32, + highdigitsams.SAM_SA_23_S_300VM_9A82ME_LN: 32, + + highdigitsams.SAM_SA_17_Buk_M1_2_LN_9A310M1_2: 40, + } """ @@ -574,6 +681,7 @@ Following tasks are present: * CAS - CAS aircraft * Transport - transport aircraft (used as targets in intercept operations) * AWACS - awacs +* AntishipStrike - units that will engage shipping * PinpointStrike - armor that will engage in ground war * AirDefense - AA units * Reconnaissance - units that will be used as targets in destroy insurgents operations @@ -633,6 +741,7 @@ UNIT_BY_TASK = { A_10C_2, A_20G, B_17G, + F_111F, B_1B, B_52H, F_117A, @@ -655,6 +764,7 @@ UNIT_BY_TASK = { RQ_1A_Predator, Rafale_A_S, Rafale_B, + S_3B, SA342L, SA342M, SA342Minigun, @@ -669,7 +779,8 @@ UNIT_BY_TASK = { Tu_160, Tu_22M3, Tu_95MS, - UH_1H, + UH_1H, + SH_60B, WingLoong_I, Hercules ], @@ -685,8 +796,14 @@ UNIT_BY_TASK = { KC_135, KC130, S_3B_Tanker, + KC135MPRS, + ], + AWACS: [ + E_3A, + E_2C, + A_50, + KJ_2000 ], - AWACS: [E_3A, A_50, KJ_2000], PinpointStrike: [ Armor.APC_MTLB, Armor.APC_MTLB, @@ -839,6 +956,7 @@ UNIT_BY_TASK = { Artillery.MLRS_BM_21_Grad, Artillery.MLRS_9K57_Uragan_BM_27, Artillery.MLRS_9A52_Smerch, + Artillery.SpGH_Dana, Artillery.M12_GMC, Artillery.Sturmpanzer_IV_Brummbär, @@ -998,7 +1116,33 @@ COMMON_OVERRIDE = { AntishipStrike: "ANTISHIP", GroundAttack: "STRIKE", Escort: "CAP", - RunwayAttack: "RUNWAY_ATTACK" + RunwayAttack: "RUNWAY_ATTACK", + FighterSweep: "CAP" +} + +""" +This is a list of mappings from the FlightType of a Flight to the type of payload defined in the +resources/payloads/UNIT_TYPE.lua file. A Flight has no concept of a PyDCS task, so COMMON_OVERRIDE cannot be +used here. This is used in the payload editor, for setting the default loadout of an object. +The left element is the FlightType name, and the right element is a tuple containing what is used in the lua file. +Some aircraft differ from the standard loadout names, so those have been included here too. +The priority goes from first to last - the first element in the tuple will be tried first, then the second, etc. +""" + +EXPANDED_TASK_PAYLOAD_OVERRIDE = { + "TARCAP": ("CAP HEAVY", "CAP"), + "BARCAP": ("CAP HEAVY", "CAP"), + "CAS": ("CAS MAVERICK F", "CAS"), + "INTERCEPTION": ("CAP HEAVY", "CAP"), + "STRIKE": ("STRIKE",), + "ANTISHIP": ("ANTISHIP",), + "SEAD": ("SEAD",), + "DEAD": ("SEAD",), + "ESCORT": ("CAP HEAVY", "CAP"), + "BAI": ( "BAI", "CAS MAVERICK F", "CAS"), + "SWEEP": ("CAP HEAVY", "CAP"), + "OCA_RUNWAY": ("RUNWAY_ATTACK","RUNWAY_STRIKE","STRIKE"), + "OCA_AIRCRAFT": ("OCA","CAS MAVERICK F", "CAS") } PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = { @@ -1016,6 +1160,7 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = { AntishipStrike: "ANTISHIP", GroundAttack: "STRIKE", Escort: "CAP HEAVY", + FighterSweep: "CAP HEAVY", }, F_A_18C: { CAP: "CAP HEAVY", @@ -1026,6 +1171,7 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = { AntishipStrike: "ANTISHIP", GroundAttack: "STRIKE", Escort: "CAP HEAVY", + FighterSweep: "CAP HEAVY", }, Tu_160: { PinpointStrike: "Kh-65*12", @@ -1041,6 +1187,7 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = { F_14A_135_GR: COMMON_OVERRIDE, F_14B: COMMON_OVERRIDE, F_15C: COMMON_OVERRIDE, + F_111F: COMMON_OVERRIDE, F_22A: COMMON_OVERRIDE, F_16C_50: COMMON_OVERRIDE, JF_17: COMMON_OVERRIDE, @@ -1066,6 +1213,7 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = { Tornado_IDS: COMMON_OVERRIDE, Mirage_2000_5: COMMON_OVERRIDE, MiG_31: COMMON_OVERRIDE, + S_3B: COMMON_OVERRIDE, SA342M: COMMON_OVERRIDE, SA342L: COMMON_OVERRIDE, SA342Mistral: COMMON_OVERRIDE, @@ -1103,6 +1251,7 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = { AH_1W: COMMON_OVERRIDE, AH_64D: COMMON_OVERRIDE, AH_64A: COMMON_OVERRIDE, + SH_60B: COMMON_OVERRIDE, Hercules: COMMON_OVERRIDE, Su_25TM: { @@ -1170,9 +1319,6 @@ REWARDS = { "derrick": 8 } -# Base post-turn bonus value -PLAYER_BUDGET_BASE = 20 - CARRIER_CAPABLE = [ FA_18C_hornet, F_14A_135_GR, @@ -1181,6 +1327,7 @@ CARRIER_CAPABLE = [ Su_33, A_4E_C, Rafale_M, + S_3B, UH_1H, Mi_8MT, @@ -1188,6 +1335,7 @@ CARRIER_CAPABLE = [ AH_1W, OH_58D, UH_60A, + SH_60B, SA342L, SA342M, @@ -1204,6 +1352,7 @@ LHA_CAPABLE = [ AH_1W, OH_58D, UH_60A, + SH_60B, SA342L, SA342M, @@ -1308,6 +1457,33 @@ def unit_type_name(unit_type) -> str: def unit_type_name_2(unit_type) -> str: return unit_type.name and unit_type.name or unit_type.id +def unit_get_expanded_info(country_name: str, unit_type, request_type: str) -> str: + original_name = unit_type.name and unit_type.name or unit_type.id + default_value = None + faction_value = None + with UNITINFOTEXT_PATH.open("r", encoding="utf-8") as fdata: + data = json.load(fdata, encoding="utf-8") + type_exists = data.get(original_name) + if type_exists is not None: + for faction in type_exists: + if default_value is None: + default_exists = faction.get("default") + if default_exists is not None: + default_value = default_exists.get(request_type) + if faction_value is None: + faction_exists = faction.get(country_name) + if faction_exists is not None: + faction_value = faction_exists.get(request_type) + if default_value is None: + if request_type == "text": + return "WIP - This unit doesn't have any description text yet." + if request_type == "name": + return original_name + else: + return "Unknown" + if faction_value is None: + return default_value + return faction_value def unit_type_from_name(name: str) -> Optional[Type[UnitType]]: if name in vehicle_map: diff --git a/game/debriefing.py b/game/debriefing.py index 2bf879c6..59477ba4 100644 --- a/game/debriefing.py +++ b/game/debriefing.py @@ -197,6 +197,11 @@ class Debriefing: continue building = self.unit_map.building_or_fortification(unit_name) + # Try appending object to the name, because we do this for building statics. + if building is None: + building = self.unit_map.building_or_fortification( + f"{unit_name} object" + ) if building is not None: if building.ground_object.control_point.captured: losses.player_buildings.append(building) diff --git a/game/event/event.py b/game/event/event.py index 79b68430..ea7702c9 100644 --- a/game/event/event.py +++ b/game/event/event.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging import math -from typing import Dict, List, TYPE_CHECKING, Type +from typing import Dict, Iterator, List, TYPE_CHECKING, Tuple, Type from dcs.mapping import Point from dcs.task import Task @@ -15,16 +15,13 @@ from game.operation.operation import Operation from game.theater import ControlPoint from gen import AirTaskingOrder from gen.ground_forces.combat_stance import CombatStance +from ..db import PRICES from ..unitmap import UnitMap if TYPE_CHECKING: from ..game import Game -DIFFICULTY_LOG_BASE = 1.1 -EVENT_DEPARTURE_MAX_DISTANCE = 340000 - - MINOR_DEFEAT_INFLUENCE = 0.1 DEFEAT_INFLUENCE = 0.3 STRONG_DEFEAT_INFLUENCE = 0.5 @@ -39,7 +36,6 @@ class Event: from_cp = None # type: ControlPoint to_cp = None # type: ControlPoint difficulty = 1 # type: int - BONUS_BASE = 5 def __init__(self, game, from_cp: ControlPoint, target_cp: ControlPoint, location: Point, attacker_name: str, defender_name: str): self.game = game @@ -57,9 +53,6 @@ class Event: def tasks(self) -> List[Type[Task]]: return [] - def bonus(self) -> int: - return int(math.log(self.to_cp.importance + 1, DIFFICULTY_LOG_BASE) * self.BONUS_BASE) - def generate(self) -> UnitMap: Operation.prepare(self.game) unit_map = Operation.generate() @@ -303,9 +296,6 @@ class Event: self.game.turn) self.game.informations.append(info) - def skip(self): - pass - def redeploy_units(self, cp): """" Auto redeploy units to newly captured base @@ -347,36 +337,63 @@ class Event: logging.info(info.text) -class UnitsDeliveryEvent(Event): - - informational = True - - def __init__(self, attacker_name: str, defender_name: str, - from_cp: ControlPoint, to_cp: ControlPoint, - game: Game) -> None: - super(UnitsDeliveryEvent, self).__init__(game=game, - location=to_cp.position, - from_cp=from_cp, - target_cp=to_cp, - attacker_name=attacker_name, - defender_name=defender_name) +class UnitsDeliveryEvent: + def __init__(self, control_point: ControlPoint) -> None: + self.to_cp = control_point self.units: Dict[Type[UnitType], int] = {} def __str__(self) -> str: return "Pending delivery to {}".format(self.to_cp) - def deliver(self, units: Dict[Type[UnitType], int]) -> None: + def order(self, units: Dict[Type[UnitType], int]) -> None: for k, v in units.items(): self.units[k] = self.units.get(k, 0) + v - def skip(self) -> None: - for k, v in self.units.items(): - if self.to_cp.captured: - name = "Ally " - else: - name = "Enemy " - self.game.message( - f"{name} reinforcements: {k.id} x {v} at {self.to_cp.name}") + def sell(self, units: Dict[Type[UnitType], int]) -> None: + for k, v in units.items(): + self.units[k] = self.units.get(k, 0) - v - self.to_cp.base.commision_units(self.units) + def consume_each_order(self) -> Iterator[Tuple[Type[UnitType], int]]: + while self.units: + yield self.units.popitem() + + def refund_all(self, game: Game) -> None: + for unit_type, count in self.consume_each_order(): + try: + price = PRICES[unit_type] + except KeyError: + logging.error(f"Could not refund {unit_type.id}, price unknown") + continue + + logging.info( + f"Refunding {count} {unit_type.id} at {self.to_cp.name}") + game.adjust_budget(price * count, player=self.to_cp.captured) + + def available_next_turn(self, unit_type: Type[UnitType]) -> int: + pending_units = self.units.get(unit_type) + if pending_units is None: + pending_units = 0 + current_units = self.to_cp.base.total_units_of_type(unit_type) + return pending_units + current_units + + def process(self, game: Game) -> None: + bought_units: Dict[Type[UnitType], int] = {} + sold_units: Dict[Type[UnitType], int] = {} + for unit_type, count in self.units.items(): + coalition = "Ally" if self.to_cp.captured else "Enemy" + aircraft = unit_type.id + name = self.to_cp.name + if count >= 0: + bought_units[unit_type] = count + game.message( + f"{coalition} reinforcements: {aircraft} x {count} at {name}") + else: + sold_units[unit_type] = -count + game.message( + f"{coalition} sold: {aircraft} x {-count} at {name}") + self.to_cp.base.commision_units(bought_units) + self.to_cp.base.commit_losses(sold_units) + self.units = {} + bought_units = {} + sold_units = {} diff --git a/game/game.py b/game/game.py index 516095f9..59bca268 100644 --- a/game/game.py +++ b/game/game.py @@ -4,7 +4,7 @@ import random import sys from datetime import date, datetime, timedelta from enum import Enum -from typing import Dict, List +from typing import Any, Dict, List from dcs.action import Coalition from dcs.mapping import Point @@ -28,9 +28,12 @@ from .event.frontlineattack import FrontlineAttackEvent from .factions.faction import Faction from .income import Income from .infos.information import Information +from .navmesh import NavMesh from .procurement import ProcurementAi from .settings import Settings -from .theater import ConflictTheater, ControlPoint +from .theater import ConflictTheater, ControlPoint, TheaterGroundObject +from game.theater.theatergroundobject import MissileSiteGroundObject +from .threatzones import ThreatZones from .unitmap import UnitMap from .weather import Conditions, TimeOfDay @@ -67,16 +70,18 @@ AWACS_BUDGET_COST = 4 # Bonus multiplier logarithm base PLAYER_BUDGET_IMPORTANCE_LOG = 2 + class TurnState(Enum): WIN = 0 LOSS = 1 CONTINUE = 2 + class Game: def __init__(self, player_name: str, enemy_name: str, theater: ConflictTheater, start_date: datetime, - settings: Settings, player_budget: int, - enemy_budget: int) -> None: + settings: Settings, player_budget: float, + enemy_budget: float) -> None: self.settings = settings self.events: List[Event] = [] self.theater = theater @@ -108,9 +113,6 @@ class Game: self.theater.controlpoints ) - for cp in self.theater.controlpoints: - cp.pending_unit_deliveries = self.units_delivery_event(cp) - self.sanitize_sides() self.on_load() @@ -126,6 +128,21 @@ class Game: self.plan_procurement(blue_planner, red_planner) + def __getstate__(self) -> Dict[str, Any]: + state = self.__dict__.copy() + # Avoid persisting any volatile types that can be deterministically + # recomputed on load for the sake of save compatibility. + del state["blue_threat_zone"] + del state["red_threat_zone"] + del state["blue_navmesh"] + del state["red_navmesh"] + return state + + def __setstate__(self, state: Dict[str, Any]) -> None: + self.__dict__.update(state) + # Regenerate any state that was not persisted. + self.on_load() + def generate_conditions(self) -> Conditions: return Conditions.generate(self.theater, self.date, self.current_turn_time_of_day, self.settings) @@ -151,6 +168,11 @@ class Game: def enemy_faction(self) -> Faction: return db.FACTIONS[self.enemy_name] + def faction_for(self, player: bool) -> Faction: + if player: + return self.player_faction + return self.enemy_faction + def _roll(self, prob, mult): if self.settings.version == "dev": # always generate all events for dev @@ -167,6 +189,12 @@ class Game: front_line.control_point_a, front_line.control_point_b) + def adjust_budget(self, amount: float, player: bool) -> None: + if player: + self.budget += amount + else: + self.enemy_budget += amount + def process_player_income(self): self.budget += Income(self, player=True).total @@ -176,15 +204,6 @@ class Game: self.enemy_budget = 0 self.enemy_budget += Income(self, player=False).total - def units_delivery_event(self, to_cp: ControlPoint) -> UnitsDeliveryEvent: - event = UnitsDeliveryEvent(attacker_name=self.player_name, - defender_name=self.player_name, - from_cp=to_cp, - to_cp=to_cp, - game=self) - self.events.append(event) - return event - def initiate_event(self, event: Event) -> UnitMap: #assert event in self.events logging.info("Generating {} (regular)".format(event)) @@ -193,8 +212,6 @@ class Game: def finish_event(self, event: Event, debriefing: Debriefing): logging.info("Finishing event {}".format(event)) event.commit(debriefing) - self.budget += int(event.bonus() * - self.settings.player_income_multiplier) if event in self.events: self.events.remove(event) @@ -211,22 +228,15 @@ class Game: LuaPluginManager.load_settings(self.settings) ObjectiveDistanceCache.set_theater(self.theater) self.compute_conflicts_position() + self.compute_threat_zones() def pass_turn(self, no_action: bool = False) -> None: logging.info("Pass turn") self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0)) self.turn += 1 - for event in self.events: - if self.settings.version == "dev": - # don't damage player CPs in by skipping in dev mode - if isinstance(event, UnitsDeliveryEvent): - event.skip() - else: - event.skip() - for control_point in self.theater.controlpoints: - control_point.process_turn() + control_point.process_turn(self) self.process_enemy_income() @@ -264,7 +274,6 @@ class Game: self.aircraft_inventory.reset() for cp in self.theater.controlpoints: - cp.pending_unit_deliveries = self.units_delivery_event(cp) self.aircraft_inventory.set_from_control_point(cp) # Check for win or loss condition @@ -274,6 +283,7 @@ class Game: # Plan flights & combat for next turn self.compute_conflicts_position() + self.compute_threat_zones() self.ground_planners = {} self.blue_ato.clear() self.red_ato.clear() @@ -294,13 +304,20 @@ class Game: def plan_procurement(self, blue_planner: CoalitionMissionPlanner, red_planner: CoalitionMissionPlanner) -> None: + # The first turn needs to buy a *lot* of aircraft to fill CAPs, so it + # gets much more of the budget that turn. Otherwise budget (after + # repairs) is split evenly between air and ground. For the default + # starting budget of 2000 this gives 600 to ground forces and 1400 to + # aircraft. + ground_portion = 0.3 if self.turn == 0 else 0.5 self.budget = ProcurementAi( self, for_player=True, faction=self.player_faction, manage_runways=self.settings.automate_runway_repair, manage_front_line=self.settings.automate_front_line_reinforcements, - manage_aircraft=self.settings.automate_aircraft_reinforcements + manage_aircraft=self.settings.automate_aircraft_reinforcements, + front_line_budget_share=ground_portion ).spend_budget(self.budget, blue_planner.procurement_requests) self.enemy_budget = ProcurementAi( @@ -309,7 +326,8 @@ class Game: faction=self.enemy_faction, manage_runways=True, manage_front_line=True, - manage_aircraft=True + manage_aircraft=True, + front_line_budget_share=ground_portion ).spend_budget(self.enemy_budget, red_planner.procurement_requests) def message(self, text: str) -> None: @@ -337,6 +355,24 @@ class Game: self.current_group_id += 1 return self.current_group_id + def compute_threat_zones(self) -> None: + self.blue_threat_zone = ThreatZones.for_faction(self, player=True) + self.red_threat_zone = ThreatZones.for_faction(self, player=False) + self.blue_navmesh = NavMesh.from_threat_zones(self.red_threat_zone, + self.theater) + self.red_navmesh = NavMesh.from_threat_zones(self.blue_threat_zone, + self.theater) + + def threat_zone_for(self, player: bool) -> ThreatZones: + if player: + return self.blue_threat_zone + return self.red_threat_zone + + def navmesh_for(self, player: bool) -> NavMesh: + if player: + return self.blue_navmesh + return self.red_navmesh + def compute_conflicts_position(self): """ Compute the current conflict center position(s), mainly used for culling calculation @@ -353,9 +389,14 @@ class Game: points.append(front_line.control_point_a.position) points.append(front_line.control_point_b.position) - # If do_not_cull_carrier is enabled, add carriers as culling point - if self.settings.perf_do_not_cull_carrier: - for cp in self.theater.controlpoints: + for cp in self.theater.controlpoints: + # Don't cull missile sites - their range is long enough to make them + # easily culled despite being a threat. + for tgo in cp.ground_objects: + if isinstance(tgo, MissileSiteGroundObject): + points.append(cp.position) + # If do_not_cull_carrier is enabled, add carriers as culling point + if self.settings.perf_do_not_cull_carrier: if cp.is_carrier or cp.is_lha: points.append(cp.position) diff --git a/game/income.py b/game/income.py index b3fe5ab5..d4e05993 100644 --- a/game/income.py +++ b/game/income.py @@ -3,8 +3,7 @@ from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING -from game.db import PLAYER_BUDGET_BASE, REWARDS -from game.theater import ControlPoint +from game.db import REWARDS if TYPE_CHECKING: from game import Game @@ -22,12 +21,6 @@ class BuildingIncome: return self.number * self.income_per_building -@dataclass(frozen=True) -class ControlPointIncome: - control_point: ControlPoint - income: int - - class Income: def __init__(self, game: Game, player: bool) -> None: if player: @@ -37,12 +30,10 @@ class Income: self.control_points = [] self.buildings = [] - self.income_per_base = PLAYER_BUDGET_BASE if player else 0 - names = set() for cp in game.theater.control_points_for(player): - self.control_points.append( - ControlPointIncome(cp, self.income_per_base)) + if cp.income_per_turn: + self.control_points.append(cp) for tgo in cp.ground_objects: names.add(tgo.obj_name) @@ -58,7 +49,7 @@ class Income: self.buildings.append(BuildingIncome(name, category, count, REWARDS[category])) - self.from_bases = sum(cp.income for cp in self.control_points) + self.from_bases = sum(cp.income_per_turn for cp in self.control_points) self.total_buildings = sum(b.income for b in self.buildings) self.total = ((self.total_buildings + self.from_bases) * self.multiplier) diff --git a/game/navmesh.py b/game/navmesh.py new file mode 100644 index 00000000..c1ac6ab0 --- /dev/null +++ b/game/navmesh.py @@ -0,0 +1,270 @@ +from __future__ import annotations + +import heapq +import math +from collections import defaultdict +from dataclasses import dataclass, field +from typing import Dict, List, Optional, Set, Tuple, Union + +from dcs.mapping import Point +from shapely.geometry import ( + LineString, + MultiPolygon, + Point as ShapelyPoint, + Polygon, + box, +) +from shapely.ops import nearest_points, triangulate + +from game.theater import ConflictTheater +from game.threatzones import ThreatZones +from game.utils import nautical_miles + + +class NavMeshPoly: + def __init__(self, ident: int, poly: Polygon, threatened: bool) -> None: + self.ident = ident + self.poly = poly + self.threatened = threatened + self.neighbors: Dict[NavMeshPoly, Union[LineString, ShapelyPoint]] = {} + + def __eq__(self, other: object) -> bool: + if not isinstance(other, NavMeshPoly): + return False + return self.ident == other.ident + + def __hash__(self) -> int: + return self.ident + + +@dataclass(frozen=True) +class NavPoint: + point: ShapelyPoint + poly: NavMeshPoly + + @property + def world_point(self) -> Point: + return Point(self.point.x, self.point.y) + + def __hash__(self) -> int: + return hash(self.poly.ident) + + def __eq__(self, other: object) -> bool: + if id(self) == id(other): + return True + + if not isinstance(other, NavPoint): + return False + + if not self.point.almost_equals(other.point): + return False + + return self.poly == other.poly + + def __str__(self) -> str: + return f"{self.point} in {self.poly.ident}" + + +@dataclass(frozen=True, order=True) +class FrontierNode: + cost: float + point: NavPoint = field(compare=False) + + +class NavFrontier: + def __init__(self) -> None: + self.nodes: List[FrontierNode] = [] + + def push(self, poly: NavPoint, cost: float) -> None: + heapq.heappush(self.nodes, FrontierNode(cost, poly)) + + def pop(self) -> Optional[NavPoint]: + try: + return heapq.heappop(self.nodes).point + except IndexError: + return None + + +class NavMesh: + def __init__(self, polys: List[NavMeshPoly]) -> None: + self.polys = polys + + def localize(self, point: Point) -> Optional[NavMeshPoly]: + # This is a naive implementation but it's O(n). Runs at about 10k + # lookups a second on a 5950X. Flights usually have 5-10 waypoints, so + # that's 1k-2k flights before we lose a full second to localization as a + # part of flight plan creation. + # + # Can improve the algorithm later if needed, but that seems unnecessary + # currently. + p = ShapelyPoint(point.x, point.y) + for navpoly in self.polys: + if navpoly.poly.contains(p): + return navpoly + return None + + @staticmethod + def travel_cost(a: NavPoint, b: NavPoint) -> float: + modifier = 1.0 + if a.poly.threatened: + modifier = 3.0 + return a.point.distance(b.point) * modifier + + def travel_heuristic(self, a: NavPoint, b: NavPoint) -> float: + return self.travel_cost(a, b) + + @staticmethod + def reconstruct_path(came_from: Dict[NavPoint, Optional[NavPoint]], + origin: NavPoint, + destination: NavPoint) -> List[Point]: + current = destination + path: List[Point] = [] + while current != origin: + path.append(current.world_point) + previous = came_from[current] + if previous is None: + raise RuntimeError( + f"Could not reconstruct path to {destination} from {origin}" + ) + current = previous + path.append(origin.world_point) + path.reverse() + return path + + @staticmethod + def dcs_to_shapely_point(point: Point) -> ShapelyPoint: + return ShapelyPoint(point.x, point.y) + + def shortest_path(self, origin: Point, destination: Point) -> List[Point]: + origin_poly = self.localize(origin) + if origin_poly is None: + raise ValueError(f"Origin point {origin} is outside the navmesh") + destination_poly = self.localize(destination) + if destination_poly is None: + raise ValueError( + f"Origin point {destination} is outside the navmesh") + + return self._shortest_path( + NavPoint(self.dcs_to_shapely_point(origin), origin_poly), + NavPoint(self.dcs_to_shapely_point(destination), destination_poly) + ) + + def _shortest_path(self, origin: NavPoint, + destination: NavPoint) -> List[Point]: + # Adapted from + # https://www.redblobgames.com/pathfinding/a-star/implementation.py. + frontier = NavFrontier() + frontier.push(origin, 0.0) + came_from: Dict[NavPoint, Optional[NavPoint]] = {origin: None} + + best_known: Dict[NavPoint, float] = defaultdict(lambda: math.inf) + best_known[origin] = 0.0 + + while (current := frontier.pop()) is not None: + if current == destination: + break + + if current.poly == destination.poly: + # Made it to the correct nav poly. Add the leg from the border + # to the target. + cost = best_known[current] + self.travel_cost( + current, destination + ) + if cost < best_known[destination]: + best_known[destination] = cost + estimated = cost + frontier.push(destination, estimated) + came_from[destination] = current + + for neighbor, boundary in current.poly.neighbors.items(): + previous = came_from[current] + if previous is not None and previous.poly == neighbor: + # Don't backtrack. + continue + if previous is None and current != origin: + raise RuntimeError + _, neighbor_point = nearest_points(current.point, boundary) + neighbor_nav = NavPoint(neighbor_point, neighbor) + cost = best_known[current] + self.travel_cost( + current, neighbor_nav + ) + if cost < best_known[neighbor_nav]: + best_known[neighbor_nav] = cost + estimated = cost + self.travel_heuristic( + neighbor_nav, destination + ) + frontier.push(neighbor_nav, estimated) + came_from[neighbor_nav] = current + + return self.reconstruct_path(came_from, origin, destination) + + @staticmethod + def map_bounds(theater: ConflictTheater) -> Polygon: + points = [] + for cp in theater.controlpoints: + points.append(ShapelyPoint(cp.position.x, cp.position.y)) + for tgo in cp.ground_objects: + points.append(ShapelyPoint(tgo.position.x, tgo.position.y)) + # Needs to be a large enough boundary beyond the known points so that + # threatened airbases at the map edges have room to retreat from the + # threat without running off the navmesh. + return box(*LineString(points).bounds).buffer( + nautical_miles(100).meters, resolution=1) + + @staticmethod + def create_navpolys(polys: List[Polygon], + threat_zones: ThreatZones) -> List[NavMeshPoly]: + return [NavMeshPoly(i, p, threat_zones.threatened(p)) + for i, p in enumerate(polys)] + + @staticmethod + def associate_neighbors(polys: List[NavMeshPoly]) -> None: + # Maps (rounded) points to polygons that have a vertex at that point. + # The points are rounded to the nearest int so we can use them as dict + # keys. This allows us to perform approximate neighbor lookups more + # efficiently than comparing each poly to every other poly by finding + # approximate neighbors before checking if the polys actually touch. + points_map: Dict[Tuple[int, int], Set[NavMeshPoly]] = defaultdict(set) + + for navpoly in polys: + # The coordinates of the polygon's boundary are a sequence of + # coordinates that define the polygon. The first point is repeated + # at the end, so skip the last vertex. + for x, y in navpoly.poly.boundary.coords[:-1]: + point = (int(x), int(y)) + neighbors = {} + for potential_neighbor in points_map[point]: + intersection = navpoly.poly.intersection( + potential_neighbor.poly) + if not intersection.is_empty: + potential_neighbor.neighbors[navpoly] = intersection + neighbors[potential_neighbor] = intersection + navpoly.neighbors.update(neighbors) + points_map[point].add(navpoly) + + @classmethod + def from_threat_zones(cls, threat_zones: ThreatZones, + theater: ConflictTheater) -> NavMesh: + # Simplify the threat poly to reduce the number of nav zones. Increase + # the size of the zone and then simplify it with the buffer size as the + # error margin. This will create a simpler poly around the threat zone. + buffer = nautical_miles(10).meters + threat_poly = threat_zones.all.buffer(buffer).simplify(buffer) + + # Threat zones can be disconnected. Create a list of threat zones. + if isinstance(threat_poly, MultiPolygon): + polys = list(threat_poly.geoms) + else: + polys = [threat_poly] + + # Subtract the threat zones from the whole-map poly to build a navmesh + # for the *safe* areas. Navigation within threatened regions is always + # a straight line to the target or out of the threatened region. + bounds = cls.map_bounds(theater) + for poly in polys: + bounds = bounds.difference(poly) + + # Triangulate the safe-region to build the navmesh. + navpolys = cls.create_navpolys(triangulate(bounds), threat_zones) + cls.associate_neighbors(navpolys) + return cls(navpolys) diff --git a/game/operation/operation.py b/game/operation/operation.py index ec2a086a..98aea571 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -26,12 +26,12 @@ from gen.environmentgen import EnvironmentGenerator from gen.forcedoptionsgen import ForcedOptionsGenerator from gen.groundobjectsgen import GroundObjectsGenerator from gen.kneeboard import KneeboardGenerator +from gen.naming import namegen from gen.radios import RadioFrequency, RadioRegistry from gen.tacan import TacanRegistry from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator from .. import db -from ..debriefing import Debriefing from ..theater import Airfield from ..unitmap import UnitMap @@ -86,7 +86,7 @@ class Operation: cls.game.enemy_country, frontline.position ) - + @classmethod def air_conflict(cls) -> Conflict: assert cls.game @@ -103,7 +103,7 @@ class Operation: cls.game.enemy_name, cls.game.player_country, cls.game.enemy_country, - mid_point + mid_point ) @classmethod @@ -295,7 +295,7 @@ class Operation: heading=d["orientation"], dead=True, ) - + @classmethod def generate(cls) -> UnitMap: """Build the final Mission to be exported""" @@ -349,7 +349,7 @@ class Operation: cls.jtacs, cls.airgen ) - + cls.reset_naming_ids() return cls.unit_map @classmethod @@ -411,6 +411,10 @@ class Operation: ground_conflict_gen.generate() cls.jtacs.extend(ground_conflict_gen.jtacs) + @classmethod + def reset_naming_ids(cls): + namegen.reset_numbers() + @classmethod def generate_lua(cls, airgen: AircraftConflictGenerator, airsupportgen: AirSupportConflictGenerator, diff --git a/game/procurement.py b/game/procurement.py index 68e0a4f9..0122ab59 100644 --- a/game/procurement.py +++ b/game/procurement.py @@ -5,18 +5,16 @@ import random from dataclasses import dataclass from typing import Iterator, List, Optional, TYPE_CHECKING, Type -from dcs.task import CAP, CAS from dcs.unittype import FlyingType, VehicleType from game import db from game.factions.faction import Faction -from game.theater import ControlPoint, MissionTarget, TYPE_SHORAD -from gen.flights.ai_flight_planner_db import ( - capable_aircraft_for_task, - preferred_aircraft_for_task, -) +from game.theater import ControlPoint, MissionTarget +from game.utils import Distance +from gen.flights.ai_flight_planner_db import aircraft_for_task from gen.flights.closestairfields import ObjectiveDistanceCache from gen.flights.flight import FlightType +from gen.ground_forces.ai_ground_planner_db import TYPE_SHORAD if TYPE_CHECKING: from game import Game @@ -25,36 +23,75 @@ if TYPE_CHECKING: @dataclass(frozen=True) class AircraftProcurementRequest: near: MissionTarget - range: int + range: Distance task_capability: FlightType number: int + def __str__(self) -> str: + task = self.task_capability.value + distance = self.range.nautical_miles + target = self.near.name + return f"{self.number} ship {task} within {distance} nm of {target}" + class ProcurementAi: def __init__(self, game: Game, for_player: bool, faction: Faction, manage_runways: bool, manage_front_line: bool, - manage_aircraft: bool) -> None: + manage_aircraft: bool, front_line_budget_share: float) -> None: + if front_line_budget_share > 1.0: + raise ValueError + self.game = game self.is_player = for_player self.faction = faction self.manage_runways = manage_runways self.manage_front_line = manage_front_line self.manage_aircraft = manage_aircraft + self.front_line_budget_share = front_line_budget_share + self.threat_zones = self.game.threat_zone_for(not self.is_player) def spend_budget( - self, budget: int, - aircraft_requests: List[AircraftProcurementRequest]) -> int: + self, budget: float, + aircraft_requests: List[AircraftProcurementRequest]) -> float: if self.manage_runways: budget = self.repair_runways(budget) if self.manage_front_line: - armor_budget = math.ceil(budget / 2) + armor_budget = math.ceil(budget * self.front_line_budget_share) budget -= armor_budget budget += self.reinforce_front_line(armor_budget) + + # Don't sell overstock aircraft until after we've bought runways and + # front lines. Any budget we free up should be earmarked for aircraft. + if not self.is_player: + budget += self.sell_incomplete_squadrons() if self.manage_aircraft: budget = self.purchase_aircraft(budget, aircraft_requests) return budget - def repair_runways(self, budget: int) -> int: + def sell_incomplete_squadrons(self) -> float: + # Selling incomplete squadrons gives us more money to spend on the next + # turn. This serves as a short term fix for + # https://github.com/Khopa/dcs_liberation/issues/41. + # + # Only incomplete squadrons which are unlikely to get used will be sold + # rather than all unused aircraft because the unused aircraft are what + # make OCA strikes worthwhile. + # + # This option is only used by the AI since players cannot cancel sales + # (https://github.com/Khopa/dcs_liberation/issues/365). + total = 0.0 + for cp in self.game.theater.control_points_for(self.is_player): + inventory = self.game.aircraft_inventory.for_control_point(cp) + for aircraft, available in inventory.all_aircraft: + # We only ever plan even groups, so the odd aircraft is unlikely + # to get used. + if available % 2 == 0: + continue + inventory.remove_aircraft(aircraft, 1) + total += db.PRICES[aircraft] + return total + + def repair_runways(self, budget: float) -> float: for control_point in self.owned_points: if budget < db.RUNWAY_REPAIR_COST: break @@ -74,7 +111,8 @@ class ProcurementAi: return budget def random_affordable_ground_unit( - self, budget: int, cp: ControlPoint) -> Optional[Type[VehicleType]]: + self, budget: float, + cp: ControlPoint) -> Optional[Type[VehicleType]]: affordable_units = [u for u in self.faction.frontline_units + self.faction.artillery_units if db.PRICES[u] <= budget] @@ -91,7 +129,7 @@ class ProcurementAi: return None return random.choice(affordable_units) - def reinforce_front_line(self, budget: int) -> int: + def reinforce_front_line(self, budget: float) -> float: if not self.faction.frontline_units and not self.faction.artillery_units: return budget @@ -107,43 +145,38 @@ class ProcurementAi: break budget -= db.PRICES[unit] - assert cp.pending_unit_deliveries is not None - cp.pending_unit_deliveries.deliver({unit: 1}) + cp.pending_unit_deliveries.order({unit: 1}) return budget def _affordable_aircraft_of_types( self, types: List[Type[FlyingType]], airbase: ControlPoint, - number: int, max_price: int) -> Optional[Type[FlyingType]]: - unit_pool = [u for u in self.faction.aircrafts if u in types] - affordable_units = [ - u for u in unit_pool - if db.PRICES[u] * number <= max_price and airbase.can_operate(u) - ] - if not affordable_units: - return None - return random.choice(affordable_units) + number: int, max_price: float) -> Optional[Type[FlyingType]]: + best_choice: Optional[Type[FlyingType]] = None + for unit in [u for u in self.faction.aircrafts if u in types]: + if db.PRICES[unit] * number > max_price: + continue + if not airbase.can_operate(unit): + continue + + # Affordable and compatible. To keep some variety, skip with a 50/50 + # chance. Might be a good idea to have the chance to skip based on + # the price compared to the rest of the choices. + best_choice = unit + if random.choice([True, False]): + break + return best_choice def affordable_aircraft_for( self, request: AircraftProcurementRequest, - airbase: ControlPoint, budget: int) -> Optional[Type[FlyingType]]: - aircraft = self._affordable_aircraft_of_types( - preferred_aircraft_for_task(request.task_capability), - airbase, request.number, budget) - if aircraft is not None: - return aircraft + airbase: ControlPoint, budget: float) -> Optional[Type[FlyingType]]: return self._affordable_aircraft_of_types( - capable_aircraft_for_task(request.task_capability), + aircraft_for_task(request.task_capability), airbase, request.number, budget) def purchase_aircraft( - self, budget: int, - aircraft_requests: List[AircraftProcurementRequest]) -> int: - unit_pool = [u for u in self.faction.aircrafts - if u in db.UNIT_BY_TASK[CAS] or u in db.UNIT_BY_TASK[CAP]] - if not unit_pool: - return budget - + self, budget: float, + aircraft_requests: List[AircraftProcurementRequest]) -> float: for request in aircraft_requests: for airbase in self.best_airbases_for(request): unit = self.affordable_aircraft_for(request, airbase, budget) @@ -156,8 +189,7 @@ class ProcurementAi: continue budget -= db.PRICES[unit] * request.number - assert airbase.pending_unit_deliveries is not None - airbase.pending_unit_deliveries.deliver({unit: request.number}) + airbase.pending_unit_deliveries.order({unit: request.number}) return budget @@ -174,6 +206,7 @@ class ProcurementAi: distance_cache = ObjectiveDistanceCache.get_closest_airfields( request.near ) + threatened = [] for cp in distance_cache.airfields_within(request.range): if not cp.is_friendly(self.is_player): continue @@ -181,7 +214,10 @@ class ProcurementAi: continue if cp.unclaimed_parking(self.game) < request.number: continue + if self.threat_zones.threatened(cp.position): + threatened.append(cp) yield cp + yield from threatened def front_line_candidates(self) -> List[ControlPoint]: candidates = [] @@ -189,7 +225,7 @@ class ProcurementAi: # Prefer to buy front line units at active front lines that are not # already overloaded. for cp in self.owned_points: - if cp.base.total_armor >= 30: + if cp.expected_ground_units_next_turn.total >= 30: # Control point is already sufficiently defended. continue for connected in cp.connected_points: @@ -197,8 +233,23 @@ class ProcurementAi: candidates.append(cp) if not candidates: - # Otherwise buy them anywhere valid. - candidates = [p for p in self.owned_points - if p.can_deploy_ground_units] + # Otherwise buy reserves, but don't exceed 10 reserve units per CP. + # These units do not exist in the world until the CP becomes + # connected to an active front line, at which point all these units + # will suddenly appear at the gates of the newly captured CP. + # + # To avoid sudden overwhelming numbers of units we avoid buying + # many. + # + # Also, do not bother buying units at bases that will never connect + # to a front line. + for cp in self.owned_points: + if not cp.can_deploy_ground_units: + continue + if cp.expected_ground_units_next_turn.total >= 10: + continue + if cp.is_global: + continue + candidates.append(cp) return candidates diff --git a/game/settings.py b/game/settings.py index 42e23761..d5872e91 100644 --- a/game/settings.py +++ b/game/settings.py @@ -19,15 +19,17 @@ class Settings: supercarrier: bool = False generate_marks: bool = True manpads: bool = True - cold_start: bool = False # Legacy parameter do not use version: Optional[str] = None player_income_multiplier: float = 1.0 enemy_income_multiplier: float = 1.0 + default_start_type: str = "Cold" + # Campaign management automate_runway_repair: bool = False automate_front_line_reinforcements: bool = False automate_aircraft_reinforcements: bool = False + restrict_weapons_by_date: bool = False # Performance oriented perf_red_alert_state: bool = True @@ -35,7 +37,6 @@ class Settings: perf_artillery: bool = True perf_moving_units: bool = True perf_infantry: bool = True - perf_ai_parking_start: bool = True perf_destroyed_units: bool = True # Performance culling @@ -48,6 +49,8 @@ class Settings: # Cheating show_red_ato: bool = False + enable_frontline_cheats: bool = False + enable_base_capture_cheat: bool = False never_delay_player_flights: bool = False diff --git a/game/theater/base.py b/game/theater/base.py index 81512f3e..28fd5666 100644 --- a/game/theater/base.py +++ b/game/theater/base.py @@ -9,6 +9,7 @@ from dcs.unittype import FlyingType, UnitType, VehicleType from dcs.vehicles import AirDefence, Armor from game import db +from game.db import PRICES from gen.ground_forces.ai_ground_planner_db import TYPE_SHORAD STRENGTH_AA_ASSEMBLE_MIN = 0.2 @@ -37,6 +38,16 @@ class Base: def total_armor(self) -> int: return sum(self.armor.values()) + @property + def total_armor_value(self) -> int: + total = 0 + for unit_type, count in self.armor.items(): + try: + total += PRICES[unit_type] * count + except KeyError: + logging.exception(f"No price found for {unit_type.id}") + return total + @property def total_frontline_aa(self) -> int: return sum([v for k, v in self.armor.items() if k in TYPE_SHORAD]) diff --git a/game/theater/conflicttheater.py b/game/theater/conflicttheater.py index aef2dd40..419f216e 100644 --- a/game/theater/conflicttheater.py +++ b/game/theater/conflicttheater.py @@ -55,7 +55,7 @@ from .controlpoint import ( Fob, ) from .landmap import Landmap, load_landmap, poly_contains -from ..utils import nm_to_meter +from ..utils import Distance, meters, nautical_miles Numeric = Union[int, float] @@ -115,7 +115,7 @@ class MizCampaignLoader: AirDefence.SAM_SA_3_S_125_LN_5P73.id, } - BASE_DEFENSE_RADIUS = nm_to_meter(2) + BASE_DEFENSE_RADIUS = nautical_miles(2) def __init__(self, miz: Path, theater: ConflictTheater) -> None: self.theater = theater @@ -317,9 +317,9 @@ class MizCampaignLoader: self.control_points[origin.id]) return front_lines - def objective_info(self, group: Group) -> Tuple[ControlPoint, int]: + def objective_info(self, group: Group) -> Tuple[ControlPoint, Distance]: closest = self.theater.closest_control_point(group.position) - distance = closest.position.distance_to_point(group.position) + distance = meters(closest.position.distance_to_point(group.position)) return closest, distance def add_preset_locations(self) -> None: @@ -447,11 +447,11 @@ class ConflictTheater: if self.is_on_land(point): return False - for exclusion_zone in self.landmap[1]: + for exclusion_zone in self.landmap.exclusion_zones: if poly_contains(point.x, point.y, exclusion_zone): return False - for sea in self.landmap[2]: + for sea in self.landmap.sea_zones: if poly_contains(point.x, point.y, sea): return True @@ -462,14 +462,13 @@ class ConflictTheater: return True is_point_included = False - for inclusion_zone in self.landmap[0]: - if poly_contains(point.x, point.y, inclusion_zone): - is_point_included = True + if poly_contains(point.x, point.y, self.landmap.inclusion_zones): + is_point_included = True if not is_point_included: return False - for exclusion_zone in self.landmap[1]: + for exclusion_zone in self.landmap.exclusion_zones: if poly_contains(point.x, point.y, exclusion_zone): return False @@ -484,7 +483,7 @@ class ConflictTheater: nearest_points = [] if not self.landmap: raise RuntimeError("Landmap not initialized") - for inclusion_zone in self.landmap[0]: + for inclusion_zone in self.landmap.inclusion_zones: nearest_pair = ops.nearest_points(point, inclusion_zone) nearest_points.append(nearest_pair[1]) min_distance = point.distance(nearest_points[0]) # type: geometry.Point @@ -528,6 +527,26 @@ class ConflictTheater: closest = control_point closest_distance = distance return closest + + def closest_target(self, point: Point) -> MissionTarget: + closest: MissionTarget = self.controlpoints[0] + closest_distance = point.distance_to_point(closest.position) + for control_point in self.controlpoints[1:]: + distance = point.distance_to_point(control_point.position) + if distance < closest_distance: + closest = control_point + closest_distance = distance + for tgo in control_point.ground_objects: + distance = point.distance_to_point(tgo.position) + if distance < closest_distance: + closest = tgo + closest_distance = distance + for conflict in self.conflicts(): + distance = conflict.position.distance_to_point(point) + if distance < closest_distance: + closest = conflict + closest_distance = distance + return closest def closest_opposing_control_points(self) -> Tuple[ControlPoint, ControlPoint]: """ diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index a22ea097..1ca122af 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -1,5 +1,6 @@ from __future__ import annotations +import heapq import itertools import logging import random @@ -7,7 +8,8 @@ import re from abc import ABC, abstractmethod from dataclasses import dataclass, field from enum import Enum -from typing import Dict, Iterator, List, Optional, TYPE_CHECKING, Type +from functools import total_ordering +from typing import Any, Dict, Iterator, List, Optional, TYPE_CHECKING, Type from dcs.mapping import Point from dcs.ships import ( @@ -20,24 +22,27 @@ from dcs.terrain.terrain import Airport, ParkingSlot from dcs.unittype import FlyingType from game import db +from gen.flights.closestairfields import ObjectiveDistanceCache from gen.ground_forces.ai_ground_planner_db import TYPE_SHORAD -from gen.runways import RunwayAssigner, RunwayData from gen.ground_forces.combat_stance import CombatStance +from gen.runways import RunwayAssigner, RunwayData from .base import Base from .missiontarget import MissionTarget from .theatergroundobject import ( BaseDefenseGroundObject, EwrGroundObject, + GenericCarrierGroundObject, SamGroundObject, TheaterGroundObject, - VehicleGroupGroundObject, GenericCarrierGroundObject, + VehicleGroupGroundObject, ) +from ..db import PRICES +from ..utils import nautical_miles from ..weather import Conditions if TYPE_CHECKING: from game import Game from gen.flights.flight import FlightType - from ..event import UnitsDeliveryEvent class ControlPointType(Enum): @@ -191,6 +196,28 @@ class RunwayStatus: return f"Runway repairing, {turns_remaining} turns remaining" +@total_ordering +class GroundUnitDestination: + def __init__(self, control_point: ControlPoint) -> None: + self.control_point = control_point + + @property + def total_value(self) -> float: + return self.control_point.base.total_armor_value + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, GroundUnitDestination): + raise TypeError + + return self.total_value == other.total_value + + def __lt__(self, other: Any) -> bool: + if not isinstance(other, GroundUnitDestination): + raise TypeError + + return self.total_value < other.total_value + + class ControlPoint(MissionTarget, ABC): position = None # type: Point @@ -208,7 +235,7 @@ class ControlPoint(MissionTarget, ABC): at: db.StartingPosition, size: int, importance: float, has_frontline=True, cptype=ControlPointType.AIRBASE): - super().__init__(" ".join(re.split(r"[ \-]", name)[:2]), position) + super().__init__(name, position) # TODO: Should be Airbase specific. self.id = cp_id self.full_name = name @@ -229,7 +256,8 @@ class ControlPoint(MissionTarget, ABC): self.cptype = cptype # TODO: Should be Airbase specific. self.stances: Dict[int, CombatStance] = {} - self.pending_unit_deliveries: Optional[UnitsDeliveryEvent] = None + from ..event import UnitsDeliveryEvent + self.pending_unit_deliveries = UnitsDeliveryEvent(self) self.target_position: Optional[Point] = None @@ -364,8 +392,90 @@ class ControlPoint(MissionTarget, ABC): base_defense.position) self.base_defenses = [] + def capture_equipment(self, game: Game) -> None: + total = self.base.total_armor_value + self.base.armor.clear() + game.adjust_budget(total, player=not self.captured) + game.message( + f"{self.name} is not connected to any friendly points. Ground " + f"vehicles have been captured and sold for ${total}M.") + + def retreat_ground_units(self, game: Game): + # When there are multiple valid destinations, deliver units to whichever + # base is least defended first. The closest approximation of unit + # strength we have is price + destinations = [GroundUnitDestination(cp) + for cp in self.connected_points + if cp.captured == self.captured] + if not destinations: + self.capture_equipment(game) + return + + heapq.heapify(destinations) + destination = heapq.heappop(destinations) + while self.base.armor: + unit_type, count = self.base.armor.popitem() + for _ in range(count): + destination.control_point.base.commision_units({unit_type: 1}) + destination = heapq.heappushpop(destinations, destination) + + def capture_aircraft(self, game: Game, airframe: Type[FlyingType], + count: int) -> None: + try: + value = PRICES[airframe] * count + except KeyError: + logging.exception(f"Unknown price for {airframe.id}") + return + + game.adjust_budget(value, player=not self.captured) + game.message( + f"No valid retreat destination in range of {self.name} for " + f"{airframe.id}. {count} aircraft have been captured and sold for " + f"${value}M.") + + def aircraft_retreat_destination( + self, game: Game, + airframe: Type[FlyingType]) -> Optional[ControlPoint]: + closest = ObjectiveDistanceCache.get_closest_airfields(self) + # TODO: Should be airframe dependent. + max_retreat_distance = nautical_miles(200) + # Skip the first airbase because that's the airbase we're retreating + # from. + airfields = list(closest.airfields_within(max_retreat_distance))[1:] + for airbase in airfields: + if not airbase.can_operate(airframe): + continue + if airbase.captured != self.captured: + continue + if airbase.unclaimed_parking(game) > 0: + return airbase + return None + + def _retreat_air_units(self, game: Game, airframe: Type[FlyingType], + count: int) -> None: + while count: + logging.debug(f"Retreating {count} {airframe.id} from {self.name}") + destination = self.aircraft_retreat_destination(game, airframe) + if destination is None: + self.capture_aircraft(game, airframe, count) + return + parking = destination.unclaimed_parking(game) + transfer_amount = min([parking, count]) + destination.base.commision_units({airframe: transfer_amount}) + count -= transfer_amount + + def retreat_air_units(self, game: Game) -> None: + # TODO: Capture in order of price to retain maximum value? + while self.base.aircraft: + airframe, count = self.base.aircraft.popitem() + self._retreat_air_units(game, airframe, count) + # TODO: Should be Airbase specific. def capture(self, game: Game, for_player: bool) -> None: + self.pending_unit_deliveries.refund_all(game) + self.retreat_ground_units(game) + self.retreat_air_units(game) + if for_player: self.captured = True else: @@ -373,9 +483,6 @@ class ControlPoint(MissionTarget, ABC): self.base.set_strength_to_minimum() - self.base.aircraft = {} - self.base.armor = {} - self.clear_base_defenses() from .start_generator import BaseDefenseGenerator BaseDefenseGenerator(game, self).generate() @@ -402,7 +509,6 @@ class ControlPoint(MissionTarget, ABC): return total def expected_aircraft_next_turn(self, game: Game) -> PendingOccupancy: - assert self.pending_unit_deliveries on_order = 0 for unit_bought in self.pending_unit_deliveries.units: if issubclass(unit_bought, FlyingType): @@ -439,7 +545,9 @@ class ControlPoint(MissionTarget, ABC): return self.runway_status.begin_repair() - def process_turn(self) -> None: + def process_turn(self, game: Game) -> None: + self.pending_unit_deliveries.process(game) + runway_status = self.runway_status if runway_status is not None: runway_status.process_turn() @@ -453,6 +561,8 @@ class ControlPoint(MissionTarget, ABC): # Move the linked unit groups for ground_object in self.ground_objects: if isinstance(ground_object, GenericCarrierGroundObject): + ground_object.position.x = ground_object.position.x + delta.x + ground_object.position.y = ground_object.position.y + delta.y for group in ground_object.groups: for u in group.units: u.position.x = u.position.x + delta.x @@ -478,6 +588,24 @@ class ControlPoint(MissionTarget, ABC): else: return 0 + @property + def expected_ground_units_next_turn(self) -> PendingOccupancy: + on_order = 0 + for unit_bought in self.pending_unit_deliveries.units: + if issubclass(unit_bought, FlyingType): + continue + if unit_bought in TYPE_SHORAD: + continue + on_order += self.pending_unit_deliveries.units[unit_bought] + + return PendingOccupancy(self.base.total_armor, on_order, + # Ground unit transfers not yet implemented. + transferring=0) + + @property + def income_per_turn(self) -> int: + return 0 + class Airfield(ControlPoint): @@ -542,6 +670,10 @@ class Airfield(ControlPoint): def can_deploy_ground_units(self) -> bool: return True + @property + def income_per_turn(self) -> int: + return 20 + class NavalControlPoint(ControlPoint, ABC): @@ -741,3 +873,7 @@ class Fob(ControlPoint): @property def can_deploy_ground_units(self) -> bool: return True + + @property + def income_per_turn(self) -> int: + return 10 diff --git a/game/theater/landmap.py b/game/theater/landmap.py index 6e510087..c5b422ad 100644 --- a/game/theater/landmap.py +++ b/game/theater/landmap.py @@ -1,11 +1,30 @@ +from dataclasses import dataclass import pickle -from typing import Collection, Optional, Tuple +from functools import cached_property +from typing import Optional, Tuple, Union import logging from shapely import geometry +from shapely.geometry import MultiPolygon, Polygon -Zone = Collection[Tuple[float, float]] -Landmap = Tuple[Collection[geometry.Polygon], Collection[geometry.Polygon], Collection[geometry.Polygon]] + +@dataclass(frozen=True) +class Landmap: + inclusion_zones: MultiPolygon + exclusion_zones: MultiPolygon + sea_zones: MultiPolygon + + def __post_init__(self): + if not self.inclusion_zones.is_valid: + raise RuntimeError("Inclusion zones not valid") + if not self.exclusion_zones.is_valid: + raise RuntimeError("Exclusion zones not valid") + if not self.sea_zones.is_valid: + raise RuntimeError("Sea zones not valid") + + @cached_property + def inclusion_zone_only(self) -> MultiPolygon: + return self.inclusion_zones - self.exclusion_zones - self.sea_zones def load_landmap(filename: str) -> Optional[Landmap]: @@ -17,7 +36,7 @@ def load_landmap(filename: str) -> Optional[Landmap]: return None -def poly_contains(x, y, poly:geometry.Polygon): +def poly_contains(x, y, poly: Union[MultiPolygon, Polygon]): return poly.contains(geometry.Point(x, y)) diff --git a/game/theater/start_generator.py b/game/theater/start_generator.py index f9ea1d47..118861d1 100644 --- a/game/theater/start_generator.py +++ b/game/theater/start_generator.py @@ -1,7 +1,6 @@ from __future__ import annotations import logging -import math import pickle import random from dataclasses import dataclass @@ -15,7 +14,6 @@ from dcs.vehicles import AirDefence from game import Game, db from game.factions.faction import Faction from game.theater import Carrier, Lha, LocationType -from game.theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW from game.theater.theatergroundobject import ( BuildingGroundObject, CarrierGroundObject, @@ -479,11 +477,11 @@ class BaseDefenseGenerator: g = SamGroundObject(namegen.random_objective_name(), group_id, position, self.control_point, for_airbase=True) - group = generate_anti_air_group(self.game, g, self.faction) - if group is None: + groups = generate_anti_air_group(self.game, g, self.faction) + if not groups: logging.error(f"Could not generate SAM at {self.control_point}") return - g.groups.append(group) + g.groups = groups self.control_point.base_defenses.append(g) def generate_shorad(self) -> None: @@ -497,13 +495,13 @@ class BaseDefenseGenerator: g = SamGroundObject(namegen.random_objective_name(), group_id, position, self.control_point, for_airbase=True) - group = generate_anti_air_group(self.game, g, self.faction, - ranges=[{AirDefenseRange.Short}]) - if group is None: + groups = generate_anti_air_group(self.game, g, self.faction, + ranges=[{AirDefenseRange.Short}]) + if not groups: logging.error( f"Could not generate SHORAD group at {self.control_point}") return - g.groups.append(group) + g.groups = groups self.control_point.base_defenses.append(g) @@ -642,12 +640,12 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): g = SamGroundObject(namegen.random_objective_name(), group_id, position, self.control_point, for_airbase=False) - group = generate_anti_air_group(self.game, g, self.faction, ranges) - if group is None: + groups = generate_anti_air_group(self.game, g, self.faction, ranges) + if not groups: logging.error("Could not generate air defense group for %s at %s", g.name, self.control_point) return - g.groups = [group] + g.groups = groups self.control_point.connected_objectives.append(g) def generate_missile_sites(self) -> None: diff --git a/game/theater/theatergroundobject.py b/game/theater/theatergroundobject.py index 431de495..701f8c9a 100644 --- a/game/theater/theatergroundobject.py +++ b/game/theater/theatergroundobject.py @@ -1,12 +1,17 @@ from __future__ import annotations import itertools +import logging from typing import Iterator, List, TYPE_CHECKING from dcs.mapping import Point from dcs.unit import Unit from dcs.unitgroup import Group +from .. import db +from ..data.radar_db import UNITS_WITH_RADAR +from ..utils import Distance, meters + if TYPE_CHECKING: from .controlpoint import ControlPoint from gen.flights.flight import FlightType @@ -85,7 +90,6 @@ class TheaterGroundObject(MissionTarget): self.dcs_identifier = dcs_identifier self.airbase_group = airbase_group self.sea_object = sea_object - # TODO: There is never more than one group. self.groups: List[Group] = [] @property @@ -147,6 +151,46 @@ class TheaterGroundObject(MissionTarget): def might_have_aa(self) -> bool: return False + @property + def has_radar(self) -> bool: + """Returns True if the ground object contains a unit with radar.""" + for group in self.groups: + for unit in group.units: + if db.unit_type_from_name(unit.type) in UNITS_WITH_RADAR: + return True + return False + + def _max_range_of_type(self, group: Group, range_type: str) -> Distance: + if not self.might_have_aa: + return meters(0) + + max_range = meters(0) + for u in group.units: + unit = db.unit_type_from_name(u.type) + if unit is None: + logging.error(f"Unknown unit type {u.type}") + continue + + # Some units in pydcs have detection_range/threat_range defined, + # but explicitly set to None. + unit_range = getattr(unit, range_type, None) + if unit_range is not None: + max_range = max(max_range, meters(unit_range)) + return max_range + + def detection_range(self, group: Group) -> Distance: + return self._max_range_of_type(group, "detection_range") + + def threat_range(self, group: Group) -> Distance: + if not self.detection_range(group): + # For simple SAMs like shilkas, the unit has both a threat and + # detection range. For complex sites like SA-2s, the launcher has a + # threat range and the search/track radars have detection ranges. If + # the site has no detection range it has no radars and can't fire, + # so it's not actually a threat even if it still has launchers. + return meters(0) + return self._max_range_of_type(group, "threat_range") + class BuildingGroundObject(TheaterGroundObject): def __init__(self, name: str, category: str, group_id: int, object_id: int, diff --git a/game/threatzones.py b/game/threatzones.py new file mode 100644 index 00000000..17f4df96 --- /dev/null +++ b/game/threatzones.py @@ -0,0 +1,158 @@ +from __future__ import annotations + +from functools import singledispatchmethod +from typing import Optional, TYPE_CHECKING, Union + +from dcs.mapping import Point as DcsPoint +from shapely.geometry import ( + LineString, + MultiPolygon, + Point as ShapelyPoint, + Polygon, +) +from shapely.geometry.base import BaseGeometry +from shapely.ops import nearest_points, unary_union + +from game.theater import ControlPoint +from game.utils import Distance, meters, nautical_miles +from gen.flights.closestairfields import ObjectiveDistanceCache +from gen.flights.flight import Flight + +if TYPE_CHECKING: + from game import Game + + +ThreatPoly = Union[MultiPolygon, Polygon] + + +class ThreatZones: + def __init__(self, airbases: ThreatPoly, air_defenses: ThreatPoly) -> None: + self.airbases = airbases + self.air_defenses = air_defenses + self.all = unary_union([airbases, air_defenses]) + + def closest_boundary(self, point: DcsPoint) -> DcsPoint: + boundary, _ = nearest_points(self.all.boundary, + self.dcs_to_shapely_point(point)) + return DcsPoint(boundary.x, boundary.y) + + @singledispatchmethod + def threatened(self, position) -> bool: + raise NotImplementedError + + @threatened.register + def _threatened_geometry(self, position: BaseGeometry) -> bool: + return self.all.intersects(position) + + @threatened.register + def _threatened_dcs_point(self, position: DcsPoint) -> bool: + return self.all.intersects(self.dcs_to_shapely_point(position)) + + def path_threatened(self, a: DcsPoint, b: DcsPoint) -> bool: + return self.threatened(LineString( + [self.dcs_to_shapely_point(a), self.dcs_to_shapely_point(b)])) + + @singledispatchmethod + def threatened_by_aircraft(self, target) -> bool: + raise NotImplementedError + + @threatened_by_aircraft.register + def _threatened_by_aircraft_geom(self, position: BaseGeometry) -> bool: + return self.airbases.intersects(position) + + @threatened_by_aircraft.register + def _threatened_by_aircraft_flight(self, flight: Flight) -> bool: + return self.threatened_by_aircraft(LineString(( + self.dcs_to_shapely_point(p.position) for p in flight.points + ))) + + @singledispatchmethod + def threatened_by_air_defense(self, target) -> bool: + raise NotImplementedError + + @threatened_by_air_defense.register + def _threatened_by_air_defense_geom(self, position: BaseGeometry) -> bool: + return self.air_defenses.intersects(position) + + @threatened_by_air_defense.register + def _threatened_by_air_defense_flight(self, flight: Flight) -> bool: + return self.threatened_by_air_defense(LineString(( + self.dcs_to_shapely_point(p.position) for p in flight.points + ))) + + @classmethod + def closest_enemy_airbase(cls, location: ControlPoint, + max_distance: Distance) -> Optional[ControlPoint]: + airfields = ObjectiveDistanceCache.get_closest_airfields(location) + for airfield in airfields.airfields_within(max_distance): + if airfield.captured != location.captured: + return airfield + return None + + @classmethod + def barcap_threat_range(cls, game: Game, + control_point: ControlPoint) -> Distance: + doctrine = game.faction_for(control_point.captured).doctrine + cap_threat_range = (doctrine.cap_max_distance_from_cp + + doctrine.cap_engagement_range) + opposing_airfield = cls.closest_enemy_airbase(control_point, + cap_threat_range * 2) + if opposing_airfield is None: + return cap_threat_range + + airfield_distance = meters( + opposing_airfield.position.distance_to_point(control_point.position) + ) + + # BARCAPs should not commit further than halfway to the closest enemy + # airfield (with some breathing room) to avoid those missions becoming + # offensive. For dissimilar doctrines we could weight this so that, as + # an example, modern US goes no closer than 70% of the way to the WW2 + # German base, and the Germans go no closer than 30% of the way to the + # US base, but for now equal weighting is fine. + max_distance = airfield_distance * 0.45 + return min(cap_threat_range, max_distance) + + @classmethod + def for_faction(cls, game: Game, player: bool) -> ThreatZones: + """Generates the threat zones projected by the given coalition. + + Args: + game: The game to generate the threat zone for. + player: True if the coalition projecting the threat zone belongs to + the player. + + Returns: + The threat zones projected by the given coalition. If the threat + zone belongs to the player, it is the zone that will be avoided by + the enemy and vice versa. + """ + airbases = [] + air_defenses = [] + for control_point in game.theater.controlpoints: + if control_point.captured != player: + continue + if control_point.runway_is_operational(): + point = ShapelyPoint(control_point.position.x, + control_point.position.y) + cap_threat_range = cls.barcap_threat_range(game, control_point) + airbases.append(point.buffer(cap_threat_range.meters)) + + for tgo in control_point.ground_objects: + for group in tgo.groups: + threat_range = tgo.threat_range(group) + # Any system with a shorter range than this is not worth + # even avoiding. + if threat_range > nautical_miles(3): + point = ShapelyPoint(tgo.position.x, tgo.position.y) + threat_zone = point.buffer(threat_range.meters) + air_defenses.append(threat_zone) + + return cls( + airbases=unary_union(airbases), + air_defenses=unary_union(air_defenses) + ) + + @staticmethod + def dcs_to_shapely_point(point: DcsPoint) -> ShapelyPoint: + return ShapelyPoint(point.x, point.y) \ No newline at end of file diff --git a/game/unitmap.py b/game/unitmap.py index 79cad92f..d8aeaa65 100644 --- a/game/unitmap.py +++ b/game/unitmap.py @@ -112,7 +112,9 @@ class UnitMap: group: Group) -> None: # The actual name is a String (the pydcs translatable string), which # doesn't define __eq__. - name = str(group.name) + # The name of the initiator in the DCS dead event will have " object" + # appended for statics. + name = f"{group.name} object" if name in self.buildings: raise RuntimeError(f"Duplicate TGO unit: {name}") self.buildings[name] = Building(ground_object) diff --git a/game/utils.py b/game/utils.py index b570e355..85fa85da 100644 --- a/game/utils.py +++ b/game/utils.py @@ -1,65 +1,18 @@ -def meter_to_feet(value_in_meter: float) -> int: - """Converts meters to feets +from __future__ import annotations - :arg value_in_meter Value in meters - """ - return int(3.28084 * value_in_meter) +import math +from dataclasses import dataclass +from typing import Union +METERS_TO_FEET = 3.28084 +FEET_TO_METERS = 1 / METERS_TO_FEET +NM_TO_METERS = 1852 +METERS_TO_NM = 1 / NM_TO_METERS -def feet_to_meter(value_in_feet: float) -> int: - """Converts feets to meters - - :arg value_in_feet Value in feets - """ - return int(value_in_feet / 3.28084) - - -def meter_to_nm(value_in_meter: float) -> int: - """Converts meters to nautic miles - - :arg value_in_meter Value in meters - """ - return int(value_in_meter / 1852) - - -def nm_to_meter(value_in_nm: float) -> int: - """Converts nautic miles to meters - - :arg value_in_nm Value in nautic miles - """ - return int(value_in_nm * 1852) - - -def knots_to_kph(value_in_knots: float) -> int: - """Converts Knots to Kilometer Per Hour - - :arg value_in_knots Knots - """ - return int(value_in_knots * 1.852) - - -def mps_to_knots(value_in_mps: float) -> int: - """Converts Meters Per Second To Knots - - :arg value_in_mps Meters Per Second - """ - return int(value_in_mps * 1.943) - - -def mps_to_kph(speed: float) -> int: - """Converts meters per second to kilometers per hour. - - :arg speed Speed in m/s. - """ - return int(speed * 3.6) - - -def kph_to_mps(speed: float) -> int: - """Converts kilometers per hour to meters per second. - - :arg speed Speed in KPH. - """ - return int(speed / 3.6) +KNOTS_TO_KPH = 1.852 +KPH_TO_KNOTS = 1 / KNOTS_TO_KPH +MS_TO_KPH = 3.6 +KPH_TO_MS = 1 / MS_TO_KPH def heading_sum(h, a) -> int: @@ -71,5 +24,157 @@ def heading_sum(h, a) -> int: else: return h + def opposite_heading(h): - return heading_sum(h, 180) \ No newline at end of file + return heading_sum(h, 180) + + +@dataclass(frozen=True, order=True) +class Distance: + distance_in_meters: float + + @property + def feet(self) -> float: + return self.distance_in_meters * METERS_TO_FEET + + @property + def meters(self) -> float: + return self.distance_in_meters + + @property + def nautical_miles(self) -> float: + return self.distance_in_meters * METERS_TO_NM + + @classmethod + def from_feet(cls, value: float) -> Distance: + return cls(value * FEET_TO_METERS) + + @classmethod + def from_meters(cls, value: float) -> Distance: + return cls(value) + + @classmethod + def from_nautical_miles(cls, value: float) -> Distance: + return cls(value * NM_TO_METERS) + + def __add__(self, other: Distance) -> Distance: + return meters(self.meters + other.meters) + + def __sub__(self, other: Distance) -> Distance: + return meters(self.meters - other.meters) + + def __mul__(self, other: Union[float, int]) -> Distance: + return meters(self.meters * other) + + def __truediv__(self, other: Union[float, int]) -> Distance: + return meters(self.meters / other) + + def __floordiv__(self, other: Union[float, int]) -> Distance: + return meters(self.meters // other) + + def __bool__(self) -> bool: + return not math.isclose(self.meters, 0.0) + + +def feet(value: float) -> Distance: + return Distance.from_feet(value) + + +def meters(value: float) -> Distance: + return Distance.from_meters(value) + + +def nautical_miles(value: float) -> Distance: + return Distance.from_nautical_miles(value) + + +@dataclass(frozen=True, order=True) +class Speed: + speed_in_kph: float + + @property + def knots(self) -> float: + return self.speed_in_kph * KPH_TO_KNOTS + + @property + def kph(self) -> float: + return self.speed_in_kph + + @property + def meters_per_second(self) -> float: + return self.speed_in_kph * KPH_TO_MS + + def mach(self, altitude: Distance = meters(0)) -> float: + c_sound = mach(1, altitude) + return self.speed_in_kph / c_sound.kph + + @classmethod + def from_knots(cls, value: float) -> Speed: + return cls(value * KNOTS_TO_KPH) + + @classmethod + def from_kph(cls, value: float) -> Speed: + return cls(value) + + @classmethod + def from_meters_per_second(cls, value: float) -> Speed: + return cls(value * MS_TO_KPH) + + @classmethod + def from_mach(cls, value: float, altitude: Distance) -> Speed: + # https://www.grc.nasa.gov/WWW/K-12/airplane/atmos.html + if altitude <= feet(36152): + temperature_f = 59 - 0.00356 * altitude.feet + else: + # There's another formula for altitudes over 82k feet, but we better + # not be planning waypoints that high... + temperature_f = -70 + + temperature_k = (temperature_f + 459.67) * (5 / 9) + + # https://www.engineeringtoolbox.com/specific-heat-ratio-d_602.html + # Dependent on temperature, but varies very little (+/-0.001) + # between -40F and 180F. + heat_capacity_ratio = 1.4 + + # https://www.grc.nasa.gov/WWW/K-12/airplane/sound.html + gas_constant = 286 # m^2/s^2/K + c_sound = math.sqrt(heat_capacity_ratio * gas_constant * temperature_k) + return mps(c_sound) * value + + def __add__(self, other: Speed) -> Speed: + return kph(self.kph + other.kph) + + def __sub__(self, other: Speed) -> Speed: + return kph(self.kph - other.kph) + + def __mul__(self, other: Union[float, int]) -> Speed: + return kph(self.kph * other) + + def __truediv__(self, other: Union[float, int]) -> Speed: + return kph(self.kph / other) + + def __floordiv__(self, other: Union[float, int]) -> Speed: + return kph(self.kph // other) + + def __bool__(self) -> bool: + return not math.isclose(self.kph, 0.0) + + +def knots(value: float) -> Speed: + return Speed.from_knots(value) + + +def kph(value: float) -> Speed: + return Speed.from_kph(value) + + +def mps(value: float) -> Speed: + return Speed.from_meters_per_second(value) + + +def mach(value: float, altitude: Distance) -> Speed: + return Speed.from_mach(value, altitude) + + +SPEED_OF_SOUND_AT_SEA_LEVEL = knots(661.5) diff --git a/game/version.py b/game/version.py index c09b6e15..bb8c3a2c 100644 --- a/game/version.py +++ b/game/version.py @@ -2,7 +2,7 @@ from pathlib import Path def _build_version_string() -> str: - components = ["2.3.4"] + components = ["2.4"] build_number_path = Path("resources/buildnumber") if build_number_path.exists(): with build_number_path.open("r") as build_number_file: diff --git a/game/weather.py b/game/weather.py index 34b19e2d..ae9a6fd2 100644 --- a/game/weather.py +++ b/game/weather.py @@ -10,6 +10,7 @@ from typing import Optional, TYPE_CHECKING from dcs.weather import Weather as PydcsWeather, Wind from game.settings import Settings +from game.utils import Distance, meters if TYPE_CHECKING: from game.theater import ConflictTheater @@ -39,7 +40,7 @@ class Clouds: @dataclass(frozen=True) class Fog: - visibility: int + visibility: Distance thickness: int @@ -56,7 +57,7 @@ class Weather: if random.randrange(5) != 0: return None return Fog( - visibility=random.randint(2500, 5000), + visibility=meters(random.randint(2500, 5000)), thickness=random.randint(100, 500) ) diff --git a/gen/aircraft.py b/gen/aircraft.py index 42454df4..70ab9d67 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -74,6 +74,7 @@ from dcs.unittype import FlyingType, UnitType from game import db from game.data.cap_capabilities_db import GUNFIGHTERS +from game.data.weapons import Pylon, Weapon from game.factions.faction import Faction from game.settings import Settings from game.theater.controlpoint import ( @@ -85,7 +86,7 @@ from game.theater.controlpoint import ( ) from game.theater.theatergroundobject import TheaterGroundObject from game.unitmap import UnitMap -from game.utils import knots_to_kph, nm_to_meter +from game.utils import Distance, meters, nautical_miles from gen.airsupportgen import AirSupport from gen.ato import AirTaskingOrder, Package from gen.callsigns import create_group_callsign_from_unit @@ -110,12 +111,10 @@ from .naming import namegen if TYPE_CHECKING: from game import Game -WARM_START_HELI_AIRSPEED = 120 -WARM_START_HELI_ALT = 500 -WARM_START_ALTITUDE = 3000 -WARM_START_AIRSPEED = 550 +WARM_START_HELI_ALT = meters(500) +WARM_START_ALTITUDE = meters(3000) -RTB_ALTITUDE = 800 +RTB_ALTITUDE = meters(800) RTB_DISTANCE = 5000 HELI_ALT = 500 @@ -263,6 +262,9 @@ class FlightData: #: The package that the flight belongs to. package: Package + #: The country that the flight belongs to. + country: str + flight_type: FlightType #: All units in the flight. @@ -300,7 +302,7 @@ class FlightData: joker_fuel: Optional[int] - def __init__(self, package: Package, flight_type: FlightType, + def __init__(self, package: Package, country: str, flight_type: FlightType, units: List[FlyingUnit], size: int, friendly: bool, departure_delay: timedelta, departure: RunwayData, arrival: RunwayData, divert: Optional[RunwayData], @@ -309,6 +311,7 @@ class FlightData: bingo_fuel: Optional[int], joker_fuel: Optional[int]) -> None: self.package = package + self.country = country self.flight_type = flight_type self.units = units self.size = size @@ -779,6 +782,7 @@ class AircraftConflictGenerator: self.flights.append(FlightData( package=package, + country=faction.country, flight_type=flight.flight_type, units=group.units, size=len(group.units), @@ -832,19 +836,21 @@ class AircraftConflictGenerator: else: alt = WARM_START_ALTITUDE - speed = knots_to_kph(GroundSpeed.for_flight(flight, alt)) + speed = GroundSpeed.for_flight(flight, alt) pos = Point(at.x + random.randint(100, 1000), at.y + random.randint(100, 1000)) - logging.info("airgen: {} for {} at {} at {}".format(flight.unit_type, side.id, alt, speed)) + logging.info( + "airgen: {} for {} at {} at {}".format(flight.unit_type, side.id, + alt, int(speed.kph))) group = self.m.flight_group( country=side, name=name, aircraft_type=flight.unit_type, airport=None, position=pos, - altitude=alt, - speed=speed, + altitude=alt.meters, + speed=speed.kph, maintask=None, group_size=flight.count) @@ -867,8 +873,10 @@ class AircraftConflictGenerator: start_type=self._start_type(start_type), group_size=count) - def _add_radio_waypoint(self, group: FlyingGroup, position, altitude: int, airspeed: int = 600): - point = group.add_waypoint(position, altitude, airspeed) + def _add_radio_waypoint(self, group: FlyingGroup, position, + altitude: Distance, + airspeed: int = 600) -> MovingPoint: + point = group.add_waypoint(position, altitude.meters, airspeed) point.alt_type = "RADIO" return point @@ -884,7 +892,8 @@ class AircraftConflictGenerator: tod_location = position.point_from_heading(heading, RTB_DISTANCE) self._add_radio_waypoint(group, tod_location, last_waypoint.alt) - destination_waypoint = self._add_radio_waypoint(group, position, RTB_ALTITUDE) + destination_waypoint = self._add_radio_waypoint(group, position, + RTB_ALTITUDE) if isinstance(at, Airport): group.land_at(at) return destination_waypoint @@ -899,22 +908,39 @@ class AircraftConflictGenerator: else: assert False - def _setup_custom_payload(self, flight, group:FlyingGroup): - if flight.use_custom_loadout: + @staticmethod + def _setup_custom_payload(flight: Flight, group: FlyingGroup) -> None: + if not flight.use_custom_loadout: + return - logging.info("Custom loadout for flight : " + flight.__repr__()) - for p in group.units: - p.pylons.clear() + logging.info("Custom loadout for flight : " + flight.__repr__()) + for p in group.units: + p.pylons.clear() - for key in flight.loadout.keys(): - if "Pylon" + key in flight.unit_type.__dict__.keys(): - print(flight.loadout) - weapon_dict = flight.unit_type.__dict__["Pylon" + key].__dict__ - if flight.loadout[key] in weapon_dict.keys(): - weapon = weapon_dict[flight.loadout[key]] - group.load_pylon(weapon, int(key)) - else: - logging.warning("Pylon not found ! => Pylon" + key + " on " + str(flight.unit_type)) + for pylon_number, weapon in flight.loadout.items(): + if weapon is None: + continue + pylon = Pylon.for_aircraft(flight.unit_type, pylon_number) + pylon.equip(group, weapon) + + def _degrade_payload_to_era(self, flight: Flight, + group: FlyingGroup) -> None: + loadout = dict(group.units[0].pylons) + for pylon_number, clsid in loadout.items(): + weapon = Weapon.from_clsid(clsid["CLSID"]) + if weapon is None: + logging.error(f"Could not find weapon for clsid {clsid}") + continue + + if not weapon.available_on(self.game.date): + pylon = Pylon.for_aircraft(flight.unit_type, pylon_number) + for fallback in weapon.fallbacks: + if not pylon.can_equip(fallback): + continue + if not fallback.available_on(self.game.date): + continue + pylon.equip(group, fallback) + break def clear_parking_slots(self) -> None: for cp in self.game.theater.controlpoints: @@ -963,13 +989,13 @@ class AircraftConflictGenerator: # Creating a flight even those this isn't a fragged mission lets us # reuse the existing debriefing code. # TODO: Special flight type? - flight = Flight(Package(control_point), aircraft, 1, + flight = Flight(Package(control_point), faction.country, aircraft, 1, FlightType.BARCAP, "Cold", departure=control_point, arrival=control_point, divert=None) group = self._generate_at_airport( - name=namegen.next_unit_name(country, control_point.id, - aircraft), + name=namegen.next_aircraft_name(country, control_point.id, + flight), side=country, unit_type=aircraft, count=1, @@ -1033,17 +1059,18 @@ class AircraftConflictGenerator: CoalitionHasAirdrome(coalition, flight.from_cp.id)) def generate_planned_flight(self, cp, country, flight:Flight): + name = namegen.next_aircraft_name(country, cp.id, flight) try: if flight.start_type == "In Flight": group = self._generate_inflight( - name=namegen.next_unit_name(country, cp.id, flight.unit_type), + name=name, side=country, flight=flight, origin=cp) elif isinstance(cp, NavalControlPoint): group_name = cp.get_carrier_group_name() group = self._generate_at_group( - name=namegen.next_unit_name(country, cp.id, flight.unit_type), + name=name, side=country, unit_type=flight.unit_type, count=flight.count, @@ -1054,8 +1081,7 @@ class AircraftConflictGenerator: raise RuntimeError( f"Attempted to spawn at airfield for non-airfield {cp}") group = self._generate_at_airport( - name=namegen.next_unit_name(country, cp.id, - flight.unit_type), + name=name, side=country, unit_type=flight.unit_type, count=flight.count, @@ -1067,7 +1093,7 @@ class AircraftConflictGenerator: logging.warning("No room on runway or parking slots. Starting from the air.") flight.start_type = "In Flight" group = self._generate_inflight( - name=namegen.next_unit_name(country, cp.id, flight.unit_type), + name=name, side=country, flight=flight, origin=cp) @@ -1312,6 +1338,8 @@ class AircraftConflictGenerator: # have their TOTs set. self.flights[-1].waypoints = [takeoff_point] + flight.points self._setup_custom_payload(flight, group) + if self.game.settings.restrict_weapons_by_date: + self._degrade_payload_to_era(flight, group) def should_delay_flight(self, flight: Flight, start_time: timedelta) -> bool: @@ -1380,11 +1408,15 @@ class PydcsWaypointBuilder: def build(self) -> MovingPoint: waypoint = self.group.add_waypoint( - Point(self.waypoint.x, self.waypoint.y), self.waypoint.alt, + Point(self.waypoint.x, self.waypoint.y), + self.waypoint.alt.meters, name=self.mission.string(self.waypoint.name)) if self.waypoint.flyover: - waypoint.type = PointAction.FlyOverPoint.value + waypoint.action = PointAction.FlyOverPoint + # It seems we need to leave waypoint.type exactly as it is even + # though it's set to "Turning Point". If I set this to "Fly Over + # Point" and then save the mission in the ME DCS resets it. waypoint.alt_type = self.waypoint.alt_type tot = self.flight.flight_plan.tot_for_waypoint(self.waypoint) @@ -1497,7 +1529,7 @@ class CasIngressBuilder(PydcsWaypointBuilder): if isinstance(self.flight.flight_plan, CasFlightPlan): waypoint.add_task(EngageTargetsInZone( position=self.flight.flight_plan.target, - radius=FRONTLINE_LENGTH / 2, + radius=int(self.flight.flight_plan.engagement_distance.meters), targets=[ Targets.All.GroundUnits.GroundVehicles, Targets.All.GroundUnits.AirDefence.AAA, @@ -1508,7 +1540,7 @@ class CasIngressBuilder(PydcsWaypointBuilder): logging.error( "No CAS waypoint found. Falling back to search and engage") waypoint.add_task(EngageTargets( - max_distance=nm_to_meter(10), + max_distance=int(nautical_miles(10).meters), targets=[ Targets.All.GroundUnits.GroundVehicles, Targets.All.GroundUnits.AirDefence.AAA, @@ -1554,7 +1586,7 @@ class OcaAircraftIngressBuilder(PydcsWaypointBuilder): position=target.position, # Al Dhafra is 4 nm across at most. Add a little wiggle room in case # the airport position from DCS is not centered. - radius=nm_to_meter(3), + radius=int(nautical_miles(3).meters), targets=[Targets.All.Air] ) task.params["attackQtyLimit"] = False @@ -1591,7 +1623,7 @@ class SeadIngressBuilder(PydcsWaypointBuilder): if tgroup is not None: waypoint.add_task(EngageTargetsInZone( position=tgroup.position, - radius=nm_to_meter(30), + radius=int(nautical_miles(30).meters), targets=[ Targets.All.GroundUnits.AirDefence, ]) @@ -1673,7 +1705,7 @@ class SweepIngressBuilder(PydcsWaypointBuilder): return waypoint waypoint.tasks.append(EngageTargets( - max_distance=nm_to_meter(50), + max_distance=int(nautical_miles(50).meters), targets=[Targets.All.Air.Planes.Fighters])) return waypoint @@ -1716,7 +1748,7 @@ class JoinPointBuilder(PydcsWaypointBuilder): # https://forums.eagle.ru/forum/english/digital-combat-simulator/dcs-world-2-5/bugs-and-problems-ai/ai-ad/250183-task-follow-and-escort-temporarily-aborted waypoint.add_task(ControlledTask(EngageTargets( # TODO: From doctrine. - max_distance=nm_to_meter(30), + max_distance=int(nautical_miles(30).meters), targets=[Targets.All.Air.Planes.Fighters] ))) @@ -1737,8 +1769,10 @@ class RaceTrackBuilder(PydcsWaypointBuilder): def build(self) -> MovingPoint: waypoint = super().build() - if not isinstance(self.flight.flight_plan, PatrollingFlightPlan): - flight_plan_type = self.flight.flight_plan.__class__.__name__ + flight_plan = self.flight.flight_plan + + if not isinstance(flight_plan, PatrollingFlightPlan): + flight_plan_type = flight_plan.__class__.__name__ logging.error( f"Cannot create race track for {self.flight} because " f"{flight_plan_type} does not define a patrol.") @@ -1756,17 +1790,18 @@ class RaceTrackBuilder(PydcsWaypointBuilder): # later. cap_types = {FlightType.BARCAP, FlightType.TARCAP} if self.flight.flight_type in cap_types: - waypoint.tasks.append(EngageTargets(max_distance=nm_to_meter(50), - targets=[Targets.All.Air])) + engagement_distance = int(flight_plan.engagement_distance.meters) + waypoint.tasks.append( + EngageTargets(max_distance=engagement_distance, + targets=[Targets.All.Air])) racetrack = ControlledTask(OrbitAction( altitude=waypoint.alt, pattern=OrbitAction.OrbitPattern.RaceTrack )) - self.set_waypoint_tot( - waypoint, self.flight.flight_plan.patrol_start_time) + self.set_waypoint_tot(waypoint, flight_plan.patrol_start_time) racetrack.stop_after_time( - int(self.flight.flight_plan.patrol_end_time.total_seconds())) + int(flight_plan.patrol_end_time.total_seconds())) waypoint.add_task(racetrack) return waypoint diff --git a/gen/airfields.py b/gen/airfields.py index f4b98b34..14a40840 100644 --- a/gen/airfields.py +++ b/gen/airfields.py @@ -410,7 +410,10 @@ AIRFIELD_DATA = { icao="OMLW", elevation=400, runway_length=10768, - atc=AtcData(MHz(4, 175), MHz(39, 250), MHz(119, 300), MHz(250, 850)), + tacan=TacanChannel(121, TacanBand.X), + tacan_callsign="OMLW", + vor=("OMLW", MHz(117,400)), + atc=AtcData(MHz(4, 225), MHz(39, 350), MHz(119, 300), MHz(250, 950)), ), "Al Dhafra AB": AirfieldData( diff --git a/gen/armor.py b/gen/armor.py index be4f1397..48311bdc 100644 --- a/gen/armor.py +++ b/gen/armor.py @@ -208,7 +208,7 @@ class GroundConflictGenerator: u = random.choice(manpads) self.mission.vehicle_group( side, - namegen.next_infantry_name(side, cp, u), u, + namegen.next_infantry_name(side, cp.id, u), u, position=infantry_position, group_size=1, heading=forward_heading, @@ -222,7 +222,7 @@ class GroundConflictGenerator: u = random.choice(possible_infantry_units) self.mission.vehicle_group( side, - namegen.next_infantry_name(side, cp, u), u, + namegen.next_infantry_name(side, cp.id, u), u, position=infantry_position, group_size=1, heading=forward_heading, @@ -233,7 +233,7 @@ class GroundConflictGenerator: position = infantry_position.random_point_within(55, 5) self.mission.vehicle_group( side, - namegen.next_infantry_name(side, cp, u), u, + namegen.next_infantry_name(side, cp.id, u), u, position=position, group_size=1, heading=forward_heading, @@ -352,9 +352,14 @@ class GroundConflictGenerator: to_cp.position.random_point_within(500, 0) ) else: + # We use an offset heading here because DCS doesn't always + # force vehicles to move if there's no heading change. + offset_heading = forward_heading - 2 + if offset_heading < 0: + offset_heading = 358 attack_point = self.find_offensive_point( dcs_group, - forward_heading, + offset_heading, AGGRESIVE_MOVE_DISTANCE ) dcs_group.add_waypoint(attack_point, PointAction.OffRoad) @@ -367,7 +372,12 @@ class GroundConflictGenerator: to_cp.position.random_point_within(500, 0) ) else: - attack_point = self.find_offensive_point(dcs_group, forward_heading, BREAKTHROUGH_OFFENSIVE_DISTANCE) + # We use an offset heading here because DCS doesn't always + # force vehicles to move if there's no heading change. + offset_heading = forward_heading - 1 + if offset_heading < 0: + offset_heading = 359 + attack_point = self.find_offensive_point(dcs_group, offset_heading, BREAKTHROUGH_OFFENSIVE_DISTANCE) dcs_group.add_waypoint(attack_point, PointAction.OffRoad) elif stance == CombatStance.ELIMINATION: # In elimination mode, the units focus on destroying as much enemy groups as possible diff --git a/gen/ato.py b/gen/ato.py index ab104bab..99a76789 100644 --- a/gen/ato.py +++ b/gen/ato.py @@ -17,8 +17,10 @@ from typing import Dict, List, Optional from dcs.mapping import Point from game.theater.missiontarget import MissionTarget +from game.utils import Speed from .flights.flight import Flight, FlightType from .flights.flightplan import FormationFlightPlan +from .flights.traveltime import TotEstimator @dataclass(frozen=True) @@ -53,13 +55,18 @@ class Package: delay: int = field(default=0) + #: True if the package ToT should be reset to ASAP whenever the player makes + #: a change. This is really a UI property rather than a game property, but + #: we want it to persist in the save. + auto_asap: bool = field(default=False) + #: Desired TOT as an offset from mission start. time_over_target: timedelta = field(default=timedelta()) waypoints: Optional[PackageWaypoints] = field(default=None) @property - def formation_speed(self) -> Optional[int]: + def formation_speed(self) -> Optional[Speed]: """The speed of the package when in formation. If none of the flights in the package will join a formation, this @@ -117,6 +124,18 @@ class Package: return max(times) return None + @property + def mission_departure_time(self) -> Optional[timedelta]: + times = [] + for flight in self.flights: + times.append(flight.flight_plan.mission_departure_time) + if times: + return max(times) + return None + + def set_tot_asap(self) -> None: + self.time_over_target = TotEstimator(self).earliest_tot() + def add_flight(self, flight: Flight) -> None: """Adds a flight to the package.""" self.flights.append(flight) diff --git a/gen/conflictgen.py b/gen/conflictgen.py index 68946ab2..b050e028 100644 --- a/gen/conflictgen.py +++ b/gen/conflictgen.py @@ -1,9 +1,9 @@ import logging -import random from typing import Tuple, Optional from dcs.country import Country from dcs.mapping import Point +from shapely.geometry import LineString, Point as ShapelyPoint from game.theater.conflicttheater import ConflictTheater, FrontLine from game.theater.controlpoint import ControlPoint @@ -56,7 +56,7 @@ class Conflict: """ center_position, heading = cls.frontline_position(from_cp, to_cp, theater) left_heading = heading_sum(heading, -90) - right_heading = heading_sum(heading, 90) + right_heading = heading_sum(heading, 90) left_position = cls.extend_ground_position(center_position, int(FRONTLINE_LENGTH / 2), left_heading, theater) right_position = cls.extend_ground_position(center_position, int(FRONTLINE_LENGTH / 2), right_heading, theater) distance = int(left_position.distance_to_point(right_position)) @@ -83,12 +83,25 @@ class Conflict: @classmethod def extend_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point: """Finds the first intersection with an exclusion zone in one heading from an initial point up to max_distance""" - pos = initial - for distance in range(0, int(max_distance), 100): - pos = initial.point_from_heading(heading, distance) - if not theater.is_on_land(pos): - return initial.point_from_heading(heading, distance - 100) - return pos + extended = initial.point_from_heading(heading, max_distance) + if theater.landmap is None: + # TODO: Why is this possible? + return extended + + p0 = ShapelyPoint(initial.x, initial.y) + p1 = ShapelyPoint(extended.x, extended.y) + line = LineString([p0, p1]) + + intersection = line.intersection( + theater.landmap.inclusion_zone_only.boundary) + if intersection.is_empty: + # Max extent does not intersect with the boundary of the inclusion + # zone, so the full front line is usable. This does assume that the + # front line was centered on a valid location. + return extended + + # Otherwise extend the front line only up to the intersection. + return initial.point_from_heading(heading, p0.distance(intersection)) @classmethod def find_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater, coerce=True) -> Optional[Point]: diff --git a/gen/environmentgen.py b/gen/environmentgen.py index 7712cea5..9d645a1d 100644 --- a/gen/environmentgen.py +++ b/gen/environmentgen.py @@ -21,7 +21,7 @@ class EnvironmentGenerator: def set_fog(self, fog: Optional[Fog]) -> None: if fog is None: return - self.mission.weather.fog_visibility = fog.visibility + self.mission.weather.fog_visibility = fog.visibility.meters self.mission.weather.fog_thickness = fog.thickness def set_wind(self, wind: WindConditions) -> None: diff --git a/gen/fleet/cn_dd_group.py b/gen/fleet/cn_dd_group.py index a8fbdc44..8a20ae10 100644 --- a/gen/fleet/cn_dd_group.py +++ b/gen/fleet/cn_dd_group.py @@ -8,7 +8,6 @@ from dcs.ships import ( Type_052C_Destroyer, Type_052B_Destroyer, Type_054A_Frigate, - CGN_1144_2_Pyotr_Velikiy, ) from game.factions.faction import Faction @@ -27,12 +26,7 @@ class ChineseNavyGroupGenerator(ShipGroupGenerator): include_frigate = random.choice([True, True, False]) include_dd = random.choice([True, False]) - if include_dd: - include_cc = random.choice([True, False]) - else: - include_cc = False - - if not any([include_frigate, include_dd, include_cc]): + if not any([include_frigate, include_dd]): include_frigate = True if include_frigate: @@ -44,10 +38,6 @@ class ChineseNavyGroupGenerator(ShipGroupGenerator): self.add_unit(dd_type, "DD1", self.position.x + 2400, self.position.y + 900, self.heading) self.add_unit(dd_type, "DD2", self.position.x + 2400, self.position.y - 900, self.heading) - if include_cc: - cc_type = random.choice([CGN_1144_2_Pyotr_Velikiy]) - self.add_unit(cc_type, "CC1", self.position.x, self.position.y, self.heading) - self.get_generated_group().points[0].speed = 20 diff --git a/gen/fleet/ru_dd_group.py b/gen/fleet/ru_dd_group.py index d67656a7..ee08577f 100644 --- a/gen/fleet/ru_dd_group.py +++ b/gen/fleet/ru_dd_group.py @@ -8,7 +8,6 @@ from dcs.ships import ( FFG_11540_Neustrashimy, FF_1135M_Rezky, CG_1164_Moskva, - CGN_1144_2_Pyotr_Velikiy, SSK_877, SSK_641B ) @@ -49,8 +48,9 @@ class RussianNavyGroupGenerator(ShipGroupGenerator): self.add_unit(dd_type, "DD2", self.position.x + 2400, self.position.y - 900, self.heading) if include_cc: - cc_type = random.choice([CG_1164_Moskva, CGN_1144_2_Pyotr_Velikiy]) - self.add_unit(cc_type, "CC1", self.position.x, self.position.y, self.heading) + # Only include the Moskva for now, the Pyotry Velikiy is an unkillable monster. + # See https://github.com/Khopa/dcs_liberation/issues/567 + self.add_unit(CG_1164_Moskva, "CC1", self.position.x, self.position.y, self.heading) self.get_generated_group().points[0].speed = 20 diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index 5d6ff05f..2267fcb4 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -3,9 +3,12 @@ from __future__ import annotations import logging import operator import random -from dataclasses import dataclass +from collections import defaultdict +from dataclasses import dataclass, field from datetime import timedelta +from enum import Enum, auto from typing import ( + Dict, Iterable, Iterator, List, @@ -18,41 +21,28 @@ from typing import ( from dcs.unittype import FlyingType -from game import db -from game.data.radar_db import UNITS_WITH_RADAR from game.infos.information import Information from game.procurement import AircraftProcurementRequest from game.theater import ( Airfield, ControlPoint, + Fob, FrontLine, MissionTarget, OffMapSpawn, SamGroundObject, TheaterGroundObject, ) -# Avoid importing some types that cause circular imports unless type checking. from game.theater.theatergroundobject import ( + BuildingGroundObject, EwrGroundObject, - NavalGroundObject, VehicleGroupGroundObject, + NavalGroundObject, + VehicleGroupGroundObject, ) -from game.utils import nm_to_meter +from game.utils import Distance, nautical_miles from gen import Conflict from gen.ato import Package -from gen.flights.ai_flight_planner_db import ( - ANTISHIP_CAPABLE, - ANTISHIP_PREFERRED, - CAP_CAPABLE, - CAP_PREFERRED, - CAS_CAPABLE, - CAS_PREFERRED, - RUNWAY_ATTACK_CAPABLE, - RUNWAY_ATTACK_PREFERRED, - SEAD_CAPABLE, - SEAD_PREFERRED, - STRIKE_CAPABLE, - STRIKE_PREFERRED, capable_aircraft_for_task, preferred_aircraft_for_task, -) +from gen.flights.ai_flight_planner_db import aircraft_for_task from gen.flights.closestairfields import ( ClosestAirfields, ObjectiveDistanceCache, @@ -64,11 +54,17 @@ from gen.flights.flight import ( from gen.flights.flightplan import FlightPlanBuilder from gen.flights.traveltime import TotEstimator +# Avoid importing some types that cause circular imports unless type checking. if TYPE_CHECKING: from game import Game from game.inventory import GlobalAircraftInventory +class EscortType(Enum): + AirToAir = auto() + Sead = auto() + + @dataclass(frozen=True) class ProposedFlight: """A flight outline proposed by the mission planner. @@ -85,7 +81,13 @@ class ProposedFlight: num_aircraft: int #: The maximum distance between the objective and the departure airfield. - max_distance: int + max_distance: Distance + + #: The type of threat this flight defends against if it is an escort. Escort + #: flights will be pruned if the rest of the package is not threatened by + #: the threat they defend against. If this flight is not an escort, this + #: field is None. + escort_type: Optional[EscortType] = field(default=None) def __str__(self) -> str: return f"{self.task} {self.num_aircraft} ship" @@ -123,7 +125,7 @@ class AircraftAllocator: def find_aircraft_for_flight( self, flight: ProposedFlight - ) -> Optional[Tuple[ControlPoint, FlyingType]]: + ) -> Optional[Tuple[ControlPoint, Type[FlyingType]]]: """Finds aircraft suitable for the given mission. Searches for aircraft capable of performing the given mission within the @@ -142,13 +144,8 @@ class AircraftAllocator: on subsequent calls. If the found aircraft are not used, the caller is responsible for returning them to the inventory. """ - result = self.find_aircraft_of_type( - flight, preferred_aircraft_for_task(flight.task) - ) - if result is not None: - return result return self.find_aircraft_of_type( - flight, capable_aircraft_for_task(flight.task) + flight, aircraft_for_task(flight.task) ) def find_aircraft_of_type( @@ -178,9 +175,11 @@ class PackageBuilder: closest_airfields: ClosestAirfields, global_inventory: GlobalAircraftInventory, is_player: bool, + package_country: str, start_type: str) -> None: self.closest_airfields = closest_airfields self.is_player = is_player + self.package_country = package_country self.package = Package(location) self.allocator = AircraftAllocator(closest_airfields, global_inventory, is_player) @@ -204,15 +203,15 @@ class PackageBuilder: else: start_type = self.start_type - flight = Flight(self.package, aircraft, plan.num_aircraft, plan.task, + flight = Flight(self.package, self.package_country, aircraft, plan.num_aircraft, plan.task, start_type, departure=airfield, arrival=airfield, divert=self.find_divert_field(aircraft, airfield)) self.package.add_flight(flight) return True - def find_divert_field(self, aircraft: FlyingType, + def find_divert_field(self, aircraft: Type[FlyingType], arrival: ControlPoint) -> Optional[ControlPoint]: - divert_limit = nm_to_meter(150) + divert_limit = nautical_miles(150) for airfield in self.closest_airfields.airfields_within(divert_limit): if airfield.captured != self.is_player: continue @@ -241,8 +240,8 @@ class ObjectiveFinder: """Identifies potential objectives for the mission planner.""" # TODO: Merge into doctrine. - AIRFIELD_THREAT_RANGE = nm_to_meter(150) - SAM_THREAT_RANGE = nm_to_meter(100) + AIRFIELD_THREAT_RANGE = nautical_miles(150) + SAM_THREAT_RANGE = nautical_miles(100) def __init__(self, game: Game, is_player: bool) -> None: self.game = game @@ -266,7 +265,7 @@ class ObjectiveFinder: if ground_object.name in found_targets: continue - if not self.object_has_radar(ground_object): + if not ground_object.has_radar: continue # TODO: Yield in order of most threatening. @@ -349,12 +348,35 @@ class ObjectiveFinder: found_targets: Set[str] = set() for enemy_cp in self.enemy_control_points(): for ground_object in enemy_cp.ground_objects: + # TODO: Reuse ground_object.mission_types. + # The mission types for ground objects are currently not + # accurate because we include things like strike and BAI for all + # targets since they have different planning behavior (waypoint + # generation is better for players with strike when the targets + # are stationary, AI behavior against weaker air defenses is + # better with BAI), so that's not a useful filter. Once we have + # better control over planning profiles and target dependent + # loadouts we can clean this up. if isinstance(ground_object, VehicleGroupGroundObject): # BAI target, not strike target. continue + if isinstance(ground_object, NavalGroundObject): # Anti-ship target, not strike target. continue + + if isinstance(ground_object, SamGroundObject): + # SAMs are targeted by DEAD. No need to double plan. + continue + + is_building = isinstance(ground_object, BuildingGroundObject) + is_fob = isinstance(enemy_cp, Fob) + if is_building and is_fob and ground_object.airbase_group: + # This is the FOB structure itself. Can't be repaired or + # targeted by the player, so shouldn't be targetable by the + # AI. + continue + if ground_object.is_dead: continue if ground_object.name in found_targets: @@ -368,15 +390,6 @@ class ObjectiveFinder: for target, _range in targets: yield target - @staticmethod - def object_has_radar(ground_object: TheaterGroundObject) -> bool: - """Returns True if the ground object contains a unit with radar.""" - for group in ground_object.groups: - for unit in group.units: - if db.unit_type_from_name(unit.type) in UNITS_WITH_RADAR: - return True - return False - def front_lines(self) -> Iterator[FrontLine]: """Iterates over all active front lines in the theater.""" for cp in self.friendly_control_points(): @@ -467,25 +480,42 @@ class CoalitionMissionPlanner: """ # TODO: Merge into doctrine, also limit by aircraft. - MAX_CAP_RANGE = nm_to_meter(100) - MAX_CAS_RANGE = nm_to_meter(50) - MAX_ANTISHIP_RANGE = nm_to_meter(150) - MAX_BAI_RANGE = nm_to_meter(150) - MAX_OCA_RANGE = nm_to_meter(150) - MAX_SEAD_RANGE = nm_to_meter(150) - MAX_STRIKE_RANGE = nm_to_meter(150) + MAX_CAP_RANGE = nautical_miles(100) + MAX_CAS_RANGE = nautical_miles(50) + MAX_ANTISHIP_RANGE = nautical_miles(150) + MAX_BAI_RANGE = nautical_miles(150) + MAX_OCA_RANGE = nautical_miles(150) + MAX_SEAD_RANGE = nautical_miles(150) + MAX_STRIKE_RANGE = nautical_miles(150) def __init__(self, game: Game, is_player: bool) -> None: self.game = game self.is_player = is_player self.objective_finder = ObjectiveFinder(self.game, self.is_player) self.ato = self.game.blue_ato if is_player else self.game.red_ato + self.threat_zones = self.game.threat_zone_for(not self.is_player) self.procurement_requests: List[AircraftProcurementRequest] = [] - def propose_missions(self) -> Iterator[ProposedMission]: - """Identifies and iterates over potential mission in priority order.""" + def critical_missions(self) -> Iterator[ProposedMission]: + """Identifies the most important missions to plan this turn. + + Non-critical missions that cannot be fulfilled will create purchase + orders for the next turn. Critical missions will create a purchase order + unless the mission can be doubly fulfilled. In other words, the AI will + attempt to have *double* the aircraft it needs for these missions to + ensure that they can be planned again next turn even if all aircraft are + eliminated this turn. + """ # Find friendly CPs within 100 nmi from an enemy airfield, plan CAP. for cp in self.objective_finder.vulnerable_control_points(): + # Plan three rounds of CAP to give ~90 minutes coverage. Spacing + # these out appropriately is done in stagger_missions. + yield ProposedMission(cp, [ + ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE), + ]) + yield ProposedMission(cp, [ + ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE), + ]) yield ProposedMission(cp, [ ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE), ]) @@ -493,10 +523,15 @@ class CoalitionMissionPlanner: # Find front lines, plan CAS. for front_line in self.objective_finder.front_lines(): yield ProposedMission(front_line, [ - ProposedFlight(FlightType.TARCAP, 2, self.MAX_CAP_RANGE), ProposedFlight(FlightType.CAS, 2, self.MAX_CAS_RANGE), + ProposedFlight(FlightType.TARCAP, 2, self.MAX_CAP_RANGE, + EscortType.AirToAir), ]) + def propose_missions(self) -> Iterator[ProposedMission]: + """Identifies and iterates over potential mission in priority order.""" + yield from self.critical_missions() + # Find enemy SAM sites with ranges that cover friendly CPs, front lines, # or objects, plan DEAD. # Find enemy SAM sites with ranges that extend to within 50 nmi of @@ -505,39 +540,55 @@ class CoalitionMissionPlanner: yield ProposedMission(sam, [ ProposedFlight(FlightType.DEAD, 2, self.MAX_SEAD_RANGE), # TODO: Max escort range. - ProposedFlight(FlightType.ESCORT, 2, self.MAX_SEAD_RANGE), + ProposedFlight(FlightType.ESCORT, 2, self.MAX_SEAD_RANGE, + EscortType.AirToAir), ]) for group in self.objective_finder.threatening_ships(): yield ProposedMission(group, [ ProposedFlight(FlightType.ANTISHIP, 2, self.MAX_ANTISHIP_RANGE), # TODO: Max escort range. - ProposedFlight(FlightType.ESCORT, 2, self.MAX_ANTISHIP_RANGE), + ProposedFlight(FlightType.ESCORT, 2, self.MAX_ANTISHIP_RANGE, + EscortType.AirToAir), ]) for group in self.objective_finder.threatening_vehicle_groups(): yield ProposedMission(group, [ ProposedFlight(FlightType.BAI, 2, self.MAX_BAI_RANGE), # TODO: Max escort range. - ProposedFlight(FlightType.ESCORT, 2, self.MAX_BAI_RANGE), + ProposedFlight(FlightType.ESCORT, 2, self.MAX_BAI_RANGE, + EscortType.AirToAir), + ProposedFlight(FlightType.SEAD, 2, self.MAX_OCA_RANGE, + EscortType.Sead), ]) for target in self.objective_finder.oca_targets(min_aircraft=20): - yield ProposedMission(target, [ - ProposedFlight(FlightType.OCA_AIRCRAFT, 2, self.MAX_OCA_RANGE), + flights = [ ProposedFlight(FlightType.OCA_RUNWAY, 2, self.MAX_OCA_RANGE), + ] + if self.game.settings.default_start_type == "Cold": + # Only schedule if the default start type is Cold. If the player + # has set anything else there are no targets to hit. + flights.append(ProposedFlight(FlightType.OCA_AIRCRAFT, 2, + self.MAX_OCA_RANGE)) + flights.extend([ # TODO: Max escort range. - ProposedFlight(FlightType.ESCORT, 2, self.MAX_OCA_RANGE), - ProposedFlight(FlightType.SEAD, 2, self.MAX_OCA_RANGE), + ProposedFlight(FlightType.ESCORT, 2, self.MAX_OCA_RANGE, + EscortType.AirToAir), + ProposedFlight(FlightType.SEAD, 2, self.MAX_OCA_RANGE, + EscortType.Sead), ]) + yield ProposedMission(target, flights) # Plan strike missions. for target in self.objective_finder.strike_targets(): yield ProposedMission(target, [ ProposedFlight(FlightType.STRIKE, 2, self.MAX_STRIKE_RANGE), # TODO: Max escort range. - ProposedFlight(FlightType.SEAD, 2, self.MAX_STRIKE_RANGE), - ProposedFlight(FlightType.ESCORT, 2, self.MAX_STRIKE_RANGE), + ProposedFlight(FlightType.ESCORT, 2, self.MAX_STRIKE_RANGE, + EscortType.AirToAir), + ProposedFlight(FlightType.SEAD, 2, self.MAX_STRIKE_RANGE, + EscortType.Sead), ]) def plan_missions(self) -> None: @@ -545,6 +596,9 @@ class CoalitionMissionPlanner: for proposed_mission in self.propose_missions(): self.plan_mission(proposed_mission) + for critical_mission in self.critical_missions(): + self.plan_mission(critical_mission, reserves=True) + self.stagger_missions() for cp in self.objective_finder.friendly_control_points(): @@ -553,48 +607,128 @@ class CoalitionMissionPlanner: self.message("Unused aircraft", f"{available} {aircraft.id} from {cp}") - def plan_mission(self, mission: ProposedMission) -> None: + def plan_flight(self, mission: ProposedMission, flight: ProposedFlight, + builder: PackageBuilder, missing_types: Set[FlightType], + for_reserves: bool) -> None: + if not builder.plan_flight(flight): + missing_types.add(flight.task) + purchase_order = AircraftProcurementRequest( + near=mission.location, + range=flight.max_distance, + task_capability=flight.task, + number=flight.num_aircraft + ) + if for_reserves: + # Reserves are planned for critical missions, so prioritize + # those orders over aircraft needed for non-critical missions. + self.procurement_requests.insert(0, purchase_order) + else: + self.procurement_requests.append(purchase_order) + + def scrub_mission_missing_aircraft( + self, mission: ProposedMission, builder: PackageBuilder, + missing_types: Set[FlightType], + not_attempted: Iterable[ProposedFlight], + reserves: bool) -> None: + # Try to plan the rest of the mission just so we can count the missing + # types to buy. + for flight in not_attempted: + self.plan_flight(mission, flight, builder, missing_types, reserves) + + missing_types_str = ", ".join( + sorted([t.name for t in missing_types])) + builder.release_planned_aircraft() + desc = "reserve aircraft" if reserves else "aircraft" + self.message( + "Insufficient aircraft", + f"Not enough {desc} in range for {mission.location.name} " + f"capable of: {missing_types_str}") + + def check_needed_escorts( + self, builder: PackageBuilder) -> Dict[EscortType, bool]: + threats = defaultdict(bool) + for flight in builder.package.flights: + if self.threat_zones.threatened_by_aircraft(flight): + threats[EscortType.AirToAir] = True + if self.threat_zones.threatened_by_air_defense(flight): + threats[EscortType.Sead] = True + return threats + + def plan_mission(self, mission: ProposedMission, + reserves: bool = False) -> None: """Allocates aircraft for a proposed mission and adds it to the ATO.""" - if self.game.settings.perf_ai_parking_start: - start_type = "Cold" + if self.is_player: + package_country = self.game.player_country else: - start_type = "Warm" + package_country = self.game.enemy_country builder = PackageBuilder( mission.location, self.objective_finder.closest_airfields_to(mission.location), self.game.aircraft_inventory, self.is_player, - start_type + package_country, + self.game.settings.default_start_type ) + # Attempt to plan all the main elements of the mission first. Escorts + # will be planned separately so we can prune escorts for packages that + # are not expected to encounter that type of threat. missing_types: Set[FlightType] = set() + escorts = [] for proposed_flight in mission.flights: - if not builder.plan_flight(proposed_flight): - missing_types.add(proposed_flight.task) - self.procurement_requests.append(AircraftProcurementRequest( - near=mission.location, - range=proposed_flight.max_distance, - task_capability=proposed_flight.task, - number=proposed_flight.num_aircraft - )) + if proposed_flight.escort_type is not None: + # Escorts are planned after the primary elements of the package. + # If the package does not need escorts they may be pruned. + escorts.append(proposed_flight) + continue + self.plan_flight(mission, proposed_flight, builder, missing_types, + reserves) if missing_types: - missing_types_str = ", ".join( - sorted([t.name for t in missing_types])) + self.scrub_mission_missing_aircraft(mission, builder, missing_types, + escorts, reserves) + return + + # Create flight plans for the main flights of the package so we can + # determine threats. This is done *after* creating all of the flights + # rather than as each flight is added because the flight plan for + # flights that will rendezvous with their package will be affected by + # the other flights in the package. Escorts will not be able to + # contribute to this. + flight_plan_builder = FlightPlanBuilder(self.game, builder.package, + self.is_player) + for flight in builder.package.flights: + flight_plan_builder.populate_flight_plan(flight) + + needed_escorts = self.check_needed_escorts(builder) + for escort in escorts: + # This list was generated from the not None set, so this should be + # impossible. + assert escort.escort_type is not None + if needed_escorts[escort.escort_type]: + self.plan_flight(mission, escort, builder, missing_types, + reserves) + + # Check again for unavailable aircraft. If the escort was required and + # none were found, scrub the mission. + if missing_types: + self.scrub_mission_missing_aircraft(mission, builder, missing_types, + escorts, reserves) + return + + if reserves: + # Mission is planned reserves which will not be used this turn. + # Return reserves to the inventory. builder.release_planned_aircraft() - self.message( - "Insufficient aircraft", - f"Not enough aircraft in range for {mission.location.name} " - f"capable of: {missing_types_str}") return package = builder.build() - flight_plan_builder = FlightPlanBuilder(self.game, package, - self.is_player) + # Add flight plans for escorts. for flight in package.flights: - flight_plan_builder.populate_flight_plan(flight) + if not flight.flight_plan.waypoints: + flight_plan_builder.populate_flight_plan(flight) self.ato.add_package(package) def stagger_missions(self) -> None: @@ -607,10 +741,12 @@ class CoalitionMissionPlanner: dca_types = { FlightType.BARCAP, - FlightType.INTERCEPTION, FlightType.TARCAP, } + previous_cap_end_time: Dict[MissionTarget, timedelta] = defaultdict( + timedelta + ) non_dca_packages = [p for p in self.ato.packages if p.primary_task not in dca_types] @@ -623,8 +759,22 @@ class CoalitionMissionPlanner: for package in self.ato.packages: tot = TotEstimator(package).earliest_tot() if package.primary_task in dca_types: - # All CAP missions should be on station ASAP. - package.time_over_target = tot + previous_end_time = previous_cap_end_time[package.target] + if tot > previous_end_time: + # Can't get there exactly on time, so get there ASAP. This + # will typically only happen for the first CAP at each + # target. + package.time_over_target = tot + else: + package.time_over_target = previous_end_time + + departure_time = package.mission_departure_time + # Should be impossible for CAPs + if departure_time is None: + logging.error( + f"Could not determine mission end time for {package}") + continue + previous_cap_end_time[package.target] = departure_time else: # But other packages should be spread out a bit. Note that take # times are delayed, but all aircraft will become active at diff --git a/gen/flights/ai_flight_planner_db.py b/gen/flights/ai_flight_planner_db.py index 2b00f2fe..99782aba 100644 --- a/gen/flights/ai_flight_planner_db.py +++ b/gen/flights/ai_flight_planner_db.py @@ -13,6 +13,7 @@ from dcs.helicopters import ( SA342L, SA342M, UH_1H, + SH_60B ) from dcs.planes import ( AJS37, @@ -63,6 +64,7 @@ from dcs.planes import ( P_51D, P_51D_30_NA, RQ_1A_Predator, + S_3B, SpitfireLFMkIX, SpitfireLFMkIXCW, Su_17M4, @@ -92,443 +94,278 @@ from pydcs_extensions.f22a.f22a import F_22A from pydcs_extensions.mb339.mb339 import MB_339PAN from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_M, Rafale_B from pydcs_extensions.su57.su57 import Su_57 +from pydcs_extensions.hercules.hercules import Hercules + +# All aircraft lists are in priority order. Aircraft higher in the list will be +# preferred over those lower in the list. # TODO: These lists really ought to be era (faction) dependent. # Factions which have F-5s, F-86s, and A-4s will should prefer F-5s for CAP, but # factions that also have F-4s should not. -# Interceptor are the aircraft prioritized for interception tasks -# If none is available, the AI will use regular CAP-capable aircraft instead -INTERCEPT_CAPABLE = [ - MiG_21Bis, - MiG_25PD, - MiG_31, - MiG_29S, - MiG_29A, - MiG_29G, - MiG_29K, - JF_17, - J_11A, - Su_27, - Su_30, - Su_33, - M_2000C, - Mirage_2000_5, - Rafale_M, - - F_14A_135_GR, - F_14B, - F_15C, - F_16A, - F_16C_50, - FA_18C_hornet, - -] - # Used for CAP, Escort, and intercept if there is not a specialised aircraft available CAP_CAPABLE = [ - - MiG_15bis, - MiG_19P, - MiG_21Bis, - MiG_23MLD, - MiG_25PD, - MiG_29A, - MiG_29G, - MiG_29S, + Su_57, + F_22A, MiG_31, - + F_14B, + F_14A_135_GR, + MiG_25PD, + Rafale_M, + Su_33, + Su_30, Su_27, J_11A, - JF_17, - Su_30, - Su_33, - Su_57, - - M_2000C, - Mirage_2000_5, - - F_86F_Sabre, - F_4E, - F_5E_3, - F_14A_135_GR, - F_14B, F_15C, - F_15E, - F_16A, + MiG_29S, + MiG_29K, + MiG_29G, + MiG_29A, F_16C_50, FA_18C_hornet, - F_22A, - + F_15E, + F_16A, + F_4E, + JF_17, + MiG_23MLD, + MiG_21Bis, + Mirage_2000_5, + M_2000C, + F_5E_3, + MiG_19P, + A_4E_C, + F_86F_Sabre, + MiG_15bis, C_101CC, L_39ZA, - P_51D_30_NA, P_51D, + SpitfireLFMkIXCW, + SpitfireLFMkIX, + Bf_109K_4, + FW_190D9, + FW_190A8, P_47D_30, P_47D_30bl1, P_47D_40, - I_16, - - SpitfireLFMkIXCW, - SpitfireLFMkIX, - - Bf_109K_4, - FW_190D9, - FW_190A8, - - A_4E_C, - Rafale_M, ] -CAP_PREFERRED = [ - MiG_15bis, - MiG_19P, - MiG_21Bis, - MiG_23MLD, - MiG_29A, - MiG_29G, - MiG_29S, - - Su_27, - J_11A, - JF_17, - Su_30, - Su_33, - Su_57, - - M_2000C, - Mirage_2000_5, - - F_86F_Sabre, - F_14A_135_GR, - F_14B, - F_15C, - F_16C_50, - F_22A, - - P_51D_30_NA, - P_51D, - - SpitfireLFMkIXCW, - SpitfireLFMkIX, - - I_16, - - Bf_109K_4, - FW_190D9, - FW_190A8, - - Rafale_M, -] # Used for CAS (Close air support) and BAI (Battlefield Interdiction) CAS_CAPABLE = [ - - MiG_15bis, - MiG_29A, - MiG_27K, - MiG_29S, - - Su_17M4, - Su_24M, - Su_24MR, - Su_25, - Su_25T, - Su_25TM, - Su_30, - Su_34, - - JF_17, - - M_2000C, - - A_10A, - A_10C, A_10C_2, - AV8BNA, - - F_86F_Sabre, - F_5E_3, - + A_10C, + B_1B, + F_14B, + F_14A_135_GR, + Su_25TM, + Su_25T, + Su_25, + F_15E, F_16C_50, FA_18C_hornet, - F_15E, - F_22A, - - Tornado_IDS, + Rafale_A_S, + Rafale_B, Tornado_GR4, - + Tornado_IDS, + JF_17, + A_10A, + A_4E_C, + AJS37, + Su_24MR, + Su_24M, + Su_17M4, + AV8BNA, + S_3B, + Su_34, + Su_30, + MiG_29S, + MiG_27K, + MiG_29A, + AH_64D, + AH_64A, + AH_1W, + OH_58D, + SA342M, + SA342L, + Ka_50, + Mi_28N, + Mi_24V, + Mi_8MT, + UH_1H, + MiG_15bis, + M_2000C, + F_5E_3, + F_86F_Sabre, C_101CC, MB_339PAN, L_39ZA, - AJS37, - - SA342M, - SA342L, - OH_58D, - - AH_64A, - AH_64D, - AH_1W, - - UH_1H, - - Mi_8MT, - Mi_28N, - Mi_24V, - Ka_50, - + A_20G, + P_47D_40, + P_47D_30bl1, + P_47D_30, P_51D_30_NA, P_51D, - P_47D_30, - P_47D_30bl1, - P_47D_40, - A_20G, - SpitfireLFMkIXCW, SpitfireLFMkIX, - I_16, - Bf_109K_4, FW_190D9, FW_190A8, - - A_4E_C, - Rafale_A_S, - Rafale_B, - WingLoong_I, MQ_9_Reaper, - RQ_1A_Predator + RQ_1A_Predator, ] -CAS_PREFERRED = [ - Su_17M4, - Su_24M, - Su_24MR, - Su_25, - Su_25T, - Su_25TM, - Su_30, - Su_34, - A_10A, - A_10C, - A_10C_2, - AV8BNA, - - Tornado_GR4, - - C_101CC, - MB_339PAN, - L_39ZA, - AJS37, - - SA342M, - SA342L, - OH_58D, - - AH_64A, - AH_64D, - AH_1W, - - Mi_28N, - Mi_24V, - Ka_50, - - P_47D_30, - P_47D_30bl1, - P_47D_40, - A_20G, - I_16, - - A_4E_C, - Rafale_A_S, - Rafale_B, - - WingLoong_I, - MQ_9_Reaper, - RQ_1A_Predator -] - -# Aircraft used for SEAD / DEAD tasks +# Aircraft used for SEAD tasks SEAD_CAPABLE = [ - F_4E, - FA_18C_hornet, - - F_16C_50, - AV8BNA, JF_17, - - Su_24M, - Su_25T, - Su_25TM, - Su_17M4, - Su_30, - Su_34, - MiG_27K, - - Tornado_IDS, - Tornado_GR4, - - A_4E_C, - Rafale_A_S, - Rafale_B -] - -SEAD_PREFERRED = [ - F_4E, - Su_25T, - Su_25TM, - Tornado_IDS, F_16C_50, FA_18C_hornet, - Su_30, - Su_34, + Tornado_IDS, + Su_25T, + Su_25TM, + Rafale_A_S, + Rafale_B, + F_4E, + A_4E_C, + AV8BNA, Su_24M, + Su_17M4, + Su_34, + Su_30, + MiG_27K, + Tornado_GR4, + F_117A, + B_17G, + A_20G, + P_47D_40, + P_47D_30bl1, + P_47D_30, + P_51D_30_NA, + P_51D, + SpitfireLFMkIXCW, + SpitfireLFMkIX, + Bf_109K_4, + FW_190D9, + FW_190A8, ] + +# Aircraft used for DEAD tasks +DEAD_CAPABLE = [ + AJS37, + F_14B, + F_14A_135_GR, + B_1B, + B_52H, + Tu_160, + Tu_95MS, +] + SEAD_CAPABLE + + # Aircraft used for Strike mission STRIKE_CAPABLE = [ - MiG_15bis, - MiG_21Bis, - MiG_27K, - MB_339PAN, - - Su_17M4, - Su_24M, - Su_24MR, - Su_25, - Su_25T, - Su_25TM, - Su_27, - Su_33, - Su_30, - Su_34, - MiG_29A, - MiG_29G, - MiG_29K, - MiG_29S, - - Tu_160, - Tu_22M3, - Tu_95MS, - - JF_17, - - M_2000C, - - A_10C, - A_10C_2, - AV8BNA, - - F_86F_Sabre, - F_5E_3, - - F_14A_135_GR, - F_14B, - F_15E, - F_16A, - F_16C_50, - FA_18C_hornet, - + F_117A, B_1B, B_52H, - F_117A, - - Tornado_IDS, + Tu_160, + Tu_95MS, + Tu_22M3, + F_15E, + AJS37, + Rafale_A_S, + Rafale_B, Tornado_GR4, - + F_16C_50, + FA_18C_hornet, + F_16A, + F_14B, + F_14A_135_GR, + Tornado_IDS, + Su_17M4, + Su_24MR, + Su_24M, + Su_25TM, + Su_25T, + Su_25, + Su_34, + Su_33, + Su_30, + Su_27, + MiG_29S, + MiG_29K, + MiG_29G, + MiG_29A, + JF_17, + A_10C_2, + A_10C, + AV8BNA, + S_3B, + A_4E_C, + M_2000C, + MiG_27K, + MiG_21Bis, + MiG_15bis, + F_5E_3, + F_86F_Sabre, + MB_339PAN, C_101CC, L_39ZA, - AJS37, - + B_17G, + A_20G, + P_47D_40, + P_47D_30bl1, + P_47D_30, P_51D_30_NA, P_51D, - P_47D_30, - P_47D_30bl1, - P_47D_40, - A_20G, - B_17G, - SpitfireLFMkIXCW, SpitfireLFMkIX, - Bf_109K_4, FW_190D9, FW_190A8, - - A_4E_C, - Rafale_A_S, - Rafale_B - ] -STRIKE_PREFERRED = [ - AJS37, - A_20G, - B_17G, - B_1B, - B_52H, - F_117A, - F_15E, - Su_24M, - Su_30, - Su_34, - Tornado_IDS, - Tornado_GR4, - Tu_160, - Tu_22M3, - Tu_95MS, -] ANTISHIP_CAPABLE = [ AJS37, - C_101CC, - Su_24M, - Su_17M4, - FA_18C_hornet, - - AV8BNA, - JF_17, - - Su_30, - Su_34, Tu_22M3, - - Tornado_IDS, - Tornado_GR4, - - Ju_88A4, - Rafale_A_S, - Rafale_B -] - -ANTISHIP_PREFERRED = [ - AJS37, - C_101CC, FA_18C_hornet, - JF_17, Rafale_A_S, Rafale_B, Su_24M, - Su_30, - Su_34, - Tu_22M3, - Ju_88A4 -] - -RUNWAY_ATTACK_PREFERRED = [ + Su_17M4, JF_17, - Su_30, Su_34, + Su_30, Tornado_IDS, + Tornado_GR4, + AV8BNA, + S_3B, + A_20G, + Ju_88A4, + C_101CC, + SH_60B, ] -RUNWAY_ATTACK_CAPABLE = STRIKE_CAPABLE + +# Duplicates some list entries but that's fine. +RUNWAY_ATTACK_CAPABLE = [ + JF_17, + Su_34, + Su_30, + Tornado_IDS, +] + STRIKE_CAPABLE + +# For any aircraft that isn't necessarily directly involved in strike +# missions in a direct combat sense, but can transport objects and infantry. +TRANSPORT_CAPABLE = [ + Hercules, + Mi_8MT, + UH_1H, +] DRONES = [ MQ_9_Reaper, @@ -537,31 +374,7 @@ DRONES = [ ] -def preferred_aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]: - cap_missions = (FlightType.BARCAP, FlightType.TARCAP) - if task in cap_missions: - return CAP_PREFERRED - elif task == FlightType.ANTISHIP: - return ANTISHIP_PREFERRED - elif task == FlightType.BAI: - return CAS_CAPABLE - elif task == FlightType.CAS: - return CAS_PREFERRED - elif task in (FlightType.DEAD, FlightType.SEAD): - return SEAD_PREFERRED - elif task == FlightType.OCA_AIRCRAFT: - return CAS_PREFERRED - elif task == FlightType.OCA_RUNWAY: - return RUNWAY_ATTACK_PREFERRED - elif task == FlightType.STRIKE: - return STRIKE_PREFERRED - elif task == FlightType.ESCORT: - return CAP_PREFERRED - else: - return [] - - -def capable_aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]: +def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]: cap_missions = (FlightType.BARCAP, FlightType.TARCAP) if task in cap_missions: return CAP_CAPABLE @@ -571,8 +384,10 @@ def capable_aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]: return CAS_CAPABLE elif task == FlightType.CAS: return CAS_CAPABLE - elif task in (FlightType.DEAD, FlightType.SEAD): + elif task == FlightType.SEAD: return SEAD_CAPABLE + elif task == FlightType.DEAD: + return DEAD_CAPABLE elif task == FlightType.OCA_AIRCRAFT: return CAS_CAPABLE elif task == FlightType.OCA_RUNWAY: diff --git a/gen/flights/closestairfields.py b/gen/flights/closestairfields.py index 5bba28db..7dfab22f 100644 --- a/gen/flights/closestairfields.py +++ b/gen/flights/closestairfields.py @@ -1,7 +1,12 @@ """Objective adjacency lists.""" -from typing import Dict, Iterator, List, Optional +from __future__ import annotations -from game.theater import ConflictTheater, ControlPoint, MissionTarget +from typing import Dict, Iterator, List, Optional, TYPE_CHECKING + +from game.utils import Distance + +if TYPE_CHECKING: + from game.theater import ConflictTheater, ControlPoint, MissionTarget class ClosestAirfields: @@ -10,18 +15,25 @@ class ClosestAirfields: def __init__(self, target: MissionTarget, all_control_points: List[ControlPoint]) -> None: self.target = target + # This cache is configured once on load, so it's important that it is + # complete and deterministic to avoid different behaviors across loads. + # E.g. https://github.com/Khopa/dcs_liberation/issues/819 self.closest_airfields: List[ControlPoint] = sorted( all_control_points, key=lambda c: self.target.distance_to(c) ) - def airfields_within(self, meters: int) -> Iterator[ControlPoint]: + @property + def operational_airfields(self) -> Iterator[ControlPoint]: + return (c for c in self.closest_airfields if c.runway_is_operational()) + + def airfields_within(self, distance: Distance) -> Iterator[ControlPoint]: """Iterates over all airfields within the given range of the target. Note that this iterates over *all* airfields, not just friendly airfields. """ for cp in self.closest_airfields: - if cp.distance_to(self.target) < meters: + if cp.distance_to(self.target) < distance.meters: yield cp else: break diff --git a/gen/flights/flight.py b/gen/flights/flight.py index b3f5c286..06ed2c8b 100644 --- a/gen/flights/flight.py +++ b/gen/flights/flight.py @@ -1,5 +1,6 @@ from __future__ import annotations +from collections import defaultdict from datetime import timedelta from enum import Enum from typing import Dict, List, Optional, TYPE_CHECKING, Type @@ -9,7 +10,9 @@ from dcs.point import MovingPoint, PointAction from dcs.unittype import FlyingType from game import db +from game.data.weapons import Weapon from game.theater.controlpoint import ControlPoint, MissionTarget +from game.utils import Distance, meters if TYPE_CHECKING: from gen.ato import Package @@ -67,7 +70,7 @@ class FlightWaypointType(Enum): class FlightWaypoint: def __init__(self, waypoint_type: FlightWaypointType, x: float, y: float, - alt: int = 0) -> None: + alt: Distance = meters(0)) -> None: """Creates a flight waypoint. Args: @@ -83,6 +86,9 @@ class FlightWaypoint: self.alt = alt self.alt_type = "BARO" self.name = "" + # TODO: Merge with pretty_name. + # Only used in the waypoint list in the flight edit page. No sense + # having three names. A short and long form is enough. self.description = "" self.targets: List[MissionTarget] = [] self.obj_name = "" @@ -105,7 +111,7 @@ class FlightWaypoint: def from_pydcs(cls, point: MovingPoint, from_cp: ControlPoint) -> "FlightWaypoint": waypoint = FlightWaypoint(FlightWaypointType.NAV, point.position.x, - point.position.y, point.alt) + point.position.y, meters(point.alt)) waypoint.alt_type = point.alt_type # Other actions exist... but none of them *should* be the first # waypoint for a flight. @@ -130,11 +136,13 @@ class FlightWaypoint: class Flight: - def __init__(self, package: Package, unit_type: Type[FlyingType], + def __init__(self, package: Package, country: str, unit_type: Type[FlyingType], count: int, flight_type: FlightType, start_type: str, departure: ControlPoint, arrival: ControlPoint, - divert: Optional[ControlPoint]) -> None: + divert: Optional[ControlPoint], + custom_name: Optional[str] = None) -> None: self.package = package + self.country = country self.unit_type = unit_type self.count = count self.departure = departure @@ -143,10 +151,11 @@ class Flight: self.flight_type = flight_type # TODO: Replace with FlightPlan. self.targets: List[MissionTarget] = [] - self.loadout: Dict[str, str] = {} + self.loadout: Dict[int, Optional[Weapon]] = {} self.start_type = start_type self.use_custom_loadout = False self.client_count = 0 + self.custom_name = custom_name # Will be replaced with a more appropriate FlightPlan by # FlightPlanBuilder, but an empty flight plan the flight begins with an @@ -168,4 +177,12 @@ class Flight: def __repr__(self): name = db.unit_type_name(self.unit_type) + if self.custom_name: + return f"{self.custom_name} {self.count} x {name}" + return f"[{self.flight_type}] {self.count} x {name}" + + def __str__(self): + name = db.unit_get_expanded_info(self.country, self.unit_type, 'name') + if self.custom_name: + return f"{self.custom_name} {self.count} x {name}" return f"[{self.flight_type}] {self.count} x {name}" diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index 478275cc..bbf45b1f 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -17,6 +17,7 @@ from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple from dcs.mapping import Point from dcs.unit import Unit +from shapely.geometry import Point as ShapelyPoint from game.data.doctrine import Doctrine from game.theater import ( @@ -28,12 +29,12 @@ from game.theater import ( TheaterGroundObject, ) from game.theater.theatergroundobject import EwrGroundObject -from game.utils import nm_to_meter, meter_to_nm +from game.utils import Distance, Speed, meters, nautical_miles from .closestairfields import ObjectiveDistanceCache from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType from .traveltime import GroundSpeed, TravelTime from .waypointbuilder import StrikeTarget, WaypointBuilder -from ..conflictgen import Conflict +from ..conflictgen import Conflict, FRONTLINE_LENGTH if TYPE_CHECKING: from game import Game @@ -86,7 +87,7 @@ class FlightPlan: return zip(self.waypoints[:last_index], self.waypoints[1:last_index]) def best_speed_between_waypoints(self, a: FlightWaypoint, - b: FlightWaypoint) -> int: + b: FlightWaypoint) -> Speed: """Desired ground speed between points a and b.""" factor = 1.0 if b.waypoint_type == FlightWaypointType.ASCEND_POINT: @@ -105,11 +106,10 @@ class FlightPlan: # We don't have an exact heightmap, but we should probably be performing # *some* adjustment for NTTR since the minimum altitude of the map is # near 2000 ft MSL. - return int( - GroundSpeed.for_flight(self.flight, min(a.alt, b.alt)) * factor) + return GroundSpeed.for_flight(self.flight, min(a.alt, b.alt)) * factor def speed_between_waypoints(self, a: FlightWaypoint, - b: FlightWaypoint) -> int: + b: FlightWaypoint) -> Speed: return self.best_speed_between_waypoints(a, b) @property @@ -126,16 +126,17 @@ class FlightPlan: def bingo_fuel(self) -> int: """Bingo fuel value for the FlightPlan """ - distance_to_arrival = meter_to_nm(self.max_distance_from(self.flight.arrival)) + distance_to_arrival = self.max_distance_from(self.flight.arrival) - bingo = 1000 # Minimum Emergency Fuel - bingo += 500 # Visual Traffic - bingo += 15 * distance_to_arrival + bingo = 1000.0 # Minimum Emergency Fuel + bingo += 500 # Visual Traffic + bingo += 15 * distance_to_arrival.nautical_miles # TODO: Per aircraft tweaks. if self.flight.divert is not None: - bingo += 10 * meter_to_nm(self.max_distance_from(self.flight.divert)) + max_divert_distance = self.max_distance_from(self.flight.divert) + bingo += 10 * max_divert_distance.nautical_miles return round(bingo / 100) * 100 @@ -145,13 +146,14 @@ class FlightPlan: """ return self.bingo_fuel + 1000 - def max_distance_from(self, cp: ControlPoint) -> int: + def max_distance_from(self, cp: ControlPoint) -> Distance: """Returns the farthest waypoint of the flight plan from a ControlPoint. :arg cp The ControlPoint to measure distance from. """ if not self.waypoints: - return 0 - return max([cp.position.distance_to_point(w.position) for w in self.waypoints]) + return meters(0) + return max([meters(cp.position.distance_to_point(w.position)) for w in + self.waypoints]) @property def tot_offset(self) -> timedelta: @@ -242,6 +244,11 @@ class FlightPlan: else: return timedelta(minutes=5) + @property + def mission_departure_time(self) -> timedelta: + """The time that the mission is complete and the flight RTBs.""" + raise NotImplementedError + @dataclass(frozen=True) class LoiterFlightPlan(FlightPlan): @@ -303,7 +310,7 @@ class FormationFlightPlan(LoiterFlightPlan): return self.split @cached_property - def best_flight_formation_speed(self) -> int: + def best_flight_formation_speed(self) -> Speed: """The best speed this flight is capable at all formation waypoints. To ease coordination with other flights, we aim to have a single mission @@ -319,7 +326,7 @@ class FormationFlightPlan(LoiterFlightPlan): return min(speeds) def speed_between_waypoints(self, a: FlightWaypoint, - b: FlightWaypoint) -> int: + b: FlightWaypoint) -> Speed: if b in self.package_speed_waypoints: # Should be impossible, as any package with at least one # FormationFlightPlan flight needs a formation speed. @@ -355,15 +362,27 @@ class FormationFlightPlan(LoiterFlightPlan): GroundSpeed.for_flight(self.flight, self.hold.alt) ) + @property + def mission_departure_time(self) -> timedelta: + return self.split_time + @dataclass(frozen=True) class PatrollingFlightPlan(FlightPlan): + nav_to: List[FlightWaypoint] + nav_from: List[FlightWaypoint] patrol_start: FlightWaypoint patrol_end: FlightWaypoint #: Maximum time to remain on station. patrol_duration: timedelta + #: The engagement range of any Search Then Engage task, or the radius of a + #: Search Then Engage in Zone task. Any enemies of the appropriate type for + #: this mission within this range of the flight's current position (or the + #: center of the zone) will be engaged by the flight. + engagement_distance: Distance + @property def patrol_start_time(self) -> timedelta: return self.package.time_over_target @@ -397,6 +416,10 @@ class PatrollingFlightPlan(FlightPlan): def tot_waypoint(self) -> Optional[FlightWaypoint]: return self.patrol_start + @property + def mission_departure_time(self) -> timedelta: + return self.patrol_end_time + @dataclass(frozen=True) class BarCapFlightPlan(PatrollingFlightPlan): @@ -405,12 +428,14 @@ class BarCapFlightPlan(PatrollingFlightPlan): divert: Optional[FlightWaypoint] def iter_waypoints(self) -> Iterator[FlightWaypoint]: + yield self.takeoff + yield from self.nav_to yield from [ - self.takeoff, self.patrol_start, self.patrol_end, - self.land, ] + yield from self.nav_from + yield self.land if self.divert is not None: yield self.divert @@ -423,13 +448,15 @@ class CasFlightPlan(PatrollingFlightPlan): divert: Optional[FlightWaypoint] def iter_waypoints(self) -> Iterator[FlightWaypoint]: + yield self.takeoff + yield from self.nav_to yield from [ - self.takeoff, self.patrol_start, self.target, self.patrol_end, - self.land, ] + yield from self.nav_from + yield self.land if self.divert is not None: yield self.divert @@ -448,12 +475,14 @@ class TarCapFlightPlan(PatrollingFlightPlan): lead_time: timedelta def iter_waypoints(self) -> Iterator[FlightWaypoint]: + yield self.takeoff + yield from self.nav_to yield from [ - self.takeoff, self.patrol_start, self.patrol_end, - self.land, ] + yield from self.nav_from + yield self.land if self.divert is not None: yield self.divert @@ -486,27 +515,27 @@ class TarCapFlightPlan(PatrollingFlightPlan): class StrikeFlightPlan(FormationFlightPlan): takeoff: FlightWaypoint hold: FlightWaypoint + nav_to: List[FlightWaypoint] join: FlightWaypoint ingress: FlightWaypoint targets: List[FlightWaypoint] egress: FlightWaypoint split: FlightWaypoint + nav_from: List[FlightWaypoint] land: FlightWaypoint divert: Optional[FlightWaypoint] def iter_waypoints(self) -> Iterator[FlightWaypoint]: - yield from [ - self.takeoff, - self.hold, - self.join, - self.ingress - ] + yield self.takeoff + yield self.hold + yield from self.nav_to + yield self.join + yield self.ingress yield from self.targets - yield from [ - self.egress, - self.split, - self.land, - ] + yield self.egress + yield self.split + yield from self.nav_from + yield self.land if self.divert is not None: yield self.divert @@ -519,7 +548,7 @@ class StrikeFlightPlan(FormationFlightPlan): } | set(self.targets) def speed_between_waypoints(self, a: FlightWaypoint, - b: FlightWaypoint) -> int: + b: FlightWaypoint) -> Speed: # FlightWaypoint is only comparable by identity, so adding # target_area_waypoint to package_speed_waypoints is useless. if b.waypoint_type == FlightWaypointType.TARGET_GROUP_LOC: @@ -537,7 +566,8 @@ class StrikeFlightPlan(FormationFlightPlan): def target_area_waypoint(self) -> FlightWaypoint: return FlightWaypoint(FlightWaypointType.TARGET_GROUP_LOC, self.package.target.position.x, - self.package.target.position.y, 0) + self.package.target.position.y, + meters(0)) @property def travel_time_to_target(self) -> timedelta: @@ -562,7 +592,7 @@ class StrikeFlightPlan(FormationFlightPlan): return total @property - def mission_speed(self) -> int: + def mission_speed(self) -> Speed: return GroundSpeed.for_flight(self.flight, self.ingress.alt) @property @@ -604,20 +634,22 @@ class StrikeFlightPlan(FormationFlightPlan): @dataclass(frozen=True) class SweepFlightPlan(LoiterFlightPlan): takeoff: FlightWaypoint + nav_to: List[FlightWaypoint] sweep_start: FlightWaypoint sweep_end: FlightWaypoint + nav_from: List[FlightWaypoint] land: FlightWaypoint divert: Optional[FlightWaypoint] lead_time: timedelta def iter_waypoints(self) -> Iterator[FlightWaypoint]: - yield from [ - self.takeoff, - self.hold, - self.sweep_start, - self.sweep_end, - self.land, - ] + yield self.takeoff + yield self.hold + yield from self.nav_to + yield self.sweep_start + yield self.sweep_end + yield from self.nav_from + yield self.land if self.divert is not None: yield self.divert @@ -660,6 +692,9 @@ class SweepFlightPlan(LoiterFlightPlan): GroundSpeed.for_flight(self.flight, self.hold.alt) ) + def mission_departure_time(self) -> timedelta: + return self.sweep_end_time + @dataclass(frozen=True) class CustomFlightPlan(FlightPlan): @@ -690,6 +725,10 @@ class CustomFlightPlan(FlightPlan): self, waypoint: FlightWaypoint) -> Optional[timedelta]: return None + @property + def mission_departure_time(self) -> timedelta: + return self.package.time_over_target + class FlightPlanBuilder: """Generates flight plans for flights.""" @@ -710,6 +749,7 @@ class FlightPlanBuilder: else: faction = self.game.enemy_faction self.doctrine: Doctrine = faction.doctrine + self.threat_zones = self.game.threat_zone_for(not self.is_player) def populate_flight_plan( self, flight: Flight, @@ -755,12 +795,79 @@ class FlightPlanBuilder: f"{task} flight plan generation not implemented") def regenerate_package_waypoints(self) -> None: - ingress_point = self._ingress_point() - egress_point = self._egress_point() + # The simple case is where the target is greater than the ingress + # distance into the threat zone and the target is not near the departure + # airfield. In this case, we can plan the shortest route from the + # departure airfield to the target, use the last non-threatened point as + # the join point, and plan the IP inside the threatened area. + # + # When the target is near the edge of the threat zone the IP may need to + # be placed outside the zone. + # + # +--------------+ +---------------+ + # | | | | + # | | IP---+-T | + # | | | | + # | | | | + # +--------------+ +---------------+ + # + # Here we want to place the IP first and route the flight to the IP + # rather than routing to the target and placing the IP based on the join + # point. + # + # The other case that we need to handle is when the target is close to + # the origin airfield. In this case we also need to set up the IP first, + # but depending on the placement of the IP we may need to place the join + # point in a retreating position. + # + # A messy (and very unlikely) case that we can't do much about: + # + # +--------------+ +---------------+ + # | | | | + # | IP-+---+-T | + # | | | | + # | | | | + # +--------------+ +---------------+ + from gen.ato import PackageWaypoints + target = self.package.target.position + + join_point = self.preferred_join_point() + if join_point is None: + # The whole path from the origin airfield to the target is + # threatened. Need to retreat out of the threat area. + join_point = self.retreat_point(self.package_airfield().position) + + attack_heading = join_point.heading_between_point(target) + ingress_point = self._ingress_point(attack_heading) + join_distance = meters(join_point.distance_to_point(target)) + ingress_distance = meters(ingress_point.distance_to_point(target)) + if join_distance < ingress_distance: + # The second case described above. The ingress point is farther from + # the target than the join point. Use the fallback behavior for now. + self.legacy_package_waypoints_impl() + return + + # The first case described above. The ingress and join points are placed + # reasonably relative to each other. + egress_point = self._egress_point(attack_heading) + self.package.waypoints = PackageWaypoints( + WaypointBuilder.perturb(join_point), + ingress_point, + egress_point, + WaypointBuilder.perturb(join_point), + ) + + def retreat_point(self, origin: Point) -> Point: + return self.threat_zones.closest_boundary(origin) + + def legacy_package_waypoints_impl(self) -> None: + from gen.ato import PackageWaypoints + ingress_point = self._ingress_point( + self._target_heading_to_package_airfield()) + egress_point = self._egress_point( + self._target_heading_to_package_airfield()) join_point = self._rendezvous_point(ingress_point) split_point = self._rendezvous_point(egress_point) - - from gen.ato import PackageWaypoints self.package.waypoints = PackageWaypoints( join_point, ingress_point, @@ -768,6 +875,14 @@ class FlightPlanBuilder: split_point, ) + def preferred_join_point(self) -> Optional[Point]: + path = self.game.navmesh_for(self.is_player).shortest_path( + self.package_airfield().position, self.package.target.position) + for point in reversed(path): + if not self.threat_zones.threatened(point): + return point + return None + def generate_strike(self, flight: Flight) -> StrikeFlightPlan: """Generates a strike flight plan. @@ -862,20 +977,25 @@ class FlightPlanBuilder: if isinstance(location, FrontLine): raise InvalidObjectiveLocation(flight.flight_type, location) - start, end = self.racetrack_for_objective(location) - patrol_alt = random.randint( - self.doctrine.min_patrol_altitude, - self.doctrine.max_patrol_altitude - ) + start, end = self.racetrack_for_objective(location, barcap=True) + patrol_alt = meters(random.randint( + int(self.doctrine.min_patrol_altitude.meters), + int(self.doctrine.max_patrol_altitude.meters) + )) - builder = WaypointBuilder(self.game.conditions, flight, self.doctrine) + builder = WaypointBuilder(flight, self.game, self.is_player) start, end = builder.race_track(start, end, patrol_alt) return BarCapFlightPlan( package=self.package, flight=flight, patrol_duration=self.doctrine.cap_duration, + engagement_distance=self.doctrine.cap_engagement_range, takeoff=builder.takeoff(flight.departure), + nav_to=builder.nav_path(flight.departure.position, start.position, + patrol_alt), + nav_from=builder.nav_path(end.position, flight.arrival.position, + patrol_alt), patrol_start=start, patrol_end=end, land=builder.land(flight.arrival), @@ -888,33 +1008,40 @@ class FlightPlanBuilder: Args: flight: The flight to generate the flight plan for. """ + assert self.package.waypoints is not None target = self.package.target.position - heading = self._heading_to_package_airfield(target) + heading = self.package.waypoints.join.heading_between_point(target) start = target.point_from_heading(heading, - -self.doctrine.sweep_distance) + -self.doctrine.sweep_distance.meters) - builder = WaypointBuilder(self.game.conditions, flight, self.doctrine) + builder = WaypointBuilder(flight, self.game, self.is_player) start, end = builder.sweep(start, target, self.doctrine.ingress_altitude) + hold = builder.hold(self._hold_point(flight)) + return SweepFlightPlan( package=self.package, flight=flight, lead_time=timedelta(minutes=5), takeoff=builder.takeoff(flight.departure), - hold=builder.hold(self._hold_point(flight)), + hold=hold, hold_duration=timedelta(minutes=5), + nav_to=builder.nav_path(hold.position, start.position, + self.doctrine.ingress_altitude), + nav_from=builder.nav_path(end.position, flight.arrival.position, + self.doctrine.ingress_altitude), sweep_start=start, sweep_end=end, land=builder.land(flight.arrival), divert=builder.divert(flight.divert) ) - def racetrack_for_objective(self, - location: MissionTarget) -> Tuple[Point, Point]: + def racetrack_for_objective(self, location: MissionTarget, + barcap: bool) -> Tuple[Point, Point]: closest_cache = ObjectiveDistanceCache.get_closest_airfields(location) - for airfield in closest_cache.closest_airfields: + for airfield in closest_cache.operational_airfields: # If the mission is a BARCAP of an enemy airfield, find the *next* # closest enemy airfield. if airfield == self.package.target: @@ -929,11 +1056,28 @@ class FlightPlanBuilder: closest_airfield.position ) - min_distance_from_enemy = nm_to_meter(20) - distance_to_airfield = int(closest_airfield.position.distance_to_point( - self.package.target.position - )) - distance_to_no_fly = distance_to_airfield - min_distance_from_enemy + position = ShapelyPoint(self.package.target.position.x, + self.package.target.position.y) + + if barcap: + # BARCAPs should remain far enough back from the enemy that their + # commit range does not enter the enemy's threat zone. Include a 5nm + # buffer. + distance_to_no_fly = meters( + position.distance(self.threat_zones.all) + ) - self.doctrine.cap_engagement_range - nautical_miles(5) + else: + # Other race tracks (TARCAPs, currently) just try to keep some + # distance from the nearest enemy airbase, but since they are by + # definition in enemy territory they can't avoid the threat zone + # without being useless. + min_distance_from_enemy = nautical_miles(20) + distance_to_airfield = meters( + closest_airfield.position.distance_to_point( + self.package.target.position + )) + distance_to_no_fly = distance_to_airfield - min_distance_from_enemy + min_cap_distance = min(self.doctrine.cap_min_distance_from_cp, distance_to_no_fly) max_cap_distance = min(self.doctrine.cap_max_distance_from_cp, @@ -941,16 +1085,17 @@ class FlightPlanBuilder: end = location.position.point_from_heading( heading, - random.randint(min_cap_distance, max_cap_distance) + random.randint(int(min_cap_distance.meters), + int(max_cap_distance.meters)) ) diameter = random.randint( - self.doctrine.cap_min_track_length, - self.doctrine.cap_max_track_length + int(self.doctrine.cap_min_track_length.meters), + int(self.doctrine.cap_max_track_length.meters) ) start = end.point_from_heading(heading - 180, diameter) return start, end - def racetrack_for_frontline(self, + def racetrack_for_frontline(self, origin: Point, front_line: FrontLine) -> Tuple[Point, Point]: ally_cp, enemy_cp = front_line.control_points @@ -960,7 +1105,8 @@ class FlightPlanBuilder: ) center = ingress.point_from_heading(heading, distance / 2) orbit_center = center.point_from_heading( - heading - 90, random.randint(nm_to_meter(6), nm_to_meter(15)) + heading - 90, random.randint(int(nautical_miles(6).meters), + int(nautical_miles(15).meters)) ) combat_width = distance / 2 @@ -970,10 +1116,12 @@ class FlightPlanBuilder: combat_width = 35000 radius = combat_width * 1.25 - orbit0p = orbit_center.point_from_heading(heading, radius) - orbit1p = orbit_center.point_from_heading(heading + 180, radius) + start = orbit_center.point_from_heading(heading, radius) + end = orbit_center.point_from_heading(heading + 180, radius) - return orbit0p, orbit1p + if end.distance_to_point(origin) < start.distance_to_point(origin): + start, end = end, start + return start, end def generate_tarcap(self, flight: Flight) -> TarCapFlightPlan: """Generate a CAP flight plan for the given front line. @@ -983,16 +1131,19 @@ class FlightPlanBuilder: """ location = self.package.target - patrol_alt = random.randint(self.doctrine.min_patrol_altitude, - self.doctrine.max_patrol_altitude) + patrol_alt = meters( + random.randint(int(self.doctrine.min_patrol_altitude.meters), + int(self.doctrine.max_patrol_altitude.meters))) # Create points - builder = WaypointBuilder(self.game.conditions, flight, self.doctrine) + builder = WaypointBuilder(flight, self.game, self.is_player) if isinstance(location, FrontLine): - orbit0p, orbit1p = self.racetrack_for_frontline(location) + orbit0p, orbit1p = self.racetrack_for_frontline( + flight.departure.position, location) else: - orbit0p, orbit1p = self.racetrack_for_objective(location) + orbit0p, orbit1p = self.racetrack_for_objective(location, + barcap=False) start, end = builder.race_track(orbit0p, orbit1p, patrol_alt) return TarCapFlightPlan( @@ -1004,7 +1155,12 @@ class FlightPlanBuilder: # requests an escort the CAP flight will remain on station for the # duration of the escorted mission, or until it is winchester/bingo. patrol_duration=self.doctrine.cap_duration, + engagement_distance=self.doctrine.cap_engagement_range, takeoff=builder.takeoff(flight.departure), + nav_to=builder.nav_path(flight.departure.position, orbit0p, + patrol_alt), + nav_from=builder.nav_path(orbit1p, flight.arrival.position, + patrol_alt), patrol_start=start, patrol_end=end, land=builder.land(flight.arrival), @@ -1099,22 +1255,29 @@ class FlightPlanBuilder: def generate_escort(self, flight: Flight) -> StrikeFlightPlan: assert self.package.waypoints is not None - builder = WaypointBuilder(self.game.conditions, flight, self.doctrine) + builder = WaypointBuilder(flight, self.game, self.is_player) ingress, target, egress = builder.escort( self.package.waypoints.ingress, self.package.target, self.package.waypoints.egress) + hold = builder.hold(self._hold_point(flight)) + join = builder.join(self.package.waypoints.join) + split = builder.split(self.package.waypoints.split) return StrikeFlightPlan( package=self.package, flight=flight, takeoff=builder.takeoff(flight.departure), - hold=builder.hold(self._hold_point(flight)), + hold=hold, hold_duration=timedelta(minutes=5), - join=builder.join(self.package.waypoints.join), + nav_to=builder.nav_path(hold.position, join.position, + self.doctrine.ingress_altitude), + join=join, ingress=ingress, targets=[target], egress=egress, - split=builder.split(self.package.waypoints.split), + split=split, + nav_from=builder.nav_path(split.position, flight.arrival.position, + self.doctrine.ingress_altitude), land=builder.land(flight.arrival), divert=builder.divert(flight.divert) ) @@ -1137,15 +1300,25 @@ class FlightPlanBuilder: center = ingress.point_from_heading(heading, distance / 2) egress = ingress.point_from_heading(heading, distance) - builder = WaypointBuilder(self.game.conditions, flight, self.doctrine) + ingress_distance = ingress.distance_to_point(flight.departure.position) + egress_distance = egress.distance_to_point(flight.departure.position) + if egress_distance < ingress_distance: + ingress, egress = egress, ingress + + builder = WaypointBuilder(flight, self.game, self.is_player) return CasFlightPlan( package=self.package, flight=flight, patrol_duration=self.doctrine.cas_duration, takeoff=builder.takeoff(flight.departure), + nav_to=builder.nav_path(flight.departure.position, ingress, + self.doctrine.ingress_altitude), + nav_from=builder.nav_path(egress, flight.arrival.position, + self.doctrine.ingress_altitude), patrol_start=builder.ingress(FlightWaypointType.INGRESS_CAS, ingress, location), + engagement_distance=meters(FRONTLINE_LENGTH) / 2, target=builder.cas(center), patrol_end=builder.egress(egress, location), land=builder.land(flight.arrival), @@ -1188,12 +1361,13 @@ class FlightPlanBuilder: # point, plan the hold point such that it retreats from the origin # airfield. return join.point_from_heading(target.heading_between_point(origin), - self.doctrine.push_distance) + self.doctrine.push_distance.meters) heading_to_join = origin.heading_between_point(join) - hold_point = origin.point_from_heading(heading_to_join, - self.doctrine.push_distance) - if hold_point.distance_to_point(join) >= self.doctrine.push_distance: + hold_point = origin.point_from_heading( + heading_to_join, self.doctrine.push_distance.meters) + hold_distance = meters(hold_point.distance_to_point(join)) + if hold_distance >= self.doctrine.push_distance: # Hold point is between the origin airfield and the join point and # spaced sufficiently. return hold_point @@ -1205,10 +1379,10 @@ class FlightPlanBuilder: # properly. origin_to_join = origin.distance_to_point(join) cos_theta = ( - (self.doctrine.hold_distance ** 2 + + (self.doctrine.hold_distance.meters ** 2 + origin_to_join ** 2 - - self.doctrine.join_distance ** 2) / - (2 * self.doctrine.hold_distance * origin_to_join) + self.doctrine.join_distance.meters ** 2) / + (2 * self.doctrine.hold_distance.meters * origin_to_join) ) try: theta = math.acos(cos_theta) @@ -1217,10 +1391,10 @@ class FlightPlanBuilder: # hold point away from the target. return origin.point_from_heading( target.heading_between_point(origin), - self.doctrine.hold_distance) + self.doctrine.hold_distance.meters) return origin.point_from_heading(heading_to_join - theta, - self.doctrine.hold_distance) + self.doctrine.hold_distance.meters) # TODO: Make a model for the waypoint builder and use that in the UI. def generate_rtb_waypoint(self, flight: Flight, @@ -1231,7 +1405,7 @@ class FlightPlanBuilder: flight: The flight to generate the landing waypoint for. arrival: Arrival airfield or carrier. """ - builder = WaypointBuilder(self.game.conditions, flight, self.doctrine) + builder = WaypointBuilder(flight, self.game, self.is_player) return builder.land(arrival) def strike_flightplan( @@ -1239,8 +1413,7 @@ class FlightPlanBuilder: ingress_type: FlightWaypointType, targets: Optional[List[StrikeTarget]] = None) -> StrikeFlightPlan: assert self.package.waypoints is not None - builder = WaypointBuilder(self.game.conditions, flight, self.doctrine, - targets) + builder = WaypointBuilder(flight, self.game, self.is_player, targets) target_waypoints: List[FlightWaypoint] = [] if targets is not None: @@ -1251,18 +1424,26 @@ class FlightPlanBuilder: target_waypoints.append( self.target_area_waypoint(flight, location, builder)) + hold = builder.hold(self._hold_point(flight)) + join = builder.join(self.package.waypoints.join) + split = builder.split(self.package.waypoints.split) + return StrikeFlightPlan( package=self.package, flight=flight, takeoff=builder.takeoff(flight.departure), - hold=builder.hold(self._hold_point(flight)), + hold=hold, hold_duration=timedelta(minutes=5), - join=builder.join(self.package.waypoints.join), + nav_to=builder.nav_path(hold.position, join.position, + self.doctrine.ingress_altitude), + join=join, ingress=builder.ingress(ingress_type, self.package.waypoints.ingress, location), targets=target_waypoints, egress=builder.egress(self.package.waypoints.egress, location), - split=builder.split(self.package.waypoints.split), + split=split, + nav_from=builder.nav_path(split.position, flight.arrival.position, + self.doctrine.ingress_altitude), land=builder.land(flight.arrival), divert=builder.divert(flight.divert) ) @@ -1272,13 +1453,13 @@ class FlightPlanBuilder: return attack_transition.point_from_heading( self.package.target.position.heading_between_point( self.package_airfield().position), - self.doctrine.join_distance) + self.doctrine.join_distance.meters) def _advancing_rendezvous_point(self, attack_transition: Point) -> Point: """Creates a rendezvous point that advances toward the target.""" heading = self._heading_to_package_airfield(attack_transition) return attack_transition.point_from_heading( - heading, -self.doctrine.join_distance) + heading, -self.doctrine.join_distance.meters) def _rendezvous_should_retreat(self, attack_transition: Point) -> bool: transition_target_distance = attack_transition.distance_to_point( @@ -1303,16 +1484,14 @@ class FlightPlanBuilder: return self._retreating_rendezvous_point(attack_transition) return self._advancing_rendezvous_point(attack_transition) - def _ingress_point(self) -> Point: - heading = self._target_heading_to_package_airfield() + def _ingress_point(self, heading: int) -> Point: return self.package.target.position.point_from_heading( - heading - 180 + 25, self.doctrine.ingress_egress_distance + heading - 180 + 15, self.doctrine.ingress_egress_distance.meters ) - def _egress_point(self) -> Point: - heading = self._target_heading_to_package_airfield() + def _egress_point(self, heading: int) -> Point: return self.package.target.position.point_from_heading( - heading - 180 - 25, self.doctrine.ingress_egress_distance + heading - 180 - 15, self.doctrine.ingress_egress_distance.meters ) def _target_heading_to_package_airfield(self) -> int: @@ -1338,7 +1517,7 @@ class FlightPlanBuilder: cache = ObjectiveDistanceCache.get_closest_airfields( self.package.target ) - for airfield in cache.closest_airfields: + for airfield in cache.operational_airfields: for flight in self.package.flights: if flight.departure == airfield: return airfield diff --git a/gen/flights/traveltime.py b/gen/flights/traveltime.py index 078dabc7..3311fe22 100644 --- a/gen/flights/traveltime.py +++ b/gen/flights/traveltime.py @@ -3,12 +3,19 @@ from __future__ import annotations import logging import math from datetime import timedelta -from typing import Optional, TYPE_CHECKING +from typing import TYPE_CHECKING from dcs.mapping import Point from dcs.unittype import FlyingType -from game.utils import meter_to_nm +from game.utils import ( + Distance, + SPEED_OF_SOUND_AT_SEA_LEVEL, + Speed, + kph, + mach, + meters, +) from gen.flights.flight import Flight if TYPE_CHECKING: @@ -18,7 +25,7 @@ if TYPE_CHECKING: class GroundSpeed: @classmethod - def for_flight(cls, flight: Flight, altitude: int) -> int: + def for_flight(cls, flight: Flight, altitude: Distance) -> Speed: if not issubclass(flight.unit_type, FlyingType): raise TypeError("Flight has non-flying unit") @@ -27,64 +34,29 @@ class GroundSpeed: # on fuel, but mission speed will be fast enough to keep the flight # safer. - c_sound_sea_level = 661.5 - - # DCS's max speed is in kph at 0 MSL. Convert to knots. - max_speed = flight.unit_type.max_speed * 0.539957 - if max_speed > c_sound_sea_level: + # DCS's max speed is in kph at 0 MSL. + max_speed = kph(flight.unit_type.max_speed) + if max_speed > SPEED_OF_SOUND_AT_SEA_LEVEL: # Aircraft is supersonic. Limit to mach 0.8 to conserve fuel and # account for heavily loaded jets. - return int(cls.from_mach(0.8, altitude)) + return mach(0.8, altitude) # For subsonic aircraft, assume the aircraft can reasonably perform at # 80% of its maximum, and that it can maintain the same mach at altitude # as it can at sea level. This probably isn't great assumption, but # might. be sufficient given the wiggle room. We can come up with # another heuristic if needed. - mach = max_speed * 0.8 / c_sound_sea_level - return int(cls.from_mach(mach, altitude)) # knots - - @staticmethod - def from_mach(mach: float, altitude_m: int) -> float: - """Returns the ground speed in knots for the given mach and altitude. - - Args: - mach: The mach number to convert to ground speed. - altitude_m: The altitude in meters. - - Returns: - The ground speed corresponding to the given altitude and mach number - in knots. - """ - # https://www.grc.nasa.gov/WWW/K-12/airplane/atmos.html - altitude_ft = altitude_m * 3.28084 - if altitude_ft <= 36152: - temperature_f = 59 - 0.00356 * altitude_ft - else: - # There's another formula for altitudes over 82k feet, but we better - # not be planning waypoints that high... - temperature_f = -70 - - temperature_k = (temperature_f + 459.67) * (5 / 9) - - # https://www.engineeringtoolbox.com/specific-heat-ratio-d_602.html - # Dependent on temperature, but varies very little (+/-0.001) - # between -40F and 180F. - heat_capacity_ratio = 1.4 - - # https://www.grc.nasa.gov/WWW/K-12/airplane/sound.html - gas_constant = 286 # m^2/s^2/K - c_sound = math.sqrt(heat_capacity_ratio * gas_constant * temperature_k) - # c_sound is in m/s, convert to knots. - return (c_sound * 1.944) * mach + cruise_mach = max_speed.mach() * 0.8 + return mach(cruise_mach, altitude) class TravelTime: @staticmethod - def between_points(a: Point, b: Point, speed: float) -> timedelta: + def between_points(a: Point, b: Point, speed: Speed) -> timedelta: error_factor = 1.1 - distance = meter_to_nm(a.distance_to_point(b)) - return timedelta(hours=distance / speed * error_factor) + distance = meters(a.distance_to_point(b)) + return timedelta( + hours=distance.nautical_miles / speed.knots * error_factor) # TODO: Most if not all of this should move into FlightPlan. diff --git a/gen/flights/waypointbuilder.py b/gen/flights/waypointbuilder.py index 85a83d08..2d5ca9d8 100644 --- a/gen/flights/waypointbuilder.py +++ b/gen/flights/waypointbuilder.py @@ -1,20 +1,31 @@ from __future__ import annotations +import random from dataclasses import dataclass -from typing import List, Optional, Tuple, Union +from typing import ( + Iterable, + Iterator, + List, + Optional, + TYPE_CHECKING, + Tuple, + Union, +) from dcs.mapping import Point from dcs.unit import Unit from dcs.unitgroup import VehicleGroup -from game.data.doctrine import Doctrine +if TYPE_CHECKING: + from game import Game + from game.theater import ( ControlPoint, MissionTarget, OffMapSpawn, TheaterGroundObject, ) -from game.weather import Conditions +from game.utils import Distance, meters, nautical_miles from .flight import Flight, FlightWaypoint, FlightWaypointType @@ -25,12 +36,13 @@ class StrikeTarget: class WaypointBuilder: - def __init__(self, conditions: Conditions, flight: Flight, - doctrine: Doctrine, + def __init__(self, flight: Flight, game: Game, player: bool, targets: Optional[List[StrikeTarget]] = None) -> None: - self.conditions = conditions self.flight = flight - self.doctrine = doctrine + self.conditions = game.conditions + self.doctrine = game.faction_for(player).doctrine + self.threat_zones = game.threat_zone_for(not player) + self.navmesh = game.navmesh_for(player) self.targets = targets @property @@ -53,7 +65,9 @@ class WaypointBuilder: FlightWaypointType.NAV, position.x, position.y, - 500 if self.is_helo else self.doctrine.rendezvous_altitude + meters( + 500 + ) if self.is_helo else self.doctrine.rendezvous_altitude ) waypoint.name = "NAV" waypoint.alt_type = "BARO" @@ -64,7 +78,7 @@ class WaypointBuilder: FlightWaypointType.TAKEOFF, position.x, position.y, - 0 + meters(0) ) waypoint.name = "TAKEOFF" waypoint.alt_type = "RADIO" @@ -84,7 +98,9 @@ class WaypointBuilder: FlightWaypointType.NAV, position.x, position.y, - 500 if self.is_helo else self.doctrine.rendezvous_altitude + meters( + 500 + ) if self.is_helo else self.doctrine.rendezvous_altitude ) waypoint.name = "NAV" waypoint.alt_type = "BARO" @@ -95,7 +111,7 @@ class WaypointBuilder: FlightWaypointType.LANDING_POINT, position.x, position.y, - 0 + meters(0) ) waypoint.name = "LANDING" waypoint.alt_type = "RADIO" @@ -116,12 +132,12 @@ class WaypointBuilder: position = divert.position if isinstance(divert, OffMapSpawn): if self.is_helo: - altitude = 500 + altitude = meters(500) else: altitude = self.doctrine.rendezvous_altitude altitude_type = "BARO" else: - altitude = 0 + altitude = meters(0) altitude_type = "RADIO" waypoint = FlightWaypoint( @@ -142,7 +158,9 @@ class WaypointBuilder: FlightWaypointType.LOITER, position.x, position.y, - 500 if self.is_helo else self.doctrine.rendezvous_altitude + meters( + 500 + ) if self.is_helo else self.doctrine.rendezvous_altitude ) waypoint.pretty_name = "Hold" waypoint.description = "Wait until push time" @@ -154,7 +172,9 @@ class WaypointBuilder: FlightWaypointType.JOIN, position.x, position.y, - 500 if self.is_helo else self.doctrine.ingress_altitude + meters( + 500 + ) if self.is_helo else self.doctrine.ingress_altitude ) waypoint.pretty_name = "Join" waypoint.description = "Rendezvous with package" @@ -166,7 +186,9 @@ class WaypointBuilder: FlightWaypointType.SPLIT, position.x, position.y, - 500 if self.is_helo else self.doctrine.ingress_altitude + meters( + 500 + ) if self.is_helo else self.doctrine.ingress_altitude ) waypoint.pretty_name = "Split" waypoint.description = "Depart from package" @@ -179,7 +201,9 @@ class WaypointBuilder: ingress_type, position.x, position.y, - 500 if self.is_helo else self.doctrine.ingress_altitude + meters( + 500 + ) if self.is_helo else self.doctrine.ingress_altitude ) waypoint.pretty_name = "INGRESS on " + objective.name waypoint.description = "INGRESS on " + objective.name @@ -193,7 +217,9 @@ class WaypointBuilder: FlightWaypointType.EGRESS, position.x, position.y, - 500 if self.is_helo else self.doctrine.ingress_altitude + meters( + 500 + ) if self.is_helo else self.doctrine.ingress_altitude ) waypoint.pretty_name = "EGRESS from " + target.name waypoint.description = "EGRESS from " + target.name @@ -218,7 +244,7 @@ class WaypointBuilder: FlightWaypointType.TARGET_POINT, target.target.position.x, target.target.position.y, - 0 + meters(0) ) waypoint.description = description waypoint.pretty_name = description @@ -249,7 +275,7 @@ class WaypointBuilder: FlightWaypointType.TARGET_GROUP_LOC, location.position.x, location.position.y, - 0 + meters(0) ) waypoint.description = name waypoint.pretty_name = name @@ -274,7 +300,7 @@ class WaypointBuilder: FlightWaypointType.CAS, position.x, position.y, - 500 if self.is_helo else 1000 + meters(500) if self.is_helo else meters(1000) ) waypoint.alt_type = "RADIO" waypoint.description = "Provide CAS" @@ -283,12 +309,12 @@ class WaypointBuilder: return waypoint @staticmethod - def race_track_start(position: Point, altitude: int) -> FlightWaypoint: + def race_track_start(position: Point, altitude: Distance) -> FlightWaypoint: """Creates a racetrack start waypoint. Args: position: Position of the waypoint. - altitude: Altitude of the racetrack in meters. + altitude: Altitude of the racetrack. """ waypoint = FlightWaypoint( FlightWaypointType.PATROL_TRACK, @@ -302,12 +328,12 @@ class WaypointBuilder: return waypoint @staticmethod - def race_track_end(position: Point, altitude: int) -> FlightWaypoint: + def race_track_end(position: Point, altitude: Distance) -> FlightWaypoint: """Creates a racetrack end waypoint. Args: position: Position of the waypoint. - altitude: Altitude of the racetrack in meters. + altitude: Altitude of the racetrack. """ waypoint = FlightWaypoint( FlightWaypointType.PATROL, @@ -321,7 +347,7 @@ class WaypointBuilder: return waypoint def race_track(self, start: Point, end: Point, - altitude: int) -> Tuple[FlightWaypoint, FlightWaypoint]: + altitude: Distance) -> Tuple[FlightWaypoint, FlightWaypoint]: """Creates two waypoint for a racetrack orbit. Args: @@ -333,7 +359,7 @@ class WaypointBuilder: self.race_track_end(end, altitude)) @staticmethod - def sweep_start(position: Point, altitude: int) -> FlightWaypoint: + def sweep_start(position: Point, altitude: Distance) -> FlightWaypoint: """Creates a sweep start waypoint. Args: @@ -352,7 +378,7 @@ class WaypointBuilder: return waypoint @staticmethod - def sweep_end(position: Point, altitude: int) -> FlightWaypoint: + def sweep_end(position: Point, altitude: Distance) -> FlightWaypoint: """Creates a sweep end waypoint. Args: @@ -371,7 +397,7 @@ class WaypointBuilder: return waypoint def sweep(self, start: Point, end: Point, - altitude: int) -> Tuple[FlightWaypoint, FlightWaypoint]: + altitude: Distance) -> Tuple[FlightWaypoint, FlightWaypoint]: """Creates two waypoint for a racetrack orbit. Args: @@ -404,7 +430,9 @@ class WaypointBuilder: FlightWaypointType.TARGET_GROUP_LOC, target.position.x, target.position.y, - 500 if self.is_helo else self.doctrine.ingress_altitude + meters( + 500 + ) if self.is_helo else self.doctrine.ingress_altitude ) waypoint.name = "TARGET" waypoint.description = "Escort the package" @@ -412,3 +440,80 @@ class WaypointBuilder: egress = self.egress(egress, target) return ingress, waypoint, egress + + @staticmethod + def nav(position: Point, altitude: Distance) -> FlightWaypoint: + """Creates a navigation point. + + Args: + position: Position of the waypoint. + altitude: Altitude of the waypoint. + """ + waypoint = FlightWaypoint( + FlightWaypointType.NAV, + position.x, + position.y, + altitude + ) + waypoint.name = "NAV" + waypoint.description = "NAV" + waypoint.pretty_name = "Nav" + return waypoint + + def nav_path(self, a: Point, b: Point, + altitude: Distance) -> List[FlightWaypoint]: + path = self.clean_nav_points(self.navmesh.shortest_path(a, b)) + return [self.nav(self.perturb(p), altitude) for p in path] + + def clean_nav_points(self, points: Iterable[Point]) -> Iterator[Point]: + # Examine a sliding window of three waypoints. `current` is the waypoint + # being checked for prunability. `previous` is the last emitted waypoint + # before `current`. `nxt` is the waypoint after `current`. + previous: Optional[Point] = None + current: Optional[Point] = None + for nxt in points: + if current is None: + current = nxt + continue + if previous is None: + previous = current + current = nxt + continue + + if self.nav_point_prunable(previous, current, nxt): + current = nxt + continue + + yield current + previous = current + current = nxt + + def nav_point_prunable(self, previous: Point, current: Point, + nxt: Point) -> bool: + previous_threatened = self.threat_zones.path_threatened(previous, + current) + next_threatened = self.threat_zones.path_threatened(current, nxt) + pruned_threatened = self.threat_zones.path_threatened(previous, nxt) + previous_distance = meters(previous.distance_to_point(current)) + distance = meters(current.distance_to_point(nxt)) + distance_without = previous_distance + distance + if distance > distance_without: + # Don't prune paths to make them longer. + return False + + # We could shorten the path by removing the intermediate + # waypoint. Do so if the new path isn't higher threat. + if not pruned_threatened: + # The new path is not threatened, so safe to prune. + return True + + # The new path is threatened. Only allow if both paths were + # threatened anyway. + return previous_threatened and next_threatened + + @staticmethod + def perturb(point: Point) -> Point: + deviation = nautical_miles(1) + x_adj = random.randint(int(-deviation.meters), int(deviation.meters)) + y_adj = random.randint(int(-deviation.meters), int(deviation.meters)) + return Point(point.x + x_adj, point.y + y_adj) diff --git a/gen/groundobjectsgen.py b/gen/groundobjectsgen.py index 672f7c1d..a19f38ae 100644 --- a/gen/groundobjectsgen.py +++ b/gen/groundobjectsgen.py @@ -35,7 +35,7 @@ from game.theater.theatergroundobject import ( LhaGroundObject, ShipGroundObject, MissileSiteGroundObject, ) from game.unitmap import UnitMap -from game.utils import knots_to_kph, kph_to_mps, mps_to_kph +from game.utils import knots, mps from .radios import RadioFrequency, RadioRegistry from .runways import RunwayData from .tacan import TacanBand, TacanChannel, TacanRegistry @@ -300,13 +300,13 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator): wind = self.game.conditions.weather.wind.at_0m brc = wind.direction + 180 # Aim for 25kts over the deck. - carrier_speed = knots_to_kph(25) - mps_to_kph(wind.speed) + carrier_speed = knots(25) - mps(wind.speed) for attempt in range(5): point = group.points[0].position.point_from_heading( brc, 100000 - attempt * 20000) if self.game.theater.is_in_sea(point): - group.points[0].speed = kph_to_mps(carrier_speed) - group.add_waypoint(point, carrier_speed) + group.points[0].speed = carrier_speed.meters_per_second + group.add_waypoint(point, carrier_speed.kph) return brc return None diff --git a/gen/kneeboard.py b/gen/kneeboard.py index 61f0af9a..97442c59 100644 --- a/gen/kneeboard.py +++ b/gen/kneeboard.py @@ -33,8 +33,7 @@ from dcs.mission import Mission from dcs.unittype import FlyingType from tabulate import tabulate -from game.utils import meter_to_nm -from . import units +from game.utils import meters from .aircraft import AIRCRAFT_DATA, FlightData from .airsupportgen import AwacsInfo, TankerInfo from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator @@ -95,6 +94,23 @@ class KneeboardPageWriter: def write(self, path: Path) -> None: self.image.save(path) + @staticmethod + def wrap_line(inputstr: str, max_length: int) -> str: + if len(inputstr) <= max_length: + return inputstr + tokens = inputstr.split(" ") + output = "" + segments = [] + for token in tokens: + combo = output + " " + token + if len(combo) > max_length: + combo = output + "\n" + token + segments.append(combo) + output = "" + else: + output = combo + return "".join(segments + [output]).strip() + class KneeboardPage: """Base class for all kneeboard pages.""" @@ -111,6 +127,9 @@ class NumberedWaypoint: class FlightPlanBuilder: + + WAYPOINT_DESC_MAX_LEN = 25 + def __init__(self, start_time: datetime.datetime) -> None: self.start_time = start_time self.rows: List[List[str]] = [] @@ -152,8 +171,10 @@ class FlightPlanBuilder: def add_waypoint_row(self, waypoint: NumberedWaypoint) -> None: self.rows.append([ str(waypoint.number), - waypoint.waypoint.pretty_name, - str(int(units.meters_to_feet(waypoint.waypoint.alt))), + KneeboardPageWriter.wrap_line( + waypoint.waypoint.pretty_name, + FlightPlanBuilder.WAYPOINT_DESC_MAX_LEN), + str(int(waypoint.waypoint.alt.feet)), self._waypoint_distance(waypoint.waypoint), self._ground_speed(waypoint.waypoint), self._format_time(waypoint.waypoint.tot), @@ -170,10 +191,10 @@ class FlightPlanBuilder: if self.last_waypoint is None: return "-" - distance = meter_to_nm(self.last_waypoint.position.distance_to_point( + distance = meters(self.last_waypoint.position.distance_to_point( waypoint.position )) - return f"{distance} NM" + return f"{distance.nautical_miles:.1f} NM" def _ground_speed(self, waypoint: FlightWaypoint) -> str: if self.last_waypoint is None: @@ -189,19 +210,11 @@ class FlightPlanBuilder: else: return "-" - distance = meter_to_nm(self.last_waypoint.position.distance_to_point( + distance = meters(self.last_waypoint.position.distance_to_point( waypoint.position )) duration = (waypoint.tot - last_time).total_seconds() / 3600 - try: - return f"{int(distance / duration)} kt" - except ZeroDivisionError: - # TODO: Improve resolution of unit conversions. - # When waypoints are very close to each other they can end up with - # identical TOTs because our unit conversion functions truncate to - # int. When waypoints have the same TOT the duration will be zero. - # https://github.com/Khopa/dcs_liberation/issues/557 - return "-" + return f"{int(distance.nautical_miles / duration)} kt" def build(self) -> List[List[str]]: return self.rows @@ -267,11 +280,9 @@ class BriefingPage(KneeboardPage): str(tanker.tacan), self.format_frequency(tanker.freq), ]) - writer.table(comm_ladder, headers=["Callsign","Task", "Type", "TACAN", "FREQ"]) - writer.heading("JTAC") jtacs = [] for jtac in self.jtacs: diff --git a/gen/naming.py b/gen/naming.py index 40da3a6b..3e59bbc7 100644 --- a/gen/naming.py +++ b/gen/naming.py @@ -1,16 +1,21 @@ -from game import db import random +import time +from typing import List -ALPHA_MILITARY = ["Alpha","Bravo","Charlie","Delta","Echo","Foxtrot", - "Golf","Hotel","India","Juliet","Kilo","Lima","Mike", - "November","Oscar","Papa","Quebec","Romeo","Sierra", - "Tango","Uniform","Victor","Whisky","XRay","Yankee", - "Zulu","Zero"] +from dcs.country import Country +from dcs.unittype import UnitType -class NameGenerator: - number = 0 +from game import db - ANIMALS = [ +from gen.flights.flight import Flight + +ALPHA_MILITARY = ["Alpha", "Bravo", "Charlie", "Delta", "Echo", "Foxtrot", + "Golf", "Hotel", "India", "Juliet", "Kilo", "Lima", "Mike", + "November", "Oscar", "Papa", "Quebec", "Romeo", "Sierra", + "Tango", "Uniform", "Victor", "Whisky", "XRay", "Yankee", + "Zulu", "Zero"] + +ANIMALS = [ "SHARK", "TORTOISE", "BAT", "PANGOLIN", "AARDWOLF", "MONKEY", "BUFFALO", "DOG", "BOBCAT", "LYNX", "PANTHER", "TIGER", "LION", "OWL", "BUTTERFLY", "BISON", "DUCK", "COBRA", "MAMBA", @@ -38,47 +43,92 @@ class NameGenerator: "ANACONDA" ] - def __init__(self): - self.number = 0 - self.ANIMALS = NameGenerator.ANIMALS.copy() +class NameGenerator: + number = 0 + infantry_number = 0 + aircraft_number = 0 - def reset(self): - self.number = 0 - self.ANIMALS = NameGenerator.ANIMALS.copy() + ANIMALS = ANIMALS + existing_alphas: List[str] = [] - def next_unit_name(self, country, parent_base_id, unit_type): - self.number += 1 - return "unit|{}|{}|{}|{}|".format(country.id, self.number, parent_base_id, db.unit_type_name(unit_type)) + @classmethod + def reset(cls): + cls.number = 0 + cls.infantry_number = 0 + cls.ANIMALS = ANIMALS + cls.existing_alphas = [] - def next_infantry_name(self, country, parent_base_id, unit_type): - self.number += 1 - return "infantry|{}|{}|{}|{}|".format(country.id, self.number, parent_base_id, db.unit_type_name(unit_type)) + @classmethod + def reset_numbers(cls): + cls.number = 0 + cls.infantry_number = 0 + cls.aircraft_number = 0 - def next_basedefense_name(self): + @classmethod + def next_aircraft_name(cls, country: Country, parent_base_id: int, flight: Flight): + cls.aircraft_number += 1 + try: + if flight.custom_name: + name_str = flight.custom_name + else: + name_str = "{} {}".format( + flight.package.target.name, flight.flight_type) + except AttributeError: # Here to maintain save compatibility with 2.3 + name_str = "{} {}".format( + flight.package.target.name, flight.flight_type) + return "{}|{}|{}|{}|{}|".format(name_str, country.id, cls.aircraft_number, parent_base_id, db.unit_type_name(flight.unit_type)) + + @classmethod + def next_unit_name(cls, country: Country, parent_base_id: int, unit_type: UnitType): + cls.number += 1 + return "unit|{}|{}|{}|{}|".format(country.id, cls.number, parent_base_id, db.unit_type_name(unit_type)) + + @classmethod + def next_infantry_name(cls, country: Country, parent_base_id: int, unit_type: UnitType): + cls.infantry_number += 1 + return "infantry|{}|{}|{}|{}|".format(country.id, cls.infantry_number, parent_base_id, db.unit_type_name(unit_type)) + + @staticmethod + def next_basedefense_name(): return "basedefense_aa|0|0|" - def next_awacs_name(self, country): - self.number += 1 - return "awacs|{}|{}|0|".format(country.id, self.number) + @classmethod + def next_awacs_name(cls, country: Country): + cls.number += 1 + return "awacs|{}|{}|0|".format(country.id, cls.number) - def next_tanker_name(self, country, unit_type): - self.number += 1 - return "tanker|{}|{}|0|{}".format(country.id, self.number, db.unit_type_name(unit_type)) + @classmethod + def next_tanker_name(cls, country: Country, unit_type: UnitType): + cls.number += 1 + return "tanker|{}|{}|0|{}".format(country.id, cls.number, db.unit_type_name(unit_type)) - def next_carrier_name(self, country): - self.number += 1 - return "carrier|{}|{}|0|".format(country.id, self.number) + @classmethod + def next_carrier_name(cls, country: Country): + cls.number += 1 + return "carrier|{}|{}|0|".format(country.id, cls.number) - def random_objective_name(self): - if len(self.ANIMALS) == 0: - return random.choice(ALPHA_MILITARY).upper() + "#" + str(random.randint(0, 100)) + @classmethod + def random_objective_name(cls): + if len(cls.ANIMALS) == 0: + for i in range(10): + new_name_generated = True + alpha_mil_name = random.choice(ALPHA_MILITARY).upper() + "#" + str(random.randint(0, 100)) + for existing_name in cls.existing_alphas: + if existing_name == alpha_mil_name: + new_name_generated = False + if new_name_generated: + cls.existing_alphas.append(alpha_mil_name) + return alpha_mil_name + + # At this point, give up trying - something has gone wrong and we haven't been able to make a new name in 10 tries. + # We'll just make a longer name using the current unix epoch in nanoseconds. That should be unique... right? + last_chance_name = alpha_mil_name + str(time.time_ns()) + cls.existing_alphas.append(last_chance_name) + return last_chance_name else: - animal = random.choice(self.ANIMALS) - self.ANIMALS.remove(animal) + animal = random.choice(cls.ANIMALS) + cls.ANIMALS.remove(animal) return animal -namegen = NameGenerator() - - - +namegen = NameGenerator diff --git a/gen/sam/aaa_ks19.py b/gen/sam/aaa_ks19.py new file mode 100644 index 00000000..c0eb50ab --- /dev/null +++ b/gen/sam/aaa_ks19.py @@ -0,0 +1,34 @@ +import random + +from gen.sam.airdefensegroupgenerator import ( + AirDefenseRange, + AirDefenseGroupGenerator, +) +from pydcs_extensions.highdigitsams import highdigitsams + + +class KS19Generator(AirDefenseGroupGenerator): + """ + This generate a KS 19 flak artillery group (KS-19 from the High Digit SAM mod) + """ + + name = "KS-19 AAA Site" + price = 98 + + def generate(self): + + spacing = random.randint(10, 40) + + self.add_unit(highdigitsams.AAA_SON_9_Fire_Can, "TR", self.position.x - 20, self.position.y - 20, self.heading) + + index = 0 + for i in range(3): + for j in range(3): + index = index + 1 + self.add_unit(highdigitsams.AAA_100mm_KS_19, "AAA#" + str(index), + self.position.x + spacing * i, + self.position.y + spacing * j, self.heading) + + @classmethod + def range(cls) -> AirDefenseRange: + return AirDefenseRange.Short diff --git a/gen/sam/airdefensegroupgenerator.py b/gen/sam/airdefensegroupgenerator.py index f58efdf4..20096046 100644 --- a/gen/sam/airdefensegroupgenerator.py +++ b/gen/sam/airdefensegroupgenerator.py @@ -1,5 +1,9 @@ +import logging from abc import ABC, abstractmethod from enum import Enum +from typing import Iterator, List + +from dcs.unitgroup import VehicleGroup from game import Game from gen.sam.group_generator import GroupGenerator @@ -21,6 +25,25 @@ class AirDefenseGroupGenerator(GroupGenerator, ABC): ground_object.skynet_capable = True super().__init__(game, ground_object) + self.auxiliary_groups: List[VehicleGroup] = [] + + def add_auxiliary_group(self, name_suffix: str) -> VehicleGroup: + group = VehicleGroup(self.game.next_group_id(), + "|".join([self.go.group_name, name_suffix])) + self.auxiliary_groups.append(group) + return group + + def get_generated_group(self) -> VehicleGroup: + raise RuntimeError( + "Deprecated call to AirDefenseGroupGenerator.get_generated_group " + "misses auxiliary groups. Use AirDefenseGroupGenerator.groups " + "instead.") + + @property + def groups(self) -> Iterator[VehicleGroup]: + yield self.vg + yield from self.auxiliary_groups + @classmethod @abstractmethod def range(cls) -> AirDefenseRange: diff --git a/gen/sam/group_generator.py b/gen/sam/group_generator.py index 9422e793..be63b777 100644 --- a/gen/sam/group_generator.py +++ b/gen/sam/group_generator.py @@ -1,11 +1,13 @@ from __future__ import annotations + import math import random from typing import TYPE_CHECKING, Type from dcs import unitgroup +from dcs.mapping import Point from dcs.point import PointAction -from dcs.unit import Vehicle, Ship +from dcs.unit import Ship, Vehicle from dcs.unittype import VehicleType from game.factions.faction import Faction @@ -40,12 +42,17 @@ class GroupGenerator: def add_unit(self, unit_type: Type[VehicleType], name: str, pos_x: float, pos_y: float, heading: int) -> Vehicle: + return self.add_unit_to_group(self.vg, unit_type, name, + Point(pos_x, pos_y), heading) + + def add_unit_to_group(self, group: unitgroup.VehicleGroup, + unit_type: Type[VehicleType], name: str, + position: Point, heading: int) -> Vehicle: unit = Vehicle(self.game.next_unit_id(), - f"{self.go.group_name}|{name}", unit_type.id) - unit.position.x = pos_x - unit.position.y = pos_y + f"{group.name}|{name}", unit_type.id) + unit.position = position unit.heading = heading - self.vg.add_unit(unit) + group.add_unit(unit) return unit def get_circular_position(self, num_units, launcher_distance, coverage=90): diff --git a/gen/sam/sam_group_generator.py b/gen/sam/sam_group_generator.py index 68e72444..dfda4048 100644 --- a/gen/sam/sam_group_generator.py +++ b/gen/sam/sam_group_generator.py @@ -11,6 +11,7 @@ from game.theater.theatergroundobject import SamGroundObject from gen.sam.aaa_bofors import BoforsGenerator from gen.sam.aaa_flak import FlakGenerator from gen.sam.aaa_flak18 import Flak18Generator +from gen.sam.aaa_ks19 import KS19Generator from gen.sam.aaa_ww2_ally_flak import AllyWW2FlakGenerator from gen.sam.aaa_zsu57 import ZSU57Generator from gen.sam.aaa_zu23_insurgent import ZU23InsurgentGenerator @@ -48,11 +49,12 @@ from gen.sam.sam_roland import RolandGenerator from gen.sam.sam_sa10 import ( SA10Generator, Tier2SA10Generator, - Tier3SA10Generator, + Tier3SA10Generator, SA10BGenerator, SA12Generator, SA20Generator, SA20BGenerator, SA23Generator, ) from gen.sam.sam_sa11 import SA11Generator from gen.sam.sam_sa13 import SA13Generator from gen.sam.sam_sa15 import SA15Generator +from gen.sam.sam_sa17 import SA17Generator from gen.sam.sam_sa19 import SA19Generator from gen.sam.sam_sa2 import SA2Generator from gen.sam.sam_sa3 import SA3Generator @@ -100,7 +102,15 @@ SAM_MAP: Dict[str, Type[AirDefenseGroupGenerator]] = { "EarlyColdWarFlakGenerator": EarlyColdWarFlakGenerator, "FreyaGenerator": FreyaGenerator, "AllyWW2FlakGenerator": AllyWW2FlakGenerator, - "ZSU57Generator": ZSU57Generator + "ZSU57Generator": ZSU57Generator, + + "KS19Generator": KS19Generator, + "SA10BGenerator": SA10BGenerator, + "SA12Generator": SA12Generator, + "SA17Generator": SA17Generator, + "SA20Generator": SA20Generator, + "SA20BGenerator": SA20BGenerator, + "SA23Generator": SA23Generator, } @@ -171,19 +181,19 @@ def get_faction_possible_ewrs_generator(faction: Faction) -> List[Type[GroupGene def _generate_anti_air_from( generators: Sequence[Type[AirDefenseGroupGenerator]], game: Game, - ground_object: SamGroundObject) -> Optional[VehicleGroup]: + ground_object: SamGroundObject) -> List[VehicleGroup]: if not generators: - return None + return [] sam_generator_class = random.choice(generators) generator = sam_generator_class(game, ground_object) generator.generate() - return generator.get_generated_group() + return list(generator.groups) def generate_anti_air_group( game: Game, ground_object: SamGroundObject, faction: Faction, ranges: Optional[Iterable[Set[AirDefenseRange]]] = None -) -> Optional[VehicleGroup]: +) -> List[VehicleGroup]: """ This generate a SAM group :param game: The Game. @@ -212,11 +222,11 @@ def generate_anti_air_group( for range_options in ranges: generators_for_range = [g for g in generators if g.range() in range_options] - group = _generate_anti_air_from(generators_for_range, game, - ground_object) - if group is not None: - return group - return None + groups = _generate_anti_air_from(generators_for_range, game, + ground_object) + if groups: + return groups + return [] def generate_ewr_group(game: Game, ground_object: TheaterGroundObject, diff --git a/gen/sam/sam_hawk.py b/gen/sam/sam_hawk.py index 382c4b69..0d526301 100644 --- a/gen/sam/sam_hawk.py +++ b/gen/sam/sam_hawk.py @@ -1,5 +1,6 @@ import random +from dcs.mapping import Point from dcs.vehicles import AirDefence from gen.sam.airdefensegroupgenerator import ( @@ -22,7 +23,9 @@ class HawkGenerator(AirDefenseGroupGenerator): self.add_unit(AirDefence.SAM_Hawk_TR_AN_MPQ_46, "TR", self.position.x + 40, self.position.y, self.heading) # Triple A for close range defense - self.add_unit(AirDefence.AAA_Vulcan_M163, "AAA", self.position.x + 20, self.position.y+30, self.heading) + aa_group = self.add_auxiliary_group("AA") + self.add_unit_to_group(aa_group, AirDefence.AAA_Vulcan_M163, "AAA", + self.position + Point(20, 30), self.heading) num_launchers = random.randint(3, 6) positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=180) diff --git a/gen/sam/sam_hq7.py b/gen/sam/sam_hq7.py index 76951e9a..a9f6eb59 100644 --- a/gen/sam/sam_hq7.py +++ b/gen/sam/sam_hq7.py @@ -1,5 +1,6 @@ import random +from dcs.mapping import Point from dcs.vehicles import AirDefence from gen.sam.airdefensegroupgenerator import ( @@ -21,8 +22,13 @@ class HQ7Generator(AirDefenseGroupGenerator): self.add_unit(AirDefence.HQ_7_Self_Propelled_LN, "LN", self.position.x + 20, self.position.y, self.heading) # Triple A for close range defense - self.add_unit(AirDefence.AAA_ZU_23_on_Ural_375, "AAA1", self.position.x + 20, self.position.y+30, self.heading) - self.add_unit(AirDefence.AAA_ZU_23_on_Ural_375, "AAA2", self.position.x - 20, self.position.y-30, self.heading) + aa_group = self.add_auxiliary_group("AA") + self.add_unit_to_group(aa_group, AirDefence.AAA_ZU_23_on_Ural_375, + "AAA1", self.position + Point(20, 30), + self.heading) + self.add_unit_to_group(aa_group, AirDefence.AAA_ZU_23_on_Ural_375, + "AAA2", self.position - Point(20, 30), + self.heading) num_launchers = random.randint(0, 3) if num_launchers > 0: diff --git a/gen/sam/sam_patriot.py b/gen/sam/sam_patriot.py index 14108083..45fcce1a 100644 --- a/gen/sam/sam_patriot.py +++ b/gen/sam/sam_patriot.py @@ -1,5 +1,6 @@ import random +from dcs.mapping import Point from dcs.vehicles import AirDefence from gen.sam.airdefensegroupgenerator import ( @@ -30,10 +31,12 @@ class PatriotGenerator(AirDefenseGroupGenerator): self.add_unit(AirDefence.SAM_Patriot_LN_M901, "LN#" + str(i), position[0], position[1], position[2]) # Short range protection for high value site + aa_group = self.add_auxiliary_group("AA") num_launchers = random.randint(3, 4) positions = self.get_circular_position(num_launchers, launcher_distance=200, coverage=360) - for i, position in enumerate(positions): - self.add_unit(AirDefence.AAA_Vulcan_M163, "SPAAA#" + str(i), position[0], position[1], position[2]) + for i, (x, y, heading) in enumerate(positions): + self.add_unit_to_group(aa_group, AirDefence.AAA_Vulcan_M163, + f"SPAAA#{i}", Point(x, y), heading) @classmethod def range(cls) -> AirDefenseRange: diff --git a/gen/sam/sam_sa10.py b/gen/sam/sam_sa10.py index 371bdb5d..b1923785 100644 --- a/gen/sam/sam_sa10.py +++ b/gen/sam/sam_sa10.py @@ -1,11 +1,16 @@ import random +from dcs.mapping import Point +from dcs.unittype import VehicleType from dcs.vehicles import AirDefence +from game import Game +from game.theater import SamGroundObject from gen.sam.airdefensegroupgenerator import ( AirDefenseRange, AirDefenseGroupGenerator, ) +from pydcs_extensions.highdigitsams import highdigitsams class SA10Generator(AirDefenseGroupGenerator): @@ -16,20 +21,30 @@ class SA10Generator(AirDefenseGroupGenerator): name = "SA-10/S-300PS Battery" price = 550 + def __init__(self, game: Game, ground_object: SamGroundObject): + super().__init__(game, ground_object) + self.sr1 = AirDefence.SAM_SA_10_S_300PS_SR_5N66M + self.sr2 = AirDefence.SAM_SA_10_S_300PS_SR_64H6E + self.cp = AirDefence.SAM_SA_10_S_300PS_CP_54K6 + self.tr1 = AirDefence.SAM_SA_10_S_300PS_TR_30N6 + self.tr2 = AirDefence.SAM_SA_10_S_300PS_TR_30N6 + self.ln1 = AirDefence.SAM_SA_10_S_300PS_LN_5P85C + self.ln2 = AirDefence.SAM_SA_10_S_300PS_LN_5P85D + def generate(self): # Search Radar - self.add_unit(AirDefence.SAM_SA_10_S_300PS_SR_5N66M, "SR1", self.position.x, self.position.y + 40, self.heading) + self.add_unit(self.sr1, "SR1", self.position.x, self.position.y + 40, self.heading) # Search radar for missiles (optionnal) - self.add_unit(AirDefence.SAM_SA_10_S_300PS_SR_64H6E, "SR2", self.position.x - 40, self.position.y, self.heading) + self.add_unit(self.sr2, "SR2", self.position.x - 40, self.position.y, self.heading) # Command Post - self.add_unit(AirDefence.SAM_SA_10_S_300PS_CP_54K6, "CP", self.position.x, self.position.y, self.heading) + self.add_unit(self.cp, "CP", self.position.x, self.position.y, self.heading) # 2 Tracking radars - self.add_unit(AirDefence.SAM_SA_10_S_300PS_TR_30N6, "TR1", self.position.x - 40, self.position.y - 40, self.heading) + self.add_unit(self.tr1, "TR1", self.position.x - 40, self.position.y - 40, self.heading) - self.add_unit(AirDefence.SAM_SA_10_S_300PS_TR_30N6, "TR2", self.position.x + 40, self.position.y - 40, + self.add_unit(self.tr2, "TR2", self.position.x + 40, self.position.y - 40, self.heading) # 2 different launcher type (C & D) @@ -37,9 +52,9 @@ class SA10Generator(AirDefenseGroupGenerator): positions = self.get_circular_position(num_launchers, launcher_distance=100, coverage=360) for i, position in enumerate(positions): if i%2 == 0: - self.add_unit(AirDefence.SAM_SA_10_S_300PS_LN_5P85C, "LN#" + str(i), position[0], position[1], position[2]) + self.add_unit(self.ln1, "LN#" + str(i), position[0], position[1], position[2]) else: - self.add_unit(AirDefence.SAM_SA_10_S_300PS_LN_5P85D, "LN#" + str(i), position[0], position[1], position[2]) + self.add_unit(self.ln2, "LN#" + str(i), position[0], position[1], position[2]) self.generate_defensive_groups() @@ -49,47 +64,126 @@ class SA10Generator(AirDefenseGroupGenerator): def generate_defensive_groups(self) -> None: # AAA for defending against close targets. + aa_group = self.add_auxiliary_group("AA") num_launchers = random.randint(6, 8) positions = self.get_circular_position( num_launchers, launcher_distance=210, coverage=360) - for i, position in enumerate(positions): - self.add_unit(AirDefence.SPAAA_ZSU_23_4_Shilka, "AA#" + str(i), - position[0], position[1], position[2]) + for i, (x, y, heading) in enumerate(positions): + self.add_unit_to_group(aa_group, AirDefence.SPAAA_ZSU_23_4_Shilka, + f"AA#{i}", Point(x, y), heading) class Tier2SA10Generator(SA10Generator): def generate_defensive_groups(self) -> None: + # Create AAA the way the main group does. + super().generate_defensive_groups() + # SA-15 for both shorter range targets and point defense. + pd_group = self.add_auxiliary_group("PD") num_launchers = random.randint(2, 4) positions = self.get_circular_position( num_launchers, launcher_distance=140, coverage=360) - for i, position in enumerate(positions): - self.add_unit(AirDefence.SAM_SA_15_Tor_9A331, "PD#" + str(i), - position[0], position[1], position[2]) - - # AAA for defending against close targets. - num_launchers = random.randint(6, 8) - positions = self.get_circular_position( - num_launchers, launcher_distance=210, coverage=360) - for i, position in enumerate(positions): - self.add_unit(AirDefence.SPAAA_ZSU_23_4_Shilka, "AA#" + str(i), - position[0], position[1], position[2]) + for i, (x, y, heading) in enumerate(positions): + self.add_unit_to_group(pd_group, AirDefence.SAM_SA_15_Tor_9A331, + f"PD#{i}", Point(x, y), heading) class Tier3SA10Generator(SA10Generator): def generate_defensive_groups(self) -> None: - # SA-15 for both shorter range targets and point defense. - num_launchers = random.randint(2, 4) - positions = self.get_circular_position( - num_launchers, launcher_distance=140, coverage=360) - for i, position in enumerate(positions): - self.add_unit(AirDefence.SAM_SA_15_Tor_9A331, "PD#" + str(i), - position[0], position[1], position[2]) - # AAA for defending against close targets. + aa_group = self.add_auxiliary_group("AA") num_launchers = random.randint(6, 8) positions = self.get_circular_position( num_launchers, launcher_distance=210, coverage=360) - for i, position in enumerate(positions): - self.add_unit(AirDefence.SAM_SA_19_Tunguska_2S6, "AA#" + str(i), - position[0], position[1], position[2]) + for i, (x, y, heading) in enumerate(positions): + self.add_unit_to_group(aa_group, AirDefence.SAM_SA_19_Tunguska_2S6, + f"AA#{i}", Point(x, y), heading) + + # SA-15 for both shorter range targets and point defense. + pd_group = self.add_auxiliary_group("PD") + num_launchers = random.randint(2, 4) + positions = self.get_circular_position( + num_launchers, launcher_distance=140, coverage=360) + for i, (x, y, heading) in enumerate(positions): + self.add_unit_to_group(pd_group, AirDefence.SAM_SA_15_Tor_9A331, + f"PD#{i}", Point(x, y), heading) + + +class SA10BGenerator(Tier3SA10Generator): + + price = 700 + name = "SA-10B/S-300PS Battery" + + def __init__(self, game: Game, ground_object: SamGroundObject): + super().__init__(game, ground_object) + self.sr1 = highdigitsams.SAM_SA_10B_S_300PS_40B6MD_SR + self.sr2 = highdigitsams.SAM_SA_10B_S_300PS_64H6E_SR + self.cp = highdigitsams.SAM_SA_10B_S_300PS_54K6_CP + self.tr1 = highdigitsams.SAM_SA_10B_S_300PS_30N6_TR + self.tr2 = highdigitsams.SAM_SA_10B_S_300PS_40B6M_TR + self.ln1 = highdigitsams.SAM_SA_10B_S_300PS_5P85SE_LN + self.ln2 = highdigitsams.SAM_SA_10B_S_300PS_5P85SU_LN + + +class SA12Generator(Tier3SA10Generator): + + price = 750 + name = "SA-12/S-300V Battery" + + def __init__(self, game: Game, ground_object: SamGroundObject): + super().__init__(game, ground_object) + self.sr1 = highdigitsams.SAM_SA_12_S_300V_9S15_SR + self.sr2 = highdigitsams.SAM_SA_12_S_300V_9S19_SR + self.cp = highdigitsams.SAM_SA_12_S_300V_9S457_CP + self.tr1 = highdigitsams.SAM_SA_12_S_300V_9S32_TR + self.tr2 = highdigitsams.SAM_SA_12_S_300V_9S32_TR + self.ln1 = highdigitsams.SAM_SA_12_S_300V_9A82_LN + self.ln2 = highdigitsams.SAM_SA_12_S_300V_9A83_LN + + +class SA20Generator(Tier3SA10Generator): + + price = 800 + name = "SA-20/S-300PMU-1 Battery" + + def __init__(self, game: Game, ground_object: SamGroundObject): + super().__init__(game, ground_object) + self.sr1 = highdigitsams.SAM_SA_20_S_300PMU1_SR_5N66E + self.sr2 = highdigitsams.SAM_SA_20_S_300PMU1_SR_64N6E + self.cp = highdigitsams.SAM_SA_20_S_300PMU1_CP_54K6 + self.tr1 = highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E + self.tr2 = highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E_truck + self.ln1 = highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85CE + self.ln2 = highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85DE + + +class SA20BGenerator(Tier3SA10Generator): + + price = 850 + name = "SA-20B/S-300PMU-2 Battery" + + def __init__(self, game: Game, ground_object: SamGroundObject): + super().__init__(game, ground_object) + self.sr1 = highdigitsams.SAM_SA_20_S_300PMU1_SR_5N66E + self.sr2 = highdigitsams.SAM_SA_20_S_300PMU1_SR_64N6E + self.cp = highdigitsams.SAM_SA_20B_S_300PMU2_CP_54K6E2 + self.tr1 = highdigitsams.SAM_SA_20B_S_300PMU2_TR_92H6E_truck + self.tr2 = highdigitsams.SAM_SA_20B_S_300PMU2_TR_92H6E_truck + self.ln1 = highdigitsams.SAM_SA_20B_S_300PMU2_LN_5P85SE2 + self.ln2 = highdigitsams.SAM_SA_20B_S_300PMU2_LN_5P85SE2 + + +class SA23Generator(Tier3SA10Generator): + + price = 950 + name = "SA-23/S-300VM Battery" + + def __init__(self, game: Game, ground_object: SamGroundObject): + super().__init__(game, ground_object) + self.sr1 = highdigitsams.SAM_SA_23_S_300VM_9S15M2_SR + self.sr2 = highdigitsams.SAM_SA_23_S_300VM_9S19M2_SR + self.cp = highdigitsams.SAM_SA_23_S_300VM_9S457ME_CP + self.tr1 = highdigitsams.SAM_SA_23_S_300VM_9S32ME_TR + self.tr2 = highdigitsams.SAM_SA_23_S_300VM_9S32ME_TR + self.ln1 = highdigitsams.SAM_SA_23_S_300VM_9A82ME_LN + self.ln2 = highdigitsams.SAM_SA_23_S_300VM_9A83ME_LN \ No newline at end of file diff --git a/gen/sam/sam_sa17.py b/gen/sam/sam_sa17.py new file mode 100644 index 00000000..eced94bf --- /dev/null +++ b/gen/sam/sam_sa17.py @@ -0,0 +1,30 @@ +from dcs.vehicles import AirDefence + +from gen.sam.airdefensegroupgenerator import ( + AirDefenseRange, + AirDefenseGroupGenerator, +) +from pydcs_extensions.highdigitsams import highdigitsams + + +class SA17Generator(AirDefenseGroupGenerator): + """ + This generate a SA-17 group + """ + + name = "SA-17 Grizzly Battery" + price = 180 + + def generate(self): + self.add_unit(AirDefence.SAM_SA_11_Buk_SR_9S18M1, "SR", self.position.x + 20, self.position.y, self.heading) + self.add_unit(AirDefence.SAM_SA_11_Buk_CC_9S470M1, "CC", self.position.x, self.position.y, self.heading) + + positions = self.get_circular_position(3, launcher_distance=140, coverage=180) + + for i, position in enumerate(positions): + self.add_unit(highdigitsams.SAM_SA_17_Buk_M1_2_LN_9A310M1_2, "LN#" + str(i), position[0], position[1], + position[2]) + + @classmethod + def range(cls) -> AirDefenseRange: + return AirDefenseRange.Medium diff --git a/pydcs b/pydcs index 059c88c9..84f116c3 160000 --- a/pydcs +++ b/pydcs @@ -1 +1 @@ -Subproject commit 059c88c91b5be4b5b6406249a52527c3ccea3db9 +Subproject commit 84f116c35879b05470ad7f30b0c5b0bca26088a5 diff --git a/pydcs_extensions/hercules/hercules.py b/pydcs_extensions/hercules/hercules.py index ffd16855..e400e8bd 100644 --- a/pydcs_extensions/hercules/hercules.py +++ b/pydcs_extensions/hercules/hercules.py @@ -132,31 +132,31 @@ class Hercules(PlaneType): charge_total = 1680 chaff_charge_size = 1 flare_charge_size = 1 - radio_frequency = 305 + radio_frequency = 118 panel_radio = { 1: { "channels": { - 1: 305, - 2: 264, - 4: 256, - 8: 257, - 16: 261, - 17: 261, - 9: 255, - 18: 251, - 5: 254, - 10: 262, - 20: 266, - 11: 259, - 3: 265, - 6: 250, - 12: 268, - 13: 269, - 7: 270, - 14: 260, - 19: 253, - 15: 263 + 1: 118, + 2: 119, + 4: 121, + 8: 125, + 16: 133, + 17: 134, + 9: 126, + 18: 135, + 5: 122, + 10: 127, + 20: 143, + 11: 128, + 3: 120, + 6: 123, + 12: 129, + 13: 130, + 7: 124, + 14: 131, + 19: 136, + 15: 132 }, }, } diff --git a/pydcs_extensions/highdigitsams/highdigitsams.py b/pydcs_extensions/highdigitsams/highdigitsams.py index 3ff206d0..a3cc4de7 100644 --- a/pydcs_extensions/highdigitsams/highdigitsams.py +++ b/pydcs_extensions/highdigitsams/highdigitsams.py @@ -1,6 +1,102 @@ from dcs import unittype +class AAA_SON_9_Fire_Can(unittype.VehicleType): + id = "Fire Can radar" + name = "AAA SON-9 Fire Can" + detection_range = 35000 + threat_range = 0 + air_weapon_dist = 0 + + +class AAA_100mm_KS_19(unittype.VehicleType): + id = "KS19" + name = "AAA 100mm KS-19" + detection_range = 0 + threat_range = 15000 + air_weapon_dist = 15000 + + +class SAM_SA_10B_S_300PS_54K6_CP(unittype.VehicleType): + id = "S-300PS SA-10B 54K6 cp" + name = "SAM SA-10B S-300PS 54K6 CP" + detection_range = 0 + threat_range = 0 + air_weapon_dist = 0 + + +class SAM_SA_10B_S_300PS_5P85SE_LN(unittype.VehicleType): + id = "S-300PS 5P85SE_mod ln" + name = "SAM SA-10B S-300PS 5P85SE LN " + detection_range = 0 + threat_range = 75000 + air_weapon_dist = 75000 + + +class SAM_SA_10B_S_300PS_5P85SU_LN(unittype.VehicleType): + id = "S-300PS 5P85SU_mod ln" + name = "SAM SA-10B S-300PS 5P85SU LN " + detection_range = 0 + threat_range = 75000 + air_weapon_dist = 75000 + + +class SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE(unittype.VehicleType): + id = "S-300PS 5P85CE ln" + name = "SAM SA-10 (5V55RUD) S-300PS LN 5P85CE" + detection_range = 0 + threat_range = 90000 + air_weapon_dist = 90000 + + +class SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE(unittype.VehicleType): + id = "S-300PS 5P85DE ln" + name = "SAM SA-10 (5V55RUD) S-300PS LN 5P85DE" + detection_range = 0 + threat_range = 90000 + air_weapon_dist = 90000 + + +class SAM_SA_10B_S_300PS_30N6_TR(unittype.VehicleType): + id = "S-300PS 30N6 TRAILER tr" + name = "SAM SA-10B S-300PS 30N6 TR" + detection_range = 160000 + threat_range = 0 + air_weapon_dist = 0 + + +class SAM_SA_10B_S_300PS_40B6M_TR(unittype.VehicleType): + id = "S-300PS SA-10B 40B6M MAST tr" + name = "SAM SA-10B S-300PS 40B6M TR" + detection_range = 160000 + threat_range = 0 + air_weapon_dist = 0 + + +class SAM_SA_10B_S_300PS_40B6MD_SR(unittype.VehicleType): + id = "S-300PS SA-10B 40B6MD MAST sr" + name = "SAM SA-10B S-300PS 40B6MD SR" + detection_range = 60000 + threat_range = 0 + air_weapon_dist = 0 + + +class SAM_SA_10B_S_300PS_64H6E_SR(unittype.VehicleType): + id = "S-300PS 64H6E TRAILER sr" + name = "SAM SA-10B S-300PS 64H6E SR" + detection_range = 160000 + threat_range = 0 + air_weapon_dist = 0 + + +class SAM_SA_20_S_300PMU1_CP_54K6(unittype.VehicleType): + id = "S-300PMU1 54K6 cp" + name = "SAM SA-20 S-300PMU1 CP 54K6" + detection_range = 0 + threat_range = 0 + air_weapon_dist = 0 + + class SAM_SA_20_S_300PMU1_TR_30N6E(unittype.VehicleType): id = "S-300PMU1 40B6M tr" name = "SAM SA-20 S-300PMU1 TR 30N6E" @@ -33,6 +129,110 @@ class SAM_SA_20_S_300PMU1_SR_64N6E(unittype.VehicleType): air_weapon_dist = 0 +class SAM_SA_20_S_300PMU1_LN_5P85CE(unittype.VehicleType): + id = "S-300PMU1 5P85CE ln" + name = "SAM SA-20 S-300PMU1 LN 5P85CE" + detection_range = 0 + threat_range = 150000 + air_weapon_dist = 150000 + + +class SAM_SA_20_S_300PMU1_LN_5P85DE(unittype.VehicleType): + id = "S-300PMU1 5P85DE ln" + name = "SAM SA-20 S-300PMU1 LN 5P85DE" + detection_range = 0 + threat_range = 150000 + air_weapon_dist = 150000 + + +class SAM_SA_20B_S_300PMU2_CP_54K6E2(unittype.VehicleType): + id = "S-300PMU2 54K6E2 cp" + name = "SAM SA-20B S-300PMU2 CP 54K6E2" + detection_range = 0 + threat_range = 0 + air_weapon_dist = 0 + + +class SAM_SA_20B_S_300PMU2_TR_92H6E_truck(unittype.VehicleType): + id = "S-300PMU2 92H6E tr" + name = "SAM SA-20B S-300PMU2 TR 92H6E(truck)" + detection_range = 270000 + threat_range = 0 + air_weapon_dist = 0 + + +class SAM_SA_20B_S_300PMU2_SR_64N6E2(unittype.VehicleType): + id = "S-300PMU2 64H6E2 sr" + name = "SAM SA-20B S-300PMU2 SR 64N6E2" + detection_range = 330000 + threat_range = 0 + air_weapon_dist = 0 + + +class SAM_SA_20B_S_300PMU2_LN_5P85SE2(unittype.VehicleType): + id = "S-300PMU2 5P85SE2 ln" + name = "SAM SA-20B S-300PMU2 LN 5P85SE2" + detection_range = 0 + threat_range = 200000 + air_weapon_dist = 200000 + + +class SAM_SA_12_S_300V_9S457_CP(unittype.VehicleType): + id = "S-300V 9S457 cp" + name = "SAM SA-12 S-300V 9S457 CP" + detection_range = 0 + threat_range = 0 + air_weapon_dist = 0 + + +class SAM_SA_12_S_300V_9A82_LN(unittype.VehicleType): + id = "S-300V 9A82 ln" + name = "SAM SA-12 S-300V 9A82 LN" + detection_range = 0 + threat_range = 100000 + air_weapon_dist = 100000 + + +class SAM_SA_12_S_300V_9A83_LN(unittype.VehicleType): + id = "S-300V 9A83 ln" + name = "SAM SA-12 S-300V 9A83 LN" + detection_range = 0 + threat_range = 75000 + air_weapon_dist = 75000 + + +class SAM_SA_12_S_300V_9S15_SR(unittype.VehicleType): + id = "S-300V 9S15 sr" + name = "SAM SA-12 S-300V 9S15 SR" + detection_range = 240000 + threat_range = 0 + air_weapon_dist = 0 + + +class SAM_SA_12_S_300V_9S19_SR(unittype.VehicleType): + id = "S-300V 9S19 sr" + name = "SAM SA-12 S-300V 9S19 SR" + detection_range = 175000 + threat_range = 0 + air_weapon_dist = 0 + + +class SAM_SA_12_S_300V_9S32_TR(unittype.VehicleType): + id = "S-300V 9S32 tr" + name = "SAM SA-12 S-300V 9S32 TR" + detection_range = 150000 + threat_range = 0 + air_weapon_dist = 0 + + +class SAM_SA_23_S_300VM_9S457ME_CP(unittype.VehicleType): + id = "S-300VM 9S457ME cp" + name = "SAM SA-23 S-300VM 9S457ME CP" + detection_range = 0 + threat_range = 0 + air_weapon_dist = 0 + + class SAM_SA_23_S_300VM_9S15M2_SR(unittype.VehicleType): id = "S-300VM 9S15M2 sr" name = "SAM SA-23 S-300VM 9S15M2 SR" @@ -57,38 +257,6 @@ class SAM_SA_23_S_300VM_9S32ME_TR(unittype.VehicleType): air_weapon_dist = 0 -class SAM_SA_20_S_300PMU1_LN_5P85CE(unittype.VehicleType): - id = "S-300PMU1 5P85CE ln" - name = "SAM SA-20 S-300PMU1 LN 5P85CE" - detection_range = 0 - threat_range = 150000 - air_weapon_dist = 150000 - - -class SAM_SA_20_S_300PMU1_LN_5P85DE(unittype.VehicleType): - id = "S-300PMU1 5P85DE ln" - name = "SAM SA-20 S-300PMU1 LN 5P85DE" - detection_range = 0 - threat_range = 150000 - air_weapon_dist = 150000 - - -class SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE(unittype.VehicleType): - id = "S-300PS 5P85CE ln" - name = "SAM SA-10 (5V55RUD) S-300PS LN 5P85CE" - detection_range = 0 - threat_range = 90000 - air_weapon_dist = 90000 - - -class SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE(unittype.VehicleType): - id = "S-300PS 5P85DE ln" - name = "SAM SA-10 (5V55RUD) S-300PS LN 5P85DE" - detection_range = 0 - threat_range = 90000 - air_weapon_dist = 90000 - - class SAM_SA_23_S_300VM_9A83ME_LN(unittype.VehicleType): id = "S-300VM 9A83ME ln" name = "SAM SA-23 S-300VM 9A83ME LN" @@ -137,22 +305,6 @@ class SAM_SA_3__V_601P__LN_5P73(unittype.VehicleType): air_weapon_dist = 18000 -class SAM_SA_20_S_300PMU1_CP_54K6(unittype.VehicleType): - id = "S-300PMU1 54K6 cp" - name = "SAM SA-20 S-300PMU1 CP 54K6" - detection_range = 0 - threat_range = 0 - air_weapon_dist = 0 - - -class SAM_SA_23_S_300VM_9S457ME_CP(unittype.VehicleType): - id = "S-300VM 9S457ME cp" - name = "SAM SA-23 S-300VM 9S457ME CP" - detection_range = 0 - threat_range = 0 - air_weapon_dist = 0 - - class SAM_SA_24_Igla_S_manpad(unittype.VehicleType): id = "SA-24 Igla-S manpad" name = "SAM SA-24 Igla-S manpad" @@ -167,3 +319,20 @@ class SAM_SA_14_Strela_3_manpad(unittype.VehicleType): detection_range = 5000 threat_range = 4500 air_weapon_dist = 4500 + + +class Polyana_D4M1_C2_node(unittype.VehicleType): + id = "polyana-d4m1 cp" + name = "Polyana-D4M1 C2 node" + detection_range = 0 + threat_range = 0 + air_weapon_dist = 0 + + +class _34Ya6E_Gazetchik_E_decoy(unittype.VehicleType): + id = "34Ya6E Gazetchik E decoy" + name = "34Ya6E Gazetchik E decoy" + detection_range = 20000 + threat_range = 0 + air_weapon_dist = 0 + diff --git a/pydcs_extensions/mod_units.py b/pydcs_extensions/mod_units.py index bf841e95..5dd1e93c 100644 --- a/pydcs_extensions/mod_units.py +++ b/pydcs_extensions/mod_units.py @@ -43,25 +43,46 @@ MODDED_VEHICLES = [ frenchpack.DIM__TOYOTA_GREEN, frenchpack.DIM__TOYOTA_DESERT, frenchpack.DIM__KAMIKAZE, + highdigitsams.AAA_SON_9_Fire_Can, + highdigitsams.AAA_100mm_KS_19, + highdigitsams.SAM_SA_10B_S_300PS_54K6_CP, + highdigitsams.SAM_SA_10B_S_300PS_5P85SE_LN, + highdigitsams.SAM_SA_10B_S_300PS_5P85SU_LN, + highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE, + highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE, + highdigitsams.SAM_SA_10B_S_300PS_30N6_TR, + highdigitsams.SAM_SA_10B_S_300PS_40B6M_TR, + highdigitsams.SAM_SA_10B_S_300PS_40B6MD_SR, + highdigitsams.SAM_SA_10B_S_300PS_64H6E_SR, + highdigitsams.SAM_SA_20_S_300PMU1_CP_54K6, highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E, highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E_truck, highdigitsams.SAM_SA_20_S_300PMU1_SR_5N66E, highdigitsams.SAM_SA_20_S_300PMU1_SR_64N6E, + highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85CE, + highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85DE, + highdigitsams.SAM_SA_20B_S_300PMU2_CP_54K6E2, + highdigitsams.SAM_SA_20B_S_300PMU2_TR_92H6E_truck, + highdigitsams.SAM_SA_20B_S_300PMU2_SR_64N6E2, + highdigitsams.SAM_SA_20B_S_300PMU2_LN_5P85SE2, + highdigitsams.SAM_SA_12_S_300V_9S457_CP, + highdigitsams.SAM_SA_12_S_300V_9A82_LN, + highdigitsams.SAM_SA_12_S_300V_9A83_LN, + highdigitsams.SAM_SA_12_S_300V_9S15_SR, + highdigitsams.SAM_SA_12_S_300V_9S19_SR, + highdigitsams.SAM_SA_12_S_300V_9S32_TR, + highdigitsams.SAM_SA_23_S_300VM_9S457ME_CP, highdigitsams.SAM_SA_23_S_300VM_9S15M2_SR, highdigitsams.SAM_SA_23_S_300VM_9S19M2_SR, highdigitsams.SAM_SA_23_S_300VM_9S32ME_TR, - highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85CE, - highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85DE, - highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE, - highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE, highdigitsams.SAM_SA_23_S_300VM_9A83ME_LN, highdigitsams.SAM_SA_23_S_300VM_9A82ME_LN, highdigitsams.SAM_SA_17_Buk_M1_2_LN_9A310M1_2, highdigitsams.SAM_SA_2__V759__LN_SM_90, highdigitsams.SAM_HQ_2_LN_SM_90, highdigitsams.SAM_SA_3__V_601P__LN_5P73, - highdigitsams.SAM_SA_20_S_300PMU1_CP_54K6, - highdigitsams.SAM_SA_23_S_300VM_9S457ME_CP, highdigitsams.SAM_SA_24_Igla_S_manpad, - highdigitsams.SAM_SA_14_Strela_3_manpad + highdigitsams.SAM_SA_14_Strela_3_manpad, + highdigitsams.Polyana_D4M1_C2_node, + highdigitsams._34Ya6E_Gazetchik_E_decoy ] \ No newline at end of file diff --git a/qt_ui/dialogs.py b/qt_ui/dialogs.py index 263bfb62..09fd2b93 100644 --- a/qt_ui/dialogs.py +++ b/qt_ui/dialogs.py @@ -60,7 +60,7 @@ class Dialog: """Opens the dialog to edit the given flight.""" cls.edit_flight_dialog = QEditFlightDialog( cls.game_model, - package_model.package, + package_model, flight, parent=parent ) diff --git a/qt_ui/displayoptions.py b/qt_ui/displayoptions.py index 55dcb10b..7b894b1d 100644 --- a/qt_ui/displayoptions.py +++ b/qt_ui/displayoptions.py @@ -1,5 +1,5 @@ """Visibility options for the game map.""" -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Iterator, Optional, Union @@ -7,6 +7,7 @@ from typing import Iterator, Optional, Union class DisplayRule: name: str _value: bool + debug_only: bool = field(default=False) @property def menu_text(self) -> str: @@ -29,8 +30,9 @@ class DisplayRule: class DisplayGroup: - def __init__(self, name: Optional[str]) -> None: + def __init__(self, name: Optional[str], debug_only: bool = False) -> None: self.name = name + self.debug_only = debug_only def __iter__(self) -> Iterator[DisplayRule]: # Python 3.6 enforces that __dict__ is order preserving by default. @@ -47,6 +49,46 @@ class FlightPathOptions(DisplayGroup): self.all = DisplayRule("Show All Flight Paths", True) +class ThreatZoneOptions(DisplayGroup): + def __init__(self, coalition_name: str) -> None: + super().__init__(f"{coalition_name} Threat Zones") + self.none = DisplayRule( + f"Hide {coalition_name.lower()} threat zones", True) + self.all = DisplayRule( + f"Show full {coalition_name.lower()} threat zones", False) + self.aircraft = DisplayRule( + f"Show {coalition_name.lower()} aircraft threat tones", False) + self.air_defenses = DisplayRule( + f"Show {coalition_name.lower()} air defenses threat zones", False) + + +class NavMeshOptions(DisplayGroup): + def __init__(self) -> None: + super().__init__("Navmeshes", debug_only=True) + self.hide = DisplayRule("DEBUG Hide Navmeshes", True) + self.blue_navmesh = DisplayRule("DEBUG Show blue navmesh", False) + self.red_navmesh = DisplayRule("DEBUG Show red navmesh", False) + + +class PathDebugFactionOptions(DisplayGroup): + def __init__(self) -> None: + super().__init__("Faction for path debugging", debug_only=True) + self.blue = DisplayRule("Debug blue paths", True) + self.red = DisplayRule("Debug red paths", False) + + +class PathDebugOptions(DisplayGroup): + def __init__(self) -> None: + super().__init__("Shortest paths", debug_only=True) + self.hide = DisplayRule("DEBUG Hide paths", True) + self.shortest_path = DisplayRule("DEBUG Show shortest path", False) + self.barcap = DisplayRule("DEBUG Show BARCAP plan", False) + self.cas = DisplayRule("DEBUG Show CAS plan", False) + self.sweep = DisplayRule("DEBUG Show fighter sweep plan", False) + self.strike = DisplayRule("DEBUG Show strike plan", False) + self.tarcap = DisplayRule("DEBUG Show TARCAP plan", False) + + class DisplayOptions: ground_objects = DisplayRule("Ground Objects", True) control_points = DisplayRule("Control Points", True) @@ -57,14 +99,27 @@ class DisplayOptions: map_poly = DisplayRule("Map Polygon Debug Mode", False) waypoint_info = DisplayRule("Waypoint Information", True) culling = DisplayRule("Display Culling Zones", False) + actual_frontline_pos = DisplayRule("Display Actual Frontline Location", + False) + barcap_commit_range = DisplayRule("Display selected BARCAP commit range", + False) flight_paths = FlightPathOptions() - actual_frontline_pos = DisplayRule("Display Actual Frontline Location", False) + blue_threat_zones = ThreatZoneOptions("Blue") + red_threat_zones = ThreatZoneOptions("Red") + navmeshes = NavMeshOptions() + path_debug_faction = PathDebugFactionOptions() + path_debug = PathDebugOptions() @classmethod def menu_items(cls) -> Iterator[Union[DisplayGroup, DisplayRule]]: + debug = False # Set to True to enable debug options. # Python 3.6 enforces that __dict__ is order preserving by default. for value in cls.__dict__.values(): if isinstance(value, DisplayRule): + if value.debug_only and not debug: + continue yield value elif isinstance(value, DisplayGroup): + if value.debug_only and not debug: + continue yield value diff --git a/qt_ui/main.py b/qt_ui/main.py index 09620098..b8984a2a 100644 --- a/qt_ui/main.py +++ b/qt_ui/main.py @@ -7,11 +7,18 @@ from pathlib import Path from typing import Optional import dcs +from dcs.weapons_data import weapon_ids + from PySide2 import QtWidgets from PySide2.QtGui import QPixmap from PySide2.QtWidgets import QApplication, QSplashScreen from game import Game, db, persistency, VERSION +from game.data.weapons import ( + WEAPON_FALLBACK_MAP, + WEAPON_INTRODUCTION_YEARS, + Weapon, +) from game.settings import Settings from game.theater.start_generator import GameGenerator, GeneratorSettings from qt_ui import ( @@ -67,6 +74,8 @@ def run_ui(game: Optional[Game] = None) -> None: uiconstants.load_event_icons() uiconstants.load_aircraft_icons() uiconstants.load_vehicle_icons() + uiconstants.load_aircraft_banners() + uiconstants.load_vehicle_banners() # Replace DCS Mission scripting file to allow DCS Liberation to work try: @@ -103,6 +112,11 @@ def parse_args() -> argparse.Namespace: raise argparse.ArgumentTypeError("path does not exist") return path + parser.add_argument( + "--warn-missing-weapon-data", action="store_true", + help="Emits a warning for weapons without date or fallback information." + ) + new_game = subparsers.add_parser("new-game") new_game.add_argument( @@ -163,6 +177,15 @@ def create_game(campaign_path: Path, blue: str, red: str, return generator.generate() +def lint_weapon_data() -> None: + for clsid in weapon_ids: + weapon = Weapon.from_clsid(clsid) + if weapon not in WEAPON_INTRODUCTION_YEARS: + logging.warning(f"{weapon} has no introduction date") + if weapon not in WEAPON_FALLBACK_MAP: + logging.warning(f"{weapon} has no fallback") + + def main(): logging_config.init_logging(VERSION) @@ -172,6 +195,11 @@ def main(): game: Optional[Game] = None args = parse_args() + + # TODO: Flesh out data and then make unconditional. + if args.warn_missing_weapon_data: + lint_weapon_data() + if args.subcommand == "new-game": game = create_game(args.campaign, args.blue, args.red, args.supercarrier, args.auto_procurement, diff --git a/qt_ui/models.py b/qt_ui/models.py index bd0d6397..670c52ad 100644 --- a/qt_ui/models.py +++ b/qt_ui/models.py @@ -100,6 +100,8 @@ class PackageModel(QAbstractListModel): #: Emitted when this package is being deleted from the ATO. deleted = Signal() + tot_changed = Signal() + def __init__(self, package: Package) -> None: super().__init__() self.package = package @@ -139,6 +141,8 @@ class PackageModel(QAbstractListModel): """Adds the given flight to the package.""" self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) self.package.add_flight(flight) + # update_tot is not called here because the new flight does not have a + # flight plan yet. Will be called manually by the caller. self.endInsertRows() def delete_flight_at_index(self, index: QModelIndex) -> None: @@ -155,15 +159,27 @@ class PackageModel(QAbstractListModel): self.beginRemoveRows(QModelIndex(), index, index) self.package.remove_flight(flight) self.endRemoveRows() + self.update_tot() def flight_at_index(self, index: QModelIndex) -> Flight: """Returns the flight located at the given index.""" return self.package.flights[index.row()] - def update_tot(self, tot: datetime.timedelta) -> None: + def set_tot(self, tot: datetime.timedelta) -> None: self.package.time_over_target = tot + self.update_tot() + # For some reason this is needed to make the UI update quickly. self.layoutChanged.emit() + def set_asap(self, asap: bool) -> None: + self.package.auto_asap = asap + self.update_tot() + + def update_tot(self) -> None: + if self.package.auto_asap: + self.package.set_tot_asap() + self.tot_changed.emit() + @property def mission_target(self) -> MissionTarget: """Returns the mission target of the package.""" diff --git a/qt_ui/uiconstants.py b/qt_ui/uiconstants.py index 0b5b4725..1ac196f6 100644 --- a/qt_ui/uiconstants.py +++ b/qt_ui/uiconstants.py @@ -72,7 +72,9 @@ COLORS: Dict[str, QColor] = { CP_SIZE = 12 +AIRCRAFT_BANNERS: Dict[str, QPixmap] = {} AIRCRAFT_ICONS: Dict[str, QPixmap] = {} +VEHICLE_BANNERS: Dict[str, QPixmap] = {} VEHICLES_ICONS: Dict[str, QPixmap] = {} ICONS: Dict[str, QPixmap] = {} @@ -130,6 +132,8 @@ def load_icons(): ICONS["ship_blue"] = QPixmap("./resources/ui/ground_assets/ship_blue.png") ICONS["missile"] = QPixmap("./resources/ui/ground_assets/missile.png") ICONS["missile_blue"] = QPixmap("./resources/ui/ground_assets/missile_blue.png") + ICONS["nothreat"] = QPixmap("./resources/ui/ground_assets/nothreat.png") + ICONS["nothreat_blue"] = QPixmap("./resources/ui/ground_assets/nothreat_blue.png") ICONS["Generator"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/generator.png") ICONS["Missile"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/missile.png") @@ -171,15 +175,25 @@ def load_event_icons(): EVENT_ICONS[image[:-4]] = QPixmap(os.path.join("./resources/ui/events/", image)) def load_aircraft_icons(): - for aircraft in os.listdir("./resources/ui/units/aircrafts/"): + for aircraft in os.listdir("./resources/ui/units/aircrafts/icons/"): if aircraft.endswith(".jpg"): - AIRCRAFT_ICONS[aircraft[:-7]] = QPixmap(os.path.join("./resources/ui/units/aircrafts/", aircraft)) + AIRCRAFT_ICONS[aircraft[:-7]] = QPixmap(os.path.join("./resources/ui/units/aircrafts/icons/", aircraft)) AIRCRAFT_ICONS["F-16C_50"] = AIRCRAFT_ICONS["F-16C"] AIRCRAFT_ICONS["FA-18C_hornet"] = AIRCRAFT_ICONS["FA-18C"] AIRCRAFT_ICONS["A-10C_2"] = AIRCRAFT_ICONS["A-10C"] def load_vehicle_icons(): - for vehicle in os.listdir("./resources/ui/units/vehicles/"): + for vehicle in os.listdir("./resources/ui/units/vehicles/icons/"): if vehicle.endswith(".jpg"): - VEHICLES_ICONS[vehicle[:-7]] = QPixmap(os.path.join("./resources/ui/units/vehicles/", vehicle)) + VEHICLES_ICONS[vehicle[:-7]] = QPixmap(os.path.join("./resources/ui/units/vehicles/icons/", vehicle)) + +def load_aircraft_banners(): + for aircraft in os.listdir("./resources/ui/units/aircrafts/banners/"): + if aircraft.endswith(".jpg"): + AIRCRAFT_BANNERS[aircraft[:-7]] = QPixmap(os.path.join("./resources/ui/units/aircrafts/banners/", aircraft)) + +def load_vehicle_banners(): + for aircraft in os.listdir("./resources/ui/units/vehicles/banners/"): + if aircraft.endswith(".jpg"): + VEHICLE_BANNERS[aircraft[:-7]] = QPixmap(os.path.join("./resources/ui/units/vehicles/banners/", aircraft)) \ No newline at end of file diff --git a/qt_ui/widgets/QBudgetBox.py b/qt_ui/widgets/QBudgetBox.py index f732b685..67ef04a5 100644 --- a/qt_ui/widgets/QBudgetBox.py +++ b/qt_ui/widgets/QBudgetBox.py @@ -35,7 +35,7 @@ class QBudgetBox(QGroupBox): :param budget: Current money available :param reward: Planned reward for next turn """ - self.money_amount.setText(str(budget) + "M (+" + str(reward) + "M)") + self.money_amount.setText(str(budget) + "M (+" + str(round(reward,2)) + "M)") def setGame(self, game): if game is None: diff --git a/qt_ui/widgets/QConditionsWidget.py b/qt_ui/widgets/QConditionsWidget.py index 6116586b..89cf2cdb 100644 --- a/qt_ui/widgets/QConditionsWidget.py +++ b/qt_ui/widgets/QConditionsWidget.py @@ -1,12 +1,18 @@ -from PySide2.QtCore import Qt -from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QVBoxLayout, QFrame, QGridLayout from PySide2.QtGui import QPixmap - -from game.weather import Conditions, TimeOfDay, Weather -from game.utils import meter_to_nm, mps_to_knots +from PySide2.QtWidgets import ( + QFrame, + QGridLayout, + QGroupBox, + QHBoxLayout, + QLabel, + QVBoxLayout, +) from dcs.weather import Weather as PydcsWeather import qt_ui.uiconstants as CONST +from game.utils import mps +from game.weather import Conditions, TimeOfDay + class QTimeTurnWidget(QGroupBox): """ @@ -163,20 +169,20 @@ class QWeatherWidget(QGroupBox): def updateWinds(self): """Updates the UI with the current conditions wind info. """ - windGlSpeed = mps_to_knots(self.conditions.weather.wind.at_0m.speed or 0) + 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('{}kts'.format(windGlSpeed)) - self.windGLDirLabel.setText('{}º'.format(windGlDir)) + self.windGLSpeedLabel.setText(f'{int(windGlSpeed.knots)}kts') + self.windGLDirLabel.setText(f'{windGlDir}º') - windFL08Speed = mps_to_knots(self.conditions.weather.wind.at_2000m.speed or 0) + 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('{}kts'.format(windFL08Speed)) - self.windFL08DirLabel.setText('{}º'.format(windFL08Dir)) + self.windFL08SpeedLabel.setText(f'{int(windFL08Speed.knots)}kts') + self.windFL08DirLabel.setText(f'{windFL08Dir}º') - windFL26Speed = mps_to_knots(self.conditions.weather.wind.at_8000m.speed or 0) + 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('{}kts'.format(windFL26Speed)) - self.windFL26DirLabel.setText('{}º'.format(windFL26Dir)) + self.windFL26SpeedLabel.setText(f'{int(windFL26Speed.knots)}kts') + self.windFL26DirLabel.setText(f'{windFL26Dir}º') def updateForecast(self): """Updates the Forecast Text and icon with the current conditions wind info. @@ -223,11 +229,10 @@ class QWeatherWidget(QGroupBox): if not fog: self.forecastFog.setText('No fog') else: - visvibilityNm = round(meter_to_nm(fog.visibility), 1) - self.forecastFog.setText('Fog vis: {}nm'.format(visvibilityNm)) + visibility = round(fog.visibility.nautical_miles, 1) + self.forecastFog.setText(f'Fog vis: {visibility}nm') icon = [time, ('cloudy' if cloudDensity > 1 else None), 'fog'] - icon_key = "Weather_{}".format('-'.join(filter(None.__ne__, icon))) icon = CONST.ICONS.get(icon_key) or CONST.ICONS['Weather_night-partly-cloudy'] self.weather_icon.setPixmap(icon) diff --git a/qt_ui/widgets/QTopPanel.py b/qt_ui/widgets/QTopPanel.py index 6bf558ee..a01b2953 100644 --- a/qt_ui/widgets/QTopPanel.py +++ b/qt_ui/widgets/QTopPanel.py @@ -1,3 +1,6 @@ +import logging +import timeit +from datetime import timedelta from typing import List, Optional from PySide2.QtWidgets import ( @@ -130,9 +133,12 @@ class QTopPanel(QFrame): self.subwindow.show() def passTurn(self): + start = timeit.default_timer() self.game.pass_turn(no_action=True) GameUpdateSignal.get_instance().updateGame(self.game) self.proceedButton.setEnabled(True) + end = timeit.default_timer() + logging.info("Skipping turn took %s", timedelta(seconds=end - start)) def negative_start_packages(self) -> List[Package]: packages = [] diff --git a/qt_ui/widgets/ato.py b/qt_ui/widgets/ato.py index c4c38e22..aece9ee1 100644 --- a/qt_ui/widgets/ato.py +++ b/qt_ui/widgets/ato.py @@ -60,16 +60,15 @@ class FlightDelegate(QStyledItemDelegate): def first_row_text(self, index: QModelIndex) -> str: flight = self.flight(index) - task = flight.flight_type - count = flight.count - name = db.unit_type_name(flight.unit_type) estimator = TotEstimator(self.package) delay = estimator.mission_start_time(flight) - return f"[{task}] {count} x {name} in {delay}" + return f"{flight} in {delay}" def second_row_text(self, index: QModelIndex) -> str: flight = self.flight(index) origin = flight.from_cp.name + if flight.arrival != flight.departure: + return f"From {origin} to {flight.arrival.name}" return f"From {origin}" def paint(self, painter: QPainter, option: QStyleOptionViewItem, diff --git a/qt_ui/widgets/combos/QAircraftTypeSelector.py b/qt_ui/widgets/combos/QAircraftTypeSelector.py index f31c611d..64e3ab07 100644 --- a/qt_ui/widgets/combos/QAircraftTypeSelector.py +++ b/qt_ui/widgets/combos/QAircraftTypeSelector.py @@ -5,12 +5,50 @@ from PySide2.QtWidgets import QComboBox from dcs.unittype import FlyingType +from gen.flights.flight import FlightType + +import gen.flights.ai_flight_planner_db + +from game import Game, db class QAircraftTypeSelector(QComboBox): """Combo box for selecting among the given aircraft types.""" - def __init__(self, aircraft_types: Iterable[Type[FlyingType]]) -> None: + def __init__(self, aircraft_types: Iterable[Type[FlyingType]], country: str, mission_type: str) -> None: super().__init__() - for aircraft in aircraft_types: - self.addItem(f"{aircraft.id}", userData=aircraft) + self.model().sort(0) + self.setSizeAdjustPolicy(self.AdjustToContents) + self.country = country + self.updateItems(mission_type, aircraft_types) + + def updateItems(self, mission_type: str, aircraft_types): + current_aircraft = self.currentData() + self.clear() + for aircraft in aircraft_types: + if mission_type in [FlightType.BARCAP, FlightType.ESCORT, FlightType.INTERCEPTION, FlightType.SWEEP, FlightType.TARCAP]: + if aircraft in gen.flights.ai_flight_planner_db.CAP_CAPABLE: + self.addItem(f"{db.unit_get_expanded_info(self.country, aircraft, 'name')}", userData=aircraft) + elif mission_type in [FlightType.CAS, FlightType.BAI, FlightType.OCA_AIRCRAFT]: + if aircraft in gen.flights.ai_flight_planner_db.CAS_CAPABLE or aircraft in gen.flights.ai_flight_planner_db.TRANSPORT_CAPABLE: + self.addItem(f"{db.unit_get_expanded_info(self.country, aircraft, 'name')}", userData=aircraft) + elif mission_type in [FlightType.SEAD]: + if aircraft in gen.flights.ai_flight_planner_db.SEAD_CAPABLE: + self.addItem(f"{db.unit_get_expanded_info(self.country, aircraft, 'name')}", userData=aircraft) + elif mission_type in [FlightType.DEAD]: + if aircraft in gen.flights.ai_flight_planner_db.DEAD_CAPABLE: + self.addItem(f"{db.unit_get_expanded_info(self.country, aircraft, 'name')}", userData=aircraft) + elif mission_type in [FlightType.STRIKE]: + if aircraft in gen.flights.ai_flight_planner_db.STRIKE_CAPABLE or aircraft in gen.flights.ai_flight_planner_db.TRANSPORT_CAPABLE: + self.addItem(f"{db.unit_get_expanded_info(self.country, aircraft, 'name')}", userData=aircraft) + elif mission_type in [FlightType.ANTISHIP]: + if aircraft in gen.flights.ai_flight_planner_db.ANTISHIP_CAPABLE: + self.addItem(f"{db.unit_get_expanded_info(self.country, aircraft, 'name')}", userData=aircraft) + elif mission_type in [FlightType.OCA_RUNWAY]: + if aircraft in gen.flights.ai_flight_planner_db.RUNWAY_ATTACK_CAPABLE: + self.addItem(f"{db.unit_get_expanded_info(self.country, aircraft, 'name')}", userData=aircraft) + current_aircraft_index = self.findData(current_aircraft) + if current_aircraft_index != -1: + self.setCurrentIndex(current_aircraft_index) + if self.count() == 0: + self.addItem("No capable aircraft available", userData=None) \ No newline at end of file diff --git a/qt_ui/widgets/combos/QOriginAirfieldSelector.py b/qt_ui/widgets/combos/QOriginAirfieldSelector.py index 5a91a74d..364f8b04 100644 --- a/qt_ui/widgets/combos/QOriginAirfieldSelector.py +++ b/qt_ui/widgets/combos/QOriginAirfieldSelector.py @@ -27,6 +27,7 @@ class QOriginAirfieldSelector(QComboBox): self.aircraft = aircraft self.rebuild_selector() self.currentIndexChanged.connect(self.index_changed) + self.setSizeAdjustPolicy(self.AdjustToContents) def change_aircraft(self, aircraft: FlyingType) -> None: if self.aircraft == aircraft: diff --git a/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py b/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py index 8f40afde..8bb72d81 100644 --- a/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py +++ b/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py @@ -2,6 +2,7 @@ from PySide2.QtGui import QStandardItem, QStandardItemModel from game import Game from game.theater import ControlPointType +from game.utils import Distance from gen import BuildingGroundObject, Conflict, FlightWaypointType from gen.flights.flight import FlightWaypoint from qt_ui.widgets.combos.QFilteredComboBox import QFilteredComboBox @@ -59,7 +60,7 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox): FlightWaypointType.CUSTOM, pos.x, pos.y, - 800) + Distance.from_meters(800)) wpt.name = "Frontline " + cp.name + "/" + ecp.name + " [CAS]" wpt.alt_type = "RADIO" wpt.pretty_name = wpt.name @@ -70,12 +71,12 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox): for cp in self.game.theater.controlpoints: if (self.include_enemy and not cp.captured) or (self.include_friendly and cp.captured): for ground_object in cp.ground_objects: - if not ground_object.is_dead and not isinstance(ground_object, BuildingGroundObject): + if not ground_object.is_dead and isinstance(ground_object, BuildingGroundObject): wpt = FlightWaypoint( FlightWaypointType.CUSTOM, ground_object.position.x, ground_object.position.y, - 0 + Distance.from_meters(0) ) wpt.alt_type = "RADIO" wpt.name = ground_object.waypoint_name @@ -99,7 +100,7 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox): FlightWaypointType.CUSTOM, u.position.x, u.position.y, - 0 + Distance.from_meters(0) ) wpt.alt_type = "RADIO" wpt.name = wpt.name = "[" + str(ground_object.obj_name) + "] : " + u.type + " #" + str(j) @@ -120,7 +121,7 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox): FlightWaypointType.CUSTOM, cp.position.x, cp.position.y, - 0 + Distance.from_meters(0) ) wpt.alt_type = "RADIO" wpt.name = cp.name diff --git a/qt_ui/widgets/map/QFrontLine.py b/qt_ui/widgets/map/QFrontLine.py index 2ca71953..9f99ab13 100644 --- a/qt_ui/widgets/map/QFrontLine.py +++ b/qt_ui/widgets/map/QFrontLine.py @@ -58,13 +58,14 @@ class QFrontLine(QGraphicsLineItem): new_package_action.triggered.connect(self.open_new_package_dialog) menu.addAction(new_package_action) - cheat_forward = QAction(f"CHEAT: Advance Frontline") - cheat_forward.triggered.connect(self.cheat_forward) - menu.addAction(cheat_forward) + if self.game_model.game.settings.enable_frontline_cheats: + cheat_forward = QAction(f"CHEAT: Advance Frontline") + cheat_forward.triggered.connect(self.cheat_forward) + menu.addAction(cheat_forward) - cheat_backward = QAction(f"CHEAT: Retreat Frontline") - cheat_backward.triggered.connect(self.cheat_backward) - menu.addAction(cheat_backward) + cheat_backward = QAction(f"CHEAT: Retreat Frontline") + cheat_backward.triggered.connect(self.cheat_backward) + menu.addAction(cheat_backward) menu.exec_(event.screenPos()) diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py index 8b49077c..fc7d09b6 100644 --- a/qt_ui/widgets/map/QLiberationMap.py +++ b/qt_ui/widgets/map/QLiberationMap.py @@ -3,10 +3,11 @@ from __future__ import annotations import datetime import logging import math +from functools import singledispatchmethod from typing import Iterable, Iterator, List, Optional, Tuple -from PySide2 import QtWidgets, QtCore -from PySide2.QtCore import QPointF, Qt, QLineF, QRectF +from PySide2 import QtCore, QtWidgets +from PySide2.QtCore import QLineF, QPointF, QRectF, Qt from PySide2.QtGui import ( QBrush, QColor, @@ -14,30 +15,52 @@ from PySide2.QtGui import ( QPen, QPixmap, QPolygonF, - QWheelEvent, ) + QWheelEvent, +) from PySide2.QtWidgets import ( QFrame, QGraphicsItem, QGraphicsOpacityEffect, QGraphicsScene, - QGraphicsView, QGraphicsSceneMouseEvent, + QGraphicsSceneMouseEvent, + QGraphicsView, ) from dcs import Point +from dcs.planes import F_16C_50 from dcs.mapping import point_from_heading +from dcs.unitgroup import Group +from shapely.geometry import ( + LineString, + MultiPolygon, + Point as ShapelyPoint, + Polygon, +) import qt_ui.uiconstants as CONST -from game import Game, db +from game import Game +from game.navmesh import NavMesh from game.theater import ControlPoint, Enum from game.theater.conflicttheater import FrontLine, ReferencePoint from game.theater.theatergroundobject import ( TheaterGroundObject, ) -from game.utils import meter_to_feet, nm_to_meter, meter_to_nm +from game.utils import Distance, meters, nautical_miles from game.weather import TimeOfDay -from gen import Conflict -from gen.flights.flight import Flight, FlightWaypoint, FlightWaypointType -from gen.flights.flightplan import FlightPlan -from qt_ui.displayoptions import DisplayOptions +from gen import Conflict, Package +from gen.flights.flight import ( + Flight, + FlightType, + FlightWaypoint, + FlightWaypointType, +) +from gen.flights.flightplan import ( + BarCapFlightPlan, + FlightPlan, + FlightPlanBuilder, + InvalidObjectiveLocation, +) +from gen.flights.traveltime import TotEstimator +from qt_ui.displayoptions import DisplayOptions, ThreatZoneOptions from qt_ui.models import GameModel from qt_ui.widgets.map.QFrontLine import QFrontLine from qt_ui.widgets.map.QLiberationScene import QLiberationScene @@ -45,7 +68,7 @@ from qt_ui.widgets.map.QMapControlPoint import QMapControlPoint from qt_ui.widgets.map.QMapGroundObject import QMapGroundObject from qt_ui.windows.GameUpdateSignal import GameUpdateSignal -MAX_SHIP_DISTANCE = 80 +MAX_SHIP_DISTANCE = nautical_miles(80) def binomial(i: int, n: int) -> float: """Binomial coefficient""" @@ -157,6 +180,9 @@ class QLiberationMap(QGraphicsView): self.nm_to_pixel_ratio: int = 0 + self.navmesh_highlight: Optional[QPolygonF] = None + self.shortest_path_segments: List[QLineF] = [] + def init_scene(self): scene = QLiberationScene(self) @@ -171,7 +197,7 @@ class QLiberationMap(QGraphicsView): self.game = game if self.game is not None: logging.debug("Reloading Map Canvas") - self.nm_to_pixel_ratio = self.km_to_pixel(float(nm_to_meter(1)) / 1000.0) + self.nm_to_pixel_ratio = self.distance_to_pixels(nautical_miles(1)) self.reload_scene() """ @@ -239,29 +265,6 @@ class QLiberationMap(QGraphicsView): def update_reference_point(point: ReferencePoint, change: Point) -> None: point.image_coordinates += change - @staticmethod - def aa_ranges(ground_object: TheaterGroundObject) -> Tuple[int, int]: - detection_range = 0 - threat_range = 0 - for g in ground_object.groups: - for u in g.units: - unit = db.unit_type_from_name(u.type) - if unit is None: - logging.error(f"Unknown unit type {u.type}") - continue - - # Some units in pydcs have detection_range and threat_range - # defined, but explicitly set to None. - unit_detection_range = getattr(unit, "detection_range", None) - if unit_detection_range is not None: - detection_range = max(detection_range, unit_detection_range) - - unit_threat_range = getattr(unit, "threat_range", None) - if unit_threat_range is not None: - threat_range = max(threat_range, unit_threat_range) - - return detection_range, threat_range - def display_culling(self, scene: QGraphicsScene) -> None: """Draws the culling distance rings on the map""" culling_points = self.game_model.game.get_culling_points() @@ -273,19 +276,186 @@ class QLiberationMap(QGraphicsView): radius = distance_point[0] - transformed[0] scene.addEllipse(transformed[0]-radius, transformed[1]-radius, 2*radius, 2*radius, CONST.COLORS["transparent"], CONST.COLORS["light_green_transparent"]) + def draw_shapely_poly(self, scene: QGraphicsScene, poly: Polygon, pen: QPen, + brush: QBrush) -> Optional[QPolygonF]: + if poly.is_empty: + return None + points = [] + for x, y in poly.exterior.coords: + x, y = self._transform_point(Point(x, y)) + points.append(QPointF(x, y)) + return scene.addPolygon(QPolygonF(points), pen, brush) + + def draw_threat_zone(self, scene: QGraphicsScene, poly: Polygon, + player: bool) -> None: + if player: + brush = QColor(0, 132, 255, 100) + else: + brush = QColor(227, 32, 0, 100) + self.draw_shapely_poly(scene, poly, CONST.COLORS["transparent"], brush) + + def display_threat_zones(self, scene: QGraphicsScene, + options: ThreatZoneOptions, player: bool) -> None: + """Draws the threat zones on the map.""" + threat_zones = self.game.threat_zone_for(player) + if options.all: + threat_poly = threat_zones.all + elif options.aircraft: + threat_poly = threat_zones.airbases + elif options.air_defenses: + threat_poly = threat_zones.air_defenses + else: + return + + if isinstance(threat_poly, MultiPolygon): + polys = threat_poly.geoms + else: + polys = [threat_poly] + for poly in polys: + self.draw_threat_zone(scene, poly, player) + + def draw_navmesh_neighbor_line(self, scene: QGraphicsScene, poly: Polygon, + begin: ShapelyPoint) -> None: + vertex = Point(begin.x, begin.y) + centroid = poly.centroid + direction = Point(centroid.x, centroid.y) + end = vertex.point_from_heading(vertex.heading_between_point(direction), + nautical_miles(2).meters) + + scene.addLine(QLineF(QPointF(*self._transform_point(vertex)), + QPointF(*self._transform_point(end))), + CONST.COLORS["yellow"]) + + @singledispatchmethod + def draw_navmesh_border(self, intersection, scene: QGraphicsScene, + poly: Polygon) -> None: + raise NotImplementedError("draw_navmesh_border not implemented for %s", + intersection.__class__.__name__) + + @draw_navmesh_border.register + def draw_navmesh_point_border(self, intersection: ShapelyPoint, + scene: QGraphicsScene, poly: Polygon) -> None: + # Draw a line from the vertex toward the center of the polygon. + self.draw_navmesh_neighbor_line(scene, poly, intersection) + + @draw_navmesh_border.register + def draw_navmesh_edge_border(self, intersection: LineString, + scene: QGraphicsScene, poly: Polygon) -> None: + # Draw a line from the center of the edge toward the center of the + # polygon. + edge_center = intersection.interpolate(0.5, normalized=True) + self.draw_navmesh_neighbor_line(scene, poly, edge_center) + + def display_navmesh(self, scene: QGraphicsScene, player: bool) -> None: + for navpoly in self.game.navmesh_for(player).polys: + self.draw_shapely_poly(scene, navpoly.poly, CONST.COLORS["black"], + CONST.COLORS["transparent"]) + + position = self._transform_point( + Point(navpoly.poly.centroid.x, navpoly.poly.centroid.y)) + text = scene.addSimpleText(f"Navmesh {navpoly.ident}", + self.waypoint_info_font) + text.setBrush(QColor(255, 255, 255)) + text.setPen(QColor(255, 255, 255)) + text.moveBy(position[0] + 8, position[1]) + text.setZValue(2) + + for border in navpoly.neighbors.values(): + self.draw_navmesh_border(border, scene, navpoly.poly) + + def highlight_mouse_navmesh(self, scene: QGraphicsScene, navmesh: NavMesh, + mouse_position: Point) -> None: + if self.navmesh_highlight is not None: + try: + scene.removeItem(self.navmesh_highlight) + except RuntimeError: + pass + navpoly = navmesh.localize(mouse_position) + if navpoly is None: + return + self.navmesh_highlight = self.draw_shapely_poly( + scene, navpoly.poly, CONST.COLORS["transparent"], + CONST.COLORS["light_green_transparent"]) + + def draw_shortest_path(self, scene: QGraphicsScene, navmesh: NavMesh, + destination: Point, player: bool) -> None: + for line in self.shortest_path_segments: + try: + scene.removeItem(line) + except RuntimeError: + pass + + if player: + origin = self.game.theater.player_points()[0] + else: + origin = self.game.theater.enemy_points()[0] + + prev_pos = self._transform_point(origin.position) + try: + path = navmesh.shortest_path(origin.position, destination) + except ValueError: + return + for waypoint in path[1:]: + new_pos = self._transform_point(waypoint) + flight_path_pen = self.flight_path_pen(player, selected=True) + # Draw the line to the *middle* of the waypoint. + offset = self.WAYPOINT_SIZE // 2 + self.shortest_path_segments.append(scene.addLine( + prev_pos[0] + offset, prev_pos[1] + offset, + new_pos[0] + offset, new_pos[1] + offset, + flight_path_pen + )) + + self.shortest_path_segments.append(scene.addEllipse( + new_pos[0], new_pos[1], self.WAYPOINT_SIZE, + self.WAYPOINT_SIZE, flight_path_pen, flight_path_pen + )) + + prev_pos = new_pos + + def draw_test_flight_plan(self, scene: QGraphicsScene, task: FlightType, + point_near_target: Point, player: bool) -> None: + for line in self.shortest_path_segments: + try: + scene.removeItem(line) + except RuntimeError: + pass + + self.clear_flight_paths(scene) + + target = self.game.theater.closest_target(point_near_target) + + if player: + origin = self.game.theater.player_points()[0] + else: + origin = self.game.theater.enemy_points()[0] + + package = Package(target) + flight = Flight(package, F_16C_50, 2, task, start_type="Warm", + departure=origin, arrival=origin, divert=None) + package.add_flight(flight) + planner = FlightPlanBuilder(self.game, package, is_player=player) + try: + planner.populate_flight_plan(flight) + except InvalidObjectiveLocation: + return + + package.time_over_target = TotEstimator(package).earliest_tot() + self.draw_flight_plan(scene, flight, selected=True) + @staticmethod def should_display_ground_objects_at(cp: ControlPoint) -> bool: return ((DisplayOptions.sam_ranges and cp.captured) or (DisplayOptions.enemy_sam_ranges and not cp.captured)) - def draw_threat_range(self, scene: QGraphicsScene, ground_object: TheaterGroundObject, cp: ControlPoint) -> None: + def draw_threat_range(self, scene: QGraphicsScene, group: Group, ground_object: TheaterGroundObject, cp: ControlPoint) -> None: go_pos = self._transform_point(ground_object.position) - detection_range, threat_range = self.aa_ranges( - ground_object - ) + detection_range = ground_object.detection_range(group) + threat_range = ground_object.threat_range(group) if threat_range: - threat_pos = self._transform_point(Point(ground_object.position.x+threat_range, - ground_object.position.y+threat_range)) + threat_pos = self._transform_point( + ground_object.position + Point(threat_range.meters, + threat_range.meters)) threat_radius = Point(*go_pos).distance_to_point(Point(*threat_pos)) # Add threat range circle @@ -294,8 +464,9 @@ class QLiberationMap(QGraphicsView): if detection_range and DisplayOptions.detection_range: # Add detection range circle - detection_pos = self._transform_point(Point(ground_object.position.x+detection_range, - ground_object.position.y+detection_range)) + detection_pos = self._transform_point( + ground_object.position + Point(detection_range.meters, + detection_range.meters)) detection_radius = Point(*go_pos).distance_to_point(Point(*detection_pos)) scene.addEllipse(go_pos[0] - detection_radius/2 + 7, go_pos[1] - detection_radius/2 + 6, detection_radius, detection_radius, self.detection_pen(cp.captured)) @@ -313,7 +484,8 @@ class QLiberationMap(QGraphicsView): should_display = self.should_display_ground_objects_at(cp) if ground_object.might_have_aa and should_display: - self.draw_threat_range(scene, ground_object, cp) + for group in ground_object.groups: + self.draw_threat_range(scene, group, ground_object, cp) added_objects.append(ground_object.obj_name) def reload_scene(self): @@ -329,6 +501,16 @@ class QLiberationMap(QGraphicsView): if DisplayOptions.culling and self.game.settings.perf_culling: self.display_culling(scene) + self.display_threat_zones(scene, DisplayOptions.blue_threat_zones, + player=True) + self.display_threat_zones(scene, DisplayOptions.red_threat_zones, + player=False) + + if DisplayOptions.navmeshes.blue_navmesh: + self.display_navmesh(scene, player=True) + if DisplayOptions.navmeshes.red_navmesh: + self.display_navmesh(scene, player=False) + for cp in self.game.theater.controlpoints: pos = self._transform_point(cp.position) @@ -428,8 +610,30 @@ class QLiberationMap(QGraphicsView): flight.flight_plan) prev_pos = tuple(new_pos) - def draw_waypoint(self, scene: QGraphicsScene, position: Tuple[int, int], - player: bool, selected: bool) -> None: + if selected and DisplayOptions.barcap_commit_range: + self.draw_barcap_commit_range(scene, flight) + + def draw_barcap_commit_range(self, scene: QGraphicsScene, + flight: Flight) -> None: + if flight.flight_type is not FlightType.BARCAP: + return + if not isinstance(flight.flight_plan, BarCapFlightPlan): + return + start = flight.flight_plan.patrol_start + end = flight.flight_plan.patrol_end + line = LineString([ + ShapelyPoint(start.x, start.y), + ShapelyPoint(end.x, end.y), + ]) + doctrine = self.game.faction_for(flight.departure.captured).doctrine + bubble = line.buffer(doctrine.cap_engagement_range.meters) + self.flight_path_items.append(self.draw_shapely_poly( + scene, bubble, CONST.COLORS["yellow"], CONST.COLORS["transparent"] + )) + + def draw_waypoint(self, scene: QGraphicsScene, + position: Tuple[float, float], player: bool, + selected: bool) -> None: waypoint_pen = self.waypoint_pen(player, selected) waypoint_brush = self.waypoint_brush(player, selected) self.flight_path_items.append(scene.addEllipse( @@ -441,7 +645,7 @@ class QLiberationMap(QGraphicsView): waypoint: FlightWaypoint, position: Tuple[int, int], flight_plan: FlightPlan) -> None: - altitude = meter_to_feet(waypoint.alt) + altitude = int(waypoint.alt.feet) altitude_type = "AGL" if waypoint.alt_type == "RADIO" else "MSL" prefix = "TOT" @@ -472,8 +676,8 @@ class QLiberationMap(QGraphicsView): item.setZValue(2) self.flight_path_items.append(item) - def draw_flight_path(self, scene: QGraphicsScene, pos0: Tuple[int, int], - pos1: Tuple[int, int], player: bool, + def draw_flight_path(self, scene: QGraphicsScene, pos0: Tuple[float, float], + pos1: Tuple[float, float], player: bool, selected: bool) -> None: flight_path_pen = self.flight_path_pen(player, selected) # Draw the line to the *middle* of the waypoint. @@ -567,7 +771,7 @@ class QLiberationMap(QGraphicsView): BIG_LINE = 5 SMALL_LINE = 2 - dist = self.km_to_pixel(nm_to_meter(scale_distance_nm)/1000.0) + dist = self.distance_to_pixels(nautical_miles(scale_distance_nm)) self.scene().addRect(POS_X, POS_Y-PADDING, PADDING*2 + dist, BIG_LINE*2+3*PADDING, pen=CONST.COLORS["black"], brush=CONST.COLORS["black"]) l = self.scene().addLine(POS_X + PADDING, POS_Y + BIG_LINE*2, POS_X + PADDING + dist, POS_Y + BIG_LINE*2) @@ -663,12 +867,12 @@ class QLiberationMap(QGraphicsView): Point(offset.x / scale.x, offset.y / scale.y)) return point_a.world_coordinates - scaled - def km_to_pixel(self, km): + def distance_to_pixels(self, distance: Distance) -> int: p1 = Point(0, 0) - p2 = Point(0, 1000*km) + p2 = Point(0, distance.meters) p1a = Point(*self._transform_point(p1)) p2a = Point(*self._transform_point(p2)) - return p1a.distance_to_point(p2a) + return int(p1a.distance_to_point(p2a)) def highlight_color(self, transparent: Optional[bool] = False) -> QColor: return QColor(255, 255, 0, 20 if transparent else 255) @@ -742,7 +946,7 @@ class QLiberationMap(QGraphicsView): # Polygon display mode if self.game.theater.landmap is not None: - for sea_zone in self.game.theater.landmap[2]: + for sea_zone in self.game.theater.landmap.sea_zones: print(sea_zone) poly = QPolygonF([QPointF(*self._transform_point(Point(point[0], point[1]))) for point in sea_zone.exterior.coords]) if self.reference_point_setup_mode: @@ -751,14 +955,14 @@ class QLiberationMap(QGraphicsView): color = "sea_blue" scene.addPolygon(poly, CONST.COLORS[color], CONST.COLORS[color]) - for inclusion_zone in self.game.theater.landmap[0]: + for inclusion_zone in self.game.theater.landmap.inclusion_zones: poly = QPolygonF([QPointF(*self._transform_point(Point(point[0], point[1]))) for point in inclusion_zone.exterior.coords]) if self.reference_point_setup_mode: scene.addPolygon(poly, CONST.COLORS["grey_transparent"], CONST.COLORS["dark_grey_transparent"]) else: scene.addPolygon(poly, CONST.COLORS["grey"], CONST.COLORS["dark_grey"]) - for exclusion_zone in self.game.theater.landmap[1]: + for exclusion_zone in self.game.theater.landmap.exclusion_zones: poly = QPolygonF([QPointF(*self._transform_point(Point(point[0], point[1]))) for point in exclusion_zone.exterior.coords]) if self.reference_point_setup_mode: scene.addPolygon(poly, CONST.COLORS["grey_transparent"], CONST.COLORS["dark_dark_grey_transparent"]) @@ -820,22 +1024,60 @@ class QLiberationMap(QGraphicsView): distance = self.selected_cp.control_point.position.distance_to_point( world_destination ) - if meter_to_nm(distance) > MAX_SHIP_DISTANCE: + if meters(distance) > MAX_SHIP_DISTANCE: return False return self.game.theater.is_in_sea(world_destination) def sceneMouseMovedEvent(self, event: QGraphicsSceneMouseEvent): + if self.game is None: + return + + mouse_position = Point(event.scenePos().x(), event.scenePos().y()) if self.state == QLiberationMapState.MOVING_UNIT: self.setCursor(Qt.PointingHandCursor) self.movement_line.setLine( QLineF(self.movement_line.line().p1(), event.scenePos())) - pos = Point(event.scenePos().x(), event.scenePos().y()) - if self.is_valid_ship_pos(pos): + if self.is_valid_ship_pos(mouse_position): self.movement_line.setPen(CONST.COLORS["green"]) else: self.movement_line.setPen(CONST.COLORS["red"]) + mouse_world_pos = self._scene_to_dcs_coords(mouse_position) + if DisplayOptions.navmeshes.blue_navmesh: + self.highlight_mouse_navmesh( + self.scene(), self.game.blue_navmesh, + self._scene_to_dcs_coords(mouse_position)) + if DisplayOptions.path_debug.shortest_path: + self.draw_shortest_path(self.scene(), self.game.blue_navmesh, + mouse_world_pos, player=True) + + if DisplayOptions.navmeshes.red_navmesh: + self.highlight_mouse_navmesh( + self.scene(), self.game.red_navmesh, mouse_world_pos) + + debug_blue = DisplayOptions.path_debug_faction.blue + if DisplayOptions.path_debug.shortest_path: + self.draw_shortest_path( + self.scene(), self.game.navmesh_for(player=debug_blue), + mouse_world_pos, player=False) + elif not DisplayOptions.path_debug.hide: + if DisplayOptions.path_debug.barcap: + task = FlightType.BARCAP + elif DisplayOptions.path_debug.cas: + task = FlightType.CAS + elif DisplayOptions.path_debug.sweep: + task = FlightType.SWEEP + elif DisplayOptions.path_debug.strike: + task = FlightType.STRIKE + elif DisplayOptions.path_debug.tarcap: + task = FlightType.TARCAP + else: + raise ValueError( + "Unexpected value for DisplayOptions.path_debug") + self.draw_test_flight_plan(self.scene(), task, mouse_world_pos, + player=debug_blue) + def sceneMousePressEvent(self, event: QGraphicsSceneMouseEvent): if self.state == QLiberationMapState.MOVING_UNIT: if event.buttons() == Qt.RightButton: diff --git a/qt_ui/widgets/map/QMapControlPoint.py b/qt_ui/widgets/map/QMapControlPoint.py index e8607ff3..035db95e 100644 --- a/qt_ui/widgets/map/QMapControlPoint.py +++ b/qt_ui/widgets/map/QMapControlPoint.py @@ -89,7 +89,7 @@ class QMapControlPoint(QMapObject): return for connected in self.control_point.connected_points: - if connected.captured: + if connected.captured and self.game_model.game.settings.enable_base_capture_cheat: menu.addAction(self.capture_action) break diff --git a/qt_ui/widgets/map/QMapGroundObject.py b/qt_ui/widgets/map/QMapGroundObject.py index 7d8217b5..da69a9fc 100644 --- a/qt_ui/widgets/map/QMapGroundObject.py +++ b/qt_ui/widgets/map/QMapGroundObject.py @@ -58,9 +58,11 @@ class QMapGroundObject(QMapObject): @property def production_per_turn(self) -> int: production = 0 - for g in self.control_point.ground_objects: - if g.category in REWARDS.keys(): - production += REWARDS[g.category] + for building in self.buildings: + if building.is_dead: + continue + if building.category in REWARDS.keys(): + production += REWARDS[building.category] return production def paint(self, painter, option, widget=None) -> None: @@ -85,10 +87,22 @@ class QMapGroundObject(QMapObject): is_dead = False break + if cat == "aa": + has_threat = False + for group in self.ground_object.groups: + if self.ground_object.threat_range(group).distance_in_meters > 0: + has_threat = True + if not is_dead and not self.control_point.captured: - painter.drawPixmap(rect, const.ICONS[cat + enemy_icons]) + if cat == "aa" and not has_threat: + painter.drawPixmap(rect, const.ICONS["nothreat" + enemy_icons]) + else: + painter.drawPixmap(rect, const.ICONS[cat + enemy_icons]) elif not is_dead: - painter.drawPixmap(rect, const.ICONS[cat + player_icons]) + if cat == "aa" and not has_threat: + painter.drawPixmap(rect, const.ICONS["nothreat" + player_icons]) + else: + painter.drawPixmap(rect, const.ICONS[cat + player_icons]) else: painter.drawPixmap(rect, const.ICONS["destroyed"]) diff --git a/qt_ui/windows/QDebriefingWindow.py b/qt_ui/windows/QDebriefingWindow.py index a2d4c25c..f54530a0 100644 --- a/qt_ui/windows/QDebriefingWindow.py +++ b/qt_ui/windows/QDebriefingWindow.py @@ -52,7 +52,7 @@ class QDebriefingWindow(QDialog): for unit_type, count in player_air_losses.items(): try: lostUnitsLayout.addWidget( - QLabel(db.unit_type_name(unit_type)), row, 0) + QLabel(db.unit_get_expanded_info(self.debriefing.player_country, unit_type, 'name')), row, 0) lostUnitsLayout.addWidget(QLabel(str(count)), row, 1) row += 1 except AttributeError: @@ -94,7 +94,7 @@ class QDebriefingWindow(QDialog): for unit_type, count in enemy_air_losses.items(): try: enemylostUnitsLayout.addWidget( - QLabel(db.unit_type_name(unit_type)), row, 0) + QLabel(db.unit_get_expanded_info(self.debriefing.enemy_country, unit_type, 'name')), row, 0) enemylostUnitsLayout.addWidget(QLabel(str(count)), row, 1) row += 1 except AttributeError: diff --git a/qt_ui/windows/QLiberationWindow.py b/qt_ui/windows/QLiberationWindow.py index fbce72c1..bd12559e 100644 --- a/qt_ui/windows/QLiberationWindow.py +++ b/qt_ui/windows/QLiberationWindow.py @@ -168,18 +168,21 @@ class QLiberationWindow(QMainWindow): displayMenu = self.menu.addMenu("&Display") - last_was_group = True + + last_was_group = False for item in DisplayOptions.menu_items(): if isinstance(item, DisplayRule): + if last_was_group: + displayMenu.addSeparator() + self.display_bar.addSeparator() action = self.make_display_rule_action(item) displayMenu.addAction(action) if action.icon(): self.display_bar.addAction(action) last_was_group = False elif isinstance(item, DisplayGroup): - if not last_was_group: - displayMenu.addSeparator() - self.display_bar.addSeparator() + displayMenu.addSeparator() + self.display_bar.addSeparator() group = QActionGroup(displayMenu) for display_rule in item: action = self.make_display_rule_action(display_rule, group) @@ -257,8 +260,6 @@ class QLiberationWindow(QMainWindow): def setGame(self, game: Optional[Game]): try: - if game is not None: - game.on_load() self.game = game if self.info_panel is not None: self.info_panel.setGame(game) diff --git a/qt_ui/windows/QUnitInfoWindow.py b/qt_ui/windows/QUnitInfoWindow.py new file mode 100644 index 00000000..71735496 --- /dev/null +++ b/qt_ui/windows/QUnitInfoWindow.py @@ -0,0 +1,118 @@ +import logging +from typing import Type + +from PySide2 import QtCore +from PySide2.QtCore import Qt +from PySide2.QtGui import QIcon, QMovie, QPixmap +from PySide2.QtWidgets import ( + QDialog, + QGridLayout, + QGroupBox, + QHBoxLayout, + QLabel, + QMessageBox, + QPushButton, + QTextBrowser, + QFrame, +) +from jinja2 import Environment, FileSystemLoader, select_autoescape +from dcs.unittype import UnitType, FlyingType, VehicleType +import dcs +from qt_ui.uiconstants import AIRCRAFT_BANNERS, VEHICLE_BANNERS + +from game.game import Game +from game import db + +import gen.flights.ai_flight_planner_db +from gen.flights.flight import FlightType + + +class QUnitInfoWindow(QDialog): + + def __init__(self, game: Game, unit_type: Type[UnitType]) -> None: + super(QUnitInfoWindow, self).__init__() + self.setModal(True) + self.game = game + self.unit_type = unit_type + self.setWindowTitle(f"Unit Info: {db.unit_get_expanded_info(self.game.player_country, self.unit_type, 'name')}") + self.setWindowIcon(QIcon("./resources/icon.png")) + self.setMinimumHeight(570) + self.setMaximumWidth(640) + self.setWindowFlags(Qt.WindowStaysOnTopHint) + + self.initUi() + + def initUi(self): + self.layout = QGridLayout() + + header = QLabel(self) + header.setGeometry(0, 0, 720, 360) + if dcs.planes.plane_map.get(self.unit_type.id) is not None or dcs.helicopters.helicopter_map.get(self.unit_type.id) is not None: + pixmap = AIRCRAFT_BANNERS.get(self.unit_type.id) + elif dcs.vehicles.vehicle_map.get(self.unit_type.id) is not None: + pixmap = VEHICLE_BANNERS.get(self.unit_type.id) + if pixmap is None: + pixmap = AIRCRAFT_BANNERS.get("Missing") + header.setPixmap(pixmap.scaled(header.width(), header.height())) + self.layout.addWidget(header, 0, 0) + + self.gridLayout = QGridLayout() + + # Build the topmost details grid. + self.details_grid = QFrame() + self.details_grid_layout = QGridLayout() + self.details_grid_layout.setMargin(0) + + self.name_box = QLabel(f"Name: {db.unit_get_expanded_info(self.game.player_country, self.unit_type, 'manufacturer')} {db.unit_get_expanded_info(self.game.player_country, self.unit_type, 'name')}") + self.name_box.setProperty("style", "info-element") + + self.country_box = QLabel(f"Country of Origin: {db.unit_get_expanded_info(self.game.player_country, self.unit_type, 'country-of-origin')}") + self.country_box.setProperty("style", "info-element") + + self.role_box = QLabel(f"Role: {db.unit_get_expanded_info(self.game.player_country, self.unit_type, 'role')}") + self.role_box.setProperty("style", "info-element") + + self.year_box = QLabel(f"Variant Introduction: {db.unit_get_expanded_info(self.game.player_country, self.unit_type, 'year-of-variant-introduction')}") + self.year_box.setProperty("style", "info-element") + + self.details_grid_layout.addWidget(self.name_box, 0, 0) + self.details_grid_layout.addWidget(self.country_box, 0, 1) + self.details_grid_layout.addWidget(self.role_box, 1, 0) + self.details_grid_layout.addWidget(self.year_box, 1, 1) + + self.details_grid.setLayout(self.details_grid_layout) + + self.gridLayout.addWidget(self.details_grid, 1, 0) + + # If it's an aircraft, include the task list. + if dcs.planes.plane_map.get(self.unit_type.id) is not None or dcs.helicopters.helicopter_map.get(self.unit_type.id) is not None: + self.tasks_box = QLabel(f"In-Game Tasks: {self.generateAircraftTasks()}") + self.tasks_box.setProperty("style", "info-element") + self.gridLayout.addWidget(self.tasks_box, 2, 0) + + # Finally, add the description box. + self.details_text = QTextBrowser() + self.details_text.setProperty("style", "info-desc") + self.details_text.setText(db.unit_get_expanded_info(self.game.player_country, self.unit_type, "text")) + self.gridLayout.addWidget(self.details_text, 3, 0) + + self.layout.addLayout(self.gridLayout, 1, 0) + self.setLayout(self.layout) + + def generateAircraftTasks(self) -> str: + aircraft_tasks = "" + if self.unit_type in gen.flights.ai_flight_planner_db.CAP_CAPABLE: + aircraft_tasks = aircraft_tasks + f"{FlightType.BARCAP}, {FlightType.ESCORT}, {FlightType.INTERCEPTION}, {FlightType.SWEEP}, {FlightType.TARCAP}, " + if self.unit_type in gen.flights.ai_flight_planner_db.CAS_CAPABLE or self.unit_type in gen.flights.ai_flight_planner_db.TRANSPORT_CAPABLE: + aircraft_tasks = aircraft_tasks + f"{FlightType.CAS}, {FlightType.BAI}, {FlightType.OCA_AIRCRAFT}, " + if self.unit_type in gen.flights.ai_flight_planner_db.SEAD_CAPABLE: + aircraft_tasks = aircraft_tasks + f"{FlightType.SEAD}, " + if self.unit_type in gen.flights.ai_flight_planner_db.DEAD_CAPABLE: + aircraft_tasks = aircraft_tasks + f"{FlightType.DEAD}, " + if self.unit_type in gen.flights.ai_flight_planner_db.ANTISHIP_CAPABLE: + aircraft_tasks = aircraft_tasks + f"{FlightType.ANTISHIP}, " + if self.unit_type in gen.flights.ai_flight_planner_db.RUNWAY_ATTACK_CAPABLE: + aircraft_tasks = aircraft_tasks + f"{FlightType.OCA_RUNWAY}, " + if self.unit_type in gen.flights.ai_flight_planner_db.STRIKE_CAPABLE or self.unit_type in gen.flights.ai_flight_planner_db.TRANSPORT_CAPABLE: + aircraft_tasks = aircraft_tasks + f"{FlightType.STRIKE}, " + return aircraft_tasks[:-2] \ No newline at end of file diff --git a/qt_ui/windows/QWaitingForMissionResultWindow.py b/qt_ui/windows/QWaitingForMissionResultWindow.py index e101389c..db8e9f54 100644 --- a/qt_ui/windows/QWaitingForMissionResultWindow.py +++ b/qt_ui/windows/QWaitingForMissionResultWindow.py @@ -2,6 +2,8 @@ from __future__ import annotations import json import os +import timeit +from datetime import timedelta from PySide2 import QtCore from PySide2.QtCore import QObject, Qt, Signal @@ -184,11 +186,14 @@ class QWaitingForMissionResultWindow(QDialog): lambda d: self.on_debriefing_update(d), self.game, self.unit_map) def process_debriefing(self): + start = timeit.default_timer() self.game.finish_event(event=self.gameEvent, debriefing=self.debriefing) self.game.pass_turn() GameUpdateSignal.get_instance().sendDebriefing(self.debriefing) GameUpdateSignal.get_instance().updateGame(self.game) + end = timeit.default_timer() + logging.info("Turn processing took %s", timedelta(seconds=end - start)) self.close() def debriefing_directory_location(self) -> str: diff --git a/qt_ui/windows/basemenu/QRecruitBehaviour.py b/qt_ui/windows/basemenu/QRecruitBehaviour.py index 933d6a3e..20deb9a0 100644 --- a/qt_ui/windows/basemenu/QRecruitBehaviour.py +++ b/qt_ui/windows/basemenu/QRecruitBehaviour.py @@ -1,6 +1,7 @@ import logging from typing import Type +from PySide2.QtCore import Qt from PySide2.QtWidgets import ( QGroupBox, QHBoxLayout, @@ -17,6 +18,7 @@ from game.event import UnitsDeliveryEvent from game.theater import ControlPoint from qt_ui.models import GameModel from qt_ui.windows.GameUpdateSignal import GameUpdateSignal +from qt_ui.windows.QUnitInfoWindow import QUnitInfoWindow class QRecruitBehaviour: @@ -36,7 +38,6 @@ class QRecruitBehaviour: @property def pending_deliveries(self) -> UnitsDeliveryEvent: - assert self.cp.pending_unit_deliveries return self.cp.pending_unit_deliveries @property @@ -59,7 +60,7 @@ class QRecruitBehaviour: existing_units = self.cp.base.total_units_of_type(unit_type) scheduled_units = self.pending_deliveries.units.get(unit_type, 0) - unitName = QLabel("" + db.unit_type_name_2(unit_type) + "") + unitName = QLabel("" + db.unit_get_expanded_info(self.game_model.game.player_country, unit_type, 'name') + "") unitName.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)) existing_units = QLabel(str(existing_units)) @@ -97,6 +98,21 @@ class QRecruitBehaviour: sell.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)) sell.clicked.connect(lambda: self.sell(unit_type)) + info = QGroupBox() + info.setProperty("style", "buy-box") + info.setMaximumHeight(36) + info.setMinimumHeight(36) + infolayout = QHBoxLayout() + info.setLayout(infolayout) + + unitInfo = QPushButton("i") + unitInfo.setProperty("style", "btn-info") + unitInfo.setDisabled(disabled) + unitInfo.setMinimumSize(16, 16) + unitInfo.setMaximumSize(16, 16) + unitInfo.clicked.connect(lambda: self.info(unit_type)) + unitInfo.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)) + existLayout.addWidget(unitName) existLayout.addItem(QSpacerItem(20, 0, QSizePolicy.Minimum, QSizePolicy.Minimum)) existLayout.addWidget(existing_units) @@ -107,8 +123,11 @@ class QRecruitBehaviour: buysellayout.addWidget(amount_bought) buysellayout.addWidget(buy) + infolayout.addWidget(unitInfo) + layout.addWidget(exist, row, 1) layout.addWidget(buysell, row, 2) + layout.addWidget(info, row, 3) return row + 1 @@ -128,7 +147,7 @@ class QRecruitBehaviour: def buy(self, unit_type: Type[UnitType]): price = db.PRICES[unit_type] if self.budget >= price: - self.pending_deliveries.deliver({unit_type: 1}) + self.pending_deliveries.order({unit_type: 1}) self.budget -= price else: # TODO : display modal warning @@ -137,20 +156,19 @@ class QRecruitBehaviour: self.update_available_budget() def sell(self, unit_type): - if self.pending_deliveries.units.get(unit_type, 0) > 0: + if self.pending_deliveries.available_next_turn(unit_type) > 0: price = db.PRICES[unit_type] self.budget += price - self.pending_deliveries.units[unit_type] = self.pending_deliveries.units[unit_type] - 1 + self.pending_deliveries.sell({unit_type: 1}) if self.pending_deliveries.units[unit_type] == 0: del self.pending_deliveries.units[unit_type] - elif self.cp.base.total_units_of_type(unit_type) > 0: - price = db.PRICES[unit_type] - self.budget += price - self.cp.base.commit_losses({unit_type: 1}) - self._update_count_label(unit_type) self.update_available_budget() + def info(self, unit_type): + self.info_window = QUnitInfoWindow(self.game_model.game, unit_type) + self.info_window.show() + def set_maximum_units(self, maximum_units): """ Set the maximum number of units that can be bought diff --git a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py index 79ba0d1f..c1016b40 100644 --- a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py +++ b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py @@ -65,7 +65,7 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour): continue unit_types.add(unit) - sorted_units = sorted(unit_types, key=lambda u: db.unit_type_name_2(u)) + sorted_units = sorted(unit_types, key=lambda u: db.unit_get_expanded_info(self.game_model.game.player_country, u, 'name')) for unit_type in sorted_units: row = self.add_purchase_row( unit_type, task_box_layout, row, @@ -92,7 +92,13 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour): self, "No space for additional aircraft", f"There is no parking space left at {self.cp.name} to accommodate another plane.", QMessageBox.Ok) return - + # If we change our mind about selling, we want the aircraft to be put + # back in the inventory immediately. + elif self.pending_deliveries.units.get(unit_type, 0) < 0: + global_inventory = self.game_model.game.aircraft_inventory + inventory = global_inventory.for_control_point(self.cp) + inventory.add_aircraft(unit_type, 1) + super().buy(unit_type) self.hangar_status.update_label() diff --git a/qt_ui/windows/basemenu/airfield/QAirfieldCommand.py b/qt_ui/windows/basemenu/airfield/QAirfieldCommand.py index 97c804bf..cac85b83 100644 --- a/qt_ui/windows/basemenu/airfield/QAirfieldCommand.py +++ b/qt_ui/windows/basemenu/airfield/QAirfieldCommand.py @@ -17,6 +17,7 @@ class QAirfieldCommand(QFrame): def init_ui(self): layout = QGridLayout() + layout.setHorizontalSpacing(1) layout.addWidget(QAircraftRecruitmentMenu(self.cp, self.game_model), 0, 0) planned = QGroupBox("Planned Flights") diff --git a/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py b/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py index c359eaaf..3f089036 100644 --- a/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py +++ b/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py @@ -5,8 +5,10 @@ from PySide2.QtWidgets import ( QScrollArea, QVBoxLayout, QWidget, + QMessageBox, ) from dcs.task import PinpointStrike +from dcs.unittype import FlyingType, UnitType from game import db from game.theater import ControlPoint @@ -42,7 +44,7 @@ class QArmorRecruitmentMenu(QFrame, QRecruitBehaviour): for task_type in units.keys(): units_column = list(set(units[task_type])) if len(units_column) == 0: continue - units_column.sort(key=lambda x: db.PRICES[x]) + units_column.sort(key=lambda u: db.unit_get_expanded_info(self.game_model.game.player_country, u, 'name')) for unit_type in units_column: row = self.add_purchase_row(unit_type, task_box_layout, row) stretch = QVBoxLayout() @@ -57,3 +59,12 @@ class QArmorRecruitmentMenu(QFrame, QRecruitBehaviour): scroll.setWidget(scroll_content) main_layout.addWidget(scroll) self.setLayout(main_layout) + + def sell(self, unit_type: UnitType): + if self.pending_deliveries.available_next_turn(unit_type) <= 0: + QMessageBox.critical( + self, "Could not sell ground unit", + f"Attempted to sell one {unit_type.id} at {self.cp.name} " + "but none are available.", QMessageBox.Ok) + return + super().sell(unit_type) \ No newline at end of file diff --git a/qt_ui/windows/basemenu/intel/QIntelInfo.py b/qt_ui/windows/basemenu/intel/QIntelInfo.py index e422ef3a..6c40d5f9 100644 --- a/qt_ui/windows/basemenu/intel/QIntelInfo.py +++ b/qt_ui/windows/basemenu/intel/QIntelInfo.py @@ -4,7 +4,10 @@ from PySide2.QtWidgets import ( QGroupBox, QLabel, QVBoxLayout, + QScrollArea, + QWidget, ) +from PySide2.QtCore import Qt from dcs.task import CAP, CAS, Embarking, PinpointStrike from game import Game, db @@ -21,10 +24,11 @@ class QIntelInfo(QFrame): def init_ui(self): layout = QVBoxLayout() - - intel = QGroupBox("Intel") + scroll_content = QWidget() intelLayout = QVBoxLayout() + + units = { CAP: db.find_unittype(CAP, self.game.enemy_name), Embarking: db.find_unittype(Embarking, self.game.enemy_name), @@ -46,14 +50,19 @@ class QIntelInfo(QFrame): existing_units = self.cp.base.total_units_of_type(unit_type) if existing_units == 0: continue - groupLayout.addWidget(QLabel("" + db.unit_type_name(unit_type) + ""), row, 0) + groupLayout.addWidget(QLabel("" + db.unit_get_expanded_info(self.game.enemy_country, unit_type, 'name') + ""), row, 0) groupLayout.addWidget(QLabel(str(existing_units)), row, 1) row += 1 intelLayout.addWidget(group) - intelLayout.addStretch() - intel.setLayout(intelLayout) - layout.addWidget(intel) - layout.addStretch() + scroll_content.setLayout(intelLayout) + scroll = QScrollArea() + scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) + scroll.setWidgetResizable(True) + scroll.setWidget(scroll_content) + + layout.addWidget(scroll) + self.setLayout(layout) \ No newline at end of file diff --git a/qt_ui/windows/finances/QFinancesMenu.py b/qt_ui/windows/finances/QFinancesMenu.py index 3dfb195b..e1151b64 100644 --- a/qt_ui/windows/finances/QFinancesMenu.py +++ b/qt_ui/windows/finances/QFinancesMenu.py @@ -1,3 +1,6 @@ +import itertools +from typing import Optional + from PySide2.QtWidgets import ( QDialog, QFrame, @@ -8,7 +11,8 @@ from PySide2.QtWidgets import ( import qt_ui.uiconstants as CONST from game.game import Game -from game.income import Income +from game.income import BuildingIncome, Income +from game.theater import ControlPoint class QHorizontalSeparationLine(QFrame): @@ -25,44 +29,65 @@ class QHorizontalSeparationLine(QFrame): class FinancesLayout(QGridLayout): def __init__(self, game: Game, player: bool) -> None: super().__init__() + self.row = itertools.count(0) income = Income(game, player) - self.addWidget(QLabel("Control Points"), 0, 0) - self.addWidget(QLabel( - f"{len(income.control_points)} bases x {income.income_per_base}M"), - 0, 1) - self.addWidget(QLabel(f"{income.from_bases}M"), 0, 2) + control_points = reversed( + sorted(income.control_points, key=lambda c: c.income_per_turn)) + for control_point in control_points: + self.add_control_point(control_point) - self.addWidget(QHorizontalSeparationLine(), 1, 0, 1, 3) + self.add_line() buildings = reversed(sorted(income.buildings, key=lambda b: b.income)) - row = 2 - for row, building in enumerate(buildings, row): - self.addWidget( - QLabel(f"{building.category.upper()} [{building.name}]"), - row, 0) - self.addWidget(QLabel( - f"{building.number} buildings x {building.income_per_building}M"), - row, 1) - rlabel = QLabel(f"{building.income}M") - rlabel.setProperty("style", "green") - self.addWidget(rlabel, row, 2) + for building in buildings: + self.add_building(building) - self.addWidget(QHorizontalSeparationLine(), row + 1, 0, 1, 3) - self.addWidget(QLabel( - f"Income multiplier: {income.multiplier:.1f}"), - row + 2, 1 - ) - self.addWidget(QLabel(f"{income.total}M"), row + 2, 2) + self.add_line() + + self.add_row(middle=f"Income multiplier: {income.multiplier:.1f}", + right=f"{income.total}M") if player: budget = game.budget else: budget = game.enemy_budget - self.addWidget(QLabel(f"Balance"), row + 3, 1) - self.addWidget(QLabel(f"{budget}M"), row + 3, 2) - self.setRowStretch(row + 4, 1) + + self.add_row(middle="Balance", right=f"{budget}M") + self.setRowStretch(next(self.row), 1) + + def add_row(self, left: Optional[str] = None, middle: Optional[str] = None, + right: Optional[str] = None) -> None: + if not any([left, middle, right]): + raise ValueError + + row = next(self.row) + if left is not None: + self.addWidget(QLabel(left), row, 0) + if middle is not None: + self.addWidget(QLabel(middle), row, 1) + if right is not None: + self.addWidget(QLabel(right), row, 2) + + def add_control_point(self, control_point: ControlPoint) -> None: + self.add_row(left=f"{control_point.name}", + right=f"{control_point.income_per_turn}M") + + def add_building(self, building: BuildingIncome) -> None: + row = next(self.row) + self.addWidget( + QLabel(f"{building.category.upper()} [{building.name}]"), + row, 0) + self.addWidget(QLabel( + f"{building.number} buildings x {building.income_per_building}M"), + row, 1) + rlabel = QLabel(f"{building.income}M") + rlabel.setProperty("style", "green") + self.addWidget(rlabel, row, 2) + + def add_line(self) -> None: + self.addWidget(QHorizontalSeparationLine(), next(self.row), 0, 1, 3) class QFinancesMenu(QDialog): diff --git a/qt_ui/windows/groundobject/QGroundObjectMenu.py b/qt_ui/windows/groundobject/QGroundObjectMenu.py index dfbc5c11..1341eb39 100644 --- a/qt_ui/windows/groundobject/QGroundObjectMenu.py +++ b/qt_ui/windows/groundobject/QGroundObjectMenu.py @@ -357,8 +357,7 @@ class QBuyGroupForGroundObjectDialog(QDialog): # Generate SAM generator = sam_generator(self.game, self.ground_object) generator.generate() - generated_group = generator.get_generated_group() - self.ground_object.groups = [generated_group] + self.ground_object.groups = list(generator.groups) GameUpdateSignal.get_instance().updateBudget(self.game) diff --git a/qt_ui/windows/intel.py b/qt_ui/windows/intel.py index 497d65fb..b2db5421 100644 --- a/qt_ui/windows/intel.py +++ b/qt_ui/windows/intel.py @@ -14,7 +14,7 @@ from PySide2.QtWidgets import ( QWidget, ) -from game.game import Game +from game.game import Game, db from qt_ui.uiconstants import ICONS from qt_ui.windows.finances.QFinancesMenu import FinancesLayout @@ -81,7 +81,7 @@ class AircraftIntelLayout(IntelTableLayout): for airframe, count in base.aircraft.items(): if not count: continue - self.add_row(airframe.id, count) + self.add_row(db.unit_get_expanded_info(game.enemy_country, airframe, 'name'), count) self.add_spacer() self.add_row("Total", total) diff --git a/qt_ui/windows/mission/QEditFlightDialog.py b/qt_ui/windows/mission/QEditFlightDialog.py index 629a7836..a6893a47 100644 --- a/qt_ui/windows/mission/QEditFlightDialog.py +++ b/qt_ui/windows/mission/QEditFlightDialog.py @@ -4,9 +4,8 @@ from PySide2.QtWidgets import ( QVBoxLayout, ) -from gen.ato import Package from gen.flights.flight import Flight -from qt_ui.models import GameModel +from qt_ui.models import GameModel, PackageModel from qt_ui.uiconstants import EVENT_ICONS from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.mission.flight.QFlightPlanner import QFlightPlanner @@ -15,17 +14,19 @@ from qt_ui.windows.mission.flight.QFlightPlanner import QFlightPlanner class QEditFlightDialog(QDialog): """Dialog window for editing flight plans and loadouts.""" - def __init__(self, game_model: GameModel, package: Package, flight: Flight, parent=None) -> None: + def __init__(self, game_model: GameModel, package_model: PackageModel, + flight: Flight, parent=None) -> None: super().__init__(parent=parent) self.game_model = game_model - self.setWindowTitle("Create flight") + self.setWindowTitle("Edit flight") self.setWindowIcon(EVENT_ICONS["strike"]) layout = QVBoxLayout() - self.flight_planner = QFlightPlanner(package, flight, game_model.game) + self.flight_planner = QFlightPlanner(package_model, flight, + game_model.game) layout.addWidget(self.flight_planner) self.setLayout(layout) diff --git a/qt_ui/windows/mission/QPackageDialog.py b/qt_ui/windows/mission/QPackageDialog.py index 2cee9ba6..10acc4fe 100644 --- a/qt_ui/windows/mission/QPackageDialog.py +++ b/qt_ui/windows/mission/QPackageDialog.py @@ -3,8 +3,9 @@ import logging from datetime import timedelta from typing import Optional -from PySide2.QtCore import QItemSelection, QTime, Signal +from PySide2.QtCore import QItemSelection, QTime, Qt, Signal from PySide2.QtWidgets import ( + QCheckBox, QDialog, QHBoxLayout, QLabel, @@ -15,16 +16,15 @@ from PySide2.QtWidgets import ( ) from game.game import Game +from game.theater.missiontarget import MissionTarget from gen.ato import Package from gen.flights.flight import Flight from gen.flights.flightplan import FlightPlanBuilder, PlanningError -from gen.flights.traveltime import TotEstimator from qt_ui.models import AtoModel, GameModel, PackageModel from qt_ui.uiconstants import EVENT_ICONS from qt_ui.widgets.ato import QFlightList from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.mission.flight.QFlightCreator import QFlightCreator -from game.theater.missiontarget import MissionTarget class QPackageDialog(QDialog): @@ -78,15 +78,23 @@ class QPackageDialog(QDialog): self.tot_spinner.setMinimumTime(QTime(0, 0)) self.tot_spinner.setDisplayFormat("T+hh:mm:ss") self.tot_spinner.timeChanged.connect(self.save_tot) + self.tot_spinner.setToolTip("Package TOT relative to mission TOT") + self.tot_spinner.setEnabled(not self.package_model.package.auto_asap) self.tot_column.addWidget(self.tot_spinner) - self.reset_tot_button = QPushButton("ASAP") - self.reset_tot_button.setToolTip( + self.auto_asap = QCheckBox("ASAP") + self.auto_asap.setToolTip( "Sets the package TOT to the earliest time that all flights can " "arrive at the target." ) - self.reset_tot_button.clicked.connect(self.reset_tot) - self.tot_column.addWidget(self.reset_tot_button) + self.auto_asap.setChecked(self.package_model.package.auto_asap) + self.auto_asap.toggled.connect(self.set_asap) + self.tot_column.addWidget(self.auto_asap) + + self.tot_help_label = QLabel("Help") + self.tot_help_label.setAlignment(Qt.AlignCenter) + self.tot_help_label.setOpenExternalLinks(True) + self.tot_column.addWidget(self.tot_help_label) self.package_view = QFlightList(self.game_model, self.package_model) self.package_view.selectionModel().selectionChanged.connect( @@ -107,6 +115,8 @@ class QPackageDialog(QDialog): self.delete_flight_button.setEnabled(model.rowCount() > 0) self.button_layout.addWidget(self.delete_flight_button) + self.package_model.tot_changed.connect(self.update_tot) + self.button_layout.addStretch() self.setLayout(self.layout) @@ -139,14 +149,14 @@ class QPackageDialog(QDialog): def save_tot(self) -> None: time = self.tot_spinner.time() seconds = time.hour() * 3600 + time.minute() * 60 + time.second() - self.package_model.update_tot(timedelta(seconds=seconds)) + self.package_model.set_tot(timedelta(seconds=seconds)) - def reset_tot(self) -> None: - if not list(self.package_model.flights): - self.package_model.update_tot(timedelta()) - else: - self.package_model.update_tot( - TotEstimator(self.package_model.package).earliest_tot()) + def set_asap(self, checked: bool) -> None: + self.package_model.set_asap(checked) + self.tot_spinner.setEnabled(not self.package_model.package.auto_asap) + self.update_tot() + + def update_tot(self) -> None: self.tot_spinner.setTime(self.tot_qtime()) def on_selection_changed(self, selected: QItemSelection, @@ -177,6 +187,7 @@ class QPackageDialog(QDialog): QMessageBox.critical( self, "Could not create flight", str(ex), QMessageBox.Ok ) + self.package_model.update_tot() # noinspection PyUnresolvedReferences self.package_changed.emit() diff --git a/qt_ui/windows/mission/flight/QFlightCreator.py b/qt_ui/windows/mission/flight/QFlightCreator.py index 0e0bf773..eafd3c90 100644 --- a/qt_ui/windows/mission/flight/QFlightCreator.py +++ b/qt_ui/windows/mission/flight/QFlightCreator.py @@ -2,10 +2,13 @@ from typing import Optional from PySide2.QtCore import Qt, Signal from PySide2.QtWidgets import ( + QComboBox, QDialog, + QLabel, QMessageBox, QPushButton, QVBoxLayout, + QLineEdit, ) from dcs.planes import PlaneType @@ -31,6 +34,8 @@ class QFlightCreator(QDialog): self.game = game self.package = package + self.custom_name_text = None + self.country = self.game.player_country self.setWindowTitle("Create flight") self.setWindowIcon(EVENT_ICONS["strike"]) @@ -41,10 +46,12 @@ class QFlightCreator(QDialog): self.game.theater, package.target ) self.task_selector.setCurrentIndex(0) + self.task_selector.currentTextChanged.connect( + self.on_task_changed) layout.addLayout(QLabeledWidget("Task:", self.task_selector)) self.aircraft_selector = QAircraftTypeSelector( - self.game.aircraft_inventory.available_types_for_player + self.game.aircraft_inventory.available_types_for_player, self.game.player_country, self.task_selector.currentData() ) self.aircraft_selector.setCurrentIndex(0) self.aircraft_selector.currentIndexChanged.connect( @@ -57,6 +64,7 @@ class QFlightCreator(QDialog): self.aircraft_selector.currentData() ) self.departure.availability_changed.connect(self.update_max_size) + self.departure.currentIndexChanged.connect(self.on_departure_changed) layout.addLayout(QLabeledWidget("Departure:", self.departure)) self.arrival = QArrivalAirfieldSelector( @@ -88,6 +96,28 @@ class QFlightCreator(QDialog): layout.addLayout( QLabeledWidget("Client Slots:", self.client_slots_spinner)) + # When an off-map spawn overrides the start type to in-flight, we save + # the selected type into this value. If a non-off-map spawn is selected + # we restore the previous choice. + self.restore_start_type: Optional[str] = None + self.start_type = QComboBox() + self.start_type.addItems(["Cold", "Warm", "Runway", "In Flight"]) + self.start_type.setCurrentText(self.game.settings.default_start_type) + layout.addLayout(QLabeledWidget( + "Start type:", self.start_type, + tooltip="Selects the start type for this flight.")) + layout.addWidget(QLabel( + "Any option other than Cold will make this flight " + + "non-targetable
by OCA/Aircraft missions. This will affect " + + "game balance." + )) + + self.custom_name = QLineEdit() + self.custom_name.textChanged.connect(self.set_custom_name_text) + layout.addLayout( + QLabeledWidget("Custom Flight Name (Optional)", self.custom_name) + ) + layout.addStretch() self.create_button = QPushButton("Create") @@ -96,12 +126,19 @@ class QFlightCreator(QDialog): self.setLayout(layout) + self.on_departure_changed(self.departure.currentIndex()) + + def set_custom_name_text(self, text: str): + self.custom_name_text = text + def verify_form(self) -> Optional[str]: aircraft: PlaneType = self.aircraft_selector.currentData() origin: ControlPoint = self.departure.currentData() arrival: ControlPoint = self.arrival.currentData() divert: ControlPoint = self.divert.currentData() size: int = self.flight_size_spinner.value() + if aircraft is None: + return "You must select an aircraft type." if not origin.captured: return f"{origin.name} is not owned by your coalition." if arrival is not None and not arrival.captured: @@ -115,6 +152,8 @@ class QFlightCreator(QDialog): return f"{origin.name} has only {available} {aircraft.id} available." if size <= 0: return f"Flight must have at least one aircraft." + if self.custom_name_text and "|" in self.custom_name_text: + return f"Cannot include | in flight name" return None def create_flight(self) -> None: @@ -134,14 +173,9 @@ class QFlightCreator(QDialog): if arrival is None: arrival = origin - if isinstance(origin, OffMapSpawn): - start_type = "In Flight" - elif self.game.settings.perf_ai_parking_start: - start_type = "Cold" - else: - start_type = "Warm" - flight = Flight(self.package, aircraft, size, task, start_type, origin, - arrival, divert) + flight = Flight(self.package, self.country, aircraft, size, task, + self.start_type.currentText(), origin, arrival, divert, + custom_name=self.custom_name_text) flight.client_count = self.client_slots_spinner.value() # noinspection PyUnresolvedReferences @@ -154,6 +188,23 @@ class QFlightCreator(QDialog): self.arrival.change_aircraft(new_aircraft) self.divert.change_aircraft(new_aircraft) + def on_departure_changed(self, index: int) -> None: + departure = self.departure.itemData(index) + if isinstance(departure, OffMapSpawn): + previous_type = self.start_type.currentText() + if previous_type != "In Flight": + self.restore_start_type = previous_type + self.start_type.setCurrentText("In Flight") + self.start_type.setEnabled(False) + else: + self.start_type.setEnabled(True) + if self.restore_start_type is not None: + self.start_type.setCurrentText(self.restore_start_type) + self.restore_start_type = None + + def on_task_changed(self) -> None: + self.aircraft_selector.updateItems(self.task_selector.currentData(), self.game.aircraft_inventory.available_types_for_player) + def update_max_size(self, available: int) -> None: self.flight_size_spinner.setMaximum(min(available, 4)) if self.flight_size_spinner.maximum() >= 2: diff --git a/qt_ui/windows/mission/flight/QFlightPlanner.py b/qt_ui/windows/mission/flight/QFlightPlanner.py index b4eb9b36..71904b48 100644 --- a/qt_ui/windows/mission/flight/QFlightPlanner.py +++ b/qt_ui/windows/mission/flight/QFlightPlanner.py @@ -1,9 +1,8 @@ -from PySide2.QtCore import Signal from PySide2.QtWidgets import QTabWidget from game import Game -from gen.ato import Package from gen.flights.flight import Flight +from qt_ui.models import PackageModel from qt_ui.windows.mission.flight.payload.QFlightPayloadTab import \ QFlightPayloadTab from qt_ui.windows.mission.flight.settings.QGeneralFlightSettingsTab import \ @@ -14,22 +13,15 @@ from qt_ui.windows.mission.flight.waypoints.QFlightWaypointTab import \ class QFlightPlanner(QTabWidget): - on_planned_flight_changed = Signal() - - def __init__(self, package: Package, flight: Flight, game: Game): + def __init__(self, package_model: PackageModel, flight: Flight, game: Game): super().__init__() self.general_settings_tab = QGeneralFlightSettingsTab( - game, package, flight + game, package_model, flight ) - # noinspection PyUnresolvedReferences - self.general_settings_tab.on_flight_settings_changed.connect( - lambda: self.on_planned_flight_changed.emit()) self.payload_tab = QFlightPayloadTab(flight, game) - self.waypoint_tab = QFlightWaypointTab(game, package, flight) - # noinspection PyUnresolvedReferences - self.waypoint_tab.on_flight_changed.connect( - lambda: self.on_planned_flight_changed.emit()) + self.waypoint_tab = QFlightWaypointTab(game, package_model.package, + flight) self.addTab(self.general_settings_tab, "General Flight settings") self.addTab(self.payload_tab, "Payload") self.addTab(self.waypoint_tab, "Waypoints") diff --git a/qt_ui/windows/mission/flight/payload/QFlightPayloadTab.py b/qt_ui/windows/mission/flight/payload/QFlightPayloadTab.py index c6c3b66a..9ad63330 100644 --- a/qt_ui/windows/mission/flight/payload/QFlightPayloadTab.py +++ b/qt_ui/windows/mission/flight/payload/QFlightPayloadTab.py @@ -1,4 +1,5 @@ -from PySide2.QtWidgets import QFrame, QGridLayout +from PySide2.QtWidgets import QFrame, QGridLayout, QLabel +from PySide2.QtCore import Qt from game import Game from gen.flights.flight import Flight @@ -15,5 +16,13 @@ class QFlightPayloadTab(QFrame): def init_ui(self): layout = QGridLayout() + + # Docs Link + docsText = QLabel("How to create your own default loadout") + docsText.setAlignment(Qt.AlignCenter) + docsText.setOpenExternalLinks(True) + layout.addWidget(self.payload_editor) + layout.addWidget(docsText) + self.setLayout(layout) diff --git a/qt_ui/windows/mission/flight/payload/QLoadoutEditor.py b/qt_ui/windows/mission/flight/payload/QLoadoutEditor.py index 1f18c293..75c0f7ec 100644 --- a/qt_ui/windows/mission/flight/payload/QLoadoutEditor.py +++ b/qt_ui/windows/mission/flight/payload/QLoadoutEditor.py @@ -1,14 +1,21 @@ -import inspect - -from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QSpinBox, QGridLayout, QVBoxLayout, QSizePolicy +from PySide2.QtWidgets import ( + QGridLayout, + QGroupBox, + QLabel, + QSizePolicy, + QVBoxLayout, +) +from game import Game +from game.data.weapons import Pylon +from gen.flights.flight import Flight from qt_ui.windows.mission.flight.payload.QPylonEditor import QPylonEditor class QLoadoutEditor(QGroupBox): - def __init__(self, flight, game): - super(QLoadoutEditor, self).__init__("Use custom loadout") + def __init__(self, flight: Flight, game: Game) -> None: + super().__init__("Use custom loadout") self.flight = flight self.game = game self.setCheckable(True) @@ -19,22 +26,23 @@ class QLoadoutEditor(QGroupBox): hboxLayout = QVBoxLayout(self) layout = QGridLayout(self) - pylons = [v for v in self.flight.unit_type.__dict__.values() if inspect.isclass(v) and v.__name__.startswith("Pylon")] - for i, pylon in enumerate(pylons): - label = QLabel("{}".format(pylon.__name__[len("Pylon"):])) - label.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)) + for i, pylon in enumerate(Pylon.iter_pylons(self.flight.unit_type)): + label = QLabel(f"{pylon.number}") + label.setSizePolicy( + QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)) layout.addWidget(label, i, 0) - try: - pylon_number = int(pylon.__name__.split("Pylon")[1]) - except: - pylon_number = i+1 - layout.addWidget(QPylonEditor(flight, pylon, pylon_number), i, 1) + layout.addWidget(QPylonEditor(game, flight, pylon), i, 1) hboxLayout.addLayout(layout) hboxLayout.addStretch() self.setLayout(hboxLayout) + if not self.isChecked(): + for i in self.findChildren(QPylonEditor): + i.default_loadout() + def on_toggle(self): self.flight.use_custom_loadout = self.isChecked() - - + if not self.isChecked(): + for i in self.findChildren(QPylonEditor): + i.default_loadout(i.pylon.number) diff --git a/qt_ui/windows/mission/flight/payload/QPylonEditor.py b/qt_ui/windows/mission/flight/payload/QPylonEditor.py index 87a7c49a..9d95bc8c 100644 --- a/qt_ui/windows/mission/flight/payload/QPylonEditor.py +++ b/qt_ui/windows/mission/flight/payload/QPylonEditor.py @@ -1,38 +1,80 @@ import logging +import operator +from typing import Optional -from PySide2.QtWidgets import QWidget, QSpinBox, QComboBox +from PySide2.QtWidgets import QComboBox + +from game import Game, db +from game.data.weapons import Pylon, Weapon +from gen.flights.flight import Flight +from dcs import weapons_data class QPylonEditor(QComboBox): - def __init__(self, flight, pylon, pylon_number): - super(QPylonEditor, self).__init__() - self.pylon = pylon - self.pylon_number = pylon_number + def __init__(self, game: Game, flight: Flight, pylon: Pylon) -> None: + super().__init__() self.flight = flight + self.pylon = pylon + self.game = game - self.possible_loadout = [i for i in self.pylon.__dict__.keys() if i[:2] != '__'] + current = self.flight.loadout.get(self.pylon.number) - if not str(self.pylon_number) in self.flight.loadout.keys(): - self.flight.loadout[str(self.pylon_number)] = "" - - self.addItem("None") - for i,k in enumerate(self.possible_loadout): - self.addItem(str(self.pylon.__dict__[k][1]["name"])) - if self.flight.loadout[str(self.pylon_number)] == str(k): + self.addItem("None", None) + if self.game.settings.restrict_weapons_by_date: + weapons = pylon.available_on(self.game.date) + else: + weapons = pylon.allowed + allowed = sorted(weapons, key=operator.attrgetter("name")) + for i, weapon in enumerate(allowed): + self.addItem(weapon.name, weapon) + if current == weapon: self.setCurrentIndex(i + 1) - - self.currentTextChanged.connect(self.on_pylon_change) + + self.currentIndexChanged.connect(self.on_pylon_change) def on_pylon_change(self): - selected = self.currentText() - if selected == "None": - logging.info("Pylon " + str(self.pylon_number) + " emptied") - self.flight.loadout[str(self.pylon_number)] = "" - else: - logging.info("Pylon " + str(self.pylon_number) + " changed to " + selected) - for i, k in enumerate(self.possible_loadout): - if selected == str(self.pylon.__dict__[k][1]["name"]): - self.flight.loadout[str(self.pylon_number)] = str(k) - break + selected: Optional[Weapon] = self.currentData() + self.flight.loadout[self.pylon.number] = selected + if selected is None: + logging.debug(f"Pylon {self.pylon.number} emptied") + else: + logging.debug( + f"Pylon {self.pylon.number} changed to {selected.name}") + + def default_loadout(self) -> None: + self.flight.unit_type.load_payloads() + self.setCurrentText("None") + pylon_default_weapon = None + historical_weapon = None + loadout = None + # Iterate through each possible payload type for a given aircraft. + # Some aircraft have custom loadouts that in aren't the standard set. + for payload_override in db.EXPANDED_TASK_PAYLOAD_OVERRIDE.get(self.flight.flight_type.name): + if loadout is None: + loadout = self.flight.unit_type.loadout_by_name(payload_override) + if loadout is not None: + for i in loadout: + if i[0] == self.pylon.number: + pylon_default_weapon = i[1]["clsid"] + # TODO: Handle removed pylons better. + if pylon_default_weapon == "": + pylon_default_weapon = None + if pylon_default_weapon is not None: + if self.game.settings.restrict_weapons_by_date: + orig_weapon = Weapon.from_clsid(pylon_default_weapon) + if not orig_weapon.available_on(self.game.date): + for fallback in orig_weapon.fallbacks: + if historical_weapon is None: + if not self.pylon.can_equip(fallback): + continue + if not fallback.available_on(self.game.date): + continue + historical_weapon = fallback + else: + historical_weapon = orig_weapon + if historical_weapon is not None: + self.setCurrentText(weapons_data.weapon_ids.get(historical_weapon.cls_id).get("name")) + else: + self.setCurrentText(weapons_data.weapon_ids.get(pylon_default_weapon).get("name")) \ No newline at end of file diff --git a/qt_ui/windows/mission/flight/settings/FlightAirfieldDisplay.py b/qt_ui/windows/mission/flight/settings/FlightAirfieldDisplay.py new file mode 100644 index 00000000..4b56cf1b --- /dev/null +++ b/qt_ui/windows/mission/flight/settings/FlightAirfieldDisplay.py @@ -0,0 +1,99 @@ +import logging + +from PySide2.QtWidgets import (QGroupBox, QLabel, QMessageBox, QVBoxLayout) + +from game import Game +from gen.flights.flight import Flight +from gen.flights.flightplan import FlightPlanBuilder, PlanningError +from gen.flights.traveltime import TotEstimator +from qt_ui.models import PackageModel +from qt_ui.widgets.QLabeledWidget import QLabeledWidget +from qt_ui.widgets.combos.QArrivalAirfieldSelector import \ + QArrivalAirfieldSelector + + +class FlightAirfieldDisplay(QGroupBox): + + def __init__(self, game: Game, package_model: PackageModel, + flight: Flight) -> None: + super().__init__("Departure/Arrival") + self.game = game + self.package_model = package_model + self.flight = flight + + layout = QVBoxLayout() + + self.departure_time = QLabel() + layout.addLayout(QLabeledWidget( + f"Departing from {flight.from_cp.name}", + self.departure_time)) + self.package_model.tot_changed.connect(self.update_departure_time) + self.update_departure_time() + + layout.addWidget(QLabel("Determined based on the package TOT. Edit the " + "package to adjust the TOT.")) + + self.arrival = QArrivalAirfieldSelector( + [cp for cp in game.theater.controlpoints if cp.captured], + flight.unit_type, "Same as departure" + ) + self.arrival.currentIndexChanged.connect(self.set_arrival) + if flight.arrival != flight.departure: + self.arrival.setCurrentText(flight.arrival.name) + layout.addLayout(QLabeledWidget("Arrival:", self.arrival)) + + self.divert = QArrivalAirfieldSelector( + [cp for cp in game.theater.controlpoints if cp.captured], + flight.unit_type, "None" + ) + self.divert.currentIndexChanged.connect(self.set_divert) + if flight.divert is not None: + self.divert.setCurrentText(flight.divert.name) + layout.addLayout(QLabeledWidget("Divert:", self.divert)) + + self.setLayout(layout) + + def update_departure_time(self) -> None: + estimator = TotEstimator(self.package_model.package) + delay = estimator.mission_start_time(self.flight) + self.departure_time.setText(f"At T+{delay}") + + def set_arrival(self, index: int) -> None: + old_arrival = self.flight.arrival + arrival = self.arrival.itemData(index) + if arrival == old_arrival: + return + + if arrival is None: + arrival = self.flight.departure + + self.flight.arrival = arrival + try: + self.update_flight_plan() + except PlanningError as ex: + self.flight.arrival = old_arrival + logging.exception("Could not change arrival airfield") + QMessageBox.critical( + self, "Could not update flight plan", str(ex), QMessageBox.Ok + ) + + def set_divert(self, index: int) -> None: + old_divert = self.flight.divert + divert = self.divert.itemData(index) + if divert == old_divert: + return + + self.flight.divert = divert + try: + self.update_flight_plan() + except PlanningError as ex: + self.flight.divert = old_divert + logging.exception("Could not change divert airfield") + QMessageBox.critical( + self, "Could not update flight plan", str(ex), QMessageBox.Ok + ) + + def update_flight_plan(self) -> None: + planner = FlightPlanBuilder(self.game, self.package_model.package, + is_player=True) + planner.populate_flight_plan(self.flight) diff --git a/qt_ui/windows/mission/flight/settings/QCustomName.py b/qt_ui/windows/mission/flight/settings/QCustomName.py new file mode 100644 index 00000000..7a22e48d --- /dev/null +++ b/qt_ui/windows/mission/flight/settings/QCustomName.py @@ -0,0 +1,16 @@ +from PySide2.QtWidgets import QGroupBox, QHBoxLayout, QLabel + +from gen.flights.flight import Flight + + +class QFlightCustomName(QGroupBox): + + def __init__(self, flight: Flight): + super(QFlightCustomName, self).__init__() + + self.flight = flight + + self.layout = QHBoxLayout() + self.custom_name_label = QLabel(f"Custom Name: {flight.custom_name}") + self.layout.addWidget(self.custom_name_label) + self.setLayout(self.layout) diff --git a/qt_ui/windows/mission/flight/settings/QFlightDepartureDisplay.py b/qt_ui/windows/mission/flight/settings/QFlightDepartureDisplay.py deleted file mode 100644 index 6d789585..00000000 --- a/qt_ui/windows/mission/flight/settings/QFlightDepartureDisplay.py +++ /dev/null @@ -1,32 +0,0 @@ -import datetime - -from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QVBoxLayout - -from gen.ato import Package -from gen.flights.flight import Flight -from gen.flights.traveltime import TotEstimator - - -# TODO: Remove? -class QFlightDepartureDisplay(QGroupBox): - - def __init__(self, package: Package, flight: Flight): - super().__init__("Departure") - - layout = QVBoxLayout() - - departure_row = QHBoxLayout() - layout.addLayout(departure_row) - - estimator = TotEstimator(package) - delay = estimator.mission_start_time(flight) - - departure_row.addWidget(QLabel( - f"Departing from {flight.from_cp.name}" - )) - departure_row.addWidget(QLabel(f"At T+{delay}")) - - layout.addWidget(QLabel("Determined based on the package TOT. Edit the " - "package to adjust the TOT.")) - - self.setLayout(layout) diff --git a/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py b/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py index d6e745e0..f21b07b0 100644 --- a/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py +++ b/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py @@ -1,15 +1,20 @@ import logging from PySide2.QtCore import Signal -from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QSpinBox, QGridLayout +from PySide2.QtWidgets import QLabel, QGroupBox, QSpinBox, QGridLayout + +from game import Game +from gen.flights.flight import Flight +from qt_ui.models import PackageModel class QFlightSlotEditor(QGroupBox): changed = Signal() - def __init__(self, flight, game): - super(QFlightSlotEditor, self).__init__("Slots") + def __init__(self, package_model: PackageModel, flight: Flight, game: Game): + super().__init__("Slots") + self.package_model = package_model self.flight = flight self.game = game self.inventory = self.game.aircraft_inventory.for_control_point( @@ -22,14 +27,14 @@ class QFlightSlotEditor(QGroupBox): layout = QGridLayout() - self.aircraft_count = QLabel("Aircraft count :") + self.aircraft_count = QLabel("Aircraft count:") self.aircraft_count_spinner = QSpinBox() self.aircraft_count_spinner.setMinimum(1) self.aircraft_count_spinner.setMaximum(max_count) self.aircraft_count_spinner.setValue(flight.count) self.aircraft_count_spinner.valueChanged.connect(self._changed_aircraft_count) - self.client_count = QLabel("Client slots count :") + self.client_count = QLabel("Client slots count:") self.client_count_spinner = QSpinBox() self.client_count_spinner.setMinimum(0) self.client_count_spinner.setMaximum(max_count) @@ -70,6 +75,7 @@ class QFlightSlotEditor(QGroupBox): def _changed_client_count(self): self.flight.client_count = int(self.client_count_spinner.value()) self._cap_client_count() + self.package_model.update_tot() self.changed.emit() def _cap_client_count(self): diff --git a/qt_ui/windows/mission/flight/settings/QFlightStartType.py b/qt_ui/windows/mission/flight/settings/QFlightStartType.py index 438c56de..17fbe042 100644 --- a/qt_ui/windows/mission/flight/settings/QFlightStartType.py +++ b/qt_ui/windows/mission/flight/settings/QFlightStartType.py @@ -1,18 +1,25 @@ -from PySide2.QtWidgets import QGroupBox, QHBoxLayout, QComboBox, QLabel -from dcs.mission import StartType +from PySide2.QtWidgets import ( + QComboBox, + QGroupBox, + QHBoxLayout, + QLabel, + QVBoxLayout, +) from gen.flights.flight import Flight +from qt_ui.models import PackageModel class QFlightStartType(QGroupBox): - def __init__(self, flight:Flight): - super(QFlightStartType, self).__init__() - + def __init__(self, package_model: PackageModel, flight: Flight): + super().__init__() + self.package_model = package_model self.flight = flight - self.layout = QHBoxLayout() - self.start_type_label = QLabel("Start type : ") + self.layout = QVBoxLayout() + self.main_row = QHBoxLayout() + self.start_type_label = QLabel("Start type:") self.start_type = QComboBox() for i, st in enumerate([b for b in ["Cold", "Warm", "Runway", "In Flight"]]): @@ -21,13 +28,17 @@ class QFlightStartType(QGroupBox): self.start_type.setCurrentIndex(i) self.start_type.currentTextChanged.connect(self._on_start_type_selected) - self.layout.addWidget(self.start_type_label) - self.layout.addWidget(self.start_type) + self.main_row.addWidget(self.start_type_label) + self.main_row.addWidget(self.start_type) + + self.layout.addLayout(self.main_row) + self.layout.addWidget(QLabel( + "Any option other than Cold will make this flight non-targetable " + + "by OCA/Aircraft missions. This will affect game balance." + )) self.setLayout(self.layout) def _on_start_type_selected(self): selected = self.start_type.currentData() self.flight.start_type = selected - - - + self.package_model.update_tot() diff --git a/qt_ui/windows/mission/flight/settings/QGeneralFlightSettingsTab.py b/qt_ui/windows/mission/flight/settings/QGeneralFlightSettingsTab.py index f1419669..804fe8dc 100644 --- a/qt_ui/windows/mission/flight/settings/QGeneralFlightSettingsTab.py +++ b/qt_ui/windows/mission/flight/settings/QGeneralFlightSettingsTab.py @@ -2,38 +2,34 @@ from PySide2.QtCore import Signal from PySide2.QtWidgets import QFrame, QGridLayout, QVBoxLayout from game import Game -from gen.ato import Package from gen.flights.flight import Flight -from qt_ui.windows.mission.flight.settings.QFlightDepartureDisplay import \ - QFlightDepartureDisplay +from qt_ui.models import PackageModel +from qt_ui.windows.mission.flight.settings.FlightAirfieldDisplay import \ + FlightAirfieldDisplay from qt_ui.windows.mission.flight.settings.QFlightSlotEditor import \ QFlightSlotEditor from qt_ui.windows.mission.flight.settings.QFlightStartType import \ QFlightStartType from qt_ui.windows.mission.flight.settings.QFlightTypeTaskInfo import \ QFlightTypeTaskInfo +from qt_ui.windows.mission.flight.settings.QCustomName import \ + QFlightCustomName class QGeneralFlightSettingsTab(QFrame): on_flight_settings_changed = Signal() - def __init__(self, game: Game, package: Package, flight: Flight): + def __init__(self, game: Game, package_model: PackageModel, flight: Flight): super().__init__() layout = QGridLayout() - flight_info = QFlightTypeTaskInfo(flight) - flight_departure = QFlightDepartureDisplay(package, flight) - flight_slots = QFlightSlotEditor(flight, game) - flight_start_type = QFlightStartType(flight) - layout.addWidget(flight_info, 0, 0) - layout.addWidget(flight_departure, 1, 0) - layout.addWidget(flight_slots, 2, 0) - layout.addWidget(flight_start_type, 3, 0) + layout.addWidget(QFlightTypeTaskInfo(flight), 0, 0) + layout.addWidget(FlightAirfieldDisplay(game, package_model, flight), 1, + 0) + layout.addWidget(QFlightSlotEditor(package_model, flight, game), 2, 0) + layout.addWidget(QFlightStartType(package_model, flight), 3, 0) + layout.addWidget(QFlightCustomName(flight), 4, 0) vstretch = QVBoxLayout() vstretch.addStretch() - layout.addLayout(vstretch, 3, 0) + layout.addLayout(vstretch, 5, 0) self.setLayout(layout) - - flight_start_type.setEnabled(flight.client_count > 0) - flight_slots.changed.connect( - lambda: flight_start_type.setEnabled(flight.client_count > 0)) diff --git a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointInfoBox.py b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointInfoBox.py index ee72d8c0..137b70b7 100644 --- a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointInfoBox.py +++ b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointInfoBox.py @@ -12,7 +12,7 @@ class QFlightWaypointInfoBox(QGroupBox): self.flight_wpt = FlightWaypoint(0,0,0) self.x_position_label = QLabel(str(self.flight_wpt.x)) self.y_position_label = QLabel(str(self.flight_wpt.y)) - self.alt_label = QLabel(str(self.flight_wpt.alt)) + self.alt_label = QLabel(str(int(self.flight_wpt.alt.feet))) self.name_label = QLabel(str(self.flight_wpt.name)) self.desc_label = QLabel(str(self.flight_wpt.description)) self.init_ui() @@ -60,7 +60,7 @@ class QFlightWaypointInfoBox(QGroupBox): self.flight_wpt = FlightWaypoint(0,0,0) self.x_position_label.setText(str(self.flight_wpt.x)) self.y_position_label.setText(str(self.flight_wpt.y)) - self.alt_label.setText(str(self.flight_wpt.alt)) + self.alt_label.setText(str(int(self.flight_wpt.alt.feet))) self.name_label.setText(str(self.flight_wpt.name)) self.desc_label.setText(str(self.flight_wpt.description)) self.setTitle(self.flight_wpt.name) diff --git a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointList.py b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointList.py index 4aa3683c..28c63d59 100644 --- a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointList.py +++ b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointList.py @@ -4,7 +4,6 @@ from PySide2.QtCore import QItemSelectionModel, QPoint from PySide2.QtGui import QStandardItem, QStandardItemModel from PySide2.QtWidgets import QHeaderView, QTableView -from game.utils import meter_to_feet from gen.ato import Package from gen.flights.flight import Flight, FlightWaypoint, FlightWaypointType from qt_ui.windows.mission.flight.waypoints.QFlightWaypointItem import \ @@ -20,7 +19,6 @@ class QFlightWaypointList(QTableView): self.model = QStandardItemModel(self) self.setModel(self.model) - self.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.model.setHorizontalHeaderLabels(["Name", "Alt", "TOT/DEPART"]) header = self.horizontalHeader() @@ -42,6 +40,11 @@ class QFlightWaypointList(QTableView): self.add_waypoint_row(row, self.flight, waypoint) self.selectionModel().setCurrentIndex(self.indexAt(QPoint(1, 1)), QItemSelectionModel.Select) + self.resizeColumnsToContents() + total_column_width = self.verticalHeader().width() + self.lineWidth() + for i in range(0, self.model.columnCount()): + total_column_width += self.columnWidth(i) + self.lineWidth() + self.setFixedWidth(total_column_width) def add_waypoint_row(self, row: int, flight: Flight, waypoint: FlightWaypoint) -> None: @@ -49,7 +52,7 @@ class QFlightWaypointList(QTableView): self.model.setItem(row, 0, QWaypointItem(waypoint, row)) - altitude = meter_to_feet(waypoint.alt) + altitude = int(waypoint.alt.feet) altitude_type = "AGL" if waypoint.alt_type == "RADIO" else "MSL" altitude_item = QStandardItem(f"{altitude} ft {altitude_type}") altitude_item.setEditable(False) diff --git a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py index 92145a1c..abd1733d 100644 --- a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py +++ b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py @@ -29,8 +29,6 @@ from qt_ui.windows.mission.flight.waypoints \ class QFlightWaypointTab(QFrame): - on_flight_changed = Signal() - def __init__(self, game: Game, package: Package, flight: Flight): super(QFlightWaypointTab, self).__init__() self.game = game @@ -163,4 +161,3 @@ class QFlightWaypointTab(QFrame): def on_change(self): self.flight_waypoint_list.update_list() - self.on_flight_changed.emit() diff --git a/qt_ui/windows/newgame/QCampaignList.py b/qt_ui/windows/newgame/QCampaignList.py index 6f981694..7c9e7cec 100644 --- a/qt_ui/windows/newgame/QCampaignList.py +++ b/qt_ui/windows/newgame/QCampaignList.py @@ -4,7 +4,7 @@ import json import logging from dataclasses import dataclass from pathlib import Path -from typing import Any, Dict, List +from typing import Any, Dict, List, Union from PySide2 import QtGui from PySide2.QtCore import QItemSelectionModel @@ -14,6 +14,10 @@ from PySide2.QtWidgets import QAbstractItemView, QListView import qt_ui.uiconstants as CONST from game.theater import ConflictTheater +PERF_FRIENDLY = 0 +PERF_MEDIUM = 1 +PERF_HARD = 2 +PERF_NASA = 3 @dataclass(frozen=True) class Campaign: @@ -21,6 +25,9 @@ class Campaign: icon_name: str authors: str description: str + recommended_player_faction: str + recommended_enemy_faction: str + performance: Union[PERF_FRIENDLY, PERF_MEDIUM, PERF_HARD, PERF_NASA] data: Dict[str, Any] path: Path @@ -35,6 +42,9 @@ class Campaign: f"Terrain_{sanitized_theater}", data.get("authors", "???"), data.get("description", ""), + data.get("recommended_player_faction", "USA 2005"), + data.get("recommended_enemy_faction", "Russia 1990"), + data.get("performance", 0), data, path ) diff --git a/qt_ui/windows/newgame/QNewGameWizard.py b/qt_ui/windows/newgame/QNewGameWizard.py index 7838af3c..4d7bb5cb 100644 --- a/qt_ui/windows/newgame/QNewGameWizard.py +++ b/qt_ui/windows/newgame/QNewGameWizard.py @@ -5,18 +5,18 @@ from typing import List, Optional from PySide2 import QtGui, QtWidgets from PySide2.QtCore import QItemSelectionModel, QPoint, Qt -from PySide2.QtWidgets import QVBoxLayout, QTextEdit +from PySide2.QtWidgets import QVBoxLayout, QTextEdit, QLabel from jinja2 import Environment, FileSystemLoader, select_autoescape from game import db from game.settings import Settings +from game.theater.start_generator import GameGenerator, GeneratorSettings from qt_ui.widgets.spinsliders import TenthsSpinSlider from qt_ui.windows.newgame.QCampaignList import ( Campaign, QCampaignList, load_campaigns, ) -from game.theater.start_generator import GameGenerator, GeneratorSettings jinja_env = Environment( loader=FileSystemLoader("resources/ui/templates"), @@ -30,7 +30,7 @@ jinja_env = Environment( ) -DEFAULT_BUDGET = 650 +DEFAULT_BUDGET = 2000 class NewGameWizard(QtWidgets.QWizard): @@ -39,9 +39,10 @@ class NewGameWizard(QtWidgets.QWizard): self.campaigns = load_campaigns() + self.faction_selection_page = FactionSelection() self.addPage(IntroPage()) - self.addPage(FactionSelection()) - self.addPage(TheaterConfiguration(self.campaigns)) + self.addPage(TheaterConfiguration(self.campaigns, self.faction_selection_page)) + self.addPage(self.faction_selection_page) self.addPage(GeneratorOptions()) self.addPage(DifficultyAndAutomationOptions()) self.addPage(ConclusionPage()) @@ -177,10 +178,16 @@ class FactionSelection(QtWidgets.QWizardPage): # Create required mod layout self.requiredModsGroup = QtWidgets.QGroupBox("Required Mods") self.requiredModsGroupLayout = QtWidgets.QHBoxLayout() - self.requiredMods = QtWidgets.QLabel("") + self.requiredMods = QtWidgets.QLabel("") + self.requiredMods.setOpenExternalLinks(True) self.requiredModsGroupLayout.addWidget(self.requiredMods) self.requiredModsGroup.setLayout(self.requiredModsGroupLayout) + # Docs Link + docsText = QtWidgets.QLabel("How to create your own faction") + docsText.setAlignment(Qt.AlignCenter) + docsText.setOpenExternalLinks(True) + # Link form fields self.registerField('blueFaction', self.blueFactionSelect) self.registerField('redFaction', self.redFactionSelect) @@ -189,12 +196,32 @@ class FactionSelection(QtWidgets.QWizardPage): layout = QtWidgets.QVBoxLayout() layout.addWidget(self.factionsGroup) layout.addWidget(self.requiredModsGroup) + layout.addWidget(docsText) self.setLayout(layout) self.updateUnitRecap() self.blueFactionSelect.activated.connect(self.updateUnitRecap) self.redFactionSelect.activated.connect(self.updateUnitRecap) + + def setDefaultFactions(self, campaign:Campaign): + """ Set default faction for selected campaign """ + + self.blueFactionSelect.clear() + self.redFactionSelect.clear() + + for f in db.FACTIONS: + self.blueFactionSelect.addItem(f) + + for i, r in enumerate(db.FACTIONS): + self.redFactionSelect.addItem(r) + if r == campaign.recommended_enemy_faction: + self.redFactionSelect.setCurrentIndex(i) + if r == campaign.recommended_player_faction: + self.blueFactionSelect.setCurrentIndex(i) + + self.updateUnitRecap() + def updateUnitRecap(self): red_faction = db.FACTIONS[self.redFactionSelect.currentText()] @@ -233,9 +260,11 @@ class FactionSelection(QtWidgets.QWizardPage): class TheaterConfiguration(QtWidgets.QWizardPage): - def __init__(self, campaigns: List[Campaign], parent=None) -> None: + def __init__(self, campaigns: List[Campaign], faction_selection: FactionSelection, parent=None) -> None: super().__init__(parent) + self.faction_selection = faction_selection + self.setTitle("Theater configuration") self.setSubTitle("\nChoose a terrain and time period for this game.") self.setPixmap(QtWidgets.QWizard.LogoPixmap, @@ -251,13 +280,21 @@ class TheaterConfiguration(QtWidgets.QWizardPage): # Faction description self.campaignMapDescription = QTextEdit("") self.campaignMapDescription.setReadOnly(True) + self.campaignMapDescription.setMaximumHeight(150) + + self.performanceText = QTextEdit("") + self.performanceText.setReadOnly(True) + self.performanceText.setMaximumHeight(150) def on_campaign_selected(): template = jinja_env.get_template("campaigntemplate_EN.j2") + template_perf = jinja_env.get_template("campaign_performance_template_EN.j2") index = campaignList.selectionModel().currentIndex().row() campaign = campaignList.campaigns[index] self.setField("selectedCampaign", campaign) self.campaignMapDescription.setText(template.render({"campaign": campaign})) + self.faction_selection.setDefaultFactions(campaign) + self.performanceText.setText(template_perf.render({"performance": campaign.performance})) campaignList.selectionModel().setCurrentIndex(campaignList.indexAt(QPoint(1, 1)), QItemSelectionModel.Rows) campaignList.selectionModel().selectionChanged.connect(on_campaign_selected) @@ -270,11 +307,6 @@ class TheaterConfiguration(QtWidgets.QWizardPage): mapSettingsLayout = QtWidgets.QGridLayout() mapSettingsLayout.addWidget(QtWidgets.QLabel("Invert Map"), 0, 0) mapSettingsLayout.addWidget(invertMap, 0, 1) - - #mapSettingsLayout.addWidget(QtWidgets.QLabel("Start at mid game"), 1, 0) - #midgame = QtWidgets.QCheckBox() - #self.registerField('midGame', midgame) - #mapSettingsLayout.addWidget(midgame, 1, 1) mapSettingsGroup.setLayout(mapSettingsLayout) # Time Period @@ -286,9 +318,13 @@ class TheaterConfiguration(QtWidgets.QWizardPage): timePeriod.setBuddy(timePeriodSelect) timePeriodSelect.setCurrentIndex(21) + # Docs Link + docsText = QtWidgets.QLabel("How to create your own theater") + docsText.setAlignment(Qt.AlignCenter) + docsText.setOpenExternalLinks(True) + # Register fields self.registerField('timePeriod', timePeriodSelect) - self.registerField('timePeriod', timePeriodSelect) timeGroupLayout = QtWidgets.QGridLayout() timeGroupLayout.addWidget(timePeriod, 0, 0) @@ -297,10 +333,12 @@ class TheaterConfiguration(QtWidgets.QWizardPage): layout = QtWidgets.QGridLayout() layout.setColumnMinimumWidth(0, 20) - layout.addWidget(campaignList, 0, 0, 3, 1) + layout.addWidget(campaignList, 0, 0, 5, 1) + layout.addWidget(docsText, 5, 0, 1, 1) layout.addWidget(self.campaignMapDescription, 0, 1, 1, 1) - layout.addWidget(mapSettingsGroup, 1, 1, 1, 1) - layout.addWidget(timeGroup, 2, 1, 1, 1) + layout.addWidget(self.performanceText, 1, 1, 1, 1) + layout.addWidget(mapSettingsGroup, 2, 1, 1, 1) + layout.addWidget(timeGroup, 3, 1, 3, 1) self.setLayout(layout) diff --git a/qt_ui/windows/settings/QSettingsWindow.py b/qt_ui/windows/settings/QSettingsWindow.py index fa0d49b4..dad76e8d 100644 --- a/qt_ui/windows/settings/QSettingsWindow.py +++ b/qt_ui/windows/settings/QSettingsWindow.py @@ -23,6 +23,7 @@ from dcs.forcedoptions import ForcedOptions import qt_ui.uiconstants as CONST from game.game import Game from game.infos.information import Information +from game.settings import Settings from qt_ui.widgets.QLabeledWidget import QLabeledWidget from qt_ui.widgets.spinsliders import TenthsSpinSlider from qt_ui.windows.GameUpdateSignal import GameUpdateSignal @@ -39,13 +40,49 @@ class CheatSettingsBox(QGroupBox): self.red_ato_checkbox = QCheckBox() self.red_ato_checkbox.setChecked(game.settings.show_red_ato) self.red_ato_checkbox.toggled.connect(apply_settings) + + self.frontline_cheat_checkbox = QCheckBox() + self.frontline_cheat_checkbox.setChecked(game.settings.enable_frontline_cheats) + self.frontline_cheat_checkbox.toggled.connect(apply_settings) + + self.base_capture_cheat_checkbox = QCheckBox() + self.base_capture_cheat_checkbox.setChecked(game.settings.enable_base_capture_cheat) + self.base_capture_cheat_checkbox.toggled.connect(apply_settings) + self.red_ato = QLabeledWidget("Show Red ATO:", self.red_ato_checkbox) self.main_layout.addLayout(self.red_ato) + self.frontline_cheat = QLabeledWidget("Enable Frontline Cheats:", self.frontline_cheat_checkbox) + self.main_layout.addLayout(self.frontline_cheat) + self.base_capture_cheat = QLabeledWidget("Enable Base Capture Cheat:", self.base_capture_cheat_checkbox) + self.main_layout.addLayout(self.base_capture_cheat) @property def show_red_ato(self) -> bool: return self.red_ato_checkbox.isChecked() + @property + def show_frontline_cheat(self) -> bool: + return self.frontline_cheat_checkbox.isChecked() + + @property + def show_base_capture_cheat(self) -> bool: + return self.base_capture_cheat_checkbox.isChecked() + + +START_TYPE_TOOLTIP = "Selects the start type used for AI aircraft." + + +class StartTypeComboBox(QComboBox): + def __init__(self, settings: Settings) -> None: + super().__init__() + self.settings = settings + self.addItems(["Cold", "Warm", "Runway", "In Flight"]) + self.currentTextChanged.connect(self.on_change) + self.setToolTip(START_TYPE_TOOLTIP) + + def on_change(self, value: str) -> None: + self.settings.default_start_type = value + class QSettingsWindow(QDialog): @@ -248,6 +285,30 @@ class QSettingsWindow(QDialog): campaign_layout.setAlignment(Qt.AlignTop) self.campaign_management_page.setLayout(campaign_layout) + general = QGroupBox("General") + campaign_layout.addWidget(general) + + general_layout = QGridLayout() + general.setLayout(general_layout) + + def set_restict_weapons_by_date(value: bool) -> None: + self.game.settings.restrict_weapons_by_date = value + + restrict_weapons = QCheckBox() + restrict_weapons.setChecked(self.game.settings.restrict_weapons_by_date) + restrict_weapons.toggled.connect(set_restict_weapons_by_date) + + tooltip_text = ( + "Restricts weapon availability based on the campaign date. Data is " + "extremely incomplete so does not affect all weapons." + ) + restrict_weapons.setToolTip(tooltip_text) + restrict_weapons_label = QLabel("Restrict weapons by date (WIP)") + restrict_weapons_label.setToolTip(tooltip_text) + + general_layout.addWidget(restrict_weapons_label, 0, 0) + general_layout.addWidget(restrict_weapons, 0, 1, Qt.AlignRight) + automation = QGroupBox("HQ Automation") campaign_layout.addWidget(automation) @@ -325,6 +386,17 @@ class QSettingsWindow(QDialog): self.gameplayLayout.addWidget(self.never_delay_players, 2, 1, Qt.AlignRight) + start_type_label = QLabel( + "Default start type for AI aircraft:
Warning: " + + "Any option other than Cold breaks OCA/Aircraft missions." + ) + start_type_label.setToolTip(START_TYPE_TOOLTIP) + start_type = StartTypeComboBox(self.game.settings) + start_type.setCurrentText(self.game.settings.default_start_type) + + self.gameplayLayout.addWidget(start_type_label, 3, 0) + self.gameplayLayout.addWidget(start_type, 3, 1) + self.performance = QGroupBox("Performance") self.performanceLayout = QGridLayout() self.performanceLayout.setAlignment(Qt.AlignTop) @@ -350,10 +422,6 @@ class QSettingsWindow(QDialog): self.infantry.setChecked(self.game.settings.perf_infantry) self.infantry.toggled.connect(self.applySettings) - self.ai_parking_start = QCheckBox() - self.ai_parking_start.setChecked(self.game.settings.perf_ai_parking_start) - self.ai_parking_start.toggled.connect(self.applySettings) - self.destroyed_units = QCheckBox() self.destroyed_units.setChecked(self.game.settings.perf_destroyed_units) self.destroyed_units.toggled.connect(self.applySettings) @@ -382,8 +450,6 @@ class QSettingsWindow(QDialog): self.performanceLayout.addWidget(self.moving_units, 3, 1, alignment=Qt.AlignRight) self.performanceLayout.addWidget(QLabel("Generate infantry squads along vehicles"), 4, 0) self.performanceLayout.addWidget(self.infantry, 4, 1, alignment=Qt.AlignRight) - self.performanceLayout.addWidget(QLabel("AI planes parking start (AI starts in flight if disabled)"), 5, 0) - self.performanceLayout.addWidget(self.ai_parking_start, 5, 1, alignment=Qt.AlignRight) self.performanceLayout.addWidget(QLabel("Include destroyed units carcass"), 6, 0) self.performanceLayout.addWidget(self.destroyed_units, 6, 1, alignment=Qt.AlignRight) @@ -423,7 +489,7 @@ class QSettingsWindow(QDialog): btn = QPushButton("Cheat " + str(amount) + "M") btn.setProperty("style", "btn-danger") btn.clicked.connect(self.cheatLambda(amount)) - self.moneyCheatBoxLayout.addWidget(btn, i/2, i%2) + self.moneyCheatBoxLayout.addWidget(btn, i/2, i%2) self.cheatLayout.addWidget(self.moneyCheatBox, stretch=1) def cheatLambda(self, amount): @@ -459,7 +525,6 @@ class QSettingsWindow(QDialog): self.game.settings.perf_artillery = self.arti.isChecked() self.game.settings.perf_moving_units = self.moving_units.isChecked() self.game.settings.perf_infantry = self.infantry.isChecked() - self.game.settings.perf_ai_parking_start = self.ai_parking_start.isChecked() self.game.settings.perf_destroyed_units = self.destroyed_units.isChecked() self.game.settings.perf_culling = self.culling.isChecked() @@ -467,6 +532,8 @@ class QSettingsWindow(QDialog): self.game.settings.perf_do_not_cull_carrier = self.culling_do_not_cull_carrier.isChecked() self.game.settings.show_red_ato = self.cheat_options.show_red_ato + self.game.settings.enable_frontline_cheats = self.cheat_options.show_frontline_cheat + self.game.settings.enable_base_capture_cheat = self.cheat_options.show_base_capture_cheat self.game.compute_conflicts_position() GameUpdateSignal.get_instance().updateGame(self.game) diff --git a/requirements.txt b/requirements.txt index 1bedb462..702ac87f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,5 @@ -Pyside2>=5.13.0 +Pyside2>=5.15.2 pyinstaller==3.6 -pyproj==2.6.1.post1 Pillow~=7.2.0 tabulate~=0.8.7 diff --git a/resources/campaigns/battle_of_britain.json b/resources/campaigns/battle_of_britain.json index e4ab5896..1eb7fb74 100644 --- a/resources/campaigns/battle_of_britain.json +++ b/resources/campaigns/battle_of_britain.json @@ -2,6 +2,9 @@ "name": "The Channel - Battle of Britain", "theater": "The Channel", "authors": "Khopa", + "recommended_player_faction": "United Kingdom 1944", + "recommended_enemy_faction": "Germany 1942", "description": "

Experience the Battle of Britain on the Channel map !

Note: It is not possible to cross the channel to capture enemy bases yet, but you can consider you won if you manage to destroy all the ennemy targets

", - "miz": "battle_of_britain.miz" + "miz": "battle_of_britain.miz", + "performance": 1 } \ No newline at end of file diff --git a/resources/campaigns/black_sea.json b/resources/campaigns/black_sea.json index 116c7a3b..83cbe20b 100644 --- a/resources/campaigns/black_sea.json +++ b/resources/campaigns/black_sea.json @@ -3,5 +3,6 @@ "theater": "Caucasus", "authors": "Colonel Panic", "description": "

A medium sized theater with bases along the coast of the Black Sea.

", - "miz": "black_sea.miz" + "miz": "black_sea.miz", + "performance": 2 } \ No newline at end of file diff --git a/resources/campaigns/black_sea_lite.json b/resources/campaigns/black_sea_lite.json new file mode 100644 index 00000000..08fddc25 --- /dev/null +++ b/resources/campaigns/black_sea_lite.json @@ -0,0 +1,8 @@ +{ + "name": "Caucasus - Black Sea Lite", + "theater": "Caucasus", + "authors": "Starfire", + "description": "

A Small sized theater with bases along the coast of the Black Sea, lite version of ColonelPanic's Black Sea campaign scenario.

", + "miz": "black_sea_lite.miz", + "performance": 1 +} \ No newline at end of file diff --git a/resources/campaigns/black_sea_lite.miz b/resources/campaigns/black_sea_lite.miz new file mode 100644 index 00000000..4c6297ab Binary files /dev/null and b/resources/campaigns/black_sea_lite.miz differ diff --git a/resources/campaigns/desert_war.json b/resources/campaigns/desert_war.json index 5cd60c84..2c076f9d 100644 --- a/resources/campaigns/desert_war.json +++ b/resources/campaigns/desert_war.json @@ -2,6 +2,9 @@ "name": "Persian Gulf - Desert War", "theater": "Persian Gulf", "authors": "Khopa", + "recommended_player_faction": "USA 2005", + "recommended_enemy_faction": "Iran 2015", "description": "

This is a simple scenario in the Desert near Dubai and Abu-Dhabi. Progress from Liwa airbase to Al-Minhad.

This scenario shouldn't require too much performance.

", - "miz": "desert_war.miz" + "miz": "desert_war.miz", + "performance": 1 } \ No newline at end of file diff --git a/resources/campaigns/desert_war.miz b/resources/campaigns/desert_war.miz index 0a11477e..a1044bac 100644 Binary files a/resources/campaigns/desert_war.miz and b/resources/campaigns/desert_war.miz differ diff --git a/resources/campaigns/dunkirk.json b/resources/campaigns/dunkirk.json index 8ccbe8ba..47859eab 100644 --- a/resources/campaigns/dunkirk.json +++ b/resources/campaigns/dunkirk.json @@ -2,6 +2,9 @@ "name": "The Channel - Dunkirk", "theater": "The Channel", "authors": "Khopa", + "recommended_player_faction": "Allies 1944", + "recommended_enemy_faction": "Germany 1942", "description": "

In this scenario, your forces starts in Dunkirk and can be supported by the airfields on the other side of the Channel.

If you select the inverted configuration, you can play a German invasion of England.

Note: B-17 should be operated from Manston airfield

", - "miz": "dunkirk.miz" + "miz": "dunkirk.miz", + "performance": 1 } \ No newline at end of file diff --git a/resources/campaigns/emirates.json b/resources/campaigns/emirates.json index 6579d3e8..78ffc4f9 100644 --- a/resources/campaigns/emirates.json +++ b/resources/campaigns/emirates.json @@ -2,6 +2,9 @@ "name": "Persian Gulf - Emirates", "theater": "Persian Gulf", "authors": "Khopa", + "recommended_player_faction": "USA 2005", + "recommended_enemy_faction": "Iran 2015", "description": "

In this scenario, you can play an invasion of the Emirates and Oman, where your forces starts in Fujairah.

Note: Fujairah airfield has very few slots for aircrafts, so it recommended to operate from carriers at the start of the campaign. Thus, a carrier-capable faction is recommended.

", - "miz": "emirates.miz" + "miz": "emirates.miz", + "performance": 1 } \ No newline at end of file diff --git a/resources/campaigns/exercise_vegas_nerve.json b/resources/campaigns/exercise_vegas_nerve.json new file mode 100644 index 00000000..03f2b7af --- /dev/null +++ b/resources/campaigns/exercise_vegas_nerve.json @@ -0,0 +1,8 @@ +{ + "name": "Nevada - Exercise Vegas Nerve", + "theater": "Nevada", + "authors": "Starfire", + "description": "

A Red Flag Exercise scenario for the NTTR comprising 4 control points.

", + "miz": "exercise_vegas_nerve.miz", + "performance": 0 +} \ No newline at end of file diff --git a/resources/campaigns/exercise_vegas_nerve.miz b/resources/campaigns/exercise_vegas_nerve.miz new file mode 100644 index 00000000..7c0e3e25 Binary files /dev/null and b/resources/campaigns/exercise_vegas_nerve.miz differ diff --git a/resources/campaigns/full_caucasus.json b/resources/campaigns/full_caucasus.json index a11173ef..35fdc4a5 100644 --- a/resources/campaigns/full_caucasus.json +++ b/resources/campaigns/full_caucasus.json @@ -3,5 +3,6 @@ "theater": "Caucasus", "authors": "george", "description": "

Full map of the Caucasus

Note: This scenario is heavy on performance, enabling \"culling\" in settings is highly recommended.

", - "miz": "full_caucasus.miz" + "miz": "full_caucasus.miz", + "performance": 3 } \ No newline at end of file diff --git a/resources/campaigns/golan_heights.json b/resources/campaigns/golan_heights.json index 01973830..bd6106b0 100644 --- a/resources/campaigns/golan_heights.json +++ b/resources/campaigns/golan_heights.json @@ -2,6 +2,9 @@ "name": "Syria - Battle for Golan Heights", "theater": "Syria", "authors": "Khopa", + "recommended_player_faction": "Israel 2000", + "recommended_enemy_faction": "Syria 2011", "description": "

In this scenario, you start in Israel and the conflict is focused around the golan heights, an historically disputed territory.

You can use the inverted configuration to start on the Syrian side.

If this scenario is too heavy, try the lite version.

", - "miz": "golan_heights.miz" + "miz": "golan_heights.miz", + "performance": 2 } \ No newline at end of file diff --git a/resources/campaigns/golan_heights_lite.json b/resources/campaigns/golan_heights_lite.json index 1abd6641..70addc83 100644 --- a/resources/campaigns/golan_heights_lite.json +++ b/resources/campaigns/golan_heights_lite.json @@ -2,6 +2,9 @@ "name": "Syria - Battle for Golan Heights - Lite", "theater": "Syria", "authors": "Khopa", + "recommended_player_faction": "Israel 2000", + "recommended_enemy_faction": "Syria 2011", "description": "

In this scenario, you start in Israel and the conflict is focused around the golan heights, an historically disputed territory.

This scenario is designed to be performance friendly.

", - "miz": "golan_heights_lite.miz" + "miz": "golan_heights_lite.miz", + "performance": 1 } \ No newline at end of file diff --git a/resources/campaigns/inherent_resolve.json b/resources/campaigns/inherent_resolve.json index 66befcd5..10d8d2fe 100644 --- a/resources/campaigns/inherent_resolve.json +++ b/resources/campaigns/inherent_resolve.json @@ -2,6 +2,9 @@ "name": "Syria - Inherent Resolve", "theater": "Syria", "authors": "Khopa", + "recommended_player_faction": "USA 2005", + "recommended_enemy_faction": "Insurgents (Hard)", "description": "

In this scenario, you start from Jordan, and have to fight your way through eastern Syria.

", - "miz": "inherent_resolve.miz" + "miz": "inherent_resolve.miz", + "performance": 1 } \ No newline at end of file diff --git a/resources/campaigns/invasion_from_turkey.json b/resources/campaigns/invasion_from_turkey.json index f259a1e4..5610312a 100644 --- a/resources/campaigns/invasion_from_turkey.json +++ b/resources/campaigns/invasion_from_turkey.json @@ -2,6 +2,9 @@ "name": "Syria - Invasion from Turkey", "theater": "Syria", "authors": "Khopa", + "recommended_player_faction": "Turkey 2005", + "recommended_enemy_faction": "Insurgents (Hard)", "description": "

In this scenario, you start from Turkey and have to invade territories in northern Syria.

", - "miz": "invasion_from_turkey.miz" + "miz": "invasion_from_turkey.miz", + "performance": 1 } \ No newline at end of file diff --git a/resources/campaigns/invasion_of_iran.json b/resources/campaigns/invasion_of_iran.json index 8a8d475f..cb22512d 100644 --- a/resources/campaigns/invasion_of_iran.json +++ b/resources/campaigns/invasion_of_iran.json @@ -2,6 +2,9 @@ "name": "Persian Gulf - Invasion of Iran", "theater": "Persian Gulf", "authors": "Khopa", + "recommended_player_faction": "USA 2005", + "recommended_enemy_faction": "Iran 2015", "description": "

In this scenario, you start in Bandar Abbas, and must invade Iran.

", - "miz": "invasion_of_iran.miz" + "miz": "invasion_of_iran.miz", + "performance": 3 } \ No newline at end of file diff --git a/resources/campaigns/invasion_of_iran_[lite].json b/resources/campaigns/invasion_of_iran_[lite].json index 684c9cc9..fbeba09e 100644 --- a/resources/campaigns/invasion_of_iran_[lite].json +++ b/resources/campaigns/invasion_of_iran_[lite].json @@ -2,6 +2,9 @@ "name": "Persian Gulf - Invasion of Iran [Lite]", "theater": "Persian Gulf", "authors": "Khopa", + "recommended_player_faction": "USA 2005", + "recommended_enemy_faction": "Iran 2015", "description": "

This is lighter version of the invasion of Iran scenario.

", - "miz": "invasion_of_iran_lite.miz" + "miz": "invasion_of_iran_lite.miz", + "performance": 1 } \ No newline at end of file diff --git a/resources/campaigns/normandy.json b/resources/campaigns/normandy.json index caf3b123..8a21115c 100644 --- a/resources/campaigns/normandy.json +++ b/resources/campaigns/normandy.json @@ -2,6 +2,9 @@ "name": "Normandy - Normandy", "theater": "Normandy", "authors": "Khopa", + "recommended_player_faction": "Allies 1944", + "recommended_enemy_faction": "Germany 1944", "description": "

Normandy 1944 D-Day scenario.

", - "miz":"normandy.miz" + "miz":"normandy.miz", + "performance": 3 } \ No newline at end of file diff --git a/resources/campaigns/normandy_small.json b/resources/campaigns/normandy_small.json index 9aa63e78..d09e9a77 100644 --- a/resources/campaigns/normandy_small.json +++ b/resources/campaigns/normandy_small.json @@ -2,6 +2,9 @@ "name": "Normandy - Normandy Small", "theater": "Normandy", "authors": "Khopa", + "recommended_player_faction": "Allies 1944", + "recommended_enemy_faction": "Germany 1944", "description": "

A lighter version of the Normandy 1944 D-Day scenario.

", - "miz": "normandy_small.miz" + "miz": "normandy_small.miz", + "performance": 2 } \ No newline at end of file diff --git a/resources/campaigns/north_caucasus.json b/resources/campaigns/north_caucasus.json index 08f60007..3090b625 100644 --- a/resources/campaigns/north_caucasus.json +++ b/resources/campaigns/north_caucasus.json @@ -97,5 +97,6 @@ "Maykop-Khanskaya", "Mineralnye Vody" ] - ] + ], + "performance": 1 } \ No newline at end of file diff --git a/resources/campaigns/north_nevada.json b/resources/campaigns/north_nevada.json deleted file mode 100644 index 86521fc2..00000000 --- a/resources/campaigns/north_nevada.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "name": "Nevada - North Nevada", - "theater": "Nevada", - "authors": "Khopa", - "description": "

A scenario taking place in the NTTR. Here you can simulate a red flag exercise.

", - "player_points": [ - { - "type": "airbase", - "id": "Nellis AFB", - "size": 2000, - "importance": 1.4 - } - ], - "enemy_points": [ - { - "type": "airbase", - "id": "Tonopah Test Range Airfield", - "size": 600, - "importance": 1, - "captured_invert": true - }, - { - "type": "airbase", - "id": "Lincoln County", - "size": 600, - "importance": 1.2 - }, - { - "type": "airbase", - "id": "Groom Lake AFB", - "size": 1000, - "importance": 1.4 - }, - { - "type": "airbase", - "id": "Creech AFB", - "size": 2000, - "importance": 1.4 - }, - { - "type": "airbase", - "id": "Mesquite", - "size": 1000, - "importance": 1.3 - } - ], - "links": [ - [ - "Lincoln County", - "Tonopah Test Range Airfield" - ], - [ - "Groom Lake AFB", - "Tonopah Test Range Airfield" - ], - [ - "Lincoln County", - "Mesquite" - ], - [ - "Groom Lake AFB", - "Mesquite" - ], - [ - "Creech AFB", - "Groom Lake AFB" - ], - [ - "Creech AFB", - "Nellis AFB" - ] - ] -} \ No newline at end of file diff --git a/resources/campaigns/persian_gulf_full_map.json b/resources/campaigns/persian_gulf_full_map.json index b47ecce3..8c6e581d 100644 --- a/resources/campaigns/persian_gulf_full_map.json +++ b/resources/campaigns/persian_gulf_full_map.json @@ -3,5 +3,6 @@ "theater": "Persian Gulf", "authors": "Plob", "description": "

Full map of the Persian Gulf

Note: This scenario is heavy on performance, enabling \"culling\" in settings is highly recommended.

", - "miz": "persian_gulf_full_map.miz" + "miz": "persian_gulf_full_map.miz", + "performance": 3 } \ No newline at end of file diff --git a/resources/campaigns/russia_small.json b/resources/campaigns/russia_small.json index 490ebf93..570c70ee 100644 --- a/resources/campaigns/russia_small.json +++ b/resources/campaigns/russia_small.json @@ -2,6 +2,9 @@ "name": "Caucasus - Russia Small", "theater": "Caucasus", "authors": "Khopa", + "recommended_player_faction": "Russia 2010", + "recommended_enemy_faction": "USA 1990", "description": "

A small theater in Russia, progress from Mozdok to Maykop.

This scenario is pretty simple, it is ideal if you want to run a short campaign. If your PC is not powerful, this is also the less performance heavy scenario.

", - "miz": "russia_small.miz" + "miz": "russia_small.miz", + "performance": 0 } \ No newline at end of file diff --git a/resources/campaigns/syria_full_map_remastered.json b/resources/campaigns/syria_full_map_remastered.json index d0df2b54..e0c9a235 100644 --- a/resources/campaigns/syria_full_map_remastered.json +++ b/resources/campaigns/syria_full_map_remastered.json @@ -2,6 +2,7 @@ "name": "Syria - Full Map", "theater": "Syria", "authors": "Hawkmoon", - "description": "

Full map of Syria

Note:For a better early game experience is suggested to give the AI an high amount of starting money This scenario is heavy on performance, enabling \"culling\" in settings is highly recommended.

", - "miz": "syria_full_map_remastered.miz" + "description": "

Full map of Syria

Note : 

For a better early game experience, it is suggested to give the AI an high amount of starting money.

", + "miz": "syria_full_map_remastered.miz", + "performance": 3 } \ No newline at end of file diff --git a/resources/campaigns/syrian_civil_war.json b/resources/campaigns/syrian_civil_war.json index 77f879fe..86fb6ad1 100644 --- a/resources/campaigns/syrian_civil_war.json +++ b/resources/campaigns/syrian_civil_war.json @@ -2,6 +2,9 @@ "name": "Syria - Syrian Civil War", "theater": "Syria", "authors": "Khopa", + "recommended_player_faction": "Russia 2010", + "recommended_enemy_faction": "Insurgents (Hard)", "description": "

This scenario can be used to simulate parts of the Syrian Civil War.

You start on the coast with an airbase in Latakia, and ground forces in Tartus.

This scenario can also be used to simulate a western invasion of Syria.

In inverted configuration you start in Aleppo.

", - "miz": "syrian_civil_war.miz" + "miz": "syrian_civil_war.miz", + "performance": 2 } \ No newline at end of file diff --git a/resources/campaigns/western_georgia.json b/resources/campaigns/western_georgia.json deleted file mode 100644 index df1c755d..00000000 --- a/resources/campaigns/western_georgia.json +++ /dev/null @@ -1,113 +0,0 @@ -{ - "name": "Caucasus - Western Georgia", - "theater": "Caucasus", - "authors": "Khopa", - "description": "

A medium sized theater in the south west of the Caucasus map. Shouldn't be too hard on performance.

", - "player_points": [ - { - "type": "airbase", - "id": "Kobuleti", - "radials": [ - 0, - 45, - 90, - 135, - 180, - 225, - 315 - ], - "size": 600, - "importance": 1.1 - }, - { - "type": "carrier", - "id": 1001, - "x": -285810.6875, - "y": 496399.1875, - "captured_invert": true - }, - { - "type": "lha", - "id": 1002, - "x": -326050.6875, - "y": 519452.1875, - "captured_invert": true - } - ], - "enemy_points": [ - { - "type": "airbase", - "id": "Kutaisi", - "size": 600, - "importance": 1 - }, - { - "type": "airbase", - "id": "Senaki-Kolkhi", - "size": 1000, - "importance": 1 - }, - { - "type": "airbase", - "id": "Sukhumi-Babushara", - "radials": [ - 315, - 0, - 45, - 90, - 135 - ], - "size": 1000, - "importance": 1.2 - }, - { - "type": "airbase", - "id": "Gudauta", - "radials": [ - 315, - 0, - 45, - 90, - 135 - ], - "size": 1000, - "importance": 1.2 - }, - { - "type": "airbase", - "id": "Sochi-Adler", - "radials": [ - 315, - 0, - 45, - 90, - 135 - ], - "size": 2000, - "importance": 1.4, - "captured_invert": true - } - ], - "links": [ - [ - "Kutaisi", - "Senaki-Kolkhi" - ], - [ - "Kobuleti", - "Senaki-Kolkhi" - ], - [ - "Senaki-Kolkhi", - "Sukhumi-Babushara" - ], - [ - "Gudauta", - "Sukhumi-Babushara" - ], - [ - "Gudauta", - "Sochi-Adler" - ] - ] -} \ No newline at end of file diff --git a/resources/caulandmap.p b/resources/caulandmap.p index c9f77f43..c5076169 100644 Binary files a/resources/caulandmap.p and b/resources/caulandmap.p differ diff --git a/resources/channellandmap.p b/resources/channellandmap.p index 9f5ba1f8..92864e8c 100644 Binary files a/resources/channellandmap.p and b/resources/channellandmap.p differ diff --git a/resources/customized_payloads/AH-64A.lua b/resources/customized_payloads/AH-64A.lua index 80613b07..1bce3138 100644 --- a/resources/customized_payloads/AH-64A.lua +++ b/resources/customized_payloads/AH-64A.lua @@ -39,6 +39,14 @@ local unitPayloads = { ["CLSID"] = "{88D18A5E-99C8-4B04-B40B-1C02F2018B6E}", ["num"] = 1, }, + [3] = { + ["CLSID"] = "{FD90A1DC-9147-49FA-BF56-CB83EF0BD32B}", + ["num"] = 3, + }, + [4] = { + ["CLSID"] = "{FD90A1DC-9147-49FA-BF56-CB83EF0BD32B}", + ["num"] = 2, + }, }, ["tasks"] = { [1] = 18, @@ -58,6 +66,14 @@ local unitPayloads = { ["CLSID"] = "{88D18A5E-99C8-4B04-B40B-1C02F2018B6E}", ["num"] = 1, }, + [3] = { + ["CLSID"] = "{FD90A1DC-9147-49FA-BF56-CB83EF0BD32B}", + ["num"] = 3, + }, + [4] = { + ["CLSID"] = "{FD90A1DC-9147-49FA-BF56-CB83EF0BD32B}", + ["num"] = 2, + }, }, ["tasks"] = { [1] = 18, @@ -101,6 +117,14 @@ local unitPayloads = { ["num"] = 4, }, [2] = { + ["CLSID"] = "{88D18A5E-99C8-4B04-B40B-1C02F2018B6E}", + ["num"] = 3, + }, + [3] = { + ["CLSID"] = "{88D18A5E-99C8-4B04-B40B-1C02F2018B6E}", + ["num"] = 2, + }, + [4] = { ["CLSID"] = "{88D18A5E-99C8-4B04-B40B-1C02F2018B6E}", ["num"] = 1, }, diff --git a/resources/customized_payloads/AH-64D.lua b/resources/customized_payloads/AH-64D.lua index f6ecce67..cac514d3 100644 --- a/resources/customized_payloads/AH-64D.lua +++ b/resources/customized_payloads/AH-64D.lua @@ -9,6 +9,14 @@ local unitPayloads = { ["num"] = 4, }, [2] = { + ["CLSID"] = "{FD90A1DC-9147-49FA-BF56-CB83EF0BD32B}", + ["num"] = 3, + }, + [3] = { + ["CLSID"] = "{FD90A1DC-9147-49FA-BF56-CB83EF0BD32B}", + ["num"] = 2, + }, + [4] = { ["CLSID"] = "{88D18A5E-99C8-4B04-B40B-1C02F2018B6E}", ["num"] = 1, }, @@ -28,6 +36,14 @@ local unitPayloads = { ["num"] = 4, }, [2] = { + ["CLSID"] = "{FD90A1DC-9147-49FA-BF56-CB83EF0BD32B}", + ["num"] = 3, + }, + [3] = { + ["CLSID"] = "{FD90A1DC-9147-49FA-BF56-CB83EF0BD32B}", + ["num"] = 2, + }, + [4] = { ["CLSID"] = "{88D18A5E-99C8-4B04-B40B-1C02F2018B6E}", ["num"] = 1, }, @@ -77,6 +93,14 @@ local unitPayloads = { ["CLSID"] = "{88D18A5E-99C8-4B04-B40B-1C02F2018B6E}", ["num"] = 1, }, + [3] = { + ["CLSID"] = "{88D18A5E-99C8-4B04-B40B-1C02F2018B6E}", + ["num"] = 3, + }, + [4] = { + ["CLSID"] = "{88D18A5E-99C8-4B04-B40B-1C02F2018B6E}", + ["num"] = 2, + }, }, ["tasks"] = { [1] = 18, @@ -93,6 +117,14 @@ local unitPayloads = { ["num"] = 4, }, [2] = { + ["CLSID"] = "{FD90A1DC-9147-49FA-BF56-CB83EF0BD32B}", + ["num"] = 3, + }, + [3] = { + ["CLSID"] = "{FD90A1DC-9147-49FA-BF56-CB83EF0BD32B}", + ["num"] = 2, + }, + [4] = { ["CLSID"] = "{88D18A5E-99C8-4B04-B40B-1C02F2018B6E}", ["num"] = 1, }, diff --git a/resources/customized_payloads/AV8BNA.lua b/resources/customized_payloads/AV8BNA.lua index d2ba669a..ef0cb25a 100644 --- a/resources/customized_payloads/AV8BNA.lua +++ b/resources/customized_payloads/AV8BNA.lua @@ -2,114 +2,6 @@ local unitPayloads = { ["name"] = "AV8BNA", ["payloads"] = { [1] = { - ["name"] = "CAP", - ["pylons"] = { - [1] = { - ["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}", - ["num"] = 8, - }, - [2] = { - ["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}", - ["num"] = 1, - }, - [3] = { - ["CLSID"] = "{AIM-9M-ON-ADAPTER}", - ["num"] = 2, - }, - [4] = { - ["CLSID"] = "{AIM-9M-ON-ADAPTER}", - ["num"] = 7, - }, - [5] = { - ["CLSID"] = "{GAU_12_Equalizer}", - ["num"] = 4, - }, - }, - ["tasks"] = { - [1] = 31, - }, - }, - [2] = { - ["name"] = "CAS", - ["pylons"] = { - [1] = { - ["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}", - ["num"] = 8, - }, - [2] = { - ["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}", - ["num"] = 1, - }, - [3] = { - ["CLSID"] = "LAU_117_AGM_65G", - ["num"] = 2, - }, - [4] = { - ["CLSID"] = "LAU_117_AGM_65G", - ["num"] = 7, - }, - [5] = { - ["CLSID"] = "{GAU_12_Equalizer}", - ["num"] = 4, - }, - [6] = { - ["CLSID"] = "{A111396E-D3E8-4b9c-8AC9-2432489304D5}", - ["num"] = 5, - }, - }, - ["tasks"] = { - [1] = 31, - }, - }, - [3] = { - ["name"] = "STRIKE", - ["pylons"] = { - [1] = { - ["CLSID"] = "{BRU-42_2*GBU-38_LEFT}", - ["num"] = 2, - }, - [2] = { - ["CLSID"] = "{BRU-42_2*GBU-38_RIGHT}", - ["num"] = 7, - }, - [3] = { - ["CLSID"] = "{A111396E-D3E8-4b9c-8AC9-2432489304D5}", - ["num"] = 5, - }, - }, - ["tasks"] = { - [1] = 31, - }, - }, - [4] = { - ["name"] = "ANTISHIP", - ["pylons"] = { - [1] = { - ["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}", - ["num"] = 8, - }, - [2] = { - ["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}", - ["num"] = 1, - }, - [3] = { - ["CLSID"] = "LAU_117_AGM_65G", - ["num"] = 2, - }, - [4] = { - ["CLSID"] = "LAU_117_AGM_65G", - ["num"] = 7, - }, - [5] = { - ["CLSID"] = "{A111396E-D3E8-4b9c-8AC9-2432489304D5}", - ["num"] = 5, - }, - }, - ["tasks"] = { - [1] = 31, - }, - }, - [5] = { ["name"] = "INTERCEPT", ["pylons"] = { [1] = { @@ -137,7 +29,7 @@ local unitPayloads = { [1] = 31, }, }, - [6] = { + [2] = { ["name"] = "SEAD", ["pylons"] = { [1] = { @@ -149,11 +41,11 @@ local unitPayloads = { ["num"] = 1, }, [3] = { - ["CLSID"] = "{LAU_7_AGM_122_SIDEARM}", + ["CLSID"] = "LAU_117_AGM_65F", ["num"] = 2, }, [4] = { - ["CLSID"] = "{LAU_7_AGM_122_SIDEARM}", + ["CLSID"] = "LAU_117_AGM_65F", ["num"] = 7, }, [5] = { @@ -161,11 +53,11 @@ local unitPayloads = { ["num"] = 5, }, [6] = { - ["CLSID"] = "LAU_117_AGM_65G", + ["CLSID"] = "LAU_117_AGM_65F", ["num"] = 6, }, [7] = { - ["CLSID"] = "LAU_117_AGM_65G", + ["CLSID"] = "LAU_117_AGM_65F", ["num"] = 3, }, }, @@ -173,6 +65,130 @@ local unitPayloads = { [1] = 31, }, }, + [3] = { + ["name"] = "CAS", + ["pylons"] = { + [1] = { + ["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}", + ["num"] = 8, + }, + [2] = { + ["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}", + ["num"] = 1, + }, + [3] = { + ["CLSID"] = "LAU_117_AGM_65F", + ["num"] = 2, + }, + [4] = { + ["CLSID"] = "LAU_117_AGM_65F", + ["num"] = 7, + }, + [5] = { + ["CLSID"] = "{GAU_12_Equalizer}", + ["num"] = 4, + }, + [6] = { + ["CLSID"] = "{A111396E-D3E8-4b9c-8AC9-2432489304D5}", + ["num"] = 5, + }, + [7] = { + ["CLSID"] = "{LAU-131 - 7 AGR-20 M282}", + ["num"] = 6, + }, + [8] = { + ["CLSID"] = "{LAU-131 - 7 AGR-20 M282}", + ["num"] = 3, + }, + }, + ["tasks"] = { + [1] = 31, + }, + }, + [4] = { + ["name"] = "ANTISHIP", + ["pylons"] = { + [1] = { + ["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}", + ["num"] = 8, + }, + [2] = { + ["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}", + ["num"] = 1, + }, + [3] = { + ["CLSID"] = "LAU_117_AGM_65F", + ["num"] = 2, + }, + [4] = { + ["CLSID"] = "LAU_117_AGM_65F", + ["num"] = 7, + }, + [5] = { + ["CLSID"] = "{A111396E-D3E8-4b9c-8AC9-2432489304D5}", + ["num"] = 5, + }, + [6] = { + ["CLSID"] = "LAU_117_AGM_65F", + ["num"] = 6, + }, + [7] = { + ["CLSID"] = "LAU_117_AGM_65F", + ["num"] = 3, + }, + }, + ["tasks"] = { + [1] = 31, + }, + }, + [5] = { + ["name"] = "STRIKE", + ["pylons"] = { + [1] = { + ["CLSID"] = "{BRU-42_2*GBU-38_LEFT}", + ["num"] = 2, + }, + [2] = { + ["CLSID"] = "{BRU-42_2*GBU-38_RIGHT}", + ["num"] = 7, + }, + [3] = { + ["CLSID"] = "{A111396E-D3E8-4b9c-8AC9-2432489304D5}", + ["num"] = 5, + }, + }, + ["tasks"] = { + [1] = 31, + }, + }, + [6] = { + ["name"] = "CAP", + ["pylons"] = { + [1] = { + ["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}", + ["num"] = 8, + }, + [2] = { + ["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}", + ["num"] = 1, + }, + [3] = { + ["CLSID"] = "{AIM-9M-ON-ADAPTER}", + ["num"] = 2, + }, + [4] = { + ["CLSID"] = "{AIM-9M-ON-ADAPTER}", + ["num"] = 7, + }, + [5] = { + ["CLSID"] = "{GAU_12_Equalizer}", + ["num"] = 4, + }, + }, + ["tasks"] = { + [1] = 31, + }, + }, }, ["tasks"] = { }, diff --git a/resources/customized_payloads/J-11A.lua b/resources/customized_payloads/J-11A.lua new file mode 100644 index 00000000..6310a08e --- /dev/null +++ b/resources/customized_payloads/J-11A.lua @@ -0,0 +1,250 @@ +local unitPayloads = { + ["name"] = "J-11A", + ["payloads"] = { + [1] = { + ["name"] = "CAP", + ["pylons"] = { + [1] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 1, + }, + [2] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 2, + }, + [3] = { + ["CLSID"] = "{B79C379A-9E87-4E50-A1EE-7F7E29C2E87A}", + ["num"] = 3, + }, + [4] = { + ["CLSID"] = "{B4C01D60-A8A3-4237-BD72-CA7655BC0FE9}", + ["num"] = 4, + }, + [5] = { + ["CLSID"] = "{B4C01D60-A8A3-4237-BD72-CA7655BC0FE9}", + ["num"] = 5, + }, + [6] = { + ["CLSID"] = "{B4C01D60-A8A3-4237-BD72-CA7655BC0FE9}", + ["num"] = 6, + }, + [7] = { + ["CLSID"] = "{B4C01D60-A8A3-4237-BD72-CA7655BC0FE9}", + ["num"] = 7, + }, + [8] = { + ["CLSID"] = "{B79C379A-9E87-4E50-A1EE-7F7E29C2E87A}", + ["num"] = 8, + }, + [9] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 9, + }, + [10] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 10, + }, + }, + ["tasks"] = { + [1] = 18, + [2] = 19, + [3] = 10, + [4] = 11, + }, + }, + [2] = { + ["name"] = "STRIKE", + ["pylons"] = { + [1] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 10, + }, + [2] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 9, + }, + [3] = { + ["CLSID"] = "{37DCC01E-9E02-432F-B61D-10C166CA2798}", + ["num"] = 8, + }, + [4] = { + ["CLSID"] = "{37DCC01E-9E02-432F-B61D-10C166CA2798}", + ["num"] = 7, + }, + [5] = { + ["CLSID"] = "{37DCC01E-9E02-432F-B61D-10C166CA2798}", + ["num"] = 6, + }, + [6] = { + ["CLSID"] = "{37DCC01E-9E02-432F-B61D-10C166CA2798}", + ["num"] = 5, + }, + [7] = { + ["CLSID"] = "{37DCC01E-9E02-432F-B61D-10C166CA2798}", + ["num"] = 4, + }, + [8] = { + ["CLSID"] = "{37DCC01E-9E02-432F-B61D-10C166CA2798}", + ["num"] = 3, + }, + [9] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 2, + }, + [10] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 30, + }, + }, + [3] = { + ["name"] = "ANTISHIP", + ["pylons"] = { + [1] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 10, + }, + [2] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 9, + }, + [3] = { + ["CLSID"] = "{TWIN_S25}", + ["num"] = 8, + }, + [4] = { + ["CLSID"] = "{37DCC01E-9E02-432F-B61D-10C166CA2798}", + ["num"] = 7, + }, + [5] = { + ["CLSID"] = "{37DCC01E-9E02-432F-B61D-10C166CA2798}", + ["num"] = 6, + }, + [6] = { + ["CLSID"] = "{37DCC01E-9E02-432F-B61D-10C166CA2798}", + ["num"] = 5, + }, + [7] = { + ["CLSID"] = "{37DCC01E-9E02-432F-B61D-10C166CA2798}", + ["num"] = 4, + }, + [8] = { + ["CLSID"] = "{TWIN_S25}", + ["num"] = 3, + }, + [9] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 2, + }, + [10] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 30, + }, + }, + [4] = { + ["name"] = "SEAD", + ["pylons"] = { + [1] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 10, + }, + [2] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 9, + }, + [3] = { + ["CLSID"] = "{A0648264-4BC0-4EE8-A543-D119F6BA4257}", + ["num"] = 8, + }, + [4] = { + ["CLSID"] = "{37DCC01E-9E02-432F-B61D-10C166CA2798}", + ["num"] = 7, + }, + [5] = { + ["CLSID"] = "{37DCC01E-9E02-432F-B61D-10C166CA2798}", + ["num"] = 6, + }, + [6] = { + ["CLSID"] = "{37DCC01E-9E02-432F-B61D-10C166CA2798}", + ["num"] = 5, + }, + [7] = { + ["CLSID"] = "{37DCC01E-9E02-432F-B61D-10C166CA2798}", + ["num"] = 4, + }, + [8] = { + ["CLSID"] = "{A0648264-4BC0-4EE8-A543-D119F6BA4257}", + ["num"] = 3, + }, + [9] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 2, + }, + [10] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 30, + }, + }, + [5] = { + ["name"] = "CAS", + ["pylons"] = { + [1] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 10, + }, + [2] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 9, + }, + [3] = { + ["CLSID"] = "{TWIN_B13L_5OF}", + ["num"] = 8, + }, + [4] = { + ["CLSID"] = "{F99BEC1A-869D-4AC7-9730-FBA0E3B1F5FC}", + ["num"] = 7, + }, + [5] = { + ["CLSID"] = "{37DCC01E-9E02-432F-B61D-10C166CA2798}", + ["num"] = 6, + }, + [6] = { + ["CLSID"] = "{37DCC01E-9E02-432F-B61D-10C166CA2798}", + ["num"] = 5, + }, + [7] = { + ["CLSID"] = "{F99BEC1A-869D-4AC7-9730-FBA0E3B1F5FC}", + ["num"] = 4, + }, + [8] = { + ["CLSID"] = "{TWIN_B13L_5OF}", + ["num"] = 3, + }, + [9] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 2, + }, + [10] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 30, + }, + }, + }, + ["unitType"] = "J-11A", +} +return unitPayloads diff --git a/resources/customized_payloads/S-3B.lua b/resources/customized_payloads/S-3B.lua new file mode 100644 index 00000000..bdbdd472 --- /dev/null +++ b/resources/customized_payloads/S-3B.lua @@ -0,0 +1,168 @@ +local unitPayloads = { + ["name"] = "S-3B", + ["payloads"] = { + [1] = { + ["name"] = "SEAD", + ["pylons"] = { + [1] = { + ["CLSID"] = "{AF42E6DF-9A60-46D8-A9A0-1708B241AADB}", + ["num"] = 1, + }, + [2] = { + ["CLSID"] = "{ADD3FAE1-EBF6-4EF9-8EFC-B36B5DDF1E6B}", + ["num"] = 2, + }, + [3] = { + ["CLSID"] = "{ADD3FAE1-EBF6-4EF9-8EFC-B36B5DDF1E6B}", + ["num"] = 3, + }, + [4] = { + ["CLSID"] = "{ADD3FAE1-EBF6-4EF9-8EFC-B36B5DDF1E6B}", + ["num"] = 4, + }, + [5] = { + ["CLSID"] = "{ADD3FAE1-EBF6-4EF9-8EFC-B36B5DDF1E6B}", + ["num"] = 5, + }, + [6] = { + ["CLSID"] = "{AF42E6DF-9A60-46D8-A9A0-1708B241AADB}", + ["num"] = 6, + }, + }, + ["tasks"] = { + [1] = 29, + }, + }, + [2] = { + ["name"] = "ANTISHIP", + ["pylons"] = { + [1] = { + ["CLSID"] = "{8B7CADF9-4954-46B3-8CFB-93F2F5B90B03}", + ["num"] = 1, + }, + [2] = { + ["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}", + ["num"] = 2, + }, + [3] = { + ["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}", + ["num"] = 3, + }, + [4] = { + ["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}", + ["num"] = 4, + }, + [5] = { + ["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}", + ["num"] = 5, + }, + [6] = { + ["CLSID"] = "{8B7CADF9-4954-46B3-8CFB-93F2F5B90B03}", + ["num"] = 6, + }, + }, + ["tasks"] = { + [1] = 30, + }, + }, + [3] = { + ["name"] = "STRIKE", + ["pylons"] = { + [1] = { + ["CLSID"] = "{69DC8AE7-8F77-427B-B8AA-B19D3F478B66}", + ["num"] = 1, + }, + [2] = { + ["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}", + ["num"] = 2, + }, + [3] = { + ["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}", + ["num"] = 3, + }, + [4] = { + ["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}", + ["num"] = 4, + }, + [5] = { + ["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}", + ["num"] = 5, + }, + [6] = { + ["CLSID"] = "{69DC8AE7-8F77-427B-B8AA-B19D3F478B66}", + ["num"] = 6, + }, + }, + ["tasks"] = { + [1] = 32, + [1] = 33, + }, + }, + [4] = { + ["name"] = "CAS", + ["pylons"] = { + [1] = { + ["CLSID"] = "{A76344EB-32D2-4532-8FA2-0C1BDC00747E}", + ["num"] = 1, + }, + [2] = { + ["CLSID"] = "{BCE4E030-38E9-423E-98ED-24BE3DA87C32}", + ["num"] = 2, + }, + [3] = { + ["CLSID"] = "{BCE4E030-38E9-423E-98ED-24BE3DA87C32}", + ["num"] = 3, + }, + [4] = { + ["CLSID"] = "{BCE4E030-38E9-423E-98ED-24BE3DA87C32}", + ["num"] = 4, + }, + [5] = { + ["CLSID"] = "{BCE4E030-38E9-423E-98ED-24BE3DA87C32}", + ["num"] = 5, + }, + [6] = { + ["CLSID"] = "{A76344EB-32D2-4532-8FA2-0C1BDC00747E}", + ["num"] = 6, + }, + }, + ["tasks"] = { + [1] = 31, + }, + }, + }, + [5] = { + ["name"] = "RUNWAY_ATTACK", + ["pylons"] = { + [1] = { + ["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}", + ["num"] = 1, + }, + [2] = { + ["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}", + ["num"] = 2, + }, + [3] = { + ["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}", + ["num"] = 3, + }, + [4] = { + ["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}", + ["num"] = 4, + }, + [5] = { + ["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}", + ["num"] = 5, + }, + [6] = { + ["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}", + ["num"] = 6, + }, + }, + ["tasks"] = { + [1] = 34, + }, + }, + ["unitType"] = "S-3B", +} +return unitPayloads diff --git a/resources/customized_payloads/SH-60B.lua b/resources/customized_payloads/SH-60B.lua new file mode 100644 index 00000000..22ff5907 --- /dev/null +++ b/resources/customized_payloads/SH-60B.lua @@ -0,0 +1,18 @@ +local unitPayloads = { + ["name"] = "SH-60B", + ["payloads"] = { + [1] = { + ["name"] = "ANTISHIP", + ["pylons"] = { + [1] = { + ["CLSID"] = "{7B8DCEB4-820B-4015-9B48-1028A4195692}", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 30, + }, + }, + ["unitType"] = "SH-60B", +} +return unitPayloads diff --git a/resources/factions/NATO_Desert_Storm.json b/resources/factions/NATO_Desert_Storm.json index 677cd77a..d108f6d6 100644 --- a/resources/factions/NATO_Desert_Storm.json +++ b/resources/factions/NATO_Desert_Storm.json @@ -80,7 +80,7 @@ "LHA_1_Tarawa" ], "destroyers": [ - "OliverHazardPerryGroupGenerator" + "Oliver_Hazzard_Perry_class" ], "cruisers": [ "Ticonderoga_class" diff --git a/resources/factions/allies_1944.json b/resources/factions/allies_1944.json index 62eefeb9..abefb69e 100644 --- a/resources/factions/allies_1944.json +++ b/resources/factions/allies_1944.json @@ -21,7 +21,6 @@ "CT_Cromwell_IV", "CT_Centaur_IV", "HIT_Churchill_VII", - "M30_Cargo_Carrier", "LAC_M8_Greyhound", "TD_M10_GMC", "Daimler_Armoured_Car", diff --git a/resources/factions/australia_2005.json b/resources/factions/australia_2005.json index 8e9109d2..36968b09 100644 --- a/resources/factions/australia_2005.json +++ b/resources/factions/australia_2005.json @@ -6,6 +6,7 @@ "aircrafts": [ "FA_18C_hornet", "UH_1H", + "SH_60B", "AH_1W" ], "awacs": [ diff --git a/resources/factions/france_1985_frenchpack.json b/resources/factions/france_1985_frenchpack.json new file mode 100644 index 00000000..3837fbc1 --- /dev/null +++ b/resources/factions/france_1985_frenchpack.json @@ -0,0 +1,79 @@ +{ + "country": "France", + "name": "France 1985 (Frenchpack)", + "authors": "Colonel Panic", + "description": "

1980s French equipment using FrenchPack.

", + "doctrine": "coldwar", + "aircrafts": [ + "M_2000C", + "SA342M", + "SA342L", + "SA342Mistral" + ], + "awacs": [ + "E_3A" + ], + "tankers": [ + "KC_135", + "KC130" + ], + "frontline_units": [ + "AMX_10RCR", + "AMX_10RCR_SEPAR", + "ERC_90", + "TRM_2000_PAMELA", + "VAB__50", + "VAB_MEPHISTO", + "VAB_T20_13", + "VAB_T20_13", + "VBL__50", + "VBL_AANF1", + "VBAE_CRAB", + "VBAE_CRAB_MMP", + "AMX_30B2", + "SAM_Roland_ADS" + ], + "artillery_units": [ + "MLRS_M270", + "SPH_M109_Paladin" + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4", + "Soldier_M249", + "Stinger_MANPADS" + ], + "air_defenses": [ + "RolandGenerator", + "HawkGenerator" + ], + "aircraft_carrier": [], + "helicopter_carrier": [ + "LHA_1_Tarawa" + ], + "destroyers": [ + "USS_Arleigh_Burke_IIa" + ], + "cruisers": [ + "Ticonderoga_class" + ], + "requirements": { + "frenchpack V3.5": "https://forums.eagle.ru/showthread.php?t=279974" + }, + "carrier_names": [ + "R91 Charles de Gaulle" + ], + "helicopter_carrier_names": [ + "R97 Jeanne d'Arc", + "L9013 Mistral", + "L9014 Tonerre", + "L9015 Dixmude" + ], + "navy_generators": [ + "ArleighBurkeGroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "SA342L" +} diff --git a/resources/factions/gdr_1985.json b/resources/factions/gdr_1985.json new file mode 100644 index 00000000..07131110 --- /dev/null +++ b/resources/factions/gdr_1985.json @@ -0,0 +1,65 @@ +{ + "country": "GDR", + "name": "German Democratic Republic 1985", + "authors": "Colonel Panic", + "description": "

The German Democratic Republic in 1985.

", + "doctrine": "coldwar", + "aircrafts": [ + "MiG_21Bis", + "MiG_23MLD", + "Su_17M4", + "L_39ZA", + "Mi_8MT", + "Mi_24V" + ], + "awacs": [], + "tankers": [], + "frontline_units": [ + "IFV_BMP_1", + "IFV_BMP_2", + "ARV_BRDM_2", + "APC_MTLB", + "MBT_T_55", + "MBT_T_72B" + ], + "artillery_units": [ + "MLRS_BM_21_Grad", + "SPH_2S1_Gvozdika" + ], + "logistics_units": [ + "Transport_Ural_375", + "Transport_UAZ_469" + ], + "infantry_units": [ + "Infantry_Soldier_Rus", + "Soldier_RPG", + "_2B11_mortar" + ], + "air_defenses": [ + "ColdWarFlakGenerator", + "SA2Generator", + "SA3Generator", + "SA6Generator", + "SA8Generator", + "SA9Generator", + "SA13Generator", + "ZSU23Generator", + "ZSU57Generator", + "ZU23Generator" + ], + "ewrs": [ + "FlatFaceGenerator" + ], + "aircraft_carrier": [], + "helicopter_carrier": [], + "destroyers": [], + "cruisers": [ + "Ticonderoga_class" + ], + "requirements": {}, + "carrier_names": [], + "helicopter_carrier_names": [], + "navy_generators": [], + "has_jtac": true, + "jtac_unit": "Mi_8MT" +} diff --git a/resources/factions/germany_1944.json b/resources/factions/germany_1944.json index 7cd95b44..e9d5ad78 100644 --- a/resources/factions/germany_1944.json +++ b/resources/factions/germany_1944.json @@ -56,7 +56,6 @@ "helicopter_carrier_names": [ ], "navy_generators": [ - "UBoatGroupGenerator", "SchnellbootGroupGenerator" ], "navy_group_count": 2, diff --git a/resources/factions/greece_2005.json b/resources/factions/greece_2005.json new file mode 100644 index 00000000..21bf75ad --- /dev/null +++ b/resources/factions/greece_2005.json @@ -0,0 +1,51 @@ +{ + "country": "Greece", + "name": "Greece 2005", + "authors": "Malakhit", + "description": "

Hellenic army in the mid/late 2000s.

", + "aircrafts": [ + "F_16C_50", + "F_4E", + "M_2000C", + "Mirage_2000_5", + "UH_1H", + "AH_64A" + ], + "tankers": [ + "KC130" + ], + "frontline_units": [ + "MBT_Leopard_2", + "MBT_Leopard_1A3", + "MBT_M60A3_Patton", + "APC_M1043_HMMWV_Armament", + "ATGM_M1045_HMMWV_TOW", + "APC_M113", + "IFV_BMP_1" + ], + "artillery_units": [ + "SPH_M109_Paladin", + "MLRS_M270" + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4", + "Soldier_M249", + "Stinger_MANPADS" + ], + "air_defenses": [ + "HawkGenerator", + "SA8Generator", + "SA10Generator", + "SA15Generator", + "ZU23Generator" + ], + "ewrs": [ + "HawkEwrGenerator", + "FlatFaceGenerator" + ], + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper" +} \ No newline at end of file diff --git a/resources/factions/iran_1988.json b/resources/factions/iran_1988.json new file mode 100644 index 00000000..406bd9ab --- /dev/null +++ b/resources/factions/iran_1988.json @@ -0,0 +1,78 @@ +{ + "country": "Iran", + "name": "Iran 1988", + "authors": "Malakhit", + "description": "

Iran at the end of the Iran-Iraq war

", + "aircrafts": [ + "MiG_21Bis", + "F_4E", + "F_5E_3", + "F_14A_135_GR", + "AH_1W" + ], + "awacs": [ + "A_50" + ], + "tankers": [ + "IL_78M" + ], + "frontline_units": [ + "APC_M113", + "APC_BTR_80", + "MBT_M60A3_Patton", + "IFV_BMP_1", + "SPAAA_ZSU_23_4_Shilka", + "AAA_ZSU_57_2" + ], + "artillery_units": [ + "MLRS_BM_21_Grad", + "SPH_2S1_Gvozdika" + ], + "logistics_units": [ + "Transport_Ural_375", + "Transport_UAZ_469" + ], + "infantry_units": [ + "Paratrooper_AKS", + "Infantry_Soldier_Insurgents", + "Soldier_RPG", + "SAM_SA_18_Igla_S_MANPADS" + ], + "air_defenses": [ + "HawkGenerator", + "RapierGenerator", + "SA2Generator", + "ZSU57Generator", + "ZSU23Generator", + "ZU23Generator", + "ZU23UralGenerator" + ], + "ewrs": [ + "TallRackGenerator" + ], + "aircraft_carrier": [ + ], + "helicopter_carrier": [ + ], + "helicopter_carrier_names": [ + ], + "destroyers": [ + "FF_1135M_Rezky" + ], + "cruisers": [ + "FSG_1241_1MP_Molniya" + ], + "requirements": {}, + "carrier_names": [ + ], + "missiles": [ + "ScudGenerator" + ], + "missiles_group_count": 1, + "navy_generators": [ + "GrishaGroupGenerator", + "MolniyaGroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper" +} diff --git a/resources/factions/iran_2015.json b/resources/factions/iran_2015.json index b8621de2..9f3d8870 100644 --- a/resources/factions/iran_2015.json +++ b/resources/factions/iran_2015.json @@ -81,8 +81,7 @@ "missiles_group_count": 1, "navy_generators": [ "GrishaGroupGenerator", - "MolniyaGroupGenerator", - "KiloSubGroupGenerator" + "MolniyaGroupGenerator" ], "has_jtac": true, "jtac_unit": "MQ_9_Reaper" diff --git a/resources/factions/iraq_1991.json b/resources/factions/iraq_1991.json index 0ff517ee..82562c1a 100644 --- a/resources/factions/iraq_1991.json +++ b/resources/factions/iraq_1991.json @@ -12,12 +12,11 @@ "Mi_8MT", "Su-25", "Su-24M", - "MiG_25PD", "Tu_22M3", - "L_39C", "L_39ZA", "Mi_24V", - "MiG_29A" + "MiG_29A", + "SA342M" ], "awacs": [ "A_50" diff --git a/resources/factions/israel_1973.json b/resources/factions/israel_1973.json index b3b30840..36bef190 100644 --- a/resources/factions/israel_1973.json +++ b/resources/factions/israel_1973.json @@ -9,7 +9,7 @@ "UH_1H" ], "awacs": [ - "E_3A" + "E_2C" ], "tankers": [ "KC_135", diff --git a/resources/factions/israel_1982.json b/resources/factions/israel_1982.json index b975c7b9..5c910edd 100644 --- a/resources/factions/israel_1982.json +++ b/resources/factions/israel_1982.json @@ -13,7 +13,7 @@ "AH_1W" ], "awacs": [ - "E_3A" + "E_2C" ], "tankers": [ "KC_135", diff --git a/resources/factions/israel_2000.json b/resources/factions/israel_2000.json index 667df3bd..0cab7580 100644 --- a/resources/factions/israel_2000.json +++ b/resources/factions/israel_2000.json @@ -13,7 +13,7 @@ "AH_64D" ], "awacs": [ - "E_3A" + "E_2C" ], "tankers": [ "KC_135", diff --git a/resources/factions/japan_2005.json b/resources/factions/japan_2005.json index a2f24933..88f57175 100644 --- a/resources/factions/japan_2005.json +++ b/resources/factions/japan_2005.json @@ -11,7 +11,7 @@ "AH_64D" ], "awacs": [ - "E_3A" + "E_2C" ], "tankers": [ "KC_135", diff --git a/resources/factions/pmc_russian.json b/resources/factions/pmc_russian.json index aec2c021..3b20801a 100644 --- a/resources/factions/pmc_russian.json +++ b/resources/factions/pmc_russian.json @@ -4,7 +4,6 @@ "authors": "Khopa", "description": "

A private military company using Russian units.

", "aircrafts": [ - "L_39C", "L_39ZA", "Mi_8MT", "Mi_24V", diff --git a/resources/factions/poland_2010.json b/resources/factions/poland_2010.json new file mode 100644 index 00000000..5090d6e7 --- /dev/null +++ b/resources/factions/poland_2010.json @@ -0,0 +1,56 @@ +{ + "country": "Poland", + "name": "Poland 2010", + "authors": "Malakhit", + "description": "

Polish army in the 2010s.

", + "aircrafts": [ + "Su_17M4", + "MiG_29A", + "F_16C_50", + "Mi_8MT" + ], + "tankers": [ + "KC130" + ], + "frontline_units": [ + "APC_M1043_HMMWV_Armament", + "ATGM_M1045_HMMWV_TOW", + "APC_M1126_Stryker_ICV", + "ARV_BRDM_2", + "IFV_BMP_1", + "APC_MTLB", + "MBT_Leopard_2", + "MBT_T_72B3" + ], + "artillery_units": [ + "MLRS_BM_21_Grad", + "SPH_2S1_Gvozdika", + "SpGH_Dana" + ], + "logistics_units": [ + "Transport_Ural_375", + "Transport_UAZ_469" + ], + "infantry_units": [ + "Paratrooper_AKS", + "Infantry_Soldier_Rus", + "Soldier_RPG", + "SAM_SA_18_Igla_S_MANPADS" + ], + "air_defenses": [ + "SA6Generator", + "SA8Generator", + "ZSU23Generator", + "ZU23Generator", + "ZU23UralGenerator" + ], + "ewrs": [ + "BigBirdGenerator" + ], + "requirements": {}, + "carrier_names": [], + "navy_generators": [ + "MolniyaGroupGenerator", + "OliverHazardPerryGroupGenerator" + ] +} diff --git a/resources/factions/redfor_china_2010.json b/resources/factions/redfor_china_2010.json new file mode 100644 index 00000000..ab7ce609 --- /dev/null +++ b/resources/factions/redfor_china_2010.json @@ -0,0 +1,120 @@ +{ + "country": "China", + "name": "Redfor (China) 2010", + "authors": "Robert Peary", + "description": "

Combined Redfor under China's leadership in the late 2000s, early 2010s.

", + "aircrafts": [ + "J_11A", + "JF_17", + "Ka_50", + "L_39ZA", + "Mi_8MT", + "Mi_24V", + "Mi_28N", + "MiG_21Bis", + "MiG_29S", + "MiG_31", + "Su_24M", + "Su_25", + "Su_25T", + "Su_27", + "Su_30", + "Su_33", + "Su_34", + "Tu_22M3" + ], + "awacs": [ + "KJ_2000" + ], + "tankers": [ + "IL_78M" + ], + "frontline_units": [ + "APC_BTR_80", + "APC_BTR_82A", + "HQ_7_Self_Propelled_LN", + "IFV_BMP_1", + "IFV_BMP_2", + "IFV_BMP_3", + "MBT_T_55", + "MBT_T_72B3", + "MBT_T_80U", + "MBT_T_90", + "SAM_SA_19_Tunguska_2S6", + "ZBD_04A", + "ZTZ_96B" + ], + "artillery_units": [ + "MLRS_9A52_Smerch", + "MLRS_9K57_Uragan_BM_27", + "SPH_2S9_Nona", + "SPH_2S19_Msta" + ], + "logistics_units": [ + "Transport_Ural_375", + "Transport_UAZ_469" + ], + "infantry_units": [ + "_2B11_mortar", + "Infantry_Soldier_Rus", + "Paratrooper_AKS", + "Paratrooper_RPG_16", + "SAM_SA_18_Igla_MANPADS" + ], + "air_defenses": [ + "HQ7Generator", + "SA2Generator", + "SA6Generator", + "SA8Generator", + "SA9Generator", + "SA10Generator", + "SA11Generator", + "SA13Generator", + "SA15Generator", + "SA19Generator", + "Tier2SA10Generator", + "Tier3SA10Generator", + "ZSU23Generator", + "ZSU57Generator", + "ZU23Generator", + "ZU23UralGenerator" + ], + "ewrs": [ + "BoxSpringGenerator", + "TallRackGenerator" + ], + "aircraft_carrier": [ + "CV_1143_5_Admiral_Kuznetsov" + ], + "carrier_names": [ + "001 Liaoning", + "002 Shandong" + ], + "helicopter_carrier": [ + "Type_071_Amphibious_Transport_Dock" + ], + "helicopter_carrier_names": [ + "Kunlun Shan", + "Jinggang Shan", + "Changbai Shan", + "Yimeng Shan", + "Longhu Shan", + "Wuzhi Shan", + "Wudang Shan" + ], + "destroyers": [ + "FF_1135M_Rezky", + "Type_052B_Destroyer", + "Type_052C_Destroyer" + ], + "cruiser": [ + "FSG_1241_1MP_Molniya", + "Type_054A_Frigate" + ], + "requirements": {}, + "navy_generators": [ + "Type54GroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "WingLoong_I" +} diff --git a/resources/factions/redfor_russia_2010.json b/resources/factions/redfor_russia_2010.json new file mode 100644 index 00000000..363139ec --- /dev/null +++ b/resources/factions/redfor_russia_2010.json @@ -0,0 +1,115 @@ +{ + "country": "Russia", + "name": "Redfor (Russia) 2010", + "authors": "Robert Peary", + "description": "

Combined Redfor under Russia's leadership in the late 2000s, early 2010s.

", + "aircrafts": [ + "J_11A", + "JF_17", + "Ka_50", + "L_39ZA", + "Mi_8MT", + "Mi_24V", + "Mi_28N", + "MiG_21Bis", + "MiG_29S", + "MiG_31", + "Su_24M", + "Su_25", + "Su_25T", + "Su_27", + "Su_30", + "Su_33", + "Su_34", + "Tu_22M3" + ], + "awacs": [ + "A_50" + ], + "tankers": [ + "IL_78M" + ], + "frontline_units": [ + "APC_BTR_80", + "APC_BTR_82A", + "HQ_7_Self_Propelled_LN", + "IFV_BMP_1", + "IFV_BMP_2", + "IFV_BMP_3", + "MBT_T_55", + "MBT_T_72B3", + "MBT_T_80U", + "MBT_T_90", + "SAM_SA_19_Tunguska_2S6", + "ZBD_04A", + "ZTZ_96B" + ], + "artillery_units": [ + "MLRS_9A52_Smerch", + "MLRS_9K57_Uragan_BM_27", + "SPH_2S9_Nona", + "SPH_2S19_Msta" + ], + "logistics_units": [ + "Transport_Ural_375", + "Transport_UAZ_469" + ], + "infantry_units": [ + "_2B11_mortar", + "Infantry_Soldier_Rus", + "Paratrooper_AKS", + "Paratrooper_RPG_16", + "SAM_SA_18_Igla_MANPADS" + ], + "air_defenses": [ + "HQ7Generator", + "SA2Generator", + "SA6Generator", + "SA8Generator", + "SA9Generator", + "SA10Generator", + "SA11Generator", + "SA13Generator", + "SA15Generator", + "SA19Generator", + "Tier2SA10Generator", + "Tier3SA10Generator", + "ZSU23Generator", + "ZSU57Generator", + "ZU23Generator", + "ZU23UralGenerator" + ], + "ewrs": [ + "BoxSpringGenerator", + "TallRackGenerator" + ], + "aircraft_carrier": [ + "CV_1143_5_Admiral_Kuznetsov" + ], + "helicopter_carrier": [ + "Type_071_Amphibious_Transport_Dock" + ], + "helicopter_carrier_names": [ + "Ivan Rogov", + "Mitrofan Moskalenko" + ], + "destroyers": [ + "FF_1135M_Rezky", + "Type_052B_Destroyer", + "Type_052C_Destroyer" + ], + "cruisers": [ + "FSG_1241_1MP_Molniya", + "Type_054A_Frigate" + ], + "requirements": {}, + "carrier_names": [ + "Admiral Kuznetov" + ], + "navy_generators": [ + "RussianNavyGroupGenerator", + "KiloSubGroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper" +} diff --git a/resources/factions/russia_1965.json b/resources/factions/russia_1965.json index 9f986f1d..1c764a45 100644 --- a/resources/factions/russia_1965.json +++ b/resources/factions/russia_1965.json @@ -7,7 +7,8 @@ "MiG_15bis", "MiG_19P", "MiG_21Bis", - "Mi_8MT" + "Mi_8MT", + "Tu_95MS" ], "awacs": [ "A_50" diff --git a/resources/factions/russia_1975.json b/resources/factions/russia_1975.json index 5630494c..4fbbf059 100644 --- a/resources/factions/russia_1975.json +++ b/resources/factions/russia_1975.json @@ -12,7 +12,9 @@ "Su_24M", "Su_25", "Mi_8MT", - "Mi_24V" + "Mi_24V", + "Tu_95MS", + "Tu_142" ], "awacs": [ "A_50" @@ -76,7 +78,7 @@ "carrier_names": [ ], "navy_generators": [ - "KiloSubGroupGenerator", "MolniyaGroupGenerator" + "MolniyaGroupGenerator" ], "has_jtac": false, "doctrine": "coldwar" diff --git a/resources/factions/russia_1990.json b/resources/factions/russia_1990.json index 74c52bf3..497cfb93 100644 --- a/resources/factions/russia_1990.json +++ b/resources/factions/russia_1990.json @@ -16,7 +16,9 @@ "Mi_8MT", "Mi_24V", "Tu_22M3", - "Tu_95MS" + "Tu_95MS", + "Tu_142", + "Tu_160" ], "awacs": [ "A_50" @@ -87,8 +89,7 @@ "Admiral Gorshkov" ], "navy_generators": [ - "RussianNavyGroupGenerator", - "KiloSubGroupGenerator" + "RussianNavyGroupGenerator" ], "has_jtac": true, "jtac_unit": "MQ_9_Reaper" diff --git a/resources/factions/russia_2010.json b/resources/factions/russia_2010.json index bd6d037a..13e2fbe9 100644 --- a/resources/factions/russia_2010.json +++ b/resources/factions/russia_2010.json @@ -18,7 +18,10 @@ "Mi_24V", "Mi_28N", "Ka_50", - "Tu_22M3" + "Tu_22M3", + "Tu_95MS", + "Tu_142", + "Tu_160" ], "awacs": [ "A_50" @@ -88,8 +91,7 @@ "Admiral Kuznetov" ], "navy_generators": [ - "RussianNavyGroupGenerator", - "KiloSubGroupGenerator" + "RussianNavyGroupGenerator" ], "has_jtac": true, "jtac_unit": "MQ_9_Reaper" diff --git a/resources/factions/russia_2010_hds.json b/resources/factions/russia_2010_hds.json new file mode 100644 index 00000000..7e632c5f --- /dev/null +++ b/resources/factions/russia_2010_hds.json @@ -0,0 +1,95 @@ +{ + "country": "Russia", + "name": "Russia 2010 (High Digit SAMs)", + "authors": "Khopa", + "description": "

Russian army in the early 2010s, featuring the High Digit SAMs mod units.

", + "aircrafts": [ + "MiG_29S", + "MiG_31", + "Su_24M", + "Su_25", + "Su_25T", + "Su_27", + "Su_30", + "Su_33", + "Su_34", + "L_39ZA", + "Mi_8MT", + "Mi_24V", + "Mi_28N", + "Ka_50", + "Tu_22M3", + "Tu_95MS", + "Tu_142", + "Tu_160" + ], + "awacs": [ + "A_50" + ], + "tankers": [ + "IL_78M" + ], + "frontline_units": [ + "IFV_BMP_1", + "IFV_BMP_2", + "IFV_BMP_3", + "APC_BTR_80", + "APC_BTR_82A", + "MBT_T_90", + "MBT_T_80U", + "MBT_T_72B3", + "SAM_SA_19_Tunguska_2S6" + ], + "artillery_units": [ + "MLRS_9K57_Uragan_BM_27", + "SPH_2S19_Msta" + ], + "logistics_units": [ + "Transport_Ural_375", + "Transport_UAZ_469" + ], + "infantry_units": [ + "Paratrooper_AKS", + "Infantry_Soldier_Rus", + "Paratrooper_RPG_16", + "_2B11_mortar", + "SAM_SA_18_Igla_MANPADS" + ], + "air_defenses": [ + "SA17Generator", + "SA13Generator", + "SA15Generator", + "SA19Generator", + "SA10BGenerator", + "SA12Generator", + "SA20Generator", + "SA20BGenerator", + "SA23Generator" + ], + "ewrs": [ + "BoxSpringGenerator", + "TallRackGenerator" + ], + "aircraft_carrier": [ + "CV_1143_5_Admiral_Kuznetsov" + ], + "helicopter_carrier": [ + ], + "helicopter_carrier_names": [ + ], + "destroyers": [ + "FF_1135M_Rezky" + ], + "cruisers": [ + "FSG_1241_1MP_Molniya" + ], + "requirements": { "High Digit SAMs": "https://github.com/Auranis/HighDigitSAMs/releases"}, + "carrier_names": [ + "Admiral Kuznetov" + ], + "navy_generators": [ + "RussianNavyGroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper" +} diff --git a/resources/factions/russia_2020.json b/resources/factions/russia_2020.json index 91554c82..730ecbf2 100644 --- a/resources/factions/russia_2020.json +++ b/resources/factions/russia_2020.json @@ -19,7 +19,10 @@ "Mi_24V", "Mi_28N", "Ka_50", - "Tu_22M3" + "Tu_22M3", + "Tu_95MS", + "Tu_142", + "Tu_160" ], "awacs": [ "A_50" @@ -86,8 +89,7 @@ "Admiral Kuznetov" ], "navy_generators": [ - "RussianNavyGroupGenerator", - "KiloSubGroupGenerator" + "RussianNavyGroupGenerator" ], "has_jtac": true, "jtac_unit": "MQ_9_Reaper" diff --git a/resources/factions/us_aggressors.json b/resources/factions/us_aggressors.json index 7568ccd5..31595c5f 100644 --- a/resources/factions/us_aggressors.json +++ b/resources/factions/us_aggressors.json @@ -13,6 +13,7 @@ "A_10C", "AV8BNA", "UH_1H", + "SH_60B", "AH_64D", "Ka_50", "B_52H", diff --git a/resources/factions/usa_1944.json b/resources/factions/usa_1944.json index 053ace3c..bdece25a 100644 --- a/resources/factions/usa_1944.json +++ b/resources/factions/usa_1944.json @@ -17,7 +17,6 @@ "MT_M4A4_Sherman_Firefly", "MT_M4_Sherman", "APC_M2A1", - "M30_Cargo_Carrier", "LAC_M8_Greyhound", "TD_M10_GMC", "AA_gun_QF_3_7" diff --git a/resources/factions/usa_1965.json b/resources/factions/usa_1965.json index e0a03e3f..e107e90a 100644 --- a/resources/factions/usa_1965.json +++ b/resources/factions/usa_1965.json @@ -9,6 +9,9 @@ "B_52H", "UH_1H" ], + "awacs": [ + "E_2C" + ], "frontline_units": [ "MBT_M60A3_Patton", "APC_M113", diff --git a/resources/factions/usa_1975.json b/resources/factions/usa_1975.json index cd818384..13093f04 100644 --- a/resources/factions/usa_1975.json +++ b/resources/factions/usa_1975.json @@ -7,9 +7,13 @@ "F_5E_3", "F_4E", "F_14A_135_GR", + "S_3B", "B_52H", "UH_1H" ], + "awacs": [ + "E_2C" + ], "frontline_units": [ "MBT_M60A3_Patton", "APC_M113", diff --git a/resources/factions/usa_1990.json b/resources/factions/usa_1990.json index e2da4d5f..f33fbdb1 100644 --- a/resources/factions/usa_1990.json +++ b/resources/factions/usa_1990.json @@ -13,6 +13,8 @@ "A_10A", "AV8BNA", "UH_1H", + "S_3B", + "SH_60B", "AH_64A", "B_52H", "B_1B", diff --git a/resources/factions/usa_2005.json b/resources/factions/usa_2005.json index 09a6fd1d..f7a5f912 100644 --- a/resources/factions/usa_2005.json +++ b/resources/factions/usa_2005.json @@ -7,16 +7,18 @@ "F_15C", "F_15E", "F_14B", + "F_117A", "FA_18C_hornet", "F_16C_50", "A_10C", "A_10C_2", "AV8BNA", "UH_1H", + "S_3B", + "SH_60B", "AH_64D", "B_52H", - "B_1B", - "F_117A" + "B_1B" ], "awacs": [ "E_3A" @@ -34,7 +36,8 @@ "APC_M1043_HMMWV_Armament", "ATGM_M1045_HMMWV_TOW", "SAM_Avenger_M1097", - "SAM_Linebacker_M6" + "SAM_Linebacker_M6", + "SPG_M1128_Stryker_MGS" ], "artillery_units": [ "MLRS_M270", diff --git a/resources/factions/usa_2005_c130.json b/resources/factions/usa_2005_c130.json index af749b94..8164ec23 100644 --- a/resources/factions/usa_2005_c130.json +++ b/resources/factions/usa_2005_c130.json @@ -13,6 +13,8 @@ "A_10C_2", "AV8BNA", "UH_1H", + "S_3B", + "SH_60B", "AH_64D", "B_52H", "B_1B", @@ -70,7 +72,9 @@ "cruisers": [ "Ticonderoga_class" ], - "requirements": {}, + "requirements": { + "C-130J-30 Super Hercules Mod by Anubis": "https://forums.eagle.ru/topic/252075-dcs-super-hercules-mod-by-anubis/" + }, "carrier_names": [ "CVN-71 Theodore Roosevelt", "CVN-72 Abraham Lincoln", diff --git a/resources/factions/usa_2005_modded.json b/resources/factions/usa_2005_modded.json index de7375e6..da821175 100644 --- a/resources/factions/usa_2005_modded.json +++ b/resources/factions/usa_2005_modded.json @@ -13,6 +13,8 @@ "A_10C_2", "AV8BNA", "UH_1H", + "S_3B", + "SH_60B", "AH_64D", "B_52H", "B_1B", diff --git a/resources/factions/usn_1985.json b/resources/factions/usn_1985.json index cfdef2bc..08230c1d 100644 --- a/resources/factions/usn_1985.json +++ b/resources/factions/usn_1985.json @@ -4,15 +4,16 @@ "authors": "HerrTom", "description": "

Highway to the Danger Zone! For Tomcat lovers.

", "aircrafts": [ + "AH_1W", "F_4E", "F_14A_135_GR", "F_14B", "S_3B", - "UH_1H", - "AH_1W" + "SH_60B", + "UH_1H" ], "awacs": [ - "E_3A" + "E_2C" ], "tankers": [ "S_3B_Tanker" diff --git a/resources/groundobject_templates.p b/resources/groundobject_templates.p index eb39274b..59406efc 100644 Binary files a/resources/groundobject_templates.p and b/resources/groundobject_templates.p differ diff --git a/resources/gulflandmap.p b/resources/gulflandmap.p index 3e2e566e..98fe5bce 100644 Binary files a/resources/gulflandmap.p and b/resources/gulflandmap.p differ diff --git a/resources/nevlandmap.p b/resources/nevlandmap.p index 1b92631a..f8ef5cbb 100644 Binary files a/resources/nevlandmap.p and b/resources/nevlandmap.p differ diff --git a/resources/normandylandmap.p b/resources/normandylandmap.p index 808a2a1e..20eb8715 100644 Binary files a/resources/normandylandmap.p and b/resources/normandylandmap.p differ diff --git a/resources/plugins/ewrs/ewrs.lua b/resources/plugins/ewrs/ewrs.lua index b66b728a..a70c1806 100644 --- a/resources/plugins/ewrs/ewrs.lua +++ b/resources/plugins/ewrs/ewrs.lua @@ -135,6 +135,7 @@ ewrs.acCategories = { --Have I left anything out? Please let me know if I have [ "Su-33" ] = ewrs.FIGHTER , [ "TF-51D" ] = ewrs.ATTACK , [ "UH-1H" ] = ewrs.HELO , +[ "J-11A" ] = ewrs.FIGHTER , } ----END OF SCRIPT OPTIONS---- diff --git a/resources/plugins/herculescargo/Hercules_Cargo.lua b/resources/plugins/herculescargo/Hercules_Cargo.lua index 4aae96c0..09689b38 100644 --- a/resources/plugins/herculescargo/Hercules_Cargo.lua +++ b/resources/plugins/herculescargo/Hercules_Cargo.lua @@ -40,7 +40,70 @@ local Cargo_Drop_Position = {} local SoldierUnitID = 12000 local SoldierGroupID = 12000 local GroupSpacing = 0 - +--added by wrench +Hercules_Cargo.types = { + ["ATGM M1045 HMMWV TOW Air [7183lb]"] = {['name'] = "M1045 HMMWV TOW", ['container'] = true}, + ["ATGM M1045 HMMWV TOW Skid [7073lb]"] = {['name'] = "M1045 HMMWV TOW", ['container'] = false}, + ["APC M1043 HMMWV Armament Air [7023lb]"] = {['name'] = "M1043 HMMWV Armament", ['container'] = true}, + ["APC M1043 HMMWV Armament Skid [6912lb]"] = {['name'] = "M1043 HMMWV Armament", ['container'] = false}, + ["SAM Avenger M1097 Air [7200lb]"] = {['name'] = "M1097 Avenger", ['container'] = true}, + ["SAM Avenger M1097 Skid [7090lb]"] = {['name'] = "M1097 Avenger", ['container'] = false}, + ["APC Cobra Air [10912lb]"] = {['name'] = "Cobra", ['container'] = true}, + ["APC Cobra Skid [10802lb]"] = {['name'] = "Cobra", ['container'] = false}, + ["APC M113 Air [21624lb]"] = {['name'] = "M-113", ['container'] = true}, + ["APC M113 Skid [21494lb]"] = {['name'] = "M-113", ['container'] = false}, + ["Tanker M978 HEMTT [34000lb]"] = {['name'] = "M978 HEMTT Tanker", ['container'] = false}, + ["HEMTT TFFT [34400lb]"] = {['name'] = "HEMTT TFFT", ['container'] = false}, + ["SPG M1128 Stryker MGS [33036lb]"] = {['name'] = "M1128 Stryker MGS", ['container'] = false}, + ["AAA Vulcan M163 Air [21666lb]"] = {['name'] = "Vulcan", ['container'] = true}, + ["AAA Vulcan M163 Skid [21577lb]"] = {['name'] = "Vulcan", ['container'] = false}, + ["APC M1126 Stryker ICV [29542lb]"] = {['name'] = "M1126 Stryker ICV", ['container'] = false}, + ["ATGM M1134 Stryker [30337lb]"] = {['name'] = "M1134 Stryker ATGM", ['container'] = false}, + ["APC LAV-25 Air [22520lb]"] = {['name'] = "LAV-25", ['container'] = true}, + ["APC LAV-25 Skid [22514lb]"] = {['name'] = "LAV-25", ['container'] = false}, + ["M1025 HMMWV Air [6160lb]"] = {['name'] = "Hummer", ['container'] = true}, + ["M1025 HMMWV Skid [6050lb]"] = {['name'] = "Hummer", ['container'] = false}, + ["IFV M2A2 Bradley [34720lb]"] = {['name'] = "M-2 Bradley", ['container'] = false}, + ["IFV MCV-80 [34720lb]"] = {['name'] = "MCV-80", ['container'] = false}, + ["IFV BMP-1 [23232lb]"] = {['name'] = "BMP-1", ['container'] = false}, + ["IFV BMP-2 [25168lb]"] = {['name'] = "BMP-2", ['container'] = false}, + ["IFV BMP-3 [32912lb]"] = {['name'] = "BMP-3", ['container'] = false}, + ["ARV BRDM-2 Air [12320lb]"] = {['name'] = "BRDM-2", ['container'] = true}, + ["ARV BRDM-2 Skid [12210lb]"] = {['name'] = "BRDM-2", ['container'] = false}, + ["APC BTR-80 Air [23936lb]"] = {['name'] = "BTR-80", ['container'] = true}, + ["APC BTR-80 Skid [23826lb]"] = {['name'] = "BTR-80", ['container'] = false}, + ["APC BTR-82A Air [24998lb]"] = {['name'] = "BTR-82A", ['container'] = true}, + ["APC BTR-82A Skid [24888lb]"] = {['name'] = "BTR-82A", ['container'] = false}, + ["SAM ROLAND ADS [34720lb]"] = {['name'] = "Roland Radar", ['container'] = false}, + ["SAM ROLAND LN [34720b]"] = {['name'] = "Roland ADS", ['container'] = false}, + ["SAM SA-13 STRELA [21624lb]"] = {['name'] = "Strela-10M3", ['container'] = false}, + ["AAA ZSU-23-4 Shilka [32912lb]"] = {['name'] = "ZSU-23-4 Shilka", ['container'] = false}, + ["SAM SA-19 Tunguska 2S6 [34720lb]"] = {['name'] = "2S6 Tunguska", ['container'] = false}, + ["Transport UAZ-469 Air [3747lb]"] = {['name'] = "UAZ-469", ['container'] = true}, + ["Transport UAZ-469 Skid [3630lb]"] = {['name'] = "UAZ-469", ['container'] = false}, + ["AAA GEPARD [34720lb]"] = {['name'] = "Gepard", ['container'] = false}, + ["SAM CHAPARRAL Air [21624lb]"] = {['name'] = "M48 Chaparral", ['container'] = true}, + ["SAM CHAPARRAL Skid [21516lb]"] = {['name'] = "M48 Chaparral", ['container'] = false}, + ["SAM LINEBACKER [34720lb]"] = {['name'] = "M6 Linebacker", ['container'] = false}, + ["Transport URAL-375 [14815lb]"] = {['name'] = "Ural-375", ['container'] = false}, + ["Transport M818 [16000lb]"] = {['name'] = "M 818", ['container'] = false}, + ["IFV MARDER [34720lb]"] = {['name'] = "Marder", ['container'] = false}, + ["Transport Tigr Air [15900lb]"] = {['name'] = "Tigr_233036", ['container'] = true}, + ["Transport Tigr Skid [15730lb]"] = {['name'] = "Tigr_233036", ['container'] = false}, + ["IFV TPZ FUCH [33440lb]"] = {['name'] = "TPZ", ['container'] = false}, + ["IFV BMD-1 Air [18040lb]"] = {['name'] = "BMD-1", ['container'] = true}, + ["IFV BMD-1 Skid [17930lb]"] = {['name'] = "BMD-1", ['container'] = false}, + ["IFV BTR-D Air [18040lb]"] = {['name'] = "BTR_D", ['container'] = true}, + ["IFV BTR-D Skid [17930lb]"] = {['name'] = "BTR_D", ['container'] = false}, + ["EWR SBORKA Air [21624lb]"] = {['name'] = "Dog Ear radar", ['container'] = true}, + ["EWR SBORKA Skid [21624lb]"] = {['name'] = "Dog Ear radar", ['container'] = false}, + ["ART 2S9 NONA Air [19140lb]"] = {['name'] = "SAU 2-C9", ['container'] = true}, + ["ART 2S9 NONA Skid [19030lb]"] = {['name'] = "SAU 2-C9", ['container'] = false}, + ["ART GVOZDIKA [34720lb]"] = {['name'] = "SAU Gvozdika", ['container'] = false}, + ["APC MTLB Air [26400lb]"] = {['name'] = "MTLB", ['container'] = true}, + ["APC MTLB Skid [26290lb]"] = {['name'] = "MTLB", ['container'] = false}, + --["Generic Crate [20000lb]"] = {['name'] = "Hercules_Container_Parachute", ['container'] = true} +} function Hercules_Cargo.Soldier_SpawnGroup(Cargo_Drop_Position, Cargo_Type_name, CargoHeading, Cargo_Country, GroupSpacing) SoldierUnitID = SoldierUnitID + 30 SoldierGroupID = SoldierGroupID + 1 @@ -488,265 +551,24 @@ end -- EventHandlers ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ function Hercules_Cargo.Hercules_Cargo_Drop_Events:onEvent(Cargo_Drop_Event) - if Cargo_Drop_Event.id == world.event.S_EVENT_SHOT then - GT_DisplayName = Weapon.getDesc(Cargo_Drop_Event.weapon).typeName:sub(15, -1)--Remove "weapons.bombs." from string - -- trigger.action.outTextForCoalition(coalition.side.BLUE, string.format("Cargo_Drop_Event: %s", Weapon.getDesc(Cargo_Drop_Event.weapon).typeName), 10) - -- trigger.action.outTextForCoalition(coalition.side.RED, string.format("Cargo_Drop_Event: %s", Weapon.getDesc(Cargo_Drop_Event.weapon).typeName), 10) - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "Squad 30 x Soldier [7950lb]") then - GT_Name = "Soldier M4 GRG" - SoldierGroup = true - ParatrooperGroupSpawnInit = true - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, SoldierGroup) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "ATGM M1045 HMMWV TOW [7183lb]") then - GT_Name = "M1045 HMMWV TOW" - Cargo_Container_Enclosed = true - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "APC M1043 HMMWV Armament [7023lb]") then - GT_Name = "M1043 HMMWV Armament" - Cargo_Container_Enclosed = true - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "SAM Avenger M1097 [7200lb]") then - GT_Name = "M1097 Avenger" - Cargo_Container_Enclosed = true - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "APC Cobra [10912lb]") then - GT_Name = "Cobra" - Cargo_Container_Enclosed = true - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "APC M113 [21624lb]") then - GT_Name = "M-113" - Cargo_Container_Enclosed = true - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "Tanker M978 HEMTT [34000lb]") then - GT_Name = "M978 HEMTT Tanker" - Cargo_Container_Enclosed = false - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "HEMTT TFFT [34400lb]") then - GT_Name = "HEMTT TFFT" - Cargo_Container_Enclosed = false - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "SPG M1128 Stryker MGS [33036lb]") then - GT_Name = "M1128 Stryker MGS" - Cargo_Container_Enclosed = false - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "AAA Vulcan M163 [21666lb]") then - GT_Name = "Vulcan" - Cargo_Container_Enclosed = true - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "APC M1126 Stryker ICV [29542lb]") then - GT_Name = "M1126 Stryker ICV" - Cargo_Container_Enclosed = false - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "ATGM M1134 Stryker [30337lb]") then - GT_Name = "M1134 Stryker ATGM" - Cargo_Container_Enclosed = false - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "APC LAV-25 [22514lb]") then - GT_Name = "LAV-25" - Cargo_Container_Enclosed = true - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "M1025 HMMWV [6160lb]") then - GT_Name = "Hummer" - Cargo_Container_Enclosed = true - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "IFV M2A2 Bradley [34720lb]") then - GT_Name = "M-2 Bradley" - Cargo_Container_Enclosed = false - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "IFV MCV-80 [34720lb]") then - GT_Name = "MCV-80" - Cargo_Container_Enclosed = false - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "IFV BMP-1 [23232lb]") then - GT_Name = "BMP-1" - Cargo_Container_Enclosed = false - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "IFV BMP-2 [25168lb]") then - GT_Name = "BMP-2" - Cargo_Container_Enclosed = false - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "IFV BMP-3 [32912lb]") then - GT_Name = "BMP-3" - Cargo_Container_Enclosed = false - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "ARV BRDM-2 [12320lb]") then - GT_Name = "BRDM-2" - Cargo_Container_Enclosed = true - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "APC BTR-80 [23936lb]") then - GT_Name = "BTR-80" - Cargo_Container_Enclosed = true - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "SAM ROLAND ADS [34720lb]") then - GT_Name = "Roland Radar" - Cargo_Container_Enclosed = false - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "SAM ROLAND LN [34720b]") then - GT_Name = "Roland ADS" - Cargo_Container_Enclosed = false - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "SAM SA-13 STRELA [21624lb]") then - GT_Name = "Strela-10M3" - Cargo_Container_Enclosed = false - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "AAA ZSU-23-4 Shilka [32912lb]") then - GT_Name = "ZSU-23-4 Shilka" - Cargo_Container_Enclosed = false - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "SAM SA-19 Tunguska 2S6 [34720lb]") then - GT_Name = "2S6 Tunguska" - Cargo_Container_Enclosed = false - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "Transport UAZ-469 [3747lb]") then - GT_Name = "UAZ-469" - Cargo_Container_Enclosed = true - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "Armed speedboat [2000lb]") then - GT_Name = "speedboat" - Cargo_Container_Enclosed = true - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "AAA GEPARD [34720lb]") then - GT_Name = "Gepard" - Cargo_Container_Enclosed = false - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "SAM CHAPARRAL [21624lb]") then - GT_Name = "M48 Chaparral" - Cargo_Container_Enclosed = true - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "SAM LINEBACKER [34720lb]") then - GT_Name = "M6 Linebacker" - Cargo_Container_Enclosed = true - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "Transport URAL-375 [14815lb]") then - GT_Name = "Ural-375" - Cargo_Container_Enclosed = true - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "Transport M818 [16000lb]") then - GT_Name = "M 818" - Cargo_Container_Enclosed = true - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "IFV MARDER [34720lb]") then - GT_Name = "Marder" - Cargo_Container_Enclosed = false - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "Transport Tigr [15900lb]") then - GT_Name = "Tigr_233036" - Cargo_Container_Enclosed = true - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "IFV TPZ FUCH [33440lb]") then - GT_Name = "TPZ" - Cargo_Container_Enclosed = false - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "IFV BMD-1 [18040lb]") then - GT_Name = "BMD-1" - Cargo_Container_Enclosed = true - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "IFV BTR-D [18040lb]") then - GT_Name = "BTR_D" - Cargo_Container_Enclosed = true - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "EWR SBORKA [21624lb]") then - GT_Name = "Dog Ear radar" - Cargo_Container_Enclosed = true - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "ART 2S9 NONA [19140lb]") then - GT_Name = "SAU 2-C9" - Cargo_Container_Enclosed = true - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "ART GVOZDIKA [34720lb]") then - GT_Name = "SAU Gvozdika" - Cargo_Container_Enclosed = false - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - if (GT_DisplayName == "APC MTLB [26000lb]") then - GT_Name = "MTLB" - Cargo_Container_Enclosed = true - Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) - end - --------------------------------------------------------------------------------------------------------------------------------- - end + if Cargo_Drop_Event.id == world.event.S_EVENT_SHOT then + GT_DisplayName = Weapon.getDesc(Cargo_Drop_Event.weapon).typeName:sub(15, -1)--Remove "weapons.bombs." from string + -- trigger.action.outTextForCoalition(coalition.side.BLUE, string.format("Cargo_Drop_Event: %s", Weapon.getDesc(Cargo_Drop_Event.weapon).typeName), 10) + -- trigger.action.outTextForCoalition(coalition.side.RED, string.format("Cargo_Drop_Event: %s", Weapon.getDesc(Cargo_Drop_Event.weapon).typeName), 10) + --------------------------------------------------------------------------------------------------------------------------------- + if (GT_DisplayName == "Squad 30 x Soldier [7950lb]") then + GT_Name = "Soldier M4 GRG" + SoldierGroup = true + ParatrooperGroupSpawnInit = true + Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, SoldierGroup) + end + --------------------------------------------------------------------------------------------------------------------------------- + if Hercules_Cargo.types[GT_DisplayName] then + local GT_Name = Hercules_Cargo.types[GT_DisplayName]['name'] + local Cargo_Container_Enclosed = Hercules_Cargo.types[GT_DisplayName]['container'] + Hercules_Cargo.Cargo_Initialize(Cargo_Drop_Event.initiator, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed) + end +end end world.addEventHandler(Hercules_Cargo.Hercules_Cargo_Drop_Events) diff --git a/resources/plugins/skynetiads/LICENSE.md b/resources/plugins/skynetiads/LICENSE.md new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/resources/plugins/skynetiads/LICENSE.md @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/resources/plugins/skynetiads/skynet-iads-compiled.lua b/resources/plugins/skynetiads/skynet-iads-compiled.lua index 61763cc4..7b7939a4 100644 --- a/resources/plugins/skynetiads/skynet-iads-compiled.lua +++ b/resources/plugins/skynetiads/skynet-iads-compiled.lua @@ -1,4 +1,4 @@ -env.info("--- SKYNET VERSION: 1.2.0 | BUILD TIME: 21.11.2020 1159Z ---") +env.info("--- SKYNET VERSION: 2.0.1 | BUILD TIME: 04.01.2021 0706Z ---") do --this file contains the required units per sam type samTypesDB = { @@ -6,8 +6,14 @@ samTypesDB = { ['type'] = 'complex', ['searchRadar'] = { ['S-300PS 40B6MD sr'] = { + ['name'] = { + ['NATO'] = 'Clam Shell', + }, }, ['S-300PS 64H6E sr'] = { + ['name'] = { + ['NATO'] = 'Big Bird', + }, }, }, ['trackingRadar'] = { @@ -34,6 +40,9 @@ samTypesDB = { ['type'] = 'complex', ['searchRadar'] = { ['SA-11 Buk SR 9S18M1'] = { + ['name'] = { + ['NATO'] = 'Snow Drift', + }, }, }, ['launchers'] = { @@ -50,11 +59,14 @@ samTypesDB = { }, ['harm_detection_chance'] = 70 }, - ['s-125'] = { + ['S-125'] = { ['type'] = 'complex', ['searchRadar'] = { ['p-19 s-125 sr'] = { - }, + ['name'] = { + ['NATO'] = 'Flat Face', + }, + }, }, ['trackingRadar'] = { ['snr s-125 tr'] = { @@ -67,12 +79,15 @@ samTypesDB = { ['name'] = { ['NATO'] = 'SA-3 Goa', }, - ['harm_detection_chance'] = 40 + ['harm_detection_chance'] = 30 }, - ['s-75'] = { + ['S-75'] = { ['type'] = 'complex', ['searchRadar'] = { ['p-19 s-125 sr'] = { + ['name'] = { + ['NATO'] = 'Flat Face', + }, }, }, ['trackingRadar'] = { @@ -92,6 +107,9 @@ samTypesDB = { ['type'] = 'complex', ['searchRadar'] = { ['Kub 1S91 str'] = { + ['name'] = { + ['NATO'] = 'Straight Flush', + }, }, }, ['launchers'] = { @@ -107,9 +125,11 @@ samTypesDB = { ['type'] = 'complex', ['searchRadar'] = { ['Patriot str'] = { + ['name'] = { + ['NATO'] = 'Patriot str', + }, }, }, - ['launchers'] = { ['Patriot ln'] = { }, @@ -139,6 +159,9 @@ samTypesDB = { ['type'] = 'complex', ['searchRadar'] = { ['Hawk sr'] = { + ['name'] = { + ['NATO'] = 'Hawk str', + }, }, }, ['trackingRadar'] = { @@ -299,6 +322,9 @@ samTypesDB = { ['HQ-7'] = { ['searchRadar'] = { ['HQ-7_STR_SP'] = { + ['name'] = { + ['NATO'] = 'CSA-4', + }, }, }, ['launchers'] = { @@ -315,109 +341,598 @@ samTypesDB = { ['type'] = 'ewr', ['searchRadar'] = { ['1L13 EWR'] = { + ['name'] = { + ['NATO'] = 'Box Spring', + }, }, }, - ['name'] = { - ['NATO'] = '1L13 EWR', - }, ['harm_detection_chance'] = 60 }, ['55G6 EWR'] = { ['type'] = 'ewr', ['searchRadar'] = { ['55G6 EWR'] = { + ['name'] = { + ['NATO'] = 'Tall Rack', + }, }, }, - ['name'] = { - ['NATO'] = '55G6 EWR', - }, ['harm_detection_chance'] = 60 }, ['Dog Ear'] = { ['type'] = 'ewr', ['searchRadar'] = { ['Dog Ear radar'] = { + ['name'] = { + ['NATO'] = 'Dog Ear', + }, }, }, - ['name'] = { - ['NATO'] = 'Dog Ear', - }, ['harm_detection_chance'] = 20 }, ['Roland Radar'] = { ['type'] = 'ewr', ['searchRadar'] = { ['Roland Radar'] = { + ['name'] = { + ['NATO'] = 'Roland EWR', + }, }, }, - ['name'] = { - ['NATO'] = 'Roland EWR', - }, + ['harm_detection_chance'] = 60 - }, - ['p-19 s-125 sr'] = { - ['searchRadar'] = { - ['p-19 s-125 sr'] = { - }, - }, - ['name'] = { - ['NATO'] = 'Flat Face', - }, - ['harm_detection_chance'] = 40 - }, - ['Patriot str'] = { - ['searchRadar'] = { - ['Patriot str'] = { - }, - }, - ['name'] = { - ['NATO'] = 'Patriot str', - }, - ['harm_detection_chance'] = 80 - }, - ['EW S-300'] = { - ['searchRadar'] = { - ['S-300PS 40B6MD sr'] = { - }, - ['S-300PS 64H6E sr'] = { - }, - }, - ['name'] = { - ['NATO'] = 'Big Bird', - }, - ['harm_detection_chance'] = 90 - }, - ['SA-11 Buk SR 9S18M1'] = { - ['searchRadar'] = { - ['SA-11 Buk SR 9S18M1'] = { - }, - }, - ['name'] = { - ['NATO'] = 'Snow Drift', - }, - ['harm_detection_chance'] = 70 - }, - ['Kub 1S91 str'] = { - ['searchRadar'] = { - ['Kub 1S91 str'] = { - }, - }, - ['name'] = { - ['NATO'] = 'Straight Flush', - }, - ['harm_detection_chance'] = 40 - }, - ['Hawk str'] = { - ['searchRadar'] = { - ['Hawk sr'] = { - }, - }, - ['name'] = { - ['NATO'] = 'Hawk str', - }, - ['harm_detection_chance'] = 40 }, } +end +do +-- this file contains the definitions for the HightDigitSAMSs: https://github.com/Auranis/HighDigitSAMs + +--[[ units in SA-10 group Gargoyle: +2020-12-10 18:27:27.050 INFO SCRIPTING: S-300PMU1 54K6 cp +2020-12-10 18:27:27.050 INFO SCRIPTING: S-300PMU1 5P85CE ln +2020-12-10 18:27:27.050 INFO SCRIPTING: S-300PMU1 5P85DE ln +2020-12-10 18:27:27.050 INFO SCRIPTING: S-300PMU1 40B6MD sr +2020-12-10 18:27:27.050 INFO SCRIPTING: S-300PMU1 64N6E sr +2020-12-10 18:27:27.050 INFO SCRIPTING: S-300PMU1 40B6M tr +2020-12-10 18:27:27.050 INFO SCRIPTING: S-300PMU1 30N6E tr +--]] +samTypesDB['S-300PMU1'] = { + ['type'] = 'complex', + ['searchRadar'] = { + ['S-300PMU1 40B6MD sr'] = { + ['name'] = { + ['NATO'] = 'Clam Shell', + }, + }, + ['S-300PMU1 64N6E sr'] = { + ['name'] = { + ['NATO'] = 'Big Bird', + }, + }, + }, + ['trackingRadar'] = { + ['S-300PMU1 40B6M tr'] = { + }, + ['S-300PMU1 30N6E tr'] = { + }, + }, + ['misc'] = { + ['S-300PMU1 54K6 cp'] = { + ['required'] = true, + }, + }, + ['launchers'] = { + ['S-300PMU1 5P85CE ln'] = { + }, + ['S-300PMU1 5P85DE ln'] = { + }, + }, + ['name'] = { + ['NATO'] = 'SA-20A Gargoyle' + }, + ['harm_detection_chance'] = 90 +} + +--[[ Units in the SA-23 Group: +2020-12-11 16:40:52.072 INFO SCRIPTING: S-300VM 9A82ME ln +2020-12-11 16:40:52.072 INFO SCRIPTING: S-300VM 9A83ME ln +2020-12-11 16:40:52.072 INFO SCRIPTING: S-300VM 9S15M2 sr +2020-12-11 16:40:52.072 INFO SCRIPTING: S-300VM 9S19M2 sr +2020-12-11 16:40:52.072 INFO SCRIPTING: S-300VM 9S32ME tr +2020-12-11 16:40:52.072 INFO SCRIPTING: S-300VM 9S457ME cp + +According to wikipedia: +dem 9A83-Startfahrzeug die Bezeichnung SA-12A Gladiator zu geben; das größere 9A82-Startfahrzeug erhielt die Bezeichnung SA-12B Giant. +9A83ME -> SA-23A Gladiator +9A82ME -> SA-23B Giant +]]-- +samTypesDB['S-300VM'] = { + ['type'] = 'complex', + ['searchRadar'] = { + ['S-300VM 9S15M2 sr'] = { + ['name'] = { + ['NATO'] = 'Bill Board-C', + }, + }, + ['S-300VM 9S19M2 sr'] = { + ['name'] = { + ['NATO'] = 'High Screen-B', + }, + }, + }, + ['trackingRadar'] = { + ['S-300VM 9S32ME tr'] = { + }, + }, + ['misc'] = { + ['S-300VM 9S457ME cp'] = { + ['required'] = true, + }, + }, + ['launchers'] = { + ['S-300VM 9A82ME ln'] = { + }, + ['S-300VM 9A83ME ln'] = { + }, + }, + ['name'] = { + ['NATO'] = 'SA-23 Gladiator/Giant' + }, + ['harm_detection_chance'] = 90 +} + +--[[ Units in the SA-10B Group: +2021-01-01 20:39:14.413 INFO SCRIPTING: S-300PS SA-10B 40B6MD MAST sr +2021-01-01 20:39:14.413 INFO SCRIPTING: S-300PS SA-10B 54K6 cp +2021-01-01 20:39:14.413 INFO SCRIPTING: S-300PS 5P85SE_mod ln +2021-01-01 20:39:14.413 INFO SCRIPTING: S-300PS 5P85SU_mod ln +2021-01-01 20:39:14.413 INFO SCRIPTING: S-300PS 64H6E TRAILER sr +2021-01-01 20:39:14.413 INFO SCRIPTING: S-300PS 30N6 TRAILER tr +2021-01-01 20:39:14.413 INFO SCRIPTING: S-300PS SA-10B 40B6M MAST tr +--]] +samTypesDB['S-300PS'] = { + ['type'] = 'complex', + ['searchRadar'] = { + ['S-300PS SA-10B 40B6MD MAST sr'] = { + }, + ['S-300PS 64H6E TRAILER sr'] = { + }, + }, + ['trackingRadar'] = { + ['S-300PS 30N6 TRAILER tr'] = { + }, + ['S-300PS SA-10B 40B6M MAST tr'] = { + }, + }, + ['misc'] = { + ['S-300PS SA-10B 54K6 cp'] = { + ['required'] = true, + }, + }, + ['launchers'] = { + ['S-300PS 5P85SE_mod ln'] = { + }, + ['S-300PS 5P85SU_mod ln'] = { + }, + }, + ['name'] = { + ['NATO'] = 'SA-10B Grumble' + }, + ['harm_detection_chance'] = 90 +} + +--[[ Extra launchers for the in game SA-10C and HighDigitSAMs SA-10B, SA-20B +2021-01-01 21:04:19.908 INFO SCRIPTING: S-300PS 5P85DE ln +2021-01-01 21:04:19.908 INFO SCRIPTING: S-300PS 5P85CE ln +--]] + +local s300launchers = samTypesDB['S-300']['launchers'] +s300launchers['S-300PS 5P85DE ln'] = {} +s300launchers['S-300PS 5P85CE ln'] = {} + +local s300launchers = samTypesDB['S-300PS']['launchers'] +s300launchers['S-300PS 5P85DE ln'] = {} +s300launchers['S-300PS 5P85CE ln'] = {} + +local s300launchers = samTypesDB['S-300PMU1']['launchers'] +s300launchers['S-300PS 5P85DE ln'] = {} +s300launchers['S-300PS 5P85CE ln'] = {} + +--[[ +New launcher for the SA-11 complex, will identify as SA-17 +SA-17 Buk M1-2 LN 9A310M1-2 + --]] +samTypesDB['Buk-M2'] = { + ['type'] = 'complex', + ['searchRadar'] = { + ['SA-11 Buk SR 9S18M1'] = { + }, + }, + ['launchers'] = { + ['SA-17 Buk M1-2 LN 9A310M1-2'] = { + }, + }, + ['misc'] = { + ['SA-11 Buk CC 9S470M1'] = { + ['required'] = true, + }, + }, + ['name'] = { + ['NATO'] = 'SA-17 Grizzly', + }, + ['harm_detection_chance'] = 90 +} + +--[[ +New launcher for the SA-2 complex: S_75M_Volhov_V759 +--]] +local s75launchers = samTypesDB['S-75']['launchers'] +s75launchers['S_75M_Volhov_V759'] = {} + +--[[ +New launcher for the SA-3 complex: +--]] +local s125launchers = samTypesDB['S-125']['launchers'] +s125launchers['5p73 V-601P ln'] = {} + +--[[ +New launcher for the SA-2 complex: HQ_2_Guideline_LN +--]] +local s125launchers = samTypesDB['S-75']['launchers'] +s125launchers['HQ_2_Guideline_LN'] = {} + +end + + + +do + +SkynetIADSLogger = {} +SkynetIADSLogger.__index = SkynetIADSLogger + +function SkynetIADSLogger:create(iads) + local logger = {} + setmetatable(logger, SkynetIADSLogger) + logger.debugOutput = {} + logger.debugOutput.IADSStatus = false + logger.debugOutput.samWentDark = false + logger.debugOutput.contacts = false + logger.debugOutput.radarWentLive = false + logger.debugOutput.jammerProbability = false + logger.debugOutput.addedEWRadar = false + logger.debugOutput.addedSAMSite = false + logger.debugOutput.warnings = true + logger.debugOutput.harmDefence = false + logger.debugOutput.samSiteStatusEnvOutput = false + logger.debugOutput.earlyWarningRadarStatusEnvOutput = false + logger.debugOutput.commandCenterStatusEnvOutput = false + logger.iads = iads + return logger +end + +function SkynetIADSLogger:getDebugSettings() + return self.debugOutput +end + +function SkynetIADSLogger:printOutput(output, typeWarning) + if typeWarning == true and self:getDebugSettings().warnings or typeWarning == nil then + if typeWarning == true then + output = "WARNING: "..output + end + trigger.action.outText(output, 4) + end +end + +function SkynetIADSLogger:printOutputToLog(output) + env.info("SKYNET: "..output, 4) +end + +function SkynetIADSLogger:printEarlyWarningRadarStatus() + local ewRadars = self.iads:getEarlyWarningRadars() + self:printOutputToLog("------------------------------------------ EW RADAR STATUS: "..self.iads:getCoalitionString().." -------------------------------") + for i = 1, #ewRadars do + local ewRadar = ewRadars[i] + local numConnectionNodes = #ewRadar:getConnectionNodes() + local numPowerSources = #ewRadar:getPowerSources() + local isActive = ewRadar:isActive() + local connectionNodes = ewRadar:getConnectionNodes() + local firstRadar = nil + local radars = ewRadar:getRadars() + + --get the first existing radar to prevent issues in calculating the distance later on: + for i = 1, #radars do + if radars[i]:isExist() then + firstRadar = radars[i] + break + end + + end + local numDamagedConnectionNodes = 0 + + + for j = 1, #connectionNodes do + local connectionNode = connectionNodes[j] + if connectionNode:isExist() == false then + numDamagedConnectionNodes = numDamagedConnectionNodes + 1 + end + end + local intactConnectionNodes = numConnectionNodes - numDamagedConnectionNodes + + local powerSources = ewRadar:getPowerSources() + local numDamagedPowerSources = 0 + for j = 1, #powerSources do + local powerSource = powerSources[j] + if powerSource:isExist() == false then + numDamagedPowerSources = numDamagedPowerSources + 1 + end + end + local intactPowerSources = numPowerSources - numDamagedPowerSources + + local detectedTargets = ewRadar:getDetectedTargets() + local samSitesInCoveredArea = ewRadar:getChildRadars() + + local unitName = "DESTROYED" + + if ewRadar:getDCSRepresentation():isExist() then + unitName = ewRadar:getDCSName() + end + + self:printOutputToLog("UNIT: "..unitName.." | TYPE: "..ewRadar:getNatoName()) + self:printOutputToLog("ACTIVE: "..tostring(isActive).."| DETECTED TARGETS: "..#detectedTargets.." | DEFENDING HARM: "..tostring(ewRadar:isDefendingHARM())) + if numConnectionNodes > 0 then + self:printOutputToLog("CONNECTION NODES: "..numConnectionNodes.." | DAMAGED: "..numDamagedConnectionNodes.." | INTACT: "..intactConnectionNodes) + else + self:printOutputToLog("NO CONNECTION NODES SET") + end + if numPowerSources > 0 then + self:printOutputToLog("POWER SOURCES : "..numPowerSources.." | DAMAGED:"..numDamagedPowerSources.." | INTACT: "..intactPowerSources) + else + self:printOutputToLog("NO POWER SOURCES SET") + end + + self:printOutputToLog("SAM SITES IN COVERED AREA: "..#samSitesInCoveredArea) + for j = 1, #samSitesInCoveredArea do + local samSiteCovered = samSitesInCoveredArea[j] + self:printOutputToLog(samSiteCovered:getDCSName()) + end + + for j = 1, #detectedTargets do + local contact = detectedTargets[j] + if firstRadar ~= nil and firstRadar:isExist() then + local distance = mist.utils.round(mist.utils.metersToNM(ewRadar:getDistanceInMetersToContact(firstRadar:getDCSRepresentation(), contact:getPosition().p)), 2) + self:printOutputToLog("CONTACT: "..contact:getName().." | TYPE: "..contact:getTypeName().." | DISTANCE NM: "..distance) + end + end + + self:printOutputToLog("---------------------------------------------------") + + end + +end + +function SkynetIADSLogger:getMetaInfo(abstractElementSupport) + local info = {} + info.numSources = #abstractElementSupport + info.numDamagedSources = 0 + info.numIntactSources = 0 + for j = 1, #abstractElementSupport do + local source = abstractElementSupport[j] + if source:isExist() == false then + info.numDamagedSources = info.numDamagedSources + 1 + end + end + info.numIntactSources = info.numSources - info.numDamagedSources + return info +end + +function SkynetIADSLogger:printSAMSiteStatus() + local samSites = self.iads:getSAMSites() + + self:printOutputToLog("------------------------------------------ SAM STATUS: "..self.iads:getCoalitionString().." -------------------------------") + for i = 1, #samSites do + local samSite = samSites[i] + local numConnectionNodes = #samSite:getConnectionNodes() + local numPowerSources = #samSite:getPowerSources() + local isAutonomous = samSite:getAutonomousState() + local isActive = samSite:isActive() + + local connectionNodes = samSite:getConnectionNodes() + local firstRadar = samSite:getRadars()[1] + local numDamagedConnectionNodes = 0 + for j = 1, #connectionNodes do + local connectionNode = connectionNodes[j] + if connectionNode:isExist() == false then + numDamagedConnectionNodes = numDamagedConnectionNodes + 1 + end + end + local intactConnectionNodes = numConnectionNodes - numDamagedConnectionNodes + + local powerSources = samSite:getPowerSources() + local numDamagedPowerSources = 0 + for j = 1, #powerSources do + local powerSource = powerSources[j] + if powerSource:isExist() == false then + numDamagedPowerSources = numDamagedPowerSources + 1 + end + end + local intactPowerSources = numPowerSources - numDamagedPowerSources + + local detectedTargets = samSite:getDetectedTargets() + + local samSitesInCoveredArea = samSite:getChildRadars() + + self:printOutputToLog("GROUP: "..samSite:getDCSName().." | TYPE: "..samSite:getNatoName()) + self:printOutputToLog("ACTIVE: "..tostring(isActive).." | AUTONOMOUS: "..tostring(isAutonomous).." | IS ACTING AS EW: "..tostring(samSite:getActAsEW()).." | DETECTED TARGETS: "..#detectedTargets.." | DEFENDING HARM: "..tostring(samSite:isDefendingHARM()).." | MISSILES IN FLIGHT: "..tostring(samSite:getNumberOfMissilesInFlight())) + + if numConnectionNodes > 0 then + self:printOutputToLog("CONNECTION NODES: "..numConnectionNodes.." | DAMAGED: "..numDamagedConnectionNodes.." | INTACT: "..intactConnectionNodes) + else + self:printOutputToLog("NO CONNECTION NODES SET") + end + if numPowerSources > 0 then + self:printOutputToLog("POWER SOURCES : "..numPowerSources.." | DAMAGED:"..numDamagedPowerSources.." | INTACT: "..intactPowerSources) + else + self:printOutputToLog("NO POWER SOURCES SET") + end + + self:printOutputToLog("SAM SITES IN COVERED AREA: "..#samSitesInCoveredArea) + for j = 1, #samSitesInCoveredArea do + local samSiteCovered = samSitesInCoveredArea[j] + self:printOutputToLog(samSiteCovered:getDCSName()) + end + + for j = 1, #detectedTargets do + local contact = detectedTargets[j] + if firstRadar ~= nil and firstRadar:isExist() then + local distance = mist.utils.round(mist.utils.metersToNM(samSite:getDistanceInMetersToContact(firstRadar:getDCSRepresentation(), contact:getPosition().p)), 2) + self:printOutputToLog("CONTACT: "..contact:getName().." | TYPE: "..contact:getTypeName().." | DISTANCE NM: "..distance) + end + end + + self:printOutputToLog("---------------------------------------------------") + end +end + +function SkynetIADSLogger:printCommandCenterStatus() + local commandCenters = self.iads:getCommandCenters() + self:printOutputToLog("------------------------------------------ COMMAND CENTER STATUS: "..self.iads:getCoalitionString().." -------------------------------") + + for i = 1, #commandCenters do + local commandCenter = commandCenters[i] + local numConnectionNodes = #commandCenter:getConnectionNodes() + local powerSourceInfo = self:getMetaInfo(commandCenter:getPowerSources()) + local connectionNodeInfo = self:getMetaInfo(commandCenter:getConnectionNodes()) + self:printOutputToLog("GROUP: "..commandCenter:getDCSName().." | TYPE: "..commandCenter:getNatoName()) + if connectionNodeInfo.numSources > 0 then + self:printOutputToLog("CONNECTION NODES: "..connectionNodeInfo.numSources.." | DAMAGED: "..connectionNodeInfo.numDamagedSources.." | INTACT: "..connectionNodeInfo.numIntactSources) + else + self:printOutputToLog("NO CONNECTION NODES SET") + end + if powerSourceInfo.numSources > 0 then + self:printOutputToLog("POWER SOURCES : "..powerSourceInfo.numSources.." | DAMAGED: "..powerSourceInfo.numDamagedSources.." | INTACT: "..powerSourceInfo.numIntactSources) + else + self:printOutputToLog("NO POWER SOURCES SET") + end + self:printOutputToLog("---------------------------------------------------") + end +end + +function SkynetIADSLogger:printSystemStatus() + + if self:getDebugSettings().IADSStatus or self:getDebugSettings().contacts then + local coalitionStr = self.iads:getCoalitionString() + self:printOutput("---- IADS: "..coalitionStr.." ------") + end + + if self:getDebugSettings().IADSStatus then + + local commandCenters = self.iads:getCommandCenters() + local numComCenters = #commandCenters + local numDestroyedComCenters = 0 + local numComCentersNoPower = 0 + local numComCentersNoConnectionNode = 0 + local numIntactComCenters = 0 + for i = 1, #commandCenters do + local commandCenter = commandCenters[i] + if commandCenter:hasWorkingPowerSource() == false then + numComCentersNoPower = numComCentersNoPower + 1 + end + if commandCenter:hasActiveConnectionNode() == false then + numComCentersNoConnectionNode = numComCentersNoConnectionNode + 1 + end + if commandCenter:isDestroyed() == false then + numIntactComCenters = numIntactComCenters + 1 + end + end + + numDestroyedComCenters = numComCenters - numIntactComCenters + + + self:printOutput("COMMAND CENTERS: "..numComCenters.." | Destroyed: "..numDestroyedComCenters.." | NoPowr: "..numComCentersNoPower.." | NoCon: "..numComCentersNoConnectionNode) + + local ewNoPower = 0 + local earlyWarningRadars = self.iads:getEarlyWarningRadars() + local ewTotal = #earlyWarningRadars + local ewNoConnectionNode = 0 + local ewActive = 0 + local ewRadarsInactive = 0 + + for i = 1, #earlyWarningRadars do + local ewRadar = earlyWarningRadars[i] + if ewRadar:hasWorkingPowerSource() == false then + ewNoPower = ewNoPower + 1 + end + if ewRadar:hasActiveConnectionNode() == false then + ewNoConnectionNode = ewNoConnectionNode + 1 + end + if ewRadar:isActive() then + ewActive = ewActive + 1 + end + end + + ewRadarsInactive = ewTotal - ewActive + local numEWRadarsDestroyed = #self.iads:getDestroyedEarlyWarningRadars() + self:printOutput("EW: "..ewTotal.." | On: "..ewActive.." | Off: "..ewRadarsInactive.." | Destroyed: "..numEWRadarsDestroyed.." | NoPowr: "..ewNoPower.." | NoCon: "..ewNoConnectionNode) + + local samSitesInactive = 0 + local samSitesActive = 0 + local samSites = self.iads:getSAMSites() + local samSitesTotal = #samSites + local samSitesNoPower = 0 + local samSitesNoConnectionNode = 0 + local samSitesOutOfAmmo = 0 + local samSiteAutonomous = 0 + local samSiteRadarDestroyed = 0 + for i = 1, #samSites do + local samSite = samSites[i] + if samSite:hasWorkingPowerSource() == false then + samSitesNoPower = samSitesNoPower + 1 + end + if samSite:hasActiveConnectionNode() == false then + samSitesNoConnectionNode = samSitesNoConnectionNode + 1 + end + if samSite:isActive() then + samSitesActive = samSitesActive + 1 + end + if samSite:hasRemainingAmmo() == false then + samSitesOutOfAmmo = samSitesOutOfAmmo + 1 + end + if samSite:getAutonomousState() == true then + samSiteAutonomous = samSiteAutonomous + 1 + end + if samSite:hasWorkingRadar() == false then + samSiteRadarDestroyed = samSiteRadarDestroyed + 1 + end + end + + samSitesInactive = samSitesTotal - samSitesActive + self:printOutput("SAM: "..samSitesTotal.." | On: "..samSitesActive.." | Off: "..samSitesInactive.." | Autonm: "..samSiteAutonomous.." | Raddest: "..samSiteRadarDestroyed.." | NoPowr: "..samSitesNoPower.." | NoCon: "..samSitesNoConnectionNode.." | NoAmmo: "..samSitesOutOfAmmo) + end + + if self:getDebugSettings().contacts then + local contacts = self.iads:getContacts() + if contacts then + for i = 1, #contacts do + local contact = contacts[i] + self:printOutput("CONTACT: "..contact:getName().." | TYPE: "..contact:getTypeName().." | GS: "..tostring(contact:getGroundSpeedInKnots()).." | LAST SEEN: "..contact:getAge()) + end + end + end + + if self:getDebugSettings().commandCenterStatusEnvOutput then + self:printCommandCenterStatus() + end + + if self:getDebugSettings().earlyWarningRadarStatusEnvOutput then + self:printEarlyWarningRadarStatus() + end + + if self:getDebugSettings().samSiteStatusEnvOutput then + self:printSAMSiteStatus() + end + +end + end do @@ -439,27 +954,12 @@ function SkynetIADS:create(name) iads.contacts = {} iads.maxTargetAge = 32 iads.name = name + iads.logger = SkynetIADSLogger:create(iads) if iads.name == nil then iads.name = "" end iads.contactUpdateInterval = 5 iads.samSetupTime = 60 - iads.destroyedUnitResponsibleForUpdateAutonomousStateOfSAMSite = nil - iads.debugOutput = {} - iads.debugOutput.IADSStatus = false - iads.debugOutput.samWentDark = false - iads.debugOutput.contacts = false - iads.debugOutput.radarWentLive = false - iads.debugOutput.ewRadarNoConnection = false - iads.debugOutput.samNoConnection = false - iads.debugOutput.jammerProbability = false - iads.debugOutput.addedEWRadar = false - iads.debugOutput.hasNoPower = false - iads.debugOutput.addedSAMSite = false - iads.debugOutput.warnings = true - iads.debugOutput.harmDefence = false - iads.debugOutput.samSiteStatusEnvOutput = false - iads.debugOutput.earlyWarningRadarStatusEnvOutput = false return iads end @@ -474,7 +974,7 @@ function SkynetIADS:setCoalition(item) self.coalitionID = coalitionID end if self.coalitionID ~= coalitionID then - self:printOutput("element: "..item:getName().." has a different coalition than the IADS", true) + self:printOutputToLog("element: "..item:getName().." has a different coalition than the IADS", true) end end end @@ -539,7 +1039,7 @@ end function SkynetIADS:addEarlyWarningRadar(earlyWarningRadarUnitName) local earlyWarningRadarUnit = Unit.getByName(earlyWarningRadarUnitName) if earlyWarningRadarUnit == nil then - self:printOutput("you have added an EW Radar that does not exist, check name of Unit in Setup and Mission editor: "..earlyWarningRadarUnitName, true) + self:printOutputToLog("you have added an EW Radar that does not exist, check name of Unit in Setup and Mission editor: "..earlyWarningRadarUnitName, true) return end self:setCoalition(earlyWarningRadarUnit) @@ -561,7 +1061,7 @@ function SkynetIADS:addEarlyWarningRadar(earlyWarningRadarUnitName) ewRadar:goLive() table.insert(self.earlyWarningRadars, ewRadar) if self:getDebugSettings().addedEWRadar then - self:printOutput(ewRadar:getDescription().." added to IADS") + self:printOutputToLog("ADDED: "..ewRadar:getDescription()) end return ewRadar end @@ -619,7 +1119,7 @@ end function SkynetIADS:addSAMSite(samSiteName) local samSiteDCS = Group.getByName(samSiteName) if samSiteDCS == nil then - self:printOutput("you have added an SAM Site that does not exist, check name of Group in Setup and Mission editor: "..tostring(samSiteName), true) + self:printOutputToLog("you have added an SAM Site that does not exist, check name of Group in Setup and Mission editor: "..tostring(samSiteName), true) return end self:setCoalition(samSiteDCS) @@ -632,13 +1132,13 @@ function SkynetIADS:addSAMSite(samSiteName) end samSite:setCachedTargetsMaxAge(self:getCachedTargetsMaxAge()) if samSite:getNatoName() == "UNKNOWN" then - self:printOutput("you have added an SAM Site that Skynet IADS can not handle: "..samSite:getDCSName(), true) + self:printOutputToLog("you have added an SAM site that Skynet IADS can not handle: "..samSite:getDCSName(), true) samSite:cleanUp() else samSite:goDark() table.insert(self.samSites, samSite) if self:getDebugSettings().addedSAMSite then - self:printOutput(samSite:getDescription().." added to IADS") + self:printOutputToLog("ADDED: "..samSite:getDescription()) end return samSite end @@ -788,7 +1288,7 @@ function SkynetIADS.evaluateContacts(self) samSite:targetCycleUpdateEnd() end - self:printSystemStatus() + self.logger:printSystemStatus() end function SkynetIADS:cleanAgedTargets() @@ -875,6 +1375,13 @@ function SkynetIADS:buildRadarCoverage() end self:addRadarsToCommandCenters() + + --we call this once on all sam sites, to make sure autonomous sites go live when IADS activates + for i = 1, #samSites do + local samSite = samSites[i] + samSite:informChildrenOfStateChange() + end + end function SkynetIADS:buildRadarCoverageForAbstractRadarElement(abstractRadarElement) @@ -887,12 +1394,12 @@ function SkynetIADS:buildRadarCoverageForAbstractRadarElement(abstractRadarEleme if getmetatable(aElementToCompare) == SkynetIADSSamSite and getmetatable(abstractRadarElement) == SkynetIADSSamSite then abstractRadarElement:addChildRadar(aElementToCompare) end - if getmetatable(aElementToCompare) == SkynetIADSSamSite and getmetatable(abstractRadarElement) == SkynetIADSEWRadar then + if getmetatable(aElementToCompare) == SkynetIADSSamSite and getmetatable(abstractRadarElement) == SkynetIADSEWRadar or getmetatable(aElementToCompare) == SkynetIADSSamSite and getmetatable(abstractRadarElement) == SkynetIADSAWACSRadar then abstractRadarElement:addChildRadar(aElementToCompare) end --EW Radars should not have parent Radars - if getmetatable(aElementToCompare) ~= SkynetIADSEWRadar then + if getmetatable(aElementToCompare) ~= SkynetIADSEWRadar and getmetatable(aElementToCompare) ~= SkynetIADSAWACSRadar then aElementToCompare:addParentRadar(abstractRadarElement) end end @@ -930,17 +1437,16 @@ function SkynetIADS:getContacts() return self.contacts end -function SkynetIADS:printOutput(output, typeWarning) - if typeWarning == true and self.debugOutput.warnings or typeWarning == nil then - if typeWarning == true then - output = "WARNING: "..output - end - trigger.action.outText(output, 4) - end +function SkynetIADS:getDebugSettings() + return self.logger.debugOutput end -function SkynetIADS:getDebugSettings() - return self.debugOutput +function SkynetIADS:printOutput(output, typeWarning) + self.logger:printOutput(output, typeWarning) +end + +function SkynetIADS:printOutputToLog(output) + self.logger:printOutputToLog(output) end -- will start going through the Early Warning Radars and SAM sites to check what targets they have detected @@ -1031,7 +1537,7 @@ function SkynetIADS:getCoalitionString() end if self.name then - coalitionStr = coalitionStr.." "..self.name + coalitionStr = "COALITION: "..coalitionStr.." | NAME: "..self.name end return coalitionStr @@ -1048,261 +1554,6 @@ function SkynetIADS:addMooseSetGroup(mooseSetGroup) self:getMooseConnector():addMooseSetGroup(mooseSetGroup) end -function SkynetIADS:printDetailedEarlyWarningRadarStatus() - local ewRadars = self:getEarlyWarningRadars() - env.info("------------------------------------------ EW RADAR STATUS: "..self:getCoalitionString().." -------------------------------") - for i = 1, #ewRadars do - local ewRadar = ewRadars[i] - local numConnectionNodes = #ewRadar:getConnectionNodes() - local numPowerSources = #ewRadar:getPowerSources() - local isActive = ewRadar:isActive() - local connectionNodes = ewRadar:getConnectionNodes() - local firstRadar = nil - local radars = ewRadar:getRadars() - - --get the first existing radar to prevent issues in calculating the distance later on: - for i = 1, #radars do - if radars[i]:isExist() then - firstRadar = radars[i] - break - end - - end - local numDamagedConnectionNodes = 0 - - - for j = 1, #connectionNodes do - local connectionNode = connectionNodes[j] - if connectionNode:isExist() == false then - numDamagedConnectionNodes = numDamagedConnectionNodes + 1 - end - end - local intactConnectionNodes = numConnectionNodes - numDamagedConnectionNodes - - local powerSources = ewRadar:getPowerSources() - local numDamagedPowerSources = 0 - for j = 1, #powerSources do - local powerSource = powerSources[j] - if powerSource:isExist() == false then - numDamagedPowerSources = numDamagedPowerSources + 1 - end - end - local intactPowerSources = numPowerSources - numDamagedPowerSources - - local detectedTargets = ewRadar:getDetectedTargets() - local samSitesInCoveredArea = ewRadar:getChildRadars() - - local unitName = "DESTROYED" - - if ewRadar:getDCSRepresentation():isExist() then - unitName = ewRadar:getDCSName() - end - - env.info("UNIT: "..unitName.." | TYPE: "..ewRadar:getNatoName()) - env.info("ACTIVE: "..tostring(isActive).."| DETECTED TARGETS: "..#detectedTargets.." | DEFENDING HARM: "..tostring(ewRadar:isDefendingHARM())) - if numConnectionNodes > 0 then - env.info("CONNECTION NODES: "..numConnectionNodes.." | DAMAGED: "..numDamagedConnectionNodes.." | INTACT: "..intactConnectionNodes) - else - env.info("NO CONNECTION NODES SET") - end - if numPowerSources > 0 then - env.info("POWER SOURCES : "..numPowerSources.." | DAMAGED:"..numDamagedPowerSources.." | INTACT: "..intactPowerSources) - else - env.info("NO POWER SOURCES SET") - end - - env.info("SAM SITES IN COVERED AREA: "..#samSitesInCoveredArea) - for j = 1, #samSitesInCoveredArea do - local samSiteCovered = samSitesInCoveredArea[j] - env.info(samSiteCovered:getDCSName()) - end - - for j = 1, #detectedTargets do - local contact = detectedTargets[j] - if firstRadar ~= nil and firstRadar:isExist() then - local distance = mist.utils.round(mist.utils.metersToNM(ewRadar:getDistanceInMetersToContact(firstRadar:getDCSRepresentation(), contact:getPosition().p)), 2) - env.info("CONTACT: "..contact:getName().." | TYPE: "..contact:getTypeName().." | DISTANCE NM: "..distance) - end - end - - env.info("---------------------------------------------------") - - end - -end - -function SkynetIADS:printDetailedSAMSiteStatus() - local samSites = self:getSAMSites() - - env.info("------------------------------------------ SAM STATUS: "..self:getCoalitionString().." -------------------------------") - for i = 1, #samSites do - local samSite = samSites[i] - local numConnectionNodes = #samSite:getConnectionNodes() - local numPowerSources = #samSite:getPowerSources() - local isAutonomous = samSite:getAutonomousState() - local isActive = samSite:isActive() - - local connectionNodes = samSite:getConnectionNodes() - local firstRadar = samSite:getRadars()[1] - local numDamagedConnectionNodes = 0 - for j = 1, #connectionNodes do - local connectionNode = connectionNodes[j] - if connectionNode:isExist() == false then - numDamagedConnectionNodes = numDamagedConnectionNodes + 1 - end - end - local intactConnectionNodes = numConnectionNodes - numDamagedConnectionNodes - - local powerSources = samSite:getPowerSources() - local numDamagedPowerSources = 0 - for j = 1, #powerSources do - local powerSource = powerSources[j] - if powerSource:isExist() == false then - numDamagedPowerSources = numDamagedPowerSources + 1 - end - end - local intactPowerSources = numPowerSources - numDamagedPowerSources - - local detectedTargets = samSite:getDetectedTargets() - - local samSitesInCoveredArea = samSite:getChildRadars() - - env.info("GROUP: "..samSite:getDCSName().." | TYPE: "..samSite:getNatoName()) - env.info("ACTIVE: "..tostring(isActive).." | AUTONOMOUS: "..tostring(isAutonomous).." | IS ACTING AS EW: "..tostring(samSite:getActAsEW()).." | DETECTED TARGETS: "..#detectedTargets.." | DEFENDING HARM: "..tostring(samSite:isDefendingHARM()).." | MISSILES IN FLIGHT:"..tostring(samSite:getNumberOfMissilesInFlight())) - - if numConnectionNodes > 0 then - env.info("CONNECTION NODES: "..numConnectionNodes.." | DAMAGED: "..numDamagedConnectionNodes.." | INTACT: "..intactConnectionNodes) - else - env.info("NO CONNECTION NODES SET") - end - if numPowerSources > 0 then - env.info("POWER SOURCES : "..numPowerSources.." | DAMAGED:"..numDamagedPowerSources.." | INTACT: "..intactPowerSources) - else - env.info("NO POWER SOURCES SET") - end - - env.info("SAM SITES IN COVERED AREA: "..#samSitesInCoveredArea) - for j = 1, #samSitesInCoveredArea do - local samSiteCovered = samSitesInCoveredArea[j] - env.info(samSiteCovered:getDCSName()) - end - - for j = 1, #detectedTargets do - local contact = detectedTargets[j] - if firstRadar ~= nil and firstRadar:isExist() then - local distance = mist.utils.round(mist.utils.metersToNM(samSite:getDistanceInMetersToContact(firstRadar:getDCSRepresentation(), contact:getPosition().p)), 2) - env.info("CONTACT: "..contact:getName().." | TYPE: "..contact:getTypeName().." | DISTANCE NM: "..distance) - end - end - - env.info("---------------------------------------------------") - end -end - -function SkynetIADS:printSystemStatus() - - if self:getDebugSettings().IADSStatus or self:getDebugSettings().contacts then - local coalitionStr = self:getCoalitionString() - self:printOutput("---- IADS: "..coalitionStr.." ------") - end - - if self:getDebugSettings().IADSStatus then - - local numComCenters = #self:getCommandCenters() - local numDestroyedComCenters = 0 - local numComCentersNoPower = 0 - local numComCentersNoConnectionNode = 0 - local numIntactComCenters = 0 - for i = 1, #self.commandCenters do - local commandCenter = self.commandCenters[i] - if commandCenter:hasWorkingPowerSource() == false then - numComCentersNoPower = numComCentersNoPower + 1 - end - if commandCenter:hasActiveConnectionNode() == false then - numComCentersNoConnectionNode = numComCentersNoConnectionNode + 1 - end - if commandCenter:isDestroyed() == false then - numIntactComCenters = numIntactComCenters + 1 - end - end - - numDestroyedComCenters = numComCenters - numIntactComCenters - - - self:printOutput("COMMAND CENTERS: "..numComCenters.." | Destroyed: "..numDestroyedComCenters.." | NoPowr: "..numComCentersNoPower.." | NoCon: "..numComCentersNoConnectionNode) - - local ewNoPower = 0 - local ewTotal = #self:getEarlyWarningRadars() - local ewNoConnectionNode = 0 - local ewActive = 0 - local ewRadarsInactive = 0 - - for i = 1, #self.earlyWarningRadars do - local ewRadar = self.earlyWarningRadars[i] - if ewRadar:hasWorkingPowerSource() == false then - ewNoPower = ewNoPower + 1 - end - if ewRadar:hasActiveConnectionNode() == false then - ewNoConnectionNode = ewNoConnectionNode + 1 - end - if ewRadar:isActive() then - ewActive = ewActive + 1 - end - end - - ewRadarsInactive = ewTotal - ewActive - local numEWRadarsDestroyed = #self:getDestroyedEarlyWarningRadars() - self:printOutput("EW: "..ewTotal.." | On: "..ewActive.." | Off: "..ewRadarsInactive.." | Destroyed: "..numEWRadarsDestroyed.." | NoPowr: "..ewNoPower.." | NoCon: "..ewNoConnectionNode) - - local samSitesInactive = 0 - local samSitesActive = 0 - local samSitesTotal = #self:getSAMSites() - local samSitesNoPower = 0 - local samSitesNoConnectionNode = 0 - local samSitesOutOfAmmo = 0 - local samSiteAutonomous = 0 - local samSiteRadarDestroyed = 0 - for i = 1, #self.samSites do - local samSite = self.samSites[i] - if samSite:hasWorkingPowerSource() == false then - samSitesNoPower = samSitesNoPower + 1 - end - if samSite:hasActiveConnectionNode() == false then - samSitesNoConnectionNode = samSitesNoConnectionNode + 1 - end - if samSite:isActive() then - samSitesActive = samSitesActive + 1 - end - if samSite:hasRemainingAmmo() == false then - samSitesOutOfAmmo = samSitesOutOfAmmo + 1 - end - if samSite:getAutonomousState() == true then - samSiteAutonomous = samSiteAutonomous + 1 - end - if samSite:hasWorkingRadar() == false then - samSiteRadarDestroyed = samSiteRadarDestroyed + 1 - end - end - - samSitesInactive = samSitesTotal - samSitesActive - self:printOutput("SAM: "..samSitesTotal.." | On: "..samSitesActive.." | Off: "..samSitesInactive.." | Autonm: "..samSiteAutonomous.." | Raddest: "..samSiteRadarDestroyed.." | NoPowr: "..samSitesNoPower.." | NoCon: "..samSitesNoConnectionNode.." | NoAmmo: "..samSitesOutOfAmmo) - end - if self:getDebugSettings().contacts then - for i = 1, #self.contacts do - local contact = self.contacts[i] - self:printOutput("CONTACT: "..contact:getName().." | TYPE: "..contact:getTypeName().." | GS: "..tostring(contact:getGroundSpeedInKnots()).." | LAST SEEN: "..contact:getAge()) - end - end - - if self:getDebugSettings().earlyWarningRadarStatusEnvOutput then - self:printDetailedEarlyWarningRadarStatus() - end - - if self:getDebugSettings().samSiteStatusEnvOutput then - self:printDetailedSAMSiteStatus() - end -end - end do @@ -1537,7 +1788,7 @@ function SkynetIADSAbstractElement:getNatoName() end function SkynetIADSAbstractElement:getDescription() - return "IADS ELEMENT: "..self:getDCSName().." | Type : "..tostring(self:getNatoName()) + return "IADS ELEMENT: "..self:getDCSName().." | Type: "..tostring(self:getNatoName()) end function SkynetIADSAbstractElement:onEvent(event) @@ -1958,7 +2209,9 @@ function SkynetIADSAbstractRadarElement:getHARMDetectionChance() end function SkynetIADSAbstractRadarElement:setHARMDetectionChance(chance) - self.harmDetectionChance = chance + if chance and chance >= 0 and chance <= 100 then + self.harmDetectionChance = chance + end return self end @@ -1986,40 +2239,44 @@ function SkynetIADSAbstractRadarElement:setupElements() end end - local numElementsCreated = #self.searchRadars + #self.trackingRadars + #self.launchers --this check ensures a unit or group has all required elements for the specific sam or ew type: if (hasLauncher and hasSearchRadar and hasTrackingRadar and #self.launchers > 0 and #self.searchRadars > 0 and #self.trackingRadars > 0 ) - or (hasSearchRadar and hasLauncher and #self.searchRadars > 0 and #self.launchers > 0) - or (hasSearchRadar and hasLauncher == false and hasTrackingRadar == false and #self.searchRadars > 0 and numUnits == 1) then + or (hasSearchRadar and hasLauncher and #self.searchRadars > 0 and #self.launchers > 0) then local harmDetection = dataType['harm_detection_chance'] - if harmDetection then - self.harmDetectionChance = harmDetection - end + self:setHARMDetectionChance(harmDetection) local natoName = dataType['name']['NATO'] - --we shorten the SA-XX names and don't return their code names eg goa, gainful.. - local pos = natoName:find(" ") - local prefix = natoName:sub(1, 2) - if string.lower(prefix) == 'sa' and pos ~= nil then - self.natoName = natoName:sub(1, (pos-1)) - else - self.natoName = natoName - end + self:buildNatoName(natoName) break end end end +function SkynetIADSAbstractRadarElement:buildNatoName(natoName) + --we shorten the SA-XX names and don't return their code names eg goa, gainful.. + local pos = natoName:find(" ") + local prefix = natoName:sub(1, 2) + if string.lower(prefix) == 'sa' and pos ~= nil then + self.natoName = natoName:sub(1, (pos-1)) + else + self.natoName = natoName + end +end + function SkynetIADSAbstractRadarElement:analyseAndAddUnit(class, tableToAdd, unitData) local units = self:getUnitsToAnalyse() for i = 1, #units do local unit = units[i] - local unitTypeName = unit:getTypeName() - for unitName, unitPerformanceData in pairs(unitData) do - if unitName == unitTypeName then - samElement = class:create(unit) - samElement:setupRangeData() - table.insert(tableToAdd, samElement) - end + self:buildSingleUnit(unit, class, tableToAdd, unitData) + end +end + +function SkynetIADSAbstractRadarElement:buildSingleUnit(unit, class, tableToAdd, unitData) + local unitTypeName = unit:getTypeName() + for unitName, unitPerformanceData in pairs(unitData) do + if unitName == unitTypeName then + samElement = class:create(unit) + samElement:setupRangeData() + table.insert(tableToAdd, samElement) end end end @@ -2102,7 +2359,7 @@ function SkynetIADSAbstractRadarElement:goLive() end self:pointDefencesStopActingAsEW() if self.iads:getDebugSettings().radarWentLive then - self.iads:printOutput(self:getDescription().." going live") + self.iads:printOutputToLog("GOING LIVE: "..self:getDescription()) end self:scanForHarms() end @@ -2138,8 +2395,8 @@ function SkynetIADSAbstractRadarElement:goDark() end self.aiState = false self:stopScanningForHARMs() - if self.iads:getDebugSettings().samWentDark then - self.iads:printOutput(self:getDescription().." going dark") + if self.iads:getDebugSettings().radarWentDark then + self.iads:printOutputToLog("GOING DARK: "..self:getDescription()) end end end @@ -2234,17 +2491,17 @@ function SkynetIADSAbstractRadarElement:jam(successProbability) local controller = self:getController() local probability = math.random(1, 100) if self.iads:getDebugSettings().jammerProbability then - self.iads:printOutput("JAMMER: "..self:getDescription()..": Probability: "..successProbability) + self.iads:printOutputToLog("JAMMER: "..self:getDescription()..": Probability: "..successProbability) end if successProbability > probability then controller:setOption(AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD) if self.iads:getDebugSettings().jammerProbability then - self.iads:printOutput("JAMMER: "..self:getDescription()..": jammed, setting to weapon hold") + self.iads:printOutputToLog("JAMMER: "..self:getDescription()..": jammed, setting to weapon hold") end else controller:setOption(AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_FREE) if self.iads:getDebugSettings().jammerProbability then - self.iads:printOutput("Jammer: "..self:getDescription()..": jammed, setting to weapon free") + self.iads:printOutputToLog("JAMMER: "..self:getDescription()..": jammed, setting to weapon free") end end self.lastJammerUpdate = timer:getTime() @@ -2274,7 +2531,7 @@ function SkynetIADSAbstractRadarElement:goSilentToEvadeHARM(timeToImpact) --self.objectsIdentifiedAsHarms = {} local harmTime = self:getHarmShutDownTime() if self.iads:getDebugSettings().harmDefence then - self.iads:printOutput("HARM DEFENCE: "..self:getDCSName().." shutting down | FOR: "..harmTime.." seconds | TTI: "..timeToImpact) + self.iads:printOutputToLog("HARM DEFENCE SHUTTING DOWN: "..self:getDCSName().." | FOR: "..harmTime.." seconds | TTI: "..timeToImpact) end self.harmSilenceID = mist.scheduleFunction(SkynetIADSAbstractRadarElement.finishHarmDefence, {self}, timer.getTime() + harmTime, 1) self:goDark() @@ -2436,7 +2693,7 @@ function SkynetIADSAbstractRadarElement.evaluateIfTargetsContainHARMs(self) end if numDetections == 2 and shallReactToHarm == false then if self.iads:getDebugSettings().harmDefence then - self.iads:printOutput("HARM DEFENCE: "..self:getDCSName().." will not react") + self.iads:printOutputToLog("HARM DEFENCE NO REACTION: "..self:getDCSName()) end end end @@ -2510,7 +2767,7 @@ function SkynetIADSCommandCenter:create(commandCenter, iads) local instance = self:superClass():create(commandCenter, iads) setmetatable(instance, self) self.__index = self - instance.natoName = "Command Center" + instance.natoName = "COMMAND CENTER" return instance end @@ -2606,7 +2863,28 @@ function SkynetIADSEWRadar:create(radarUnit, iads) return instance end ---an Early Warning Radar has simplified check to detrmine if its autonomous or not +function SkynetIADSEWRadar:setupElements() + local unit = self:getDCSRepresentation() + local unitType = unit:getTypeName() + for typeName, dataType in pairs(SkynetIADS.database) do + for entry, unitData in pairs(dataType) do + if entry == 'searchRadar' then + self:buildSingleUnit(unit, SkynetIADSSAMSearchRadar, self.searchRadars, unitData) + if #self.searchRadars > 0 then + local harmDetection = dataType['harm_detection_chance'] + self:setHARMDetectionChance(harmDetection) + if unitData[unitType]['name'] then + local natoName = unitData[unitType]['name']['NATO'] + self:buildNatoName(natoName) + end + return + end + end + end + end +end + +--an Early Warning Radar has simplified check to determine if its autonomous or not function SkynetIADSEWRadar:setToCorrectAutonomousState() if self:hasActiveConnectionNode() and self:hasWorkingPowerSource() and self.iads:isCommandCenterUsable() then self:resetAutonomousState() @@ -2897,7 +3175,7 @@ function SkynetIADSSamSite:targetCycleUpdateStart() end function SkynetIADSSamSite:targetCycleUpdateEnd() - if self.targetsInRange == false and self.actAsEW == false then + if self.targetsInRange == false and self.actAsEW == false and self:getAutonomousState() == false and self:getAutonomousBehaviour() == SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DCS_AI then self:goDark() end end diff --git a/resources/plugins/skynetiads/skynetiads-config.lua b/resources/plugins/skynetiads/skynetiads-config.lua index 8a384c96..fafa8104 100644 --- a/resources/plugins/skynetiads/skynetiads-config.lua +++ b/resources/plugins/skynetiads/skynetiads-config.lua @@ -12,108 +12,122 @@ env.info("DCSLiberation|Skynet-IADS plugin - configuration") if dcsLiberation and SkynetIADS then -- specific options - local createRedIADS = false - local createBlueIADS = false - local includeRedInRadio = false - local includeBlueInRadio = false - local debugRED = false - local debugBLUE = false - + local createRedIADS = false + local createBlueIADS = false + local includeRedInRadio = false + local includeBlueInRadio = false + local debugRED = false + local debugBLUE = false + -- retrieve specific options values if dcsLiberation.plugins then if dcsLiberation.plugins.skynetiads then - createRedIADS = dcsLiberation.plugins.skynetiads.createRedIADS - createBlueIADS = dcsLiberation.plugins.skynetiads.createBlueIADS - includeRedInRadio = dcsLiberation.plugins.skynetiads.includeRedInRadio - includeBlueInRadio = dcsLiberation.plugins.skynetiads.includeBlueInRadio - debugRED = dcsLiberation.plugins.skynetiads.debugRED - debugBLUE = dcsLiberation.plugins.skynetiads.debugBLUE - end + createRedIADS = dcsLiberation.plugins.skynetiads.createRedIADS + createBlueIADS = dcsLiberation.plugins.skynetiads.createBlueIADS + includeRedInRadio = dcsLiberation.plugins.skynetiads.includeRedInRadio + includeBlueInRadio = dcsLiberation.plugins.skynetiads.includeBlueInRadio + debugRED = dcsLiberation.plugins.skynetiads.debugRED + debugBLUE = dcsLiberation.plugins.skynetiads.debugBLUE + end end - - env.info(string.format("DCSLiberation|Skynet-IADS plugin - createRedIADS=%s",tostring(createRedIADS))) - env.info(string.format("DCSLiberation|Skynet-IADS plugin - createBlueIADS=%s",tostring(createBlueIADS))) - env.info(string.format("DCSLiberation|Skynet-IADS plugin - includeRedInRadio=%s",tostring(includeRedInRadio))) - env.info(string.format("DCSLiberation|Skynet-IADS plugin - includeBlueInRadio=%s",tostring(includeBlueInRadio))) - env.info(string.format("DCSLiberation|Skynet-IADS plugin - debugRED=%s",tostring(debugRED))) - env.info(string.format("DCSLiberation|Skynet-IADS plugin - debugBLUE=%s",tostring(debugBLUE))) - -- actual configuration code + env.info(string.format("DCSLiberation|Skynet-IADS plugin - createRedIADS=%s",tostring(createRedIADS))) + env.info(string.format("DCSLiberation|Skynet-IADS plugin - createBlueIADS=%s",tostring(createBlueIADS))) + env.info(string.format("DCSLiberation|Skynet-IADS plugin - includeRedInRadio=%s",tostring(includeRedInRadio))) + env.info(string.format("DCSLiberation|Skynet-IADS plugin - includeBlueInRadio=%s",tostring(includeBlueInRadio))) + env.info(string.format("DCSLiberation|Skynet-IADS plugin - debugRED=%s",tostring(debugRED))) + env.info(string.format("DCSLiberation|Skynet-IADS plugin - debugBLUE=%s",tostring(debugBLUE))) - local function initializeIADS(iads, coalition, inRadio, debug) + -- actual configuration code - local coalitionPrefix = "BLUE" - if coalition == 1 then - coalitionPrefix = "RED" - end + local function initializeIADS(iads, coalition, inRadio, debug) - if debug then - env.info("adding debug information") - local iadsDebug = iads:getDebugSettings() - iadsDebug.IADSStatus = true - iadsDebug.samWentDark = true - iadsDebug.contacts = true - iadsDebug.radarWentLive = true - iadsDebug.noWorkingCommmandCenter = true - iadsDebug.ewRadarNoConnection = true - iadsDebug.samNoConnection = true - iadsDebug.jammerProbability = true - iadsDebug.addedEWRadar = true - iadsDebug.hasNoPower = true - iadsDebug.harmDefence = true - iadsDebug.samSiteStatusEnvOutput = true - iadsDebug.earlyWarningRadarStatusEnvOutput = true - end + local coalitionPrefix = "BLUE" + if coalition == 1 then + coalitionPrefix = "RED" + end - --add EW units to the IADS: - iads:addEarlyWarningRadarsByPrefix(coalitionPrefix .. "|EWR|") + if debug then + env.info("adding debug information") + local iadsDebug = iads:getDebugSettings() + iadsDebug.IADSStatus = true + iadsDebug.samWentDark = true + iadsDebug.contacts = true + iadsDebug.radarWentLive = true + iadsDebug.noWorkingCommmandCenter = true + iadsDebug.ewRadarNoConnection = true + iadsDebug.samNoConnection = true + iadsDebug.jammerProbability = true + iadsDebug.addedEWRadar = true + iadsDebug.hasNoPower = true + iadsDebug.harmDefence = true + iadsDebug.samSiteStatusEnvOutput = true + iadsDebug.earlyWarningRadarStatusEnvOutput = true + end - --add SAM groups to the IADS: - iads:addSAMSitesByPrefix(coalitionPrefix .. "|SAM|") + --add EW units to the IADS: + iads:addEarlyWarningRadarsByPrefix(coalitionPrefix .. "|EWR|") - -- add the AWACS - if dcsLiberation.AWACs then - for _, data in pairs(dcsLiberation.AWACs) do - env.info(string.format("DCSLiberation|Skynet-IADS plugin - processing AWACS %s", data.dcsGroupName)) - local group = Group.getByName(data.dcsGroupName) - if group then - if group:getCoalition() == coalition then - local unit = group:getUnit(1) - if unit then - local unitName = unit:getName() - env.info(string.format("DCSLiberation|Skynet-IADS plugin - adding AWACS %s", unitName)) - iads:addEarlyWarningRadar(unitName) - end - end - end - end - end + --add SAM groups to the IADS: + iads:addSAMSitesByPrefix(coalitionPrefix .. "|SAM|") - -- TODO: Add ships. + -- add the AWACS + if dcsLiberation.AWACs then + for _, data in pairs(dcsLiberation.AWACs) do + env.info(string.format("DCSLiberation|Skynet-IADS plugin - processing AWACS %s", data.dcsGroupName)) + local group = Group.getByName(data.dcsGroupName) + if group then + if group:getCoalition() == coalition then + local unit = group:getUnit(1) + if unit then + local unitName = unit:getName() + env.info(string.format("DCSLiberation|Skynet-IADS plugin - adding AWACS %s", unitName)) + iads:addEarlyWarningRadar(unitName) + end + end + end + end + end - if inRadio then - --activate the radio menu to toggle IADS Status output - env.info("DCSLiberation|Skynet-IADS plugin - adding in radio menu") - iads:addRadioMenu() - end + local sites = iads:getSAMSites() + for i = 1, #sites do + local site = sites[i] + local name = site:getDCSName() + if not string.match(name, "|PD") then + env.info(string.format("DCSLiberation|Skynet-IADS plugin - Checking %s for PD", name)) + local pds = iads:getSAMSitesByPrefix(name .. "|PD") + for j = 1, #pds do + pd = pds[j] + env.info(string.format("DCSLiberation|Skynet-IADS plugin - Adding %s as PD for %s", pd:getDCSName(), name)) + site:addPointDefence(pd) + site:setIgnoreHARMSWhilePointDefencesHaveAmmo(true) + end + end + end - --activate the IADS - iads:setupSAMSitesAndThenActivate() - end + if inRadio then + --activate the radio menu to toggle IADS Status output + env.info("DCSLiberation|Skynet-IADS plugin - adding in radio menu") + iads:addRadioMenu() + end - ------------------------------------------------------------------------------------------------------------------------------------------------------------ - -- create the IADS networks - ------------------------------------------------------------------------------------------------------------------------------------------------------------- - if createRedIADS then - env.info("DCSLiberation|Skynet-IADS plugin - creating red IADS") - redIADS = SkynetIADS:create("IADS") - initializeIADS(redIADS, 1, includeRedInRadio, debugRED) -- RED - end + --activate the IADS + iads:setupSAMSitesAndThenActivate() + end + + ------------------------------------------------------------------------------------------------------------------------------------------------------------ + -- create the IADS networks + ------------------------------------------------------------------------------------------------------------------------------------------------------------- + if createRedIADS then + env.info("DCSLiberation|Skynet-IADS plugin - creating red IADS") + redIADS = SkynetIADS:create("IADS") + initializeIADS(redIADS, 1, includeRedInRadio, debugRED) -- RED + end + + if createBlueIADS then + env.info("DCSLiberation|Skynet-IADS plugin - creating blue IADS") + blueIADS = SkynetIADS:create("IADS") + initializeIADS(blueIADS, 2, includeBlueInRadio, debugBLUE) -- BLUE + end - if createBlueIADS then - env.info("DCSLiberation|Skynet-IADS plugin - creating blue IADS") - blueIADS = SkynetIADS:create("IADS") - initializeIADS(blueIADS, 2, includeBlueInRadio, debugBLUE) -- BLUE - end - end \ No newline at end of file diff --git a/resources/stylesheets/style-dcs.css b/resources/stylesheets/style-dcs.css index f8539e83..ae829133 100644 --- a/resources/stylesheets/style-dcs.css +++ b/resources/stylesheets/style-dcs.css @@ -179,6 +179,21 @@ QPushButton[style="btn-sell"]:hover{ background:#D84545; } +/* Info button */ +QPushButton[style="btn-info"]{ + background-color:#329E9E; + color: white; + border-radius:2px; + font-weight:bold; + text-transform:lowercase; + margin: 0px; + padding: 2px; +} + +QPushButton[style="btn-info"]:hover{ + background:#45D8D8; +} + QPushButton[style="btn-danger"]{ @@ -300,6 +315,18 @@ QLabel[style="SEAD"]{ padding:2px 6px; } +QLabel[style="info-element"]{ + border: 1px solid #435466; + color:white; + padding:2px 6px; +} + +QTextBrowser[style="info-desc"]{ + border: 1px solid #435466; + color:white; + padding:2px 6px; +} + /*QGroupBox these are the sections that look like fieldsets*/ QGroupBox { margin-top: 1ex; /* leave space at the top for the title */ diff --git a/resources/syrialandmap.p b/resources/syrialandmap.p index 1b5cc168..f39fb3bb 100644 Binary files a/resources/syrialandmap.p and b/resources/syrialandmap.p differ diff --git a/resources/tools/generate_landmap.py b/resources/tools/generate_landmap.py index 1812ac4b..a23c7fd1 100644 --- a/resources/tools/generate_landmap.py +++ b/resources/tools/generate_landmap.py @@ -1,7 +1,36 @@ import pickle +from functools import singledispatch from dcs.mission import Mission -from shapely import geometry +from shapely.geometry import GeometryCollection, MultiPolygon, Polygon +from shapely.ops import unary_union + +from game.theater.landmap import Landmap + + +@singledispatch +def to_multipoly(obj) -> MultiPolygon: + raise NotImplementedError( + f"to_multipoly not implemented for {obj.__class__.__name__}") + + +@to_multipoly.register +def _poly_to_multipoly(obj: Polygon) -> MultiPolygon: + return MultiPolygon([obj]) + + +@to_multipoly.register +def _multipoly_to_multipoly(obj: MultiPolygon) -> MultiPolygon: + return obj + + +@to_multipoly.register +def _geometry_collection_to_multipoly(obj: GeometryCollection) -> MultiPolygon: + if obj.is_empty: + return MultiPolygon() + raise RuntimeError( + f"Not sure how to convert collection to multipoly: {obj.wkt}") + for terrain in ["cau", "nev", "syria", "channel", "normandy", "gulf"]: print("Terrain " + terrain) @@ -16,17 +45,22 @@ for terrain in ["cau", "nev", "syria", "channel", "normandy", "gulf"]: if terrain == "cau" and inclusion_zones: # legacy - exclusion_zones.append(geometry.Polygon(zone)) + exclusion_zones.append(Polygon(zone)) else: + poly = Polygon(zone) + if not poly.is_valid: + raise RuntimeError(f"{plane_group} is invalid") if plane_group.units[0].type == "F-15C": - exclusion_zones.append(geometry.Polygon(zone)) + exclusion_zones.append(poly) else: - inclusion_zones.append(geometry.Polygon(zone)) + inclusion_zones.append(poly) for ship_group in m.country("USA").ship_group: zone = [(x.position.x, x.position.y) for x in ship_group.points] - seas_zones.append(geometry.Polygon(zone)) + seas_zones.append(Polygon(zone)) with open("../{}landmap.p".format(terrain), "wb") as f: print(len(inclusion_zones), len(exclusion_zones), len(seas_zones)) - pickle.dump((inclusion_zones, exclusion_zones, seas_zones), f) + pickle.dump(Landmap(to_multipoly(unary_union(inclusion_zones)), + to_multipoly(unary_union(exclusion_zones)), + to_multipoly(unary_union(seas_zones))), f) diff --git a/resources/tools/groundobject_templates.miz b/resources/tools/groundobject_templates.miz index b4fe1437..da8d133c 100644 Binary files a/resources/tools/groundobject_templates.miz and b/resources/tools/groundobject_templates.miz differ diff --git a/resources/ui/ground_assets/nothreat.png b/resources/ui/ground_assets/nothreat.png new file mode 100644 index 00000000..175fa290 Binary files /dev/null and b/resources/ui/ground_assets/nothreat.png differ diff --git a/resources/ui/ground_assets/nothreat_blue.png b/resources/ui/ground_assets/nothreat_blue.png new file mode 100644 index 00000000..46f2532a Binary files /dev/null and b/resources/ui/ground_assets/nothreat_blue.png differ diff --git a/resources/ui/templates/campaign_performance_template_EN.j2 b/resources/ui/templates/campaign_performance_template_EN.j2 new file mode 100644 index 00000000..637f9ca7 --- /dev/null +++ b/resources/ui/templates/campaign_performance_template_EN.j2 @@ -0,0 +1,33 @@ +{% if performance == 0 %} +
+{% elif performance == 1 %} +
+{% elif performance == 2 %} +
+{% else %} +
+{%endif %} + +Performance impact :  {{performance}}/3 + +{% if performance == 0 %} +

+This scenario is rather performance friendly. +

+{% elif performance == 1 %} +

+This scenario requires a very good computer to run. +

+{% elif performance == 2 %} +

+This scenario is not performance friendly. The usage of the culling settings is recommended for most users. +

+{% elif performance == 3 %} +

+This theater is huge, and will generate very complex DCS missions with hundreds of units, pushing the limits of DCS engine. +An insanely powerful computer will be necessary to run it. +Usage of culling settings will probably be mandatory for decent FPS. +

+{%endif %} + +
\ No newline at end of file diff --git a/resources/ui/templates/campaign_performance_template_FR.j2 b/resources/ui/templates/campaign_performance_template_FR.j2 new file mode 100644 index 00000000..455d2648 --- /dev/null +++ b/resources/ui/templates/campaign_performance_template_FR.j2 @@ -0,0 +1,33 @@ +{% if performance == 0 %} +
+{% elif performance == 1 %} +
+{% elif performance == 2 %} +
+{% else %} +
+{%endif %} + +Impact sur les performances :  {{performance}}/3 + +{% if performance == 0 %} +

+Ce scénario est d'une taille limitée et pensé pour obtenir de bonnes performances en jeu. +

+{% elif performance == 1 %} +

+Ce scénario nécessite une machine puissante. +

+{% elif performance == 2 %} +

+Ce scénario n'est pas pensé pour la performance et requiert une machine très puissante. +L'usage des paramètres "culling" est recommandé pour la plupart des utilisateurs. +

+{% elif performance == 3 %} +

+Ce scénario est gigantesque, et générera des missions DCS d'une grande compléxité contenant des centaines/milliers d'unités. +Une machine extrêmement puissante est nécessaire, et l'utilisation des paramètres de "culling" sera obligatoire pour des performances décentes. +

+{%endif %} + +
\ No newline at end of file diff --git a/resources/ui/templates/campaigntemplate_EN.j2 b/resources/ui/templates/campaigntemplate_EN.j2 index 5d721dcc..2ec97f6e 100644 --- a/resources/ui/templates/campaigntemplate_EN.j2 +++ b/resources/ui/templates/campaigntemplate_EN.j2 @@ -1,3 +1,8 @@ Author(s): {{ campaign.authors }} +

+ +Default factions: + {{campaign.recommended_player_faction}} VS  {{campaign.recommended_enemy_faction}} +
{{ campaign.description|safe }} diff --git a/resources/ui/templates/campaigntemplate_FR.j2 b/resources/ui/templates/campaigntemplate_FR.j2 index e71616f7..5469160a 100644 --- a/resources/ui/templates/campaigntemplate_FR.j2 +++ b/resources/ui/templates/campaigntemplate_FR.j2 @@ -1,3 +1,7 @@ Auteur(s) : {{ campaign.authors }} +Factions par défaut : + {{campaign.recommended_player_faction}} VS  {{campaign.recommended_enemy_faction}} +
+ {{ campaign.description|safe }} diff --git a/resources/ui/units/aircrafts/banners/A-10A_24.jpg b/resources/ui/units/aircrafts/banners/A-10A_24.jpg new file mode 100644 index 00000000..d64e52a6 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/A-10A_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/A-10C_24.jpg b/resources/ui/units/aircrafts/banners/A-10C_24.jpg new file mode 100644 index 00000000..a605d02b Binary files /dev/null and b/resources/ui/units/aircrafts/banners/A-10C_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/A-10C_2_24.jpg b/resources/ui/units/aircrafts/banners/A-10C_2_24.jpg new file mode 100644 index 00000000..5e3a57b1 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/A-10C_2_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/A-20G_24.jpg b/resources/ui/units/aircrafts/banners/A-20G_24.jpg new file mode 100644 index 00000000..1850c2d3 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/A-20G_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/A-4E-C_24.jpg b/resources/ui/units/aircrafts/banners/A-4E-C_24.jpg new file mode 100644 index 00000000..5a9d34c6 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/A-4E-C_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/AH-1W_24.jpg b/resources/ui/units/aircrafts/banners/AH-1W_24.jpg new file mode 100644 index 00000000..90cbc755 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/AH-1W_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/AH-64A_24.jpg b/resources/ui/units/aircrafts/banners/AH-64A_24.jpg new file mode 100644 index 00000000..b7dc4b87 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/AH-64A_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/AH-64D_24.jpg b/resources/ui/units/aircrafts/banners/AH-64D_24.jpg new file mode 100644 index 00000000..76879f3f Binary files /dev/null and b/resources/ui/units/aircrafts/banners/AH-64D_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/AJS37_24.jpg b/resources/ui/units/aircrafts/banners/AJS37_24.jpg new file mode 100644 index 00000000..d3556b49 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/AJS37_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/AV8BNA_24.jpg b/resources/ui/units/aircrafts/banners/AV8BNA_24.jpg new file mode 100644 index 00000000..57d3f5e9 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/AV8BNA_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/B-17G_24.jpg b/resources/ui/units/aircrafts/banners/B-17G_24.jpg new file mode 100644 index 00000000..4ec81b53 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/B-17G_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/B-1B_24.jpg b/resources/ui/units/aircrafts/banners/B-1B_24.jpg new file mode 100644 index 00000000..9a889370 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/B-1B_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/B-52H_24.jpg b/resources/ui/units/aircrafts/banners/B-52H_24.jpg new file mode 100644 index 00000000..f9eb4232 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/B-52H_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/Bf-109K-4_24.jpg b/resources/ui/units/aircrafts/banners/Bf-109K-4_24.jpg new file mode 100644 index 00000000..e1dd8e57 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/Bf-109K-4_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/C-101CC_24.jpg b/resources/ui/units/aircrafts/banners/C-101CC_24.jpg new file mode 100644 index 00000000..f5309465 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/C-101CC_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/F-117A_24.jpg b/resources/ui/units/aircrafts/banners/F-117A_24.jpg new file mode 100644 index 00000000..ae16570d Binary files /dev/null and b/resources/ui/units/aircrafts/banners/F-117A_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/F-14A-135-GR_24.jpg b/resources/ui/units/aircrafts/banners/F-14A-135-GR_24.jpg new file mode 100644 index 00000000..3dc949bb Binary files /dev/null and b/resources/ui/units/aircrafts/banners/F-14A-135-GR_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/F-14B_24.jpg b/resources/ui/units/aircrafts/banners/F-14B_24.jpg new file mode 100644 index 00000000..be515b1f Binary files /dev/null and b/resources/ui/units/aircrafts/banners/F-14B_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/F-15C_24.jpg b/resources/ui/units/aircrafts/banners/F-15C_24.jpg new file mode 100644 index 00000000..b87d8e15 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/F-15C_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/F-15E_24.jpg b/resources/ui/units/aircrafts/banners/F-15E_24.jpg new file mode 100644 index 00000000..53d8b75c Binary files /dev/null and b/resources/ui/units/aircrafts/banners/F-15E_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/F-16C_50_24.jpg b/resources/ui/units/aircrafts/banners/F-16C_50_24.jpg new file mode 100644 index 00000000..13b0ce19 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/F-16C_50_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/F-4E_24.jpg b/resources/ui/units/aircrafts/banners/F-4E_24.jpg new file mode 100644 index 00000000..a880fcdc Binary files /dev/null and b/resources/ui/units/aircrafts/banners/F-4E_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/F-5E-3_24.jpg b/resources/ui/units/aircrafts/banners/F-5E-3_24.jpg new file mode 100644 index 00000000..75985e55 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/F-5E-3_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/F-86F_24.jpg b/resources/ui/units/aircrafts/banners/F-86F_24.jpg new file mode 100644 index 00000000..1fdd30bd Binary files /dev/null and b/resources/ui/units/aircrafts/banners/F-86F_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/FA-18C_hornet_24.jpg b/resources/ui/units/aircrafts/banners/FA-18C_hornet_24.jpg new file mode 100644 index 00000000..68e758a3 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/FA-18C_hornet_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/FW-190A8_24.jpg b/resources/ui/units/aircrafts/banners/FW-190A8_24.jpg new file mode 100644 index 00000000..e4ccec08 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/FW-190A8_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/FW-190D9_24.jpg b/resources/ui/units/aircrafts/banners/FW-190D9_24.jpg new file mode 100644 index 00000000..9265398c Binary files /dev/null and b/resources/ui/units/aircrafts/banners/FW-190D9_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/I-16_24.jpg b/resources/ui/units/aircrafts/banners/I-16_24.jpg new file mode 100644 index 00000000..a55b31b1 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/I-16_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/J-11A_24.jpg b/resources/ui/units/aircrafts/banners/J-11A_24.jpg new file mode 100644 index 00000000..6625129c Binary files /dev/null and b/resources/ui/units/aircrafts/banners/J-11A_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/JF-17_24.jpg b/resources/ui/units/aircrafts/banners/JF-17_24.jpg new file mode 100644 index 00000000..1359ddb3 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/JF-17_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/Ju-88A4_24.jpg b/resources/ui/units/aircrafts/banners/Ju-88A4_24.jpg new file mode 100644 index 00000000..00fe95a7 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/Ju-88A4_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/Ka-50_24.jpg b/resources/ui/units/aircrafts/banners/Ka-50_24.jpg new file mode 100644 index 00000000..94fee740 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/Ka-50_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/L-39ZA_24.jpg b/resources/ui/units/aircrafts/banners/L-39ZA_24.jpg new file mode 100644 index 00000000..605acb55 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/L-39ZA_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/M-2000C_24.jpg b/resources/ui/units/aircrafts/banners/M-2000C_24.jpg new file mode 100644 index 00000000..e380badc Binary files /dev/null and b/resources/ui/units/aircrafts/banners/M-2000C_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/Mi-24V_24.jpg b/resources/ui/units/aircrafts/banners/Mi-24V_24.jpg new file mode 100644 index 00000000..14b5dca1 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/Mi-24V_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/Mi-28N_24.jpg b/resources/ui/units/aircrafts/banners/Mi-28N_24.jpg new file mode 100644 index 00000000..30d57738 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/Mi-28N_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/Mi-8MT_24.jpg b/resources/ui/units/aircrafts/banners/Mi-8MT_24.jpg new file mode 100644 index 00000000..199c3241 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/Mi-8MT_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/MiG-15bis_24.jpg b/resources/ui/units/aircrafts/banners/MiG-15bis_24.jpg new file mode 100644 index 00000000..541a30e4 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/MiG-15bis_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/MiG-19P_24.jpg b/resources/ui/units/aircrafts/banners/MiG-19P_24.jpg new file mode 100644 index 00000000..4078f8cc Binary files /dev/null and b/resources/ui/units/aircrafts/banners/MiG-19P_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/MiG-21Bis_24.jpg b/resources/ui/units/aircrafts/banners/MiG-21Bis_24.jpg new file mode 100644 index 00000000..dc7ffce6 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/MiG-21Bis_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/MiG-23MLD_24.jpg b/resources/ui/units/aircrafts/banners/MiG-23MLD_24.jpg new file mode 100644 index 00000000..212393f4 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/MiG-23MLD_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/MiG-25PD_24.jpg b/resources/ui/units/aircrafts/banners/MiG-25PD_24.jpg new file mode 100644 index 00000000..67fb469a Binary files /dev/null and b/resources/ui/units/aircrafts/banners/MiG-25PD_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/MiG-27K_24.jpg b/resources/ui/units/aircrafts/banners/MiG-27K_24.jpg new file mode 100644 index 00000000..25312ccd Binary files /dev/null and b/resources/ui/units/aircrafts/banners/MiG-27K_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/MiG-29A_24.jpg b/resources/ui/units/aircrafts/banners/MiG-29A_24.jpg new file mode 100644 index 00000000..bac81243 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/MiG-29A_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/MiG-29G_24.jpg b/resources/ui/units/aircrafts/banners/MiG-29G_24.jpg new file mode 100644 index 00000000..13dc9010 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/MiG-29G_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/MiG-29S_24.jpg b/resources/ui/units/aircrafts/banners/MiG-29S_24.jpg new file mode 100644 index 00000000..0be534f6 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/MiG-29S_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/MiG-31_24.jpg b/resources/ui/units/aircrafts/banners/MiG-31_24.jpg new file mode 100644 index 00000000..0ce90765 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/MiG-31_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/Mirage 2000-5_24.jpg b/resources/ui/units/aircrafts/banners/Mirage 2000-5_24.jpg new file mode 100644 index 00000000..82d5a828 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/Mirage 2000-5_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/Missing_24.jpg b/resources/ui/units/aircrafts/banners/Missing_24.jpg new file mode 100644 index 00000000..b965ab5b Binary files /dev/null and b/resources/ui/units/aircrafts/banners/Missing_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/OH-58D_24.jpg b/resources/ui/units/aircrafts/banners/OH-58D_24.jpg new file mode 100644 index 00000000..b81d698b Binary files /dev/null and b/resources/ui/units/aircrafts/banners/OH-58D_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/P-47D-30_24.jpg b/resources/ui/units/aircrafts/banners/P-47D-30_24.jpg new file mode 100644 index 00000000..a5486de0 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/P-47D-30_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/P-47D-30bl1_24.jpg b/resources/ui/units/aircrafts/banners/P-47D-30bl1_24.jpg new file mode 100644 index 00000000..08a12287 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/P-47D-30bl1_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/P-47D-40_24.jpg b/resources/ui/units/aircrafts/banners/P-47D-40_24.jpg new file mode 100644 index 00000000..1591a658 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/P-47D-40_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/P-51D-30-NA_24.jpg b/resources/ui/units/aircrafts/banners/P-51D-30-NA_24.jpg new file mode 100644 index 00000000..c1bc72ac Binary files /dev/null and b/resources/ui/units/aircrafts/banners/P-51D-30-NA_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/P-51D_24.jpg b/resources/ui/units/aircrafts/banners/P-51D_24.jpg new file mode 100644 index 00000000..cbbba314 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/P-51D_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/S-3B_24.jpg b/resources/ui/units/aircrafts/banners/S-3B_24.jpg new file mode 100644 index 00000000..bd96ab36 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/S-3B_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/SA342L_24.jpg b/resources/ui/units/aircrafts/banners/SA342L_24.jpg new file mode 100644 index 00000000..da94a7f6 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/SA342L_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/SA342M_24.jpg b/resources/ui/units/aircrafts/banners/SA342M_24.jpg new file mode 100644 index 00000000..20cc99b4 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/SA342M_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/SA342Mistral_24.jpg b/resources/ui/units/aircrafts/banners/SA342Mistral_24.jpg new file mode 100644 index 00000000..97b353cf Binary files /dev/null and b/resources/ui/units/aircrafts/banners/SA342Mistral_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/SH-60B_24.jpg b/resources/ui/units/aircrafts/banners/SH-60B_24.jpg new file mode 100644 index 00000000..d661aeef Binary files /dev/null and b/resources/ui/units/aircrafts/banners/SH-60B_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/SpitfireLFMkIXCW_24.jpg b/resources/ui/units/aircrafts/banners/SpitfireLFMkIXCW_24.jpg new file mode 100644 index 00000000..aee809d4 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/SpitfireLFMkIXCW_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/SpitfireLFMkIX_24.jpg b/resources/ui/units/aircrafts/banners/SpitfireLFMkIX_24.jpg new file mode 100644 index 00000000..03a44f9e Binary files /dev/null and b/resources/ui/units/aircrafts/banners/SpitfireLFMkIX_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/Su-17M4_24.jpg b/resources/ui/units/aircrafts/banners/Su-17M4_24.jpg new file mode 100644 index 00000000..ded8d32f Binary files /dev/null and b/resources/ui/units/aircrafts/banners/Su-17M4_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/Su-24M_24.jpg b/resources/ui/units/aircrafts/banners/Su-24M_24.jpg new file mode 100644 index 00000000..9952c949 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/Su-24M_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/Su-25T_25.jpg b/resources/ui/units/aircrafts/banners/Su-25T_25.jpg new file mode 100644 index 00000000..fc783153 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/Su-25T_25.jpg differ diff --git a/resources/ui/units/aircrafts/banners/Su-25_25.jpg b/resources/ui/units/aircrafts/banners/Su-25_25.jpg new file mode 100644 index 00000000..249ab7e6 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/Su-25_25.jpg differ diff --git a/resources/ui/units/aircrafts/banners/Su-27_24.jpg b/resources/ui/units/aircrafts/banners/Su-27_24.jpg new file mode 100644 index 00000000..751af137 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/Su-27_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/Su-30_24.jpg b/resources/ui/units/aircrafts/banners/Su-30_24.jpg new file mode 100644 index 00000000..9441d321 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/Su-30_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/Su-33_24.jpg b/resources/ui/units/aircrafts/banners/Su-33_24.jpg new file mode 100644 index 00000000..fb098ec9 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/Su-33_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/Su-34_24.jpg b/resources/ui/units/aircrafts/banners/Su-34_24.jpg new file mode 100644 index 00000000..60bc9704 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/Su-34_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/Tornado GR4_24.jpg b/resources/ui/units/aircrafts/banners/Tornado GR4_24.jpg new file mode 100644 index 00000000..01ac443d Binary files /dev/null and b/resources/ui/units/aircrafts/banners/Tornado GR4_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/Tornado IDS_24.jpg b/resources/ui/units/aircrafts/banners/Tornado IDS_24.jpg new file mode 100644 index 00000000..38ba26f6 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/Tornado IDS_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/Tu-142_24.jpg b/resources/ui/units/aircrafts/banners/Tu-142_24.jpg new file mode 100644 index 00000000..3b7b87d1 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/Tu-142_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/Tu-160_24.jpg b/resources/ui/units/aircrafts/banners/Tu-160_24.jpg new file mode 100644 index 00000000..f8ceea42 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/Tu-160_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/Tu-22M3_24.jpg b/resources/ui/units/aircrafts/banners/Tu-22M3_24.jpg new file mode 100644 index 00000000..d947877a Binary files /dev/null and b/resources/ui/units/aircrafts/banners/Tu-22M3_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/Tu-95MS_24.jpg b/resources/ui/units/aircrafts/banners/Tu-95MS_24.jpg new file mode 100644 index 00000000..39230aed Binary files /dev/null and b/resources/ui/units/aircrafts/banners/Tu-95MS_24.jpg differ diff --git a/resources/ui/units/aircrafts/banners/UH-1H_24.jpg b/resources/ui/units/aircrafts/banners/UH-1H_24.jpg new file mode 100644 index 00000000..0b3508a7 Binary files /dev/null and b/resources/ui/units/aircrafts/banners/UH-1H_24.jpg differ diff --git a/resources/ui/units/aircrafts/A-10A_24.jpg b/resources/ui/units/aircrafts/icons/A-10A_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/A-10A_24.jpg rename to resources/ui/units/aircrafts/icons/A-10A_24.jpg diff --git a/resources/ui/units/aircrafts/A-10C_24.jpg b/resources/ui/units/aircrafts/icons/A-10C_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/A-10C_24.jpg rename to resources/ui/units/aircrafts/icons/A-10C_24.jpg diff --git a/resources/ui/units/aircrafts/A-20G_24.jpg b/resources/ui/units/aircrafts/icons/A-20G_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/A-20G_24.jpg rename to resources/ui/units/aircrafts/icons/A-20G_24.jpg diff --git a/resources/ui/units/aircrafts/A-4E-C_24.jpg b/resources/ui/units/aircrafts/icons/A-4E-C_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/A-4E-C_24.jpg rename to resources/ui/units/aircrafts/icons/A-4E-C_24.jpg diff --git a/resources/ui/units/aircrafts/A-50_24.jpg b/resources/ui/units/aircrafts/icons/A-50_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/A-50_24.jpg rename to resources/ui/units/aircrafts/icons/A-50_24.jpg diff --git a/resources/ui/units/aircrafts/AH-1W_24.jpg b/resources/ui/units/aircrafts/icons/AH-1W_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/AH-1W_24.jpg rename to resources/ui/units/aircrafts/icons/AH-1W_24.jpg diff --git a/resources/ui/units/aircrafts/AH-64A_24.jpg b/resources/ui/units/aircrafts/icons/AH-64A_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/AH-64A_24.jpg rename to resources/ui/units/aircrafts/icons/AH-64A_24.jpg diff --git a/resources/ui/units/aircrafts/AH-64D_24.jpg b/resources/ui/units/aircrafts/icons/AH-64D_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/AH-64D_24.jpg rename to resources/ui/units/aircrafts/icons/AH-64D_24.jpg diff --git a/resources/ui/units/aircrafts/AJS37_24.jpg b/resources/ui/units/aircrafts/icons/AJS37_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/AJS37_24.jpg rename to resources/ui/units/aircrafts/icons/AJS37_24.jpg diff --git a/resources/ui/units/aircrafts/AV8BNA_24.jpg b/resources/ui/units/aircrafts/icons/AV8BNA_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/AV8BNA_24.jpg rename to resources/ui/units/aircrafts/icons/AV8BNA_24.jpg diff --git a/resources/ui/units/aircrafts/An-26B_24.jpg b/resources/ui/units/aircrafts/icons/An-26B_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/An-26B_24.jpg rename to resources/ui/units/aircrafts/icons/An-26B_24.jpg diff --git a/resources/ui/units/aircrafts/An-30M_24.jpg b/resources/ui/units/aircrafts/icons/An-30M_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/An-30M_24.jpg rename to resources/ui/units/aircrafts/icons/An-30M_24.jpg diff --git a/resources/ui/units/aircrafts/B-17G_24.jpg b/resources/ui/units/aircrafts/icons/B-17G_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/B-17G_24.jpg rename to resources/ui/units/aircrafts/icons/B-17G_24.jpg diff --git a/resources/ui/units/aircrafts/B-1B_24.jpg b/resources/ui/units/aircrafts/icons/B-1B_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/B-1B_24.jpg rename to resources/ui/units/aircrafts/icons/B-1B_24.jpg diff --git a/resources/ui/units/aircrafts/B-52H_24.jpg b/resources/ui/units/aircrafts/icons/B-52H_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/B-52H_24.jpg rename to resources/ui/units/aircrafts/icons/B-52H_24.jpg diff --git a/resources/ui/units/aircrafts/Bf-109K-4_24.jpg b/resources/ui/units/aircrafts/icons/Bf-109K-4_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/Bf-109K-4_24.jpg rename to resources/ui/units/aircrafts/icons/Bf-109K-4_24.jpg diff --git a/resources/ui/units/aircrafts/C-101CC_24.jpg b/resources/ui/units/aircrafts/icons/C-101CC_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/C-101CC_24.jpg rename to resources/ui/units/aircrafts/icons/C-101CC_24.jpg diff --git a/resources/ui/units/aircrafts/C-130_24.jpg b/resources/ui/units/aircrafts/icons/C-130_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/C-130_24.jpg rename to resources/ui/units/aircrafts/icons/C-130_24.jpg diff --git a/resources/ui/units/aircrafts/CH-47D_24.jpg b/resources/ui/units/aircrafts/icons/CH-47D_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/CH-47D_24.jpg rename to resources/ui/units/aircrafts/icons/CH-47D_24.jpg diff --git a/resources/ui/units/aircrafts/CH-53E_24.jpg b/resources/ui/units/aircrafts/icons/CH-53E_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/CH-53E_24.jpg rename to resources/ui/units/aircrafts/icons/CH-53E_24.jpg diff --git a/resources/ui/units/aircrafts/Christen Eagle II_24.jpg b/resources/ui/units/aircrafts/icons/Christen Eagle II_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/Christen Eagle II_24.jpg rename to resources/ui/units/aircrafts/icons/Christen Eagle II_24.jpg diff --git a/resources/ui/units/aircrafts/E-2D_24.jpg b/resources/ui/units/aircrafts/icons/E-2D_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/E-2D_24.jpg rename to resources/ui/units/aircrafts/icons/E-2D_24.jpg diff --git a/resources/ui/units/aircrafts/E-3C_24.jpg b/resources/ui/units/aircrafts/icons/E-3C_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/E-3C_24.jpg rename to resources/ui/units/aircrafts/icons/E-3C_24.jpg diff --git a/resources/ui/units/aircrafts/F-117A_24.jpg b/resources/ui/units/aircrafts/icons/F-117A_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/F-117A_24.jpg rename to resources/ui/units/aircrafts/icons/F-117A_24.jpg diff --git a/resources/ui/units/aircrafts/F-14A-135-GR_24.jpg b/resources/ui/units/aircrafts/icons/F-14A-135-GR_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/F-14A-135-GR_24.jpg rename to resources/ui/units/aircrafts/icons/F-14A-135-GR_24.jpg diff --git a/resources/ui/units/aircrafts/F-14A_24.jpg b/resources/ui/units/aircrafts/icons/F-14A_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/F-14A_24.jpg rename to resources/ui/units/aircrafts/icons/F-14A_24.jpg diff --git a/resources/ui/units/aircrafts/F-14B_24.jpg b/resources/ui/units/aircrafts/icons/F-14B_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/F-14B_24.jpg rename to resources/ui/units/aircrafts/icons/F-14B_24.jpg diff --git a/resources/ui/units/aircrafts/F-15C_24.jpg b/resources/ui/units/aircrafts/icons/F-15C_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/F-15C_24.jpg rename to resources/ui/units/aircrafts/icons/F-15C_24.jpg diff --git a/resources/ui/units/aircrafts/F-15E_24.jpg b/resources/ui/units/aircrafts/icons/F-15E_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/F-15E_24.jpg rename to resources/ui/units/aircrafts/icons/F-15E_24.jpg diff --git a/resources/ui/units/aircrafts/F-16C_24.jpg b/resources/ui/units/aircrafts/icons/F-16C_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/F-16C_24.jpg rename to resources/ui/units/aircrafts/icons/F-16C_24.jpg diff --git a/resources/ui/units/aircrafts/F-4E_24.jpg b/resources/ui/units/aircrafts/icons/F-4E_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/F-4E_24.jpg rename to resources/ui/units/aircrafts/icons/F-4E_24.jpg diff --git a/resources/ui/units/aircrafts/F-5E-3_24.jpg b/resources/ui/units/aircrafts/icons/F-5E-3_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/F-5E-3_24.jpg rename to resources/ui/units/aircrafts/icons/F-5E-3_24.jpg diff --git a/resources/ui/units/aircrafts/F-86F_24.jpg b/resources/ui/units/aircrafts/icons/F-86F_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/F-86F_24.jpg rename to resources/ui/units/aircrafts/icons/F-86F_24.jpg diff --git a/resources/ui/units/aircrafts/FA-18C_24.jpg b/resources/ui/units/aircrafts/icons/FA-18C_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/FA-18C_24.jpg rename to resources/ui/units/aircrafts/icons/FA-18C_24.jpg diff --git a/resources/ui/units/aircrafts/FW-190A8_24.jpg b/resources/ui/units/aircrafts/icons/FW-190A8_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/FW-190A8_24.jpg rename to resources/ui/units/aircrafts/icons/FW-190A8_24.jpg diff --git a/resources/ui/units/aircrafts/FW-190D9_24.jpg b/resources/ui/units/aircrafts/icons/FW-190D9_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/FW-190D9_24.jpg rename to resources/ui/units/aircrafts/icons/FW-190D9_24.jpg diff --git a/resources/ui/units/aircrafts/Hawk T.1A_24.jpg b/resources/ui/units/aircrafts/icons/Hawk T.1A_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/Hawk T.1A_24.jpg rename to resources/ui/units/aircrafts/icons/Hawk T.1A_24.jpg diff --git a/resources/ui/units/aircrafts/I-16_24.jpg b/resources/ui/units/aircrafts/icons/I-16_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/I-16_24.jpg rename to resources/ui/units/aircrafts/icons/I-16_24.jpg diff --git a/resources/ui/units/aircrafts/IL-76MD_24.jpg b/resources/ui/units/aircrafts/icons/IL-76MD_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/IL-76MD_24.jpg rename to resources/ui/units/aircrafts/icons/IL-76MD_24.jpg diff --git a/resources/ui/units/aircrafts/IL-78M_24.jpg b/resources/ui/units/aircrafts/icons/IL-78M_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/IL-78M_24.jpg rename to resources/ui/units/aircrafts/icons/IL-78M_24.jpg diff --git a/resources/ui/units/aircrafts/J-11A_24.jpg b/resources/ui/units/aircrafts/icons/J-11A_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/J-11A_24.jpg rename to resources/ui/units/aircrafts/icons/J-11A_24.jpg diff --git a/resources/ui/units/aircrafts/JF-17_24.jpg b/resources/ui/units/aircrafts/icons/JF-17_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/JF-17_24.jpg rename to resources/ui/units/aircrafts/icons/JF-17_24.jpg diff --git a/resources/ui/units/aircrafts/Ju-88-A4_24.jpg b/resources/ui/units/aircrafts/icons/Ju-88-A4_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/Ju-88-A4_24.jpg rename to resources/ui/units/aircrafts/icons/Ju-88-A4_24.jpg diff --git a/resources/ui/units/aircrafts/KC-135_24.jpg b/resources/ui/units/aircrafts/icons/KC-135_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/KC-135_24.jpg rename to resources/ui/units/aircrafts/icons/KC-135_24.jpg diff --git a/resources/ui/units/aircrafts/KJ-2000_24.jpg b/resources/ui/units/aircrafts/icons/KJ-2000_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/KJ-2000_24.jpg rename to resources/ui/units/aircrafts/icons/KJ-2000_24.jpg diff --git a/resources/ui/units/aircrafts/Ka-27_24.jpg b/resources/ui/units/aircrafts/icons/Ka-27_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/Ka-27_24.jpg rename to resources/ui/units/aircrafts/icons/Ka-27_24.jpg diff --git a/resources/ui/units/aircrafts/Ka-50_24.jpg b/resources/ui/units/aircrafts/icons/Ka-50_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/Ka-50_24.jpg rename to resources/ui/units/aircrafts/icons/Ka-50_24.jpg diff --git a/resources/ui/units/aircrafts/L-39C_24.jpg b/resources/ui/units/aircrafts/icons/L-39C_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/L-39C_24.jpg rename to resources/ui/units/aircrafts/icons/L-39C_24.jpg diff --git a/resources/ui/units/aircrafts/L-39ZA_24.jpg b/resources/ui/units/aircrafts/icons/L-39ZA_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/L-39ZA_24.jpg rename to resources/ui/units/aircrafts/icons/L-39ZA_24.jpg diff --git a/resources/ui/units/aircrafts/M-2000C_24.jpg b/resources/ui/units/aircrafts/icons/M-2000C_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/M-2000C_24.jpg rename to resources/ui/units/aircrafts/icons/M-2000C_24.jpg diff --git a/resources/ui/units/aircrafts/MB-339A PAN_24.jpg b/resources/ui/units/aircrafts/icons/MB-339A PAN_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/MB-339A PAN_24.jpg rename to resources/ui/units/aircrafts/icons/MB-339A PAN_24.jpg diff --git a/resources/ui/units/aircrafts/MB-339PAN_24.jpg b/resources/ui/units/aircrafts/icons/MB-339PAN_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/MB-339PAN_24.jpg rename to resources/ui/units/aircrafts/icons/MB-339PAN_24.jpg diff --git a/resources/ui/units/aircrafts/MQ RQ-1_24.jpg b/resources/ui/units/aircrafts/icons/MQ RQ-1_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/MQ RQ-1_24.jpg rename to resources/ui/units/aircrafts/icons/MQ RQ-1_24.jpg diff --git a/resources/ui/units/aircrafts/MQ-1A Predator_24.jpg b/resources/ui/units/aircrafts/icons/MQ-1A Predator_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/MQ-1A Predator_24.jpg rename to resources/ui/units/aircrafts/icons/MQ-1A Predator_24.jpg diff --git a/resources/ui/units/aircrafts/MQ-9 Reaper_24.jpg b/resources/ui/units/aircrafts/icons/MQ-9 Reaper_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/MQ-9 Reaper_24.jpg rename to resources/ui/units/aircrafts/icons/MQ-9 Reaper_24.jpg diff --git a/resources/ui/units/aircrafts/Mi-24_24.jpg b/resources/ui/units/aircrafts/icons/Mi-24_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/Mi-24_24.jpg rename to resources/ui/units/aircrafts/icons/Mi-24_24.jpg diff --git a/resources/ui/units/aircrafts/Mi-26_24.jpg b/resources/ui/units/aircrafts/icons/Mi-26_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/Mi-26_24.jpg rename to resources/ui/units/aircrafts/icons/Mi-26_24.jpg diff --git a/resources/ui/units/aircrafts/Mi-28N_24.jpg b/resources/ui/units/aircrafts/icons/Mi-28N_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/Mi-28N_24.jpg rename to resources/ui/units/aircrafts/icons/Mi-28N_24.jpg diff --git a/resources/ui/units/aircrafts/Mi-8MT_24.jpg b/resources/ui/units/aircrafts/icons/Mi-8MT_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/Mi-8MT_24.jpg rename to resources/ui/units/aircrafts/icons/Mi-8MT_24.jpg diff --git a/resources/ui/units/aircrafts/MiG-15bis_24.jpg b/resources/ui/units/aircrafts/icons/MiG-15bis_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/MiG-15bis_24.jpg rename to resources/ui/units/aircrafts/icons/MiG-15bis_24.jpg diff --git a/resources/ui/units/aircrafts/MiG-19P_24.jpg b/resources/ui/units/aircrafts/icons/MiG-19P_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/MiG-19P_24.jpg rename to resources/ui/units/aircrafts/icons/MiG-19P_24.jpg diff --git a/resources/ui/units/aircrafts/MiG-21Bis_24.jpg b/resources/ui/units/aircrafts/icons/MiG-21Bis_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/MiG-21Bis_24.jpg rename to resources/ui/units/aircrafts/icons/MiG-21Bis_24.jpg diff --git a/resources/ui/units/aircrafts/MiG-23MLD_24.jpg b/resources/ui/units/aircrafts/icons/MiG-23MLD_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/MiG-23MLD_24.jpg rename to resources/ui/units/aircrafts/icons/MiG-23MLD_24.jpg diff --git a/resources/ui/units/aircrafts/MiG-25PD_24.jpg b/resources/ui/units/aircrafts/icons/MiG-25PD_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/MiG-25PD_24.jpg rename to resources/ui/units/aircrafts/icons/MiG-25PD_24.jpg diff --git a/resources/ui/units/aircrafts/MiG-25RBT_24.jpg b/resources/ui/units/aircrafts/icons/MiG-25RBT_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/MiG-25RBT_24.jpg rename to resources/ui/units/aircrafts/icons/MiG-25RBT_24.jpg diff --git a/resources/ui/units/aircrafts/MiG-27K_24.jpg b/resources/ui/units/aircrafts/icons/MiG-27K_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/MiG-27K_24.jpg rename to resources/ui/units/aircrafts/icons/MiG-27K_24.jpg diff --git a/resources/ui/units/aircrafts/MiG-29A_24.jpg b/resources/ui/units/aircrafts/icons/MiG-29A_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/MiG-29A_24.jpg rename to resources/ui/units/aircrafts/icons/MiG-29A_24.jpg diff --git a/resources/ui/units/aircrafts/MiG-29S_24.jpg b/resources/ui/units/aircrafts/icons/MiG-29S_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/MiG-29S_24.jpg rename to resources/ui/units/aircrafts/icons/MiG-29S_24.jpg diff --git a/resources/ui/units/aircrafts/MiG-31_24.jpg b/resources/ui/units/aircrafts/icons/MiG-31_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/MiG-31_24.jpg rename to resources/ui/units/aircrafts/icons/MiG-31_24.jpg diff --git a/resources/ui/units/aircrafts/Mirage 2000-5_24.jpg b/resources/ui/units/aircrafts/icons/Mirage 2000-5_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/Mirage 2000-5_24.jpg rename to resources/ui/units/aircrafts/icons/Mirage 2000-5_24.jpg diff --git a/resources/ui/units/aircrafts/OH-58D_24.jpg b/resources/ui/units/aircrafts/icons/OH-58D_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/OH-58D_24.jpg rename to resources/ui/units/aircrafts/icons/OH-58D_24.jpg diff --git a/resources/ui/units/aircrafts/P-47D-30_24.jpg b/resources/ui/units/aircrafts/icons/P-47D-30_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/P-47D-30_24.jpg rename to resources/ui/units/aircrafts/icons/P-47D-30_24.jpg diff --git a/resources/ui/units/aircrafts/P-47D-30bl1_24.jpg b/resources/ui/units/aircrafts/icons/P-47D-30bl1_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/P-47D-30bl1_24.jpg rename to resources/ui/units/aircrafts/icons/P-47D-30bl1_24.jpg diff --git a/resources/ui/units/aircrafts/P-47D-40_24.jpg b/resources/ui/units/aircrafts/icons/P-47D-40_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/P-47D-40_24.jpg rename to resources/ui/units/aircrafts/icons/P-47D-40_24.jpg diff --git a/resources/ui/units/aircrafts/P-51D-30-NA_24.jpg b/resources/ui/units/aircrafts/icons/P-51D-30-NA_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/P-51D-30-NA_24.jpg rename to resources/ui/units/aircrafts/icons/P-51D-30-NA_24.jpg diff --git a/resources/ui/units/aircrafts/P-51D_24.jpg b/resources/ui/units/aircrafts/icons/P-51D_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/P-51D_24.jpg rename to resources/ui/units/aircrafts/icons/P-51D_24.jpg diff --git a/resources/ui/units/aircrafts/Rafale_A_S_24.jpg b/resources/ui/units/aircrafts/icons/Rafale_A_S_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/Rafale_A_S_24.jpg rename to resources/ui/units/aircrafts/icons/Rafale_A_S_24.jpg diff --git a/resources/ui/units/aircrafts/Rafale_M_24.jpg b/resources/ui/units/aircrafts/icons/Rafale_M_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/Rafale_M_24.jpg rename to resources/ui/units/aircrafts/icons/Rafale_M_24.jpg diff --git a/resources/ui/units/aircrafts/S-3B_24.jpg b/resources/ui/units/aircrafts/icons/S-3B_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/S-3B_24.jpg rename to resources/ui/units/aircrafts/icons/S-3B_24.jpg diff --git a/resources/ui/units/aircrafts/SA342L_24.jpg b/resources/ui/units/aircrafts/icons/SA342L_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/SA342L_24.jpg rename to resources/ui/units/aircrafts/icons/SA342L_24.jpg diff --git a/resources/ui/units/aircrafts/SA342M_24.jpg b/resources/ui/units/aircrafts/icons/SA342M_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/SA342M_24.jpg rename to resources/ui/units/aircrafts/icons/SA342M_24.jpg diff --git a/resources/ui/units/aircrafts/SA342Minigun_24.jpg b/resources/ui/units/aircrafts/icons/SA342Minigun_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/SA342Minigun_24.jpg rename to resources/ui/units/aircrafts/icons/SA342Minigun_24.jpg diff --git a/resources/ui/units/aircrafts/SA342Mistral_24.jpg b/resources/ui/units/aircrafts/icons/SA342Mistral_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/SA342Mistral_24.jpg rename to resources/ui/units/aircrafts/icons/SA342Mistral_24.jpg diff --git a/resources/ui/units/aircrafts/SH-60B_24.jpg b/resources/ui/units/aircrafts/icons/SH-60B_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/SH-60B_24.jpg rename to resources/ui/units/aircrafts/icons/SH-60B_24.jpg diff --git a/resources/ui/units/aircrafts/SpitfireLFMkIXCW_24.jpg b/resources/ui/units/aircrafts/icons/SpitfireLFMkIXCW_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/SpitfireLFMkIXCW_24.jpg rename to resources/ui/units/aircrafts/icons/SpitfireLFMkIXCW_24.jpg diff --git a/resources/ui/units/aircrafts/SpitfireLFMkIX_24.jpg b/resources/ui/units/aircrafts/icons/SpitfireLFMkIX_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/SpitfireLFMkIX_24.jpg rename to resources/ui/units/aircrafts/icons/SpitfireLFMkIX_24.jpg diff --git a/resources/ui/units/aircrafts/Su-17M4_24.jpg b/resources/ui/units/aircrafts/icons/Su-17M4_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/Su-17M4_24.jpg rename to resources/ui/units/aircrafts/icons/Su-17M4_24.jpg diff --git a/resources/ui/units/aircrafts/Su-24MR_24.jpg b/resources/ui/units/aircrafts/icons/Su-24MR_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/Su-24MR_24.jpg rename to resources/ui/units/aircrafts/icons/Su-24MR_24.jpg diff --git a/resources/ui/units/aircrafts/Su-24M_24.jpg b/resources/ui/units/aircrafts/icons/Su-24M_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/Su-24M_24.jpg rename to resources/ui/units/aircrafts/icons/Su-24M_24.jpg diff --git a/resources/ui/units/aircrafts/Su-25TM_24.jpg b/resources/ui/units/aircrafts/icons/Su-25TM_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/Su-25TM_24.jpg rename to resources/ui/units/aircrafts/icons/Su-25TM_24.jpg diff --git a/resources/ui/units/aircrafts/Su-25T_24.jpg b/resources/ui/units/aircrafts/icons/Su-25T_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/Su-25T_24.jpg rename to resources/ui/units/aircrafts/icons/Su-25T_24.jpg diff --git a/resources/ui/units/aircrafts/Su-25_24.jpg b/resources/ui/units/aircrafts/icons/Su-25_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/Su-25_24.jpg rename to resources/ui/units/aircrafts/icons/Su-25_24.jpg diff --git a/resources/ui/units/aircrafts/Su-27_24.jpg b/resources/ui/units/aircrafts/icons/Su-27_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/Su-27_24.jpg rename to resources/ui/units/aircrafts/icons/Su-27_24.jpg diff --git a/resources/ui/units/aircrafts/Su-30_24.jpg b/resources/ui/units/aircrafts/icons/Su-30_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/Su-30_24.jpg rename to resources/ui/units/aircrafts/icons/Su-30_24.jpg diff --git a/resources/ui/units/aircrafts/Su-33_24.jpg b/resources/ui/units/aircrafts/icons/Su-33_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/Su-33_24.jpg rename to resources/ui/units/aircrafts/icons/Su-33_24.jpg diff --git a/resources/ui/units/aircrafts/Su-34_24.jpg b/resources/ui/units/aircrafts/icons/Su-34_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/Su-34_24.jpg rename to resources/ui/units/aircrafts/icons/Su-34_24.jpg diff --git a/resources/ui/units/aircrafts/Su-57_24.jpg b/resources/ui/units/aircrafts/icons/Su-57_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/Su-57_24.jpg rename to resources/ui/units/aircrafts/icons/Su-57_24.jpg diff --git a/resources/ui/units/aircrafts/Tornado_24.jpg b/resources/ui/units/aircrafts/icons/Tornado_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/Tornado_24.jpg rename to resources/ui/units/aircrafts/icons/Tornado_24.jpg diff --git a/resources/ui/units/aircrafts/Tu-142M_24.jpg b/resources/ui/units/aircrafts/icons/Tu-142M_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/Tu-142M_24.jpg rename to resources/ui/units/aircrafts/icons/Tu-142M_24.jpg diff --git a/resources/ui/units/aircrafts/Tu-160_24.jpg b/resources/ui/units/aircrafts/icons/Tu-160_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/Tu-160_24.jpg rename to resources/ui/units/aircrafts/icons/Tu-160_24.jpg diff --git a/resources/ui/units/aircrafts/Tu-22M3_24.jpg b/resources/ui/units/aircrafts/icons/Tu-22M3_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/Tu-22M3_24.jpg rename to resources/ui/units/aircrafts/icons/Tu-22M3_24.jpg diff --git a/resources/ui/units/aircrafts/Tu-95_24.jpg b/resources/ui/units/aircrafts/icons/Tu-95_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/Tu-95_24.jpg rename to resources/ui/units/aircrafts/icons/Tu-95_24.jpg diff --git a/resources/ui/units/aircrafts/UH-1H_24.jpg b/resources/ui/units/aircrafts/icons/UH-1H_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/UH-1H_24.jpg rename to resources/ui/units/aircrafts/icons/UH-1H_24.jpg diff --git a/resources/ui/units/aircrafts/UH-60A_24.jpg b/resources/ui/units/aircrafts/icons/UH-60A_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/UH-60A_24.jpg rename to resources/ui/units/aircrafts/icons/UH-60A_24.jpg diff --git a/resources/ui/units/aircrafts/WingLoong-I_24.jpg b/resources/ui/units/aircrafts/icons/WingLoong-I_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/WingLoong-I_24.jpg rename to resources/ui/units/aircrafts/icons/WingLoong-I_24.jpg diff --git a/resources/ui/units/aircrafts/Yak-40_24.jpg b/resources/ui/units/aircrafts/icons/Yak-40_24.jpg similarity index 100% rename from resources/ui/units/aircrafts/Yak-40_24.jpg rename to resources/ui/units/aircrafts/icons/Yak-40_24.jpg diff --git a/resources/ui/units/vehicles/banners/BMP-1_24.jpg b/resources/ui/units/vehicles/banners/BMP-1_24.jpg new file mode 100644 index 00000000..18beb605 Binary files /dev/null and b/resources/ui/units/vehicles/banners/BMP-1_24.jpg differ diff --git a/resources/ui/units/vehicles/banners/Smerch_HE_24.jpg b/resources/ui/units/vehicles/banners/Smerch_HE_24.jpg new file mode 100644 index 00000000..7d0fbb88 Binary files /dev/null and b/resources/ui/units/vehicles/banners/Smerch_HE_24.jpg differ diff --git a/resources/ui/units/vehicles/banners/Uragan_BM-27_24.jpg b/resources/ui/units/vehicles/banners/Uragan_BM-27_24.jpg new file mode 100644 index 00000000..becf020b Binary files /dev/null and b/resources/ui/units/vehicles/banners/Uragan_BM-27_24.jpg differ diff --git a/resources/ui/units/vehicles/2S6 Tunguska_24.jpg b/resources/ui/units/vehicles/icons/2S6 Tunguska_24.jpg similarity index 100% rename from resources/ui/units/vehicles/2S6 Tunguska_24.jpg rename to resources/ui/units/vehicles/icons/2S6 Tunguska_24.jpg diff --git a/resources/ui/units/vehicles/5p73 s-125 ln_24.jpg b/resources/ui/units/vehicles/icons/5p73 s-125 ln_24.jpg similarity index 100% rename from resources/ui/units/vehicles/5p73 s-125 ln_24.jpg rename to resources/ui/units/vehicles/icons/5p73 s-125 ln_24.jpg diff --git a/resources/ui/units/vehicles/AAV-7_24.jpg b/resources/ui/units/vehicles/icons/AAV-7_24.jpg similarity index 100% rename from resources/ui/units/vehicles/AAV-7_24.jpg rename to resources/ui/units/vehicles/icons/AAV-7_24.jpg diff --git a/resources/ui/units/vehicles/BMD-1_24.jpg b/resources/ui/units/vehicles/icons/BMD-1_24.jpg similarity index 100% rename from resources/ui/units/vehicles/BMD-1_24.jpg rename to resources/ui/units/vehicles/icons/BMD-1_24.jpg diff --git a/resources/ui/units/vehicles/BMP-1_24.jpg b/resources/ui/units/vehicles/icons/BMP-1_24.jpg similarity index 100% rename from resources/ui/units/vehicles/BMP-1_24.jpg rename to resources/ui/units/vehicles/icons/BMP-1_24.jpg diff --git a/resources/ui/units/vehicles/BMP-2_24.jpg b/resources/ui/units/vehicles/icons/BMP-2_24.jpg similarity index 100% rename from resources/ui/units/vehicles/BMP-2_24.jpg rename to resources/ui/units/vehicles/icons/BMP-2_24.jpg diff --git a/resources/ui/units/vehicles/BMP-3_24.jpg b/resources/ui/units/vehicles/icons/BMP-3_24.jpg similarity index 100% rename from resources/ui/units/vehicles/BMP-3_24.jpg rename to resources/ui/units/vehicles/icons/BMP-3_24.jpg diff --git a/resources/ui/units/vehicles/BRDM-2_24.jpg b/resources/ui/units/vehicles/icons/BRDM-2_24.jpg similarity index 100% rename from resources/ui/units/vehicles/BRDM-2_24.jpg rename to resources/ui/units/vehicles/icons/BRDM-2_24.jpg diff --git a/resources/ui/units/vehicles/BTR-80_24.jpg b/resources/ui/units/vehicles/icons/BTR-80_24.jpg similarity index 100% rename from resources/ui/units/vehicles/BTR-80_24.jpg rename to resources/ui/units/vehicles/icons/BTR-80_24.jpg diff --git a/resources/ui/units/vehicles/BTR-82A_24.jpg b/resources/ui/units/vehicles/icons/BTR-82A_24.jpg similarity index 100% rename from resources/ui/units/vehicles/BTR-82A_24.jpg rename to resources/ui/units/vehicles/icons/BTR-82A_24.jpg diff --git a/resources/ui/units/vehicles/BTR-RD_24.jpg b/resources/ui/units/vehicles/icons/BTR-RD_24.jpg similarity index 100% rename from resources/ui/units/vehicles/BTR-RD_24.jpg rename to resources/ui/units/vehicles/icons/BTR-RD_24.jpg diff --git a/resources/ui/units/vehicles/Bedford_MWD_24.jpg b/resources/ui/units/vehicles/icons/Bedford_MWD_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Bedford_MWD_24.jpg rename to resources/ui/units/vehicles/icons/Bedford_MWD_24.jpg diff --git a/resources/ui/units/vehicles/Blitz_36-6700A_24.jpg b/resources/ui/units/vehicles/icons/Blitz_36-6700A_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Blitz_36-6700A_24.jpg rename to resources/ui/units/vehicles/icons/Blitz_36-6700A_24.jpg diff --git a/resources/ui/units/vehicles/CCKW 353_24.jpg b/resources/ui/units/vehicles/icons/CCKW 353_24.jpg similarity index 100% rename from resources/ui/units/vehicles/CCKW 353_24.jpg rename to resources/ui/units/vehicles/icons/CCKW 353_24.jpg diff --git a/resources/ui/units/vehicles/Centaur_IV_24.jpg b/resources/ui/units/vehicles/icons/Centaur_IV_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Centaur_IV_24.jpg rename to resources/ui/units/vehicles/icons/Centaur_IV_24.jpg diff --git a/resources/ui/units/vehicles/Challenger 2_24.jpg b/resources/ui/units/vehicles/icons/Challenger 2_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Challenger 2_24.jpg rename to resources/ui/units/vehicles/icons/Challenger 2_24.jpg diff --git a/resources/ui/units/vehicles/Churchill_VII_24.jpg b/resources/ui/units/vehicles/icons/Churchill_VII_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Churchill_VII_24.jpg rename to resources/ui/units/vehicles/icons/Churchill_VII_24.jpg diff --git a/resources/ui/units/vehicles/Cobra_24.jpg b/resources/ui/units/vehicles/icons/Cobra_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Cobra_24.jpg rename to resources/ui/units/vehicles/icons/Cobra_24.jpg diff --git a/resources/ui/units/vehicles/Cromwell_IV_24.jpg b/resources/ui/units/vehicles/icons/Cromwell_IV_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Cromwell_IV_24.jpg rename to resources/ui/units/vehicles/icons/Cromwell_IV_24.jpg diff --git a/resources/ui/units/vehicles/Daimler_AC_24.jpg b/resources/ui/units/vehicles/icons/Daimler_AC_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Daimler_AC_24.jpg rename to resources/ui/units/vehicles/icons/Daimler_AC_24.jpg diff --git a/resources/ui/units/vehicles/Elefant_SdKfz_184_24.jpg b/resources/ui/units/vehicles/icons/Elefant_SdKfz_184_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Elefant_SdKfz_184_24.jpg rename to resources/ui/units/vehicles/icons/Elefant_SdKfz_184_24.jpg diff --git a/resources/ui/units/vehicles/Flakscheinwerfer_37_24.jpg b/resources/ui/units/vehicles/icons/Flakscheinwerfer_37_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Flakscheinwerfer_37_24.jpg rename to resources/ui/units/vehicles/icons/Flakscheinwerfer_37_24.jpg diff --git a/resources/ui/units/vehicles/FuMG-401_24.jpg b/resources/ui/units/vehicles/icons/FuMG-401_24.jpg similarity index 100% rename from resources/ui/units/vehicles/FuMG-401_24.jpg rename to resources/ui/units/vehicles/icons/FuMG-401_24.jpg diff --git a/resources/ui/units/vehicles/Gepard_24.jpg b/resources/ui/units/vehicles/icons/Gepard_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Gepard_24.jpg rename to resources/ui/units/vehicles/icons/Gepard_24.jpg diff --git a/resources/ui/units/vehicles/Grad-Ural.jpg b/resources/ui/units/vehicles/icons/Grad-Ural.jpg similarity index 100% rename from resources/ui/units/vehicles/Grad-Ural.jpg rename to resources/ui/units/vehicles/icons/Grad-Ural.jpg diff --git a/resources/ui/units/vehicles/HQ-7 Mobile Launcher_24.jpg b/resources/ui/units/vehicles/icons/HQ-7 Mobile Launcher_24.jpg similarity index 100% rename from resources/ui/units/vehicles/HQ-7 Mobile Launcher_24.jpg rename to resources/ui/units/vehicles/icons/HQ-7 Mobile Launcher_24.jpg diff --git a/resources/ui/units/vehicles/HQ-7 Mobile Radar_24.jpg b/resources/ui/units/vehicles/icons/HQ-7 Mobile Radar_24.jpg similarity index 100% rename from resources/ui/units/vehicles/HQ-7 Mobile Radar_24.jpg rename to resources/ui/units/vehicles/icons/HQ-7 Mobile Radar_24.jpg diff --git a/resources/ui/units/vehicles/Hawk ln_24.jpg b/resources/ui/units/vehicles/icons/Hawk ln_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Hawk ln_24.jpg rename to resources/ui/units/vehicles/icons/Hawk ln_24.jpg diff --git a/resources/ui/units/vehicles/Hawk sr_24.jpg b/resources/ui/units/vehicles/icons/Hawk sr_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Hawk sr_24.jpg rename to resources/ui/units/vehicles/icons/Hawk sr_24.jpg diff --git a/resources/ui/units/vehicles/Hawk tr_24.jpg b/resources/ui/units/vehicles/icons/Hawk tr_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Hawk tr_24.jpg rename to resources/ui/units/vehicles/icons/Hawk tr_24.jpg diff --git a/resources/ui/units/vehicles/JagdPz_IV_24.jpg b/resources/ui/units/vehicles/icons/JagdPz_IV_24.jpg similarity index 100% rename from resources/ui/units/vehicles/JagdPz_IV_24.jpg rename to resources/ui/units/vehicles/icons/JagdPz_IV_24.jpg diff --git a/resources/ui/units/vehicles/Jagdpanther_G1_24.jpg b/resources/ui/units/vehicles/icons/Jagdpanther_G1_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Jagdpanther_G1_24.jpg rename to resources/ui/units/vehicles/icons/Jagdpanther_G1_24.jpg diff --git a/resources/ui/units/vehicles/KAMAZ Truck_24.jpg b/resources/ui/units/vehicles/icons/KAMAZ Truck_24.jpg similarity index 100% rename from resources/ui/units/vehicles/KAMAZ Truck_24.jpg rename to resources/ui/units/vehicles/icons/KAMAZ Truck_24.jpg diff --git a/resources/ui/units/vehicles/KDO_Mod40_24.jpg b/resources/ui/units/vehicles/icons/KDO_Mod40_24.jpg similarity index 100% rename from resources/ui/units/vehicles/KDO_Mod40_24.jpg rename to resources/ui/units/vehicles/icons/KDO_Mod40_24.jpg diff --git a/resources/ui/units/vehicles/Kub 1S91 str_24.jpg b/resources/ui/units/vehicles/icons/Kub 1S91 str_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Kub 1S91 str_24.jpg rename to resources/ui/units/vehicles/icons/Kub 1S91 str_24.jpg diff --git a/resources/ui/units/vehicles/Kub 2P25 ln_24.jpg b/resources/ui/units/vehicles/icons/Kub 2P25 ln_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Kub 2P25 ln_24.jpg rename to resources/ui/units/vehicles/icons/Kub 2P25 ln_24.jpg diff --git a/resources/ui/units/vehicles/Kubelwagen_82_24.jpg b/resources/ui/units/vehicles/icons/Kubelwagen_82_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Kubelwagen_82_24.jpg rename to resources/ui/units/vehicles/icons/Kubelwagen_82_24.jpg diff --git a/resources/ui/units/vehicles/LAV-25_24.jpg b/resources/ui/units/vehicles/icons/LAV-25_24.jpg similarity index 100% rename from resources/ui/units/vehicles/LAV-25_24.jpg rename to resources/ui/units/vehicles/icons/LAV-25_24.jpg diff --git a/resources/ui/units/vehicles/Leclerc_24.jpg b/resources/ui/units/vehicles/icons/Leclerc_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Leclerc_24.jpg rename to resources/ui/units/vehicles/icons/Leclerc_24.jpg diff --git a/resources/ui/units/vehicles/Leopard 1A3_24.jpg b/resources/ui/units/vehicles/icons/Leopard 1A3_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Leopard 1A3_24.jpg rename to resources/ui/units/vehicles/icons/Leopard 1A3_24.jpg diff --git a/resources/ui/units/vehicles/Leopard-2_24.jpg b/resources/ui/units/vehicles/icons/Leopard-2_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Leopard-2_24.jpg rename to resources/ui/units/vehicles/icons/Leopard-2_24.jpg diff --git a/resources/ui/units/vehicles/M 818_24.jpg b/resources/ui/units/vehicles/icons/M 818_24.jpg similarity index 100% rename from resources/ui/units/vehicles/M 818_24.jpg rename to resources/ui/units/vehicles/icons/M 818_24.jpg diff --git a/resources/ui/units/vehicles/M-1 Abrams_24.jpg b/resources/ui/units/vehicles/icons/M-1 Abrams_24.jpg similarity index 100% rename from resources/ui/units/vehicles/M-1 Abrams_24.jpg rename to resources/ui/units/vehicles/icons/M-1 Abrams_24.jpg diff --git a/resources/ui/units/vehicles/M-113_24.jpg b/resources/ui/units/vehicles/icons/M-113_24.jpg similarity index 100% rename from resources/ui/units/vehicles/M-113_24.jpg rename to resources/ui/units/vehicles/icons/M-113_24.jpg diff --git a/resources/ui/units/vehicles/M-163 Vulcan_24.jpg b/resources/ui/units/vehicles/icons/M-163 Vulcan_24.jpg similarity index 100% rename from resources/ui/units/vehicles/M-163 Vulcan_24.jpg rename to resources/ui/units/vehicles/icons/M-163 Vulcan_24.jpg diff --git a/resources/ui/units/vehicles/M-2 Bradley_24.jpg b/resources/ui/units/vehicles/icons/M-2 Bradley_24.jpg similarity index 100% rename from resources/ui/units/vehicles/M-2 Bradley_24.jpg rename to resources/ui/units/vehicles/icons/M-2 Bradley_24.jpg diff --git a/resources/ui/units/vehicles/M-60_24.jpg b/resources/ui/units/vehicles/icons/M-60_24.jpg similarity index 100% rename from resources/ui/units/vehicles/M-60_24.jpg rename to resources/ui/units/vehicles/icons/M-60_24.jpg diff --git a/resources/ui/units/vehicles/M1043 HMMWV Armament_24.jpg b/resources/ui/units/vehicles/icons/M1043 HMMWV Armament_24.jpg similarity index 100% rename from resources/ui/units/vehicles/M1043 HMMWV Armament_24.jpg rename to resources/ui/units/vehicles/icons/M1043 HMMWV Armament_24.jpg diff --git a/resources/ui/units/vehicles/M1045 HMMWV TOW_24.jpg b/resources/ui/units/vehicles/icons/M1045 HMMWV TOW_24.jpg similarity index 100% rename from resources/ui/units/vehicles/M1045 HMMWV TOW_24.jpg rename to resources/ui/units/vehicles/icons/M1045 HMMWV TOW_24.jpg diff --git a/resources/ui/units/vehicles/M1097 Avenger_24.jpg b/resources/ui/units/vehicles/icons/M1097 Avenger_24.jpg similarity index 100% rename from resources/ui/units/vehicles/M1097 Avenger_24.jpg rename to resources/ui/units/vehicles/icons/M1097 Avenger_24.jpg diff --git a/resources/ui/units/vehicles/M109_24.jpg b/resources/ui/units/vehicles/icons/M109_24.jpg similarity index 100% rename from resources/ui/units/vehicles/M109_24.jpg rename to resources/ui/units/vehicles/icons/M109_24.jpg diff --git a/resources/ui/units/vehicles/M10_GMC_24.jpg b/resources/ui/units/vehicles/icons/M10_GMC_24.jpg similarity index 100% rename from resources/ui/units/vehicles/M10_GMC_24.jpg rename to resources/ui/units/vehicles/icons/M10_GMC_24.jpg diff --git a/resources/ui/units/vehicles/M1126 Stryker ICV_24.jpg b/resources/ui/units/vehicles/icons/M1126 Stryker ICV_24.jpg similarity index 100% rename from resources/ui/units/vehicles/M1126 Stryker ICV_24.jpg rename to resources/ui/units/vehicles/icons/M1126 Stryker ICV_24.jpg diff --git a/resources/ui/units/vehicles/M1128 Stryker MGS_24.jpg b/resources/ui/units/vehicles/icons/M1128 Stryker MGS_24.jpg similarity index 100% rename from resources/ui/units/vehicles/M1128 Stryker MGS_24.jpg rename to resources/ui/units/vehicles/icons/M1128 Stryker MGS_24.jpg diff --git a/resources/ui/units/vehicles/M1134 Stryker ATGM_24.jpg b/resources/ui/units/vehicles/icons/M1134 Stryker ATGM_24.jpg similarity index 100% rename from resources/ui/units/vehicles/M1134 Stryker ATGM_24.jpg rename to resources/ui/units/vehicles/icons/M1134 Stryker ATGM_24.jpg diff --git a/resources/ui/units/vehicles/M12_GMC_24.jpg b/resources/ui/units/vehicles/icons/M12_GMC_24.jpg similarity index 100% rename from resources/ui/units/vehicles/M12_GMC_24.jpg rename to resources/ui/units/vehicles/icons/M12_GMC_24.jpg diff --git a/resources/ui/units/vehicles/M1A2_24.jpg b/resources/ui/units/vehicles/icons/M1A2_24.jpg similarity index 100% rename from resources/ui/units/vehicles/M1A2_24.jpg rename to resources/ui/units/vehicles/icons/M1A2_24.jpg diff --git a/resources/ui/units/vehicles/M1_37mm_24.jpg b/resources/ui/units/vehicles/icons/M1_37mm_24.jpg similarity index 100% rename from resources/ui/units/vehicles/M1_37mm_24.jpg rename to resources/ui/units/vehicles/icons/M1_37mm_24.jpg diff --git a/resources/ui/units/vehicles/M2A1_halftrack_24.jpg b/resources/ui/units/vehicles/icons/M2A1_halftrack_24.jpg similarity index 100% rename from resources/ui/units/vehicles/M2A1_halftrack_24.jpg rename to resources/ui/units/vehicles/icons/M2A1_halftrack_24.jpg diff --git a/resources/ui/units/vehicles/M2A2_24.jpg b/resources/ui/units/vehicles/icons/M2A2_24.jpg similarity index 100% rename from resources/ui/units/vehicles/M2A2_24.jpg rename to resources/ui/units/vehicles/icons/M2A2_24.jpg diff --git a/resources/ui/units/vehicles/M30_CC_24.jpg b/resources/ui/units/vehicles/icons/M30_CC_24.jpg similarity index 100% rename from resources/ui/units/vehicles/M30_CC_24.jpg rename to resources/ui/units/vehicles/icons/M30_CC_24.jpg diff --git a/resources/ui/units/vehicles/M45_Quadmount_24.jpg b/resources/ui/units/vehicles/icons/M45_Quadmount_24.jpg similarity index 100% rename from resources/ui/units/vehicles/M45_Quadmount_24.jpg rename to resources/ui/units/vehicles/icons/M45_Quadmount_24.jpg diff --git a/resources/ui/units/vehicles/M48 Chaparral_24.jpg b/resources/ui/units/vehicles/icons/M48 Chaparral_24.jpg similarity index 100% rename from resources/ui/units/vehicles/M48 Chaparral_24.jpg rename to resources/ui/units/vehicles/icons/M48 Chaparral_24.jpg diff --git a/resources/ui/units/vehicles/M4A4_Sherman_FF_24.jpg b/resources/ui/units/vehicles/icons/M4A4_Sherman_FF_24.jpg similarity index 100% rename from resources/ui/units/vehicles/M4A4_Sherman_FF_24.jpg rename to resources/ui/units/vehicles/icons/M4A4_Sherman_FF_24.jpg diff --git a/resources/ui/units/vehicles/M4_Sherman_24.jpg b/resources/ui/units/vehicles/icons/M4_Sherman_24.jpg similarity index 100% rename from resources/ui/units/vehicles/M4_Sherman_24.jpg rename to resources/ui/units/vehicles/icons/M4_Sherman_24.jpg diff --git a/resources/ui/units/vehicles/M4_Tractor_24.jpg b/resources/ui/units/vehicles/icons/M4_Tractor_24.jpg similarity index 100% rename from resources/ui/units/vehicles/M4_Tractor_24.jpg rename to resources/ui/units/vehicles/icons/M4_Tractor_24.jpg diff --git a/resources/ui/units/vehicles/M6 Linebacker_24.jpg b/resources/ui/units/vehicles/icons/M6 Linebacker_24.jpg similarity index 100% rename from resources/ui/units/vehicles/M6 Linebacker_24.jpg rename to resources/ui/units/vehicles/icons/M6 Linebacker_24.jpg diff --git a/resources/ui/units/vehicles/M818_24.jpg b/resources/ui/units/vehicles/icons/M818_24.jpg similarity index 100% rename from resources/ui/units/vehicles/M818_24.jpg rename to resources/ui/units/vehicles/icons/M818_24.jpg diff --git a/resources/ui/units/vehicles/M8_Greyhound_24.jpg b/resources/ui/units/vehicles/icons/M8_Greyhound_24.jpg similarity index 100% rename from resources/ui/units/vehicles/M8_Greyhound_24.jpg rename to resources/ui/units/vehicles/icons/M8_Greyhound_24.jpg diff --git a/resources/ui/units/vehicles/MCV-80 Warrior_24.jpg b/resources/ui/units/vehicles/icons/MCV-80 Warrior_24.jpg similarity index 100% rename from resources/ui/units/vehicles/MCV-80 Warrior_24.jpg rename to resources/ui/units/vehicles/icons/MCV-80 Warrior_24.jpg diff --git a/resources/ui/units/vehicles/MTLB_24.jpg b/resources/ui/units/vehicles/icons/MTLB_24.jpg similarity index 100% rename from resources/ui/units/vehicles/MTLB_24.jpg rename to resources/ui/units/vehicles/icons/MTLB_24.jpg diff --git a/resources/ui/units/vehicles/Marder_24.jpg b/resources/ui/units/vehicles/icons/Marder_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Marder_24.jpg rename to resources/ui/units/vehicles/icons/Marder_24.jpg diff --git a/resources/ui/units/vehicles/Maschinensatz_33_24.jpg b/resources/ui/units/vehicles/icons/Maschinensatz_33_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Maschinensatz_33_24.jpg rename to resources/ui/units/vehicles/icons/Maschinensatz_33_24.jpg diff --git a/resources/ui/units/vehicles/Merkava Mk4_24.jpg b/resources/ui/units/vehicles/icons/Merkava Mk4_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Merkava Mk4_24.jpg rename to resources/ui/units/vehicles/icons/Merkava Mk4_24.jpg diff --git a/resources/ui/units/vehicles/Osa 9A33 ln_24.jpg b/resources/ui/units/vehicles/icons/Osa 9A33 ln_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Osa 9A33 ln_24.jpg rename to resources/ui/units/vehicles/icons/Osa 9A33 ln_24.jpg diff --git a/resources/ui/units/vehicles/Patriot AMG_24.jpg b/resources/ui/units/vehicles/icons/Patriot AMG_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Patriot AMG_24.jpg rename to resources/ui/units/vehicles/icons/Patriot AMG_24.jpg diff --git a/resources/ui/units/vehicles/Patriot ECS_24.jpg b/resources/ui/units/vehicles/icons/Patriot ECS_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Patriot ECS_24.jpg rename to resources/ui/units/vehicles/icons/Patriot ECS_24.jpg diff --git a/resources/ui/units/vehicles/Patriot EPP_24.jpg b/resources/ui/units/vehicles/icons/Patriot EPP_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Patriot EPP_24.jpg rename to resources/ui/units/vehicles/icons/Patriot EPP_24.jpg diff --git a/resources/ui/units/vehicles/Patriot ICC_24.jpg b/resources/ui/units/vehicles/icons/Patriot ICC_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Patriot ICC_24.jpg rename to resources/ui/units/vehicles/icons/Patriot ICC_24.jpg diff --git a/resources/ui/units/vehicles/Patriot ln_24.jpg b/resources/ui/units/vehicles/icons/Patriot ln_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Patriot ln_24.jpg rename to resources/ui/units/vehicles/icons/Patriot ln_24.jpg diff --git a/resources/ui/units/vehicles/Patriot str_24.jpg b/resources/ui/units/vehicles/icons/Patriot str_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Patriot str_24.jpg rename to resources/ui/units/vehicles/icons/Patriot str_24.jpg diff --git a/resources/ui/units/vehicles/Pz_IV_H_24.jpg b/resources/ui/units/vehicles/icons/Pz_IV_H_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Pz_IV_H_24.jpg rename to resources/ui/units/vehicles/icons/Pz_IV_H_24.jpg diff --git a/resources/ui/units/vehicles/Pz_V_Panther_G_24.jpg b/resources/ui/units/vehicles/icons/Pz_V_Panther_G_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Pz_V_Panther_G_24.jpg rename to resources/ui/units/vehicles/icons/Pz_V_Panther_G_24.jpg diff --git a/resources/ui/units/vehicles/QF_37_AA_24.jpg b/resources/ui/units/vehicles/icons/QF_37_AA_24.jpg similarity index 100% rename from resources/ui/units/vehicles/QF_37_AA_24.jpg rename to resources/ui/units/vehicles/icons/QF_37_AA_24.jpg diff --git a/resources/ui/units/vehicles/Roland ADS_24.jpg b/resources/ui/units/vehicles/icons/Roland ADS_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Roland ADS_24.jpg rename to resources/ui/units/vehicles/icons/Roland ADS_24.jpg diff --git a/resources/ui/units/vehicles/Roland Radar_24.jpg b/resources/ui/units/vehicles/icons/Roland Radar_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Roland Radar_24.jpg rename to resources/ui/units/vehicles/icons/Roland Radar_24.jpg diff --git a/resources/ui/units/vehicles/S-300PS 40B6M tr_24.jpg b/resources/ui/units/vehicles/icons/S-300PS 40B6M tr_24.jpg similarity index 100% rename from resources/ui/units/vehicles/S-300PS 40B6M tr_24.jpg rename to resources/ui/units/vehicles/icons/S-300PS 40B6M tr_24.jpg diff --git a/resources/ui/units/vehicles/S-300PS 40B6MD sr_24.jpg b/resources/ui/units/vehicles/icons/S-300PS 40B6MD sr_24.jpg similarity index 100% rename from resources/ui/units/vehicles/S-300PS 40B6MD sr_24.jpg rename to resources/ui/units/vehicles/icons/S-300PS 40B6MD sr_24.jpg diff --git a/resources/ui/units/vehicles/S-300PS 54K6 cp_24.jpg b/resources/ui/units/vehicles/icons/S-300PS 54K6 cp_24.jpg similarity index 100% rename from resources/ui/units/vehicles/S-300PS 54K6 cp_24.jpg rename to resources/ui/units/vehicles/icons/S-300PS 54K6 cp_24.jpg diff --git a/resources/ui/units/vehicles/S-300PS 5P85C ln_24.jpg b/resources/ui/units/vehicles/icons/S-300PS 5P85C ln_24.jpg similarity index 100% rename from resources/ui/units/vehicles/S-300PS 5P85C ln_24.jpg rename to resources/ui/units/vehicles/icons/S-300PS 5P85C ln_24.jpg diff --git a/resources/ui/units/vehicles/S-300PS 5P85D ln_24.jpg b/resources/ui/units/vehicles/icons/S-300PS 5P85D ln_24.jpg similarity index 100% rename from resources/ui/units/vehicles/S-300PS 5P85D ln_24.jpg rename to resources/ui/units/vehicles/icons/S-300PS 5P85D ln_24.jpg diff --git a/resources/ui/units/vehicles/S-300PS 64H6E sr_24.jpg b/resources/ui/units/vehicles/icons/S-300PS 64H6E sr_24.jpg similarity index 100% rename from resources/ui/units/vehicles/S-300PS 64H6E sr_24.jpg rename to resources/ui/units/vehicles/icons/S-300PS 64H6E sr_24.jpg diff --git a/resources/ui/units/vehicles/SAO 2-C9_24.jpg b/resources/ui/units/vehicles/icons/SAO 2-C9_24.jpg similarity index 100% rename from resources/ui/units/vehicles/SAO 2-C9_24.jpg rename to resources/ui/units/vehicles/icons/SAO 2-C9_24.jpg diff --git a/resources/ui/units/vehicles/SAU 2S3 Akatsia_24.jpg b/resources/ui/units/vehicles/icons/SAU 2S3 Akatsia_24.jpg similarity index 100% rename from resources/ui/units/vehicles/SAU 2S3 Akatsia_24.jpg rename to resources/ui/units/vehicles/icons/SAU 2S3 Akatsia_24.jpg diff --git a/resources/ui/units/vehicles/SAU Gvozdika_24.jpg b/resources/ui/units/vehicles/icons/SAU Gvozdika_24.jpg similarity index 100% rename from resources/ui/units/vehicles/SAU Gvozdika_24.jpg rename to resources/ui/units/vehicles/icons/SAU Gvozdika_24.jpg diff --git a/resources/ui/units/vehicles/SAU Msta_24.jpg b/resources/ui/units/vehicles/icons/SAU Msta_24.jpg similarity index 100% rename from resources/ui/units/vehicles/SAU Msta_24.jpg rename to resources/ui/units/vehicles/icons/SAU Msta_24.jpg diff --git a/resources/ui/units/vehicles/SNR_75V_24.jpg b/resources/ui/units/vehicles/icons/SNR_75V_24.jpg similarity index 100% rename from resources/ui/units/vehicles/SNR_75V_24.jpg rename to resources/ui/units/vehicles/icons/SNR_75V_24.jpg diff --git a/resources/ui/units/vehicles/S_75M_Volhov_24.jpg b/resources/ui/units/vehicles/icons/S_75M_Volhov_24.jpg similarity index 100% rename from resources/ui/units/vehicles/S_75M_Volhov_24.jpg rename to resources/ui/units/vehicles/icons/S_75M_Volhov_24.jpg diff --git a/resources/ui/units/vehicles/Sd_Kfz_234_2_Puma_24.jpg b/resources/ui/units/vehicles/icons/Sd_Kfz_234_2_Puma_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Sd_Kfz_234_2_Puma_24.jpg rename to resources/ui/units/vehicles/icons/Sd_Kfz_234_2_Puma_24.jpg diff --git a/resources/ui/units/vehicles/Sd_Kfz_251_24.jpg b/resources/ui/units/vehicles/icons/Sd_Kfz_251_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Sd_Kfz_251_24.jpg rename to resources/ui/units/vehicles/icons/Sd_Kfz_251_24.jpg diff --git a/resources/ui/units/vehicles/Sd_Kfz_2_24.jpg b/resources/ui/units/vehicles/icons/Sd_Kfz_2_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Sd_Kfz_2_24.jpg rename to resources/ui/units/vehicles/icons/Sd_Kfz_2_24.jpg diff --git a/resources/ui/units/vehicles/Sd_Kfz_7_24.jpg b/resources/ui/units/vehicles/icons/Sd_Kfz_7_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Sd_Kfz_7_24.jpg rename to resources/ui/units/vehicles/icons/Sd_Kfz_7_24.jpg diff --git a/resources/ui/units/vehicles/ShKH vz. 77 DANA_24.jpg b/resources/ui/units/vehicles/icons/ShKH vz. 77 DANA_24.jpg similarity index 100% rename from resources/ui/units/vehicles/ShKH vz. 77 DANA_24.jpg rename to resources/ui/units/vehicles/icons/ShKH vz. 77 DANA_24.jpg diff --git a/resources/ui/units/vehicles/Shilka ZSU-23-4_24.jpg b/resources/ui/units/vehicles/icons/Shilka ZSU-23-4_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Shilka ZSU-23-4_24.jpg rename to resources/ui/units/vehicles/icons/Shilka ZSU-23-4_24.jpg diff --git a/resources/ui/units/vehicles/Strela-10M3_24.jpg b/resources/ui/units/vehicles/icons/Strela-10M3_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Strela-10M3_24.jpg rename to resources/ui/units/vehicles/icons/Strela-10M3_24.jpg diff --git a/resources/ui/units/vehicles/Stug_III_24.jpg b/resources/ui/units/vehicles/icons/Stug_III_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Stug_III_24.jpg rename to resources/ui/units/vehicles/icons/Stug_III_24.jpg diff --git a/resources/ui/units/vehicles/Stug_IV_24.jpg b/resources/ui/units/vehicles/icons/Stug_IV_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Stug_IV_24.jpg rename to resources/ui/units/vehicles/icons/Stug_IV_24.jpg diff --git a/resources/ui/units/vehicles/Sturmpanzer IV_24.jpg b/resources/ui/units/vehicles/icons/Sturmpanzer IV_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Sturmpanzer IV_24.jpg rename to resources/ui/units/vehicles/icons/Sturmpanzer IV_24.jpg diff --git a/resources/ui/units/vehicles/T-55_24.jpg b/resources/ui/units/vehicles/icons/T-55_24.jpg similarity index 100% rename from resources/ui/units/vehicles/T-55_24.jpg rename to resources/ui/units/vehicles/icons/T-55_24.jpg diff --git a/resources/ui/units/vehicles/T-72B3_24.jpg b/resources/ui/units/vehicles/icons/T-72B3_24.jpg similarity index 100% rename from resources/ui/units/vehicles/T-72B3_24.jpg rename to resources/ui/units/vehicles/icons/T-72B3_24.jpg diff --git a/resources/ui/units/vehicles/T-72B_24.jpg b/resources/ui/units/vehicles/icons/T-72B_24.jpg similarity index 100% rename from resources/ui/units/vehicles/T-72B_24.jpg rename to resources/ui/units/vehicles/icons/T-72B_24.jpg diff --git a/resources/ui/units/vehicles/T-80UD_24.jpg b/resources/ui/units/vehicles/icons/T-80UD_24.jpg similarity index 100% rename from resources/ui/units/vehicles/T-80UD_24.jpg rename to resources/ui/units/vehicles/icons/T-80UD_24.jpg diff --git a/resources/ui/units/vehicles/T-80U_24.jpg b/resources/ui/units/vehicles/icons/T-80U_24.jpg similarity index 100% rename from resources/ui/units/vehicles/T-80U_24.jpg rename to resources/ui/units/vehicles/icons/T-80U_24.jpg diff --git a/resources/ui/units/vehicles/T-90_24.jpg b/resources/ui/units/vehicles/icons/T-90_24.jpg similarity index 100% rename from resources/ui/units/vehicles/T-90_24.jpg rename to resources/ui/units/vehicles/icons/T-90_24.jpg diff --git a/resources/ui/units/vehicles/TPZ_24.jpg b/resources/ui/units/vehicles/icons/TPZ_24.jpg similarity index 100% rename from resources/ui/units/vehicles/TPZ_24.jpg rename to resources/ui/units/vehicles/icons/TPZ_24.jpg diff --git a/resources/ui/units/vehicles/Tetrarch_24.jpg b/resources/ui/units/vehicles/icons/Tetrarch_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Tetrarch_24.jpg rename to resources/ui/units/vehicles/icons/Tetrarch_24.jpg diff --git a/resources/ui/units/vehicles/Tiger_II_H_24.jpg b/resources/ui/units/vehicles/icons/Tiger_II_H_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Tiger_II_H_24.jpg rename to resources/ui/units/vehicles/icons/Tiger_II_H_24.jpg diff --git a/resources/ui/units/vehicles/Tiger_I_24.jpg b/resources/ui/units/vehicles/icons/Tiger_I_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Tiger_I_24.jpg rename to resources/ui/units/vehicles/icons/Tiger_I_24.jpg diff --git a/resources/ui/units/vehicles/Tor 9A331_24.jpg b/resources/ui/units/vehicles/icons/Tor 9A331_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Tor 9A331_24.jpg rename to resources/ui/units/vehicles/icons/Tor 9A331_24.jpg diff --git a/resources/ui/units/vehicles/Tunguska 2C6M_24.jpg b/resources/ui/units/vehicles/icons/Tunguska 2C6M_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Tunguska 2C6M_24.jpg rename to resources/ui/units/vehicles/icons/Tunguska 2C6M_24.jpg diff --git a/resources/ui/units/vehicles/UAZ-469_24.jpg b/resources/ui/units/vehicles/icons/UAZ-469_24.jpg similarity index 100% rename from resources/ui/units/vehicles/UAZ-469_24.jpg rename to resources/ui/units/vehicles/icons/UAZ-469_24.jpg diff --git a/resources/ui/units/vehicles/Ural-375 ZU-23_24.jpg b/resources/ui/units/vehicles/icons/Ural-375 ZU-23_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Ural-375 ZU-23_24.jpg rename to resources/ui/units/vehicles/icons/Ural-375 ZU-23_24.jpg diff --git a/resources/ui/units/vehicles/Ural-375_24.jpg b/resources/ui/units/vehicles/icons/Ural-375_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Ural-375_24.jpg rename to resources/ui/units/vehicles/icons/Ural-375_24.jpg diff --git a/resources/ui/units/vehicles/Vulcan_24.jpg b/resources/ui/units/vehicles/icons/Vulcan_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Vulcan_24.jpg rename to resources/ui/units/vehicles/icons/Vulcan_24.jpg diff --git a/resources/ui/units/vehicles/Willys_MB_24.jpg b/resources/ui/units/vehicles/icons/Willys_MB_24.jpg similarity index 100% rename from resources/ui/units/vehicles/Willys_MB_24.jpg rename to resources/ui/units/vehicles/icons/Willys_MB_24.jpg diff --git a/resources/ui/units/vehicles/ZBD-04A_24.jpg b/resources/ui/units/vehicles/icons/ZBD-04A_24.jpg similarity index 100% rename from resources/ui/units/vehicles/ZBD-04A_24.jpg rename to resources/ui/units/vehicles/icons/ZBD-04A_24.jpg diff --git a/resources/ui/units/vehicles/ZSU-23-4 Shilka_24.jpg b/resources/ui/units/vehicles/icons/ZSU-23-4 Shilka_24.jpg similarity index 100% rename from resources/ui/units/vehicles/ZSU-23-4 Shilka_24.jpg rename to resources/ui/units/vehicles/icons/ZSU-23-4 Shilka_24.jpg diff --git a/resources/ui/units/vehicles/ZSU-57-2_24.jpg b/resources/ui/units/vehicles/icons/ZSU-57-2_24.jpg similarity index 100% rename from resources/ui/units/vehicles/ZSU-57-2_24.jpg rename to resources/ui/units/vehicles/icons/ZSU-57-2_24.jpg diff --git a/resources/ui/units/vehicles/ZU-23 Emplacement Closed_24.jpg b/resources/ui/units/vehicles/icons/ZU-23 Emplacement Closed_24.jpg similarity index 100% rename from resources/ui/units/vehicles/ZU-23 Emplacement Closed_24.jpg rename to resources/ui/units/vehicles/icons/ZU-23 Emplacement Closed_24.jpg diff --git a/resources/ui/units/vehicles/ZU-23_24.jpg b/resources/ui/units/vehicles/icons/ZU-23_24.jpg similarity index 100% rename from resources/ui/units/vehicles/ZU-23_24.jpg rename to resources/ui/units/vehicles/icons/ZU-23_24.jpg diff --git a/resources/ui/units/vehicles/bofors40_24.jpg b/resources/ui/units/vehicles/icons/bofors40_24.jpg similarity index 100% rename from resources/ui/units/vehicles/bofors40_24.jpg rename to resources/ui/units/vehicles/icons/bofors40_24.jpg diff --git a/resources/ui/units/vehicles/flak18_24.jpg b/resources/ui/units/vehicles/icons/flak18_24.jpg similarity index 100% rename from resources/ui/units/vehicles/flak18_24.jpg rename to resources/ui/units/vehicles/icons/flak18_24.jpg diff --git a/resources/ui/units/vehicles/flak30_24.jpg b/resources/ui/units/vehicles/icons/flak30_24.jpg similarity index 100% rename from resources/ui/units/vehicles/flak30_24.jpg rename to resources/ui/units/vehicles/icons/flak30_24.jpg diff --git a/resources/ui/units/vehicles/flak36_24.jpg b/resources/ui/units/vehicles/icons/flak36_24.jpg similarity index 100% rename from resources/ui/units/vehicles/flak36_24.jpg rename to resources/ui/units/vehicles/icons/flak36_24.jpg diff --git a/resources/ui/units/vehicles/flak37_24.jpg b/resources/ui/units/vehicles/icons/flak37_24.jpg similarity index 100% rename from resources/ui/units/vehicles/flak37_24.jpg rename to resources/ui/units/vehicles/icons/flak37_24.jpg diff --git a/resources/ui/units/vehicles/flak38_24.jpg b/resources/ui/units/vehicles/icons/flak38_24.jpg similarity index 100% rename from resources/ui/units/vehicles/flak38_24.jpg rename to resources/ui/units/vehicles/icons/flak38_24.jpg diff --git a/resources/ui/units/vehicles/flak41_24.jpg b/resources/ui/units/vehicles/icons/flak41_24.jpg similarity index 100% rename from resources/ui/units/vehicles/flak41_24.jpg rename to resources/ui/units/vehicles/icons/flak41_24.jpg diff --git a/resources/ui/units/vehicles/p-19 s-125 sr_24.jpg b/resources/ui/units/vehicles/icons/p-19 s-125 sr_24.jpg similarity index 100% rename from resources/ui/units/vehicles/p-19 s-125 sr_24.jpg rename to resources/ui/units/vehicles/icons/p-19 s-125 sr_24.jpg diff --git a/resources/ui/units/vehicles/snr s-125 tr_24.jpg b/resources/ui/units/vehicles/icons/snr s-125 tr_24.jpg similarity index 100% rename from resources/ui/units/vehicles/snr s-125 tr_24.jpg rename to resources/ui/units/vehicles/icons/snr s-125 tr_24.jpg diff --git a/resources/units/unit_info_text.json b/resources/units/unit_info_text.json new file mode 100644 index 00000000..d158ff0a --- /dev/null +++ b/resources/units/unit_info_text.json @@ -0,0 +1,1794 @@ +{ + "A-10A": [{ + "default": { + "name": "A-10A Thunderbolt II", + "text": "The A-10A Thunderbolt II, also known as the Warthog, is a 'flying gun'. The aircraft was used extensively during Operation Desert Storm, in support of NATO operations in response to the Kosovo crisis, in Operation Enduring Freedom in Afghanistan and in Operation Iraqi Freedom. The A-10A is a high-survivability and versatile aircraft, popular with pilots for the 'get home' effectiveness.The mission of the aircraft is ground attack against tanks, armored vehicles and installations, and close air support of ground forces. The Warthog is famous for its massive 30mm cannon, but it can also be armed with Maverick guided missiles and several types of bombs and rockets.", + "country-of-origin": "USA", + "manufacturer": "Fairchild Republic", + "role": "Close Air Support/Attack", + "year-of-variant-introduction": "1977" + } + }], + "A-10C": [{ + "default": { + "name": "A-10C Thunderbolt II (Suite 3)", + "country-of-origin": "USA", + "manufacturer": "Fairchild Republic", + "role": "Close Air Support/Attack", + "year-of-variant-introduction": "2005" + } + }], + "A-10C_2": [{ + "default": { + "name": "A-10C Thunderbolt II (Suite 7)", + "country-of-origin": "USA", + "manufacturer": "Fairchild Republic", + "role": "Close Air Support/Attack", + "year-of-variant-introduction": "2012" + } + }], + "A-20G": [{ + "default": { + "name": "A-20G Havoc", + "country-of-origin": "USA", + "manufacturer": "Douglas", + "role": "Medium Bomber/Attack", + "year-of-variant-introduction": "1943" + }, + "UK": { + "name": "Boston Mk.III" + } + }], + "A-4E-C": [{ + "default": { + "name": "A-4E Skyhawk", + "country-of-origin": "USA", + "manufacturer": "Douglas", + "role": "Carrier-based Attack/Light Fighter", + "year-of-variant-introduction": "1962" + } + }], + "AH_1W": [{ + "default": { + "name": "AH-1W SuperCobra", + "country-of-origin": "USA", + "manufacturer": "Bell", + "role": "Attack", + "year-of-variant-introduction": "1986" + }, + "Iran": { + "name": "AH-1J SeaCobra" + } + }], + "AH-64A": [{ + "default": { + "name": "AH-64A Apache", + "country-of-origin": "USA", + "manufacturer": "Boeing", + "role": "Attack", + "year-of-variant-introduction": "1986" + } + }], + "AH-64D": [{ + "default": { + "name": "AH-64D Apache Longbow", + "country-of-origin": "USA", + "manufacturer": "Boeing", + "role": "Attack", + "year-of-variant-introduction": "2003" + } + }], + "AJS37": [{ + "default": { + "name": "AJS-37 Viggen", + "text": "The AJS-37 Viggen is a Swedish double-delta supersonic attack aircraft from the late Cold War. It was the backbone of the Swedish Air Force during the Cold war, serving as the main attack and anti-ship platform. The AJS is the 90’s upgrade of this 70's era aircraft, adding several advanced weapons and systems functionalities. The aircraft was designed around the pilot, with an excellent man-machine interface, supporting the pilot through the smart use of autopilot systems, radar and HUD symbology in order to deliver the ordnance onto targets from treetop level with high speed attack runs.\n\nThe aircraft is armed with multiple weapon systems ranging from programmable stand-off weapons such as the RB-15F antiship missile to the BK90 Cluster munitions dispenser to various bombs, rockets and missiles for a wide range of target types. The aircraft can also carry gun pods and the Sidewinder series of infrared-guided missiles for air defence and self-protection purposes.", + "country-of-origin": "Sweden", + "manufacturer": "Saab", + "role": "Attack/Reconnaissance", + "year-of-variant-introduction": "1993" + } + }], + "AV8BNA": [{ + "default": { + "name": "AV-8B Harrier II Night Attack", + "text": "The AV-8B project was born in the early 1970's as an effort to address the operational inadequacies of the AV-8A first generation Harrier, aimed to dramatically improve the capabilities and performance of the early AV-8A's. The AV-8B made its maiden flight in November 1981 and entered service with the United States Marine Corps in January 1985. It later evolved into the AV-8B N/A (Night Attack) and AV-8B Harrier II Plus.\n\nFirst flight of a modified AV-8B in the night attack configuration was on June 26th, 1987. Deliveries to the USMC began in September of 1989 to VMA-214 at Yuma, Arizona. Follow-up units based out of Yuma received their Night Attack AV-8Bs by the end of 1992.The AV-8B N/A variant (originally known as the AV-8D) had its first operational development in 1984 and included use of the NAVFLIR (Navigation Forward-Looking Infrared camera, consisting of a GEC-Marconi FLIR system mounted in the nose) for night operations. Additionally, GEC Cat's Eyes night vision goggles were provided to the pilot as well as a revised cockpit with color MFDs, a wider field-of-view HUD display, a color CRT digital moving map, and a complete \"heads-down\" operation capability. The AV-8B N/A also sports four Tracor ALE-39 countermeasures dispensers along the top of the rear fuselage, in addition to two ALE-39 dispensers along the lower rear of the fuselage. The AV-8B N/A also fields an updated version of the Rolls-Royce Pegasus 11-61 (F402-RR-408) vectored-thrust turbofan engine.", + "country-of-origin": "USA/UK", + "manufacturer": "McDonnell Douglas", + "role": "V/STOL Attack", + "year-of-variant-introduction": "1989" + } + }], + "B-1B": [{ + "default": { + "name": "B-1B Lancer", + "country-of-origin": "USA", + "manufacturer": "Rockwell", + "role": "Supersonic Strategic Bomber", + "year-of-variant-introduction": "1986" + } + }], + "B-17G": [{ + "default": { + "name": "B-17G Flying Fortress", + "country-of-origin": "USA", + "manufacturer": "Boeing", + "role": "Heavy Bomber", + "year-of-variant-introduction": "1943" + }, + "UK": { + "name": "Fortress Mk.III" + } + }], + "B-52H": [{ + "default": { + "name": "B-52H Stratofortress", + "country-of-origin": "USA", + "manufacturer": "Boeing", + "role": "Strategic Bomber", + "year-of-variant-introduction": "1961" + } + }], + "Bf-109K-4": [{ + "default": { + "name": "Bf 109 K-4 Kurfürst", + "manufacturer": "Messerschmitt", + "role": "Fighter", + "year-of-variant-introduction": "1944" + } + }], + "C-101CC": [{ + "default": { + "name": "C-101CC Aviojet", + "text": "The C-101CC Aviojet, with its 7 hard-points and uprated engine, is a versatile light attack aircraft that has seen combat with the Honduras Air Force against drug traffickers. It is also in service with the Jordanian and Chilean air forces.", + "country-of-origin": "Spain", + "manufacturer": "CASA", + "role": "Light Attack", + "year-of-variant-introduction": "1980" + } + }], + "C-101EB": [{ + "default": { + "name": "C-101EB Aviojet", + "text": "The C-101EB Aviojet is the primary jet trainer and aerobatic display aircraft of the Spanish Air Force.", + "country-of-origin": "Spain", + "manufacturer": "CASA", + "role": "Trainer", + "year-of-variant-introduction": "1980" + } + }], + "F-4E": [{ + "default": { + "name": "F-4E Phantom II", + "country-of-origin": "USA", + "manufacturer": "McDonnell Douglas", + "role": "Fighter-Bomber", + "year-of-variant-introduction": "1968" + }, + "Germany": { + "name": "F-4F Phantom II" + }, + "Japan": { + "name": "F-4EJ Kai Phantom II" + }, + "UK": { + "name": "Phantom F.3" + } + }], + "F-5E-3": [{ + "default": { + "name": "F-5E Tiger II", + "text": "The F-5E was developed by Northrop Corporation in early 1970s. The light tactical fighter is an upgraded version based on previous F-5A developments. The F-5s' combat role encompasses air superiority, ground support, and ground attack. Given its mission flexibility, ease of operation, and low cost, the Tiger II has, and continues to serve, air forces across the globe.\n\nThe F-5Е is armed with two 20-mm М39-А3 cannons with 280 rounds per each cannon. The cannons are located in the nose section, forward of the cockpit. Special deflectors are used to avoid compressor stall conditions caused by hot gas ingestion as a bi-product of operating the M-39-A3. Each cannon is capable of firing at a rate of 1500 to 1700 rounds per minute.\n\nEach wingtip incorporates a launcher rail capable of firing AIM-9 infrared-guided missiles.\n\nFive hard points (one centerline pylon and four underwing pylons) allow the aircraft to carry different types of air-to-ground weapons (bombs, cluster munitions, and rockets) 6,400 pounds (about 3000 kg) in total. In addition, illumination ammunition and cargo containers can be attached. To increase flight duration and range, external fuel tanks can be attached to three hard points (a centerline pylon and two inboard pylons). Maneuverability and speed can be maximized in combat by jettisoning all external stores.", + "country-of-origin": "USA", + "manufacturer": "Northrop", + "role": "Light Fighter", + "year-of-variant-introduction": "1975" + } + }], + "F-14A-135-GR": [{ + "default": { + "name": "F-14A Tomcat (Block 135-GR Late)", + "text": "The Grumman F-14 Tomcat is a two-crew, variable wing-geometry, maritime air superiority fighter that served with the US Navy for 32 years and continues to serve with the IRIAF in Iran. The F-14 was the US Navy's frontline fighter from the 1970s to the mid-2000s. Over the course of its long service it also became one of the US Navy’s premier precision ground-attack platform and its lone airborne reconnaissance asset.\n\nNoteworthy features of the Tomcat are its swing-wing configuration, two-man crew, and the powerful AN/AWG-9 Weapons Control System (WCS) and radar. The AWG-9 allows employment of the long-range AIM-54 Phoenix air-to-air missile, and the LANTIRN targeting pod allows precision ground strikes using laser-guided bombs. The F-14 Tomcat was present in several historic events that include the two Gulf of Sidra incidents, Operations Desert Storm Iraqi Freedom, the Yugoslavian conflict, and Operation Enduring Freedom over Afghanistan. It was also immortalized in the iconic motion picture, Top Gun, and starred in several other feature films including The Final Countdown, Executive Decision, and others.\n\nThe Tomcat was also played a vital role in the Iran-Iraq war of the 1980s, where is flew for the Islamic Republic of Iran Air Force.", + "country-of-origin": "USA", + "manufacturer": "Grumman", + "role": "Carrier-based Air-Superiority Fighter/Fighter Bomber", + "year-of-variant-introduction": "1984" + } + }], + "F-14B": [{ + "default": { + "name": "F-14B Tomcat", + "text": "The Grumman F-14 Tomcat is a two-crew, variable wing-geometry, maritime air superiority fighter that served with the US Navy for 32 years and continues to serve with the IRIAF in Iran. The F-14 was the US Navy's frontline fighter from the 1970s to the mid-2000s. Over the course of its long service it also became one of the US Navy’s premier precision ground-attack platform and its lone airborne reconnaissance asset.\n\nNoteworthy features of the Tomcat are its swing-wing configuration, two-man crew, and the powerful AN/AWG-9 Weapons Control System (WCS) and radar. The AWG-9 allows employment of the long-range AIM-54 Phoenix air-to-air missile, and the LANTIRN targeting pod allows precision ground strikes using laser-guided bombs. The F-14 Tomcat was present in several historic events that include the two Gulf of Sidra incidents, Operations Desert Storm Iraqi Freedom, the Yugoslavian conflict, and Operation Enduring Freedom over Afghanistan. It was also immortalized in the iconic motion picture, Top Gun, and starred in several other feature films including The Final Countdown, Executive Decision, and others.\n\nThe Tomcat was also played a vital role in the Iran-Iraq war of the 1980s, where is flew for the Islamic Republic of Iran Air Force.", + "country-of-origin": "USA", + "manufacturer": "Grumman", + "role": "Carrier-based Air-Superiority Fighter/Fighter Bomber", + "year-of-variant-introduction": "1987" + } + }], + "F-15C": [{ + "default": { + "name": "F-15C Eagle", + "text": "The F-15 has often been labeled as the greatest U.S. fighter aircraft from the 1970s until the early 21st century. The F-15C is a pure fighter with outstanding performance and has scored over 100 air-to-air victories without suffering any confirmed losses.", + "country-of-origin": "USA", + "manufacturer": "McDonnell Douglas", + "role": "Air-Superiority Fighter", + "year-of-variant-introduction": "1978" + }, + "Japan": { + "name": "F-15J Eagle" + } + }], + "F-15E": [{ + "default": { + "name": "F-15E Strike Eagle", + "country-of-origin": "USA", + "manufacturer": "McDonnell Douglas", + "role": "Multirole Strike Fighter", + "year-of-variant-introduction": "1988" + } + }], + "F-16C_50": [{ + "default": { + "name": "F-16CM Fighting Falcon (Block 50)", + "text": "The F-16C is a single seat, single engine multirole fighter that was developed in the 1970s. More than 4,500 units were manufactured and are operated today by 26 countries where the aircraft performs numerous missions that include air superiority, close air support, precision bombing, air defense suppression, reconnaissance and more. Few other aircraft can match its versatility, maneuverability, firepower, and huge production numbers.\n\nNicknamed the Viper by its pilots, the F-16 was designed with a reclined seating position for high G tolerance and a single-piece bubble canopy for exceptional visibility and comfort. Its lightweight and powerful F-110-GE-129 engines provide a greater than 1:1 thrust ratio.\n\nHoused in the nose of the Viper is a multifunction APG-68(V)5 radar. The aircraft can also be equipped with multiple sensors such as the LITENING targeting pod and HARM Targeting System (HTS).\n\nThe Viper is armed for air-to-air combat with Sidewinders, AMRAAMs, and an internal 20mm 6-barrel Gatling gun. It can also be loaded with a wide range of air-to-ground weapons that include general purpose bombs, rockets, canister munition, Mavericks, laser- and GPS-guided bombs, and more", + "country-of-origin": "USA", + "manufacturer": "General Dynamics", + "role": "Multirole Fighter", + "year-of-variant-introduction": "1991" + }, + "Japan": { + "name": "F-2A" + } + }], + "F-22A":[{ + "default": { + "name": "F-22A Raptor", + "country-of-origin": "USA", + "manufacturer": "Lockheed Martin", + "role": "Stealth Air-Superiority Fighter", + "year-of-variant-introduction": "2005" + } + }], + "F-86F Sabre": [{ + "default": { + "name": "F-86F Sabre", + "text": "The North American F-86F Sabre was the most capable western fighter of the early- to mid-1950s. This swept wing, single engine jet was the most important western aircraft of the Korean War and often tangled with Russian-made MiG-15s over the infamous “MiG Alley”. It was a hard struggle not only for the Korean sky, but also between two excellent aircraft builders of the East and West. In addition to its primary role as an air-to-air fighter, the Sabre could also carry bombs and air-to-ground rockets to attack ground targets.", + "country-of-origin": "USA", + "manufacturer": "North American", + "role": "Fighter", + "year-of-variant-introduction": "1953" + } + }], + "F-111F": [{ + "default": { + "name": "F-111F Aardvark", + "country-of-origin": "USA", + "manufacturer": "General Dynamics", + "role": "Fighter-Bomber", + "year-of-variant-introduction": "1970" + } + }], + "F-117A": [{ + "default": { + "name": "F-117A Nighthawk", + "country-of-origin": "USA", + "manufacturer": "Lockheed", + "role": "Stealth Attack", + "year-of-variant-introduction": "1983" + } + }], + "FA-18C_hornet": [{ + "default": { + "name": "F/A-18C Hornet (Lot 20)", + "text": "The F/A-18C Hornet is twin engine, supersonic fighter that is flown by a single pilot in a \"glass cockpit\". It combines extreme maneuverability , a deadly arsenal of weapons, and the ability to operate from an aircraft carrier. Operated by several nations, this multi-role fighter has been instrumental in conflicts from 1986 to today.\n\nThe Hornet is equipped with a large suite of sensors that includes a radar, targeting pod, and a helmet mounted sight. In addition to its internal 20mm cannon, the Hornet can be armed with a large assortment of unguided bombs and rockets, laser and GPS-guided bombs, air-to-surface missiles of all sorts, and both radar and infrared-guided air-to-air missiles.\n\nThe Hornet is also known for its extreme, slow-speed maneuverability in a dogfight. Although incredibly deadly, the Hornet is also a very easy aircraft to fly.", + "country-of-origin": "USA", + "manufacturer": "McDonnell Douglas", + "role": "Carrier-based Multirole Fighter", + "year-of-variant-introduction": "1987" + }, + "Canada": { + "name": "CF-188 Hornet" + }, + "Spain": { + "name": "EF-18A+ Hornet" + } + }], + "FW-190A8": [{ + "default": { + "name": "Fw 190 A-8 Anton", + "text": "Designed for the German Luftwaffe by famed aircraft designer Kurt Tank in the late-1930s, the Fw 190 was the backbone of the Luftwaffe in both fighter and attack bomber roles. Powered by a large radial engine, the A version of the Focke-Wulf 190 was superior in many ways to the Bf 109s and Spitfires at the time of its introduction. In fact, this led to the development of the Mk.IX version of the Spitfire.\n\nMany of the Luftwaffe's aces racked up their impressive kill counts in the Fw 190 A due to its impressive fire power, excellent low to medium altitude performance, durability, and ease of flying. It saw action on both the eastern and western fronts where it was both respected and feared by allied pilots. Armament included two fuselage-mounted 13-mm MG 131 machine guns and four wing-mounted MG 151/20E 20mm cannons. The Anton could also be loaded with unguided rockets and bombs.\n\nThe A-8 version of the Fw 190 entered production in February 1944, and it was powered by a BMW 801 D-2 radial engine. In addition to excellent low- and medium-altitude performance, the engine was also very rugged. Along with the F-8 version, it also had thicker armor around the engine. The engine also received the C3 injection system that provided an emergency boost of 1,980 PS in short durations.", + "country-of-origin": "Germany", + "manufacturer": "Focke-Wulf", + "role": "Fighter", + "year-of-variant-introduction": "1944" + } + }], + "FW-190D9": [{ + "default": { + "name": "Fw 190 D-9 Dora", + "text": "The Focke-Wulf Fw 190 is widely regarded as Germany's best fighter aircraft of World War II. Its appearance in the skies over France in August 1941 was a rude shock to the Allies, as it was clearly superior to any other plane. For nearly a year, the Fw 190 was the unmatched champion of the air war in Europe. The Fw 190 had speed and high altitude performance as its two great assets.\n\nThe development of advanced allied fighters resulted in the Fw 190 D–9 variant which first saw service in September 1944. This variant had a larger nose that housed a more powerful Junkers Jumo engine that produced 2,100 hp with the MW-50 boost system. The D-9 was designed for high altitude aerial combat and is a worthy adversary to the P-51D Mustang.", + "country-of-origin": "Germany", + "manufacturer": "Focke-Wulf", + "role": "Fighter", + "year-of-variant-introduction": "1944" + } + }], + "Hercules": [{ + "default": { + "name": "C-130J-30 Super Hercules", + "country-of-origin": "USA", + "manufacturer": "Lockheed", + "role": "Transport", + "year-of-variant-introduction": "1999" + } + }], + "I-16": [{ + "default": { + "name": "I-16 Ishak", + "text": "I-16 - Soviet single-engine monoplane fighter of 1930s created by aircraft designer Nikolai Polikarpov’s design bureau. It was the one of the world’s first fighters with landing gear retraction system. It was I-16 which stood the air fighting against famous Messerschmitt Bf 109.\n\nOver a period of its history I-16 was upgraded a lot. New modifications of aircraft were created and adopted almost every year. I-16 type 24 was further development of I-16 type 18.\n\nIt is one of the most famous fighters taking part in World War II. It was different from all its “contemporaries” in configuration and flight characteristics. Its uniqueness and distinction were defined by minimum size, dome-shaped fuselage, small wing, dorsal spine.", + "country-of-origin": "USSR/Russia", + "manufacturer": "Polikarpov", + "role": "Fighter", + "year-of-variant-introduction": "1935" + } + }], + "J-11A": [{ + "default": { + "name": "J-11A Flanker-L", + "country-of-origin": "China", + "manufacturer": "Shenyang", + "role": "Air-Superiority Fighter", + "year-of-variant-introduction": "1998" + } + }], + "JF-17": [{ + "default": { + "name": "JF-17 Thunder", + "text": "JF-17 is a single seat, single engine, multirole light fighter that was joint developed by AVIC Chengdu and Pakistan Aeronautical Complex (PAC). The design phase of JF-17 \"Thunder\" finished at May 31st, 2002, and the maiden flight was made on August 25th, 2003. The first plane delivered to PAF (Pakistan Air Force) in 2007. Currently several different blocks of JF-17s are in service in Pakistan and Myanmar air forces. There are also several countries interested in purchasing this fighter jet. On February 27th, 2019, \"Thunder\" has withstood the test of actual combat and helped PAF win an appreciable victory.\n\n\"Thunder\" is a type of fighter that specifically tailored for PAF. The development plan of her predecessor can even be traced back to 1985. At first, PAF was only looking for a fighter that can replace Shenyang J-6 (Chinese version of Mig-19), but they were not satisfied with Chengdu’s J-7M. After more than 20 years of development, the final product \"Thunder\" becomes completely different from J-7M.\n\n\"Thunder\" has a bubble canopy of great view, pretty strake-wing layout and advanced avionics. KLJ-7 radar provides excellent air to ground capability. WMD-7 targeting pod can help \"Thunder\" searching for targets in combat.", + "country-of-origin": "Pakistan/China", + "manufacturer": "PAC/CAC", + "role": "Multirole Fighter", + "year-of-variant-introduction": "2007" + }, + "China": { + "name": "FC-1 Fierce Dragon", + "year-of-variant-introduction": "N/A" + } + }], + "Ju-88A4": [{ + "default": { + "name": "Ju 88 A-4", + "country-of-origin": "Germany", + "manufacturer": "Junkers", + "role": "Tactical/Torpedo Bomber", + "year-of-variant-introduction": "1940" + } + }], + "Ka-50": [{ + "default": { + "name": "Ka-50 Hokum", + "text": "The Ka-50 Black Shark (NATO reporting name: Hokum) is a unique and deadly single-seat, Russian attack helicopter that has seen combat in the Northern Caucasus. It combines a high performance dual rotor system with a deadly weapons payload of guided missiles, rockets, bombs, and a 30mm cannon. The Ka-50 is also unique in that it has an ejection seat.", + "country-of-origin": "USSR/Russia", + "manufacturer": "Kamov", + "role": "Attack", + "year-of-variant-introduction": "1995" + } + }], + "L-39C": [{ + "default": { + "name": "L-39C Albatros", + "text": "Two seat Jet trainer aircraft L-39C is intended for basic and advanced pilot training in visual and instrument flight rules weather conditions, day and night and also for combat use against air and ground targets. Its development started in the middle 60s of the last century by the Czech “Aero Vodochody”. In the 70s the aircraft has entered service and is still in the operational use in over 30 countries worldwide.", + "country-of-origin": "Czechoslovakia", + "manufacturer": "Aero", + "role": "Trainer", + "year-of-variant-introduction": "1972" + } + }], + "L-39ZA": [{ + "default": { + "name": "L-39ZA Albatros", + "text": "Two seat Jet trainer aircraft L-39C is intended for basic and advanced pilot training in visual and instrument flight rules weather conditions, day and night and also for combat use against air and ground targets. Its development started in the middle 60s of the last century by the Czech “Aero Vodochody”. In the 70s the aircraft has entered service and is still in the operational use in over 30 countries worldwide.", + "country-of-origin": "Czechoslovakia", + "manufacturer": "Aero", + "role": "Light Attack", + "year-of-variant-introduction": "1977" + } + }], + "M-2000C": [{ + "default": { + "name": "Mirage 2000C", + "text": "The M-2000C is a multi-role, French-designed, 4th generation fighter. It was designed in the 1970s as a lightweight fighter and in excess of 600 M-2000C aircraft have been built. The M2000C is a single-engine fighter will a low-set delta wing with no horizontal tail. It has excellent maneuverability given its relaxed stability and fly-by-wire flight control system. The M2000C also includes a multi-mode RDI radar that is capable tracking and engaging targets at beyond visual ranges. In addition to engaging other aircraft with cannon and missiles, the M2000C can also engage ground targets with cannon, rockets and bombs.", + "country-of-origin": "France", + "manufacturer": "Dassault", + "role": "Multirole Fighter", + "year-of-variant-introduction": "1983" + } + }], + "MB-339PAN":[{ + "default": { + "name": "MB-339PAN", + "country-of-origin": "Italy", + "manufacturer": "Aermacchi", + "role": "Aerobatic", + "year-of-variant-introduction": "1982" + } + }], + "Mirage 2000-5": [{ + "default": { + "name": "Mirage 2000-5", + "country-of-origin": "France", + "manufacturer": "Dassault", + "role": "Multirole Fighter", + "year-of-variant-introduction": "1997" + } + }], + "Mi-24V": [{ + "default": { + "name": "Mi-24V Hind-E", + "country-of-origin": "USSR/Russia", + "manufacturer": "Mil", + "role": "Attack/Transport", + "year-of-variant-introduction": "1976" + } + }], + "Mi-28N": [{ + "default": { + "name": "Mi-28N Havoc", + "country-of-origin": "USSR/Russia", + "manufacturer": "Mil", + "role": "Attack", + "year-of-variant-introduction": "2009" + } + }], + "Mi-8MT": [{ + "default": { + "name": "Mi-8MTV2 Hip", + "text": "The Mil Mi-8MTV2 is an upgraded version of one of the most widely produced helicopters in history and a combat transport and fire support veteran of countless operations around the world.", + "country-of-origin": "USSR/Russia", + "manufacturer": "Mil", + "role": "Transport/Light Attack", + "year-of-variant-introduction": "1981" + } + }], + "MiG-15bis": [{ + "default": { + "name": "MiG-15bis Fagot", + "text": "Developed in the years immediately following World War II, the MiG-15bis was a first-generation jet fighter designed by the Mikoyan-Gurevich design bureau of the Soviet Union. The MiG-15bis is a single engine, swept-wing jet that saw over 15,000 copies produced. The MiG-15 gained fame in the skies over Korea where it battled the F-86 Sabre and other allied aircraft. It proved an excellent match to the Sabre, and it often came down to the skill of the pilot that determined who made it home and who was left dangling from a parachute. Having an excellent thrust-to-weight ratio and good climbing characteristics, the MiG-15bis was also armed with two NR-23 23mm cannons and a single, powerful N-37 37mm cannon. Not surprisingly, it is considered by many as one of the deadliest fighters of the era.", + "country-of-origin": "USSR/Russia", + "manufacturer": "Mikoyan-Gurevich", + "role": "Fighter", + "year-of-variant-introduction": "1950" + } + }], + "MiG-19P": [{ + "default": { + "name": "MiG-19P Farmer-B", + "text": "The MiG-19P Farmer was designed by the legendary Mikoyan Design Bureau in the Early 1950’s. The MiG-19 fighter was the Soviet Union’s first true supersonic Interceptor that could exceed Mach 1 in level flight.\n\nDesigned to take on enemy fighters and bombers at any time of day or night and in any weather condition, the Farmer was equipped with the RP-5 lzumrud radar in the nose and armed with two NR-30 30mm cannons in the wing roots. The Farmer is also able to carry an array of ground attack weapons that includes S-5M rockets and various general-purpose bombs. It is a lethal interceptor with conventional ground attack capabilities.", + "country-of-origin": "USSR/Russia", + "manufacturer": "Mikoyan-Gurevich", + "role": "Fighter", + "year-of-variant-introduction": "1955" + }, + "China": { + "name": "J-6A", + "country-of-origin": "China", + "manufacturer": "Shenyang", + "year-of-variant-introduction": "1962" + } + }], + "MiG-21Bis": [{ + "default": { + "name": "MiG-21bis Fishbed-N", + "text": "The MiG-21bis is a delta wing, supersonic, fighter-interceptor jet aircraft. Much like the AK-47 became the everyman's rifle, the MiG-21 has been operated by more than 40 countries worldwide, and has enjoyed the longest production run of any modern jet fighter to date. The MiG-21, in all of its variants, has fought in wars stretching all the way from the Vietnam War in the 1960's to the modern day Syrian Civil War. Owing to its unique blend of versatility, ruggedness and maintainability, the MiG-21 remains in active service to this very day.", + "country-of-origin": "USSR/Russia", + "manufacturer": "Mikoyan-Gurevich", + "role": "Fighter", + "year-of-variant-introduction": "1972" + }, + "China": { + "name": "J-7B", + "country-of-origin": "China", + "manufacturer": "Chengdu", + "year-of-variant-introduction": "1992" + } + }], + "MiG-23MLD": [{ + "default": { + "name": "MiG-23MLD Flogger-K", + "country-of-origin": "USSR/Russia", + "manufacturer": "Mikoyan-Gurevich", + "role": "Fighter", + "year-of-variant-introduction": "1982" + }, + "Iraq": { + "name": "MiG-23ML Flogger-G", + "year-of-variant-introduction": "1981" + } + }], + "MiG-25PD": [{ + "default": { + "name": "MiG-25PD Foxbat-E", + "country-of-origin": "USSR/Russia", + "manufacturer": "Mikoyan-Gurevich", + "role": "Interceptor", + "year-of-variant-introduction": "1979" + } + }], + "MiG-25RBT": [{ + "default": { + "name": "MiG-25RBT Foxbat-B", + "country-of-origin": "USSR/Russia", + "manufacturer": "Mikoyan-Gurevich", + "role": "Strike Fighter", + "year-of-variant-introduction": "1970" + } + }], + "MiG-27K": [{ + "default": { + "name": "MiG-27K Flogger-J2", + "country-of-origin": "USSR/Russia", + "manufacturer": "Mikoyan", + "role": "Attack", + "year-of-variant-introduction": "1975" + } + }], + "MiG-29A": [{ + "default": { + "name": "MiG-29A Fulcrum-A", + "text": "The MiG-29 \"Fulcrum\" is a Russian-designed, twin-engine, supersonic fighter. First operational in the early 1980s, the Fulcrum is a \"light weight\" fighter, comparable to the American F/A-18 Hornet and F-16. Designed to work in conjunction with the larger Su-27 Flanker, the MiG-29 is armed with an internal 30mm cannon and both infrared and radar guided air-to-air missiles. For air-to-ground tasks, the MiG-29 can be armed with a large array of unguided bombs and rockets.\n\nIn addition to a sophisticated pulse doppler radar, the MiG-29 is also equipped with a passive Infrared Search and Track (IRST) sensor that allows the Fulcrum to detect and target enemy aircraft just based on target infrared emissions. This allows the MiG-29 to make stealthy attacks with no warning!\n\nThe Fulcrum is a highly-maneuverable fighter in a dogfight, and when paired with the helmet mounted sight and the AA-11 \"Archer\" air-to-air missile, it is a very lethal adversary.\n\nThe MiG-29 has also been widely exported and has served in many countries that include Germany, Iran, Ukraine, and Poland.", + "country-of-origin": "USSR/Russia", + "manufacturer": "Mikoyan", + "role": "Multirole Fighter", + "year-of-variant-introduction": "1983" + } + }], + "MiG-29G": [{ + "default": { + "name": "MiG-29G Fulcrum-A", + "text": "The MiG-29 \"Fulcrum\" is a Russian-designed, twin-engine, supersonic fighter. First operational in the early 1980s, the Fulcrum is a \"light weight\" fighter, comparable to the American F/A-18 Hornet and F-16. Designed to work in conjunction with the larger Su-27 Flanker, the MiG-29 is armed with an internal 30mm cannon and both infrared and radar guided air-to-air missiles. For air-to-ground tasks, the MiG-29 can be armed with a large array of unguided bombs and rockets.\n\nIn addition to a sophisticated pulse doppler radar, the MiG-29 is also equipped with a passive Infrared Search and Track (IRST) sensor that allows the Fulcrum to detect and target enemy aircraft just based on target infrared emissions. This allows the MiG-29 to make stealthy attacks with no warning!\n\nThe Fulcrum is a highly-maneuverable fighter in a dogfight, and when paired with the helmet mounted sight and the AA-11 \"Archer\" air-to-air missile, it is a very lethal adversary.\n\nThe MiG-29 has also been widely exported and has served in many countries that include Germany, Iran, Ukraine, and Poland.", + "country-of-origin": "USSR/Russia", + "manufacturer": "Mikoyan", + "role": "Multirole Fighter", + "year-of-variant-introduction": "1993" + } + }], + "MiG-29S": [{ + "default": { + "name": "MiG-29S Fulcrum-C", + "text": "The MiG-29 \"Fulcrum\" is a Russian-designed, twin-engine, supersonic fighter. First operational in the early 1980s, the Fulcrum is a \"light weight\" fighter, comparable to the American F/A-18 Hornet and F-16. Designed to work in conjunction with the larger Su-27 Flanker, the MiG-29 is armed with an internal 30mm cannon and both infrared and radar guided air-to-air missiles. For air-to-ground tasks, the MiG-29 can be armed with a large array of unguided bombs and rockets.\n\nIn addition to a sophisticated pulse doppler radar, the MiG-29 is also equipped with a passive Infrared Search and Track (IRST) sensor that allows the Fulcrum to detect and target enemy aircraft just based on target infrared emissions. This allows the MiG-29 to make stealthy attacks with no warning!\n\nThe Fulcrum is a highly-maneuverable fighter in a dogfight, and when paired with the helmet mounted sight and the AA-11 \"Archer\" air-to-air missile, it is a very lethal adversary.\n\nThe MiG-29 has also been widely exported and has served in many countries that include Germany, Iran, Ukraine, and Poland.", + "country-of-origin": "USSR/Russia", + "manufacturer": "Mikoyan", + "role": "Multirole Fighter", + "year-of-variant-introduction": "1983" + } + }], + "MiG-31": [{ + "default": { + "name": "MiG-31 Foxhound", + "country-of-origin": "USSR/Russia", + "manufacturer": "Mikoyan", + "role": "Interceptor", + "year-of-variant-introduction": "1981" + } + }], + "OH-58D": [{ + "default": { + "name": "OH-58D Kiowa Warrior", + "country-of-origin": "USA", + "manufacturer": "Bell", + "role": "Light Attack/Forward Air Control", + "year-of-variant-introduction": "1983" + } + }], + "P-47D-30": [{ + "default": { + "name": "P-47D-30 Thunderbolt (Late)", + "text": "The P-47 Thunderbolt, nicknamed the Jug, served the United States Army Air Forces (USAAF) in World War II with distinction. In total 15,636 were built between 1941 and 1945. France, the United Kingdom, the Soviet Union, Mexico and Brazil also operated the P-47. It was armed with an impressive eight .50-caliber machine guns with 425 rounds per gun. In addition the Jug was armed with bombs and rockets and it excelled in the ground attack role. The P-47 also served in the bomber escort role before the introduction of the P-51 Mustang which had longer range.\n\nPowered by an R-2800-59 Double Wasp turbocharged radial engine, the aircraft enjoyed exceptional power and durability. The Jug substantial weight gave it tremendous dive speed acceleration but the aircraft suffered from quite low Mach limits and hence the aircraft was equipped with dive flaps to avoid dangerous effects of compressibility. The bubble canopy of the D version of the P-47 provided excellent all round visibility.", + "country-of-origin": "USA", + "manufacturer": "Republic", + "role": "Fighter-Bomber", + "year-of-variant-introduction": "1944" + }, + "UK": { + "name": "Thunderbolt Mk.II (Mid)" + } + }], + "P-47D-30bl1": [{ + "default": { + "name": "P-47D-30 Thunderbolt (Early)", + "text": "The P-47 Thunderbolt, nicknamed the Jug, served the United States Army Air Forces (USAAF) in World War II with distinction. In total 15,636 were built between 1941 and 1945. France, the United Kingdom, the Soviet Union, Mexico and Brazil also operated the P-47. It was armed with an impressive eight .50-caliber machine guns with 425 rounds per gun. In addition the Jug was armed with bombs and rockets and it excelled in the ground attack role. The P-47 also served in the bomber escort role before the introduction of the P-51 Mustang which had longer range.\n\nPowered by an R-2800-59 Double Wasp turbocharged radial engine, the aircraft enjoyed exceptional power and durability. The Jug substantial weight gave it tremendous dive speed acceleration but the aircraft suffered from quite low Mach limits and hence the aircraft was equipped with dive flaps to avoid dangerous effects of compressibility. The bubble canopy of the D version of the P-47 provided excellent all round visibility.", + "country-of-origin": "USA", + "manufacturer": "Republic", + "role": "Fighter-Bomber", + "year-of-variant-introduction": "1944" + }, + "UK": { + "name": "Thunderbolt Mk.II (Early)" + } + }], + "P-47D-40": [{ + "default": { + "name": "P-47D-40 Thunderbolt", + "text": "The P-47 Thunderbolt, nicknamed the Jug, served the United States Army Air Forces (USAAF) in World War II with distinction. In total 15,636 were built between 1941 and 1945. France, the United Kingdom, the Soviet Union, Mexico and Brazil also operated the P-47. It was armed with an impressive eight .50-caliber machine guns with 425 rounds per gun. In addition the Jug was armed with bombs and rockets and it excelled in the ground attack role. The P-47 also served in the bomber escort role before the introduction of the P-51 Mustang which had longer range.\n\nPowered by an R-2800-59 Double Wasp turbocharged radial engine, the aircraft enjoyed exceptional power and durability. The Jug substantial weight gave it tremendous dive speed acceleration but the aircraft suffered from quite low Mach limits and hence the aircraft was equipped with dive flaps to avoid dangerous effects of compressibility. The bubble canopy of the D version of the P-47 provided excellent all round visibility.", + "country-of-origin": "USA", + "manufacturer": "Republic", + "role": "Fighter-Bomber", + "year-of-variant-introduction": "1944" + }, + "UK": { + "name": "Thunderbolt Mk.II (Late)" + } + }], + "P-51D": [{ + "default": { + "name": "P-51D-25-NA Mustang", + "country-of-origin": "USA", + "manufacturer": "North American", + "role": "Fighter", + "year-of-variant-introduction": "1944" + }, + "UK": { + "name": "Mustang Mk.IV (Early)" + } + }], + "P-51D-30-NA": [{ + "default": { + "name": "P-51D-30-NA Mustang", + "country-of-origin": "USA", + "manufacturer": "North American", + "role": "Fighter", + "year-of-variant-introduction": "1944" + }, + "UK": { + "name": "Mustang Mk.IV (Late)" + } + }], + "Rafale_A_S": [{ + "default": { + "name": "Rafale M (Air-to-Ground)", + "country-of-origin": "France", + "manufacturer": "Dassault", + "role": "Multirole Fighter", + "year-of-variant-introduction": "2001" + } + }], + "Rafale_B": [{ + "default": { + "name": "Rafale B", + "country-of-origin": "France", + "manufacturer": "Dassault", + "role": "Carrier-based Multirole Fighter", + "year-of-variant-introduction": "2006" + } + }], + "Rafale_M": [{ + "default": { + "name": "Rafale M (Air-to-Air)", + "country-of-origin": "France", + "manufacturer": "Dassault", + "role": "Multirole Fighter", + "year-of-variant-introduction": "2001" + } + }], + "S-3B": [{ + "default": { + "name": "S-3B Viking", + "country-of-origin": "USA", + "manufacturer": "Lockheed", + "role": "Carrier-based Attack", + "year-of-variant-introduction": "1984" + } + }], + "SA342L": [{ + "default": { + "name": "SA 342L Gazelle", + "text": "The SA342 Gazelle is a light scout/attack and transport helicopter. It was introduced in 1968 as a result of cooperation between Aérospatiale and Westland Aircraft. Operated by 23 countries, the Gazelle has served in combat operations across the world including the 1991 Gulf War, 1982 Lebanon War, Syria, and other conflicts.\n\nThe Gazelle is powered by a single turbine engine that is connected to three glass-fiber reinforced plastic main rotor blades with a bearingless main rotor developed by Bölkow GmbH. It is also the first helicopter which features the famous Fenestron tail rotor.", + "country-of-origin": "France", + "manufacturer": "Aérospatiale", + "role": "Light Attack", + "year-of-variant-introduction": "1977" + } + }], + "SA342M": [{ + "default": { + "name": "SA 342M Gazelle", + "text": "The SA342 Gazelle is a light scout/attack and transport helicopter. It was introduced in 1968 as a result of cooperation between Aérospatiale and Westland Aircraft. Operated by 23 countries, the Gazelle has served in combat operations across the world including the 1991 Gulf War, 1982 Lebanon War, Syria, and other conflicts.\n\nThe Gazelle is powered by a single turbine engine that is connected to three glass-fiber reinforced plastic main rotor blades with a bearingless main rotor developed by Bölkow GmbH. It is also the first helicopter which features the famous Fenestron tail rotor.", + "country-of-origin": "France", + "manufacturer": "Aérospatiale", + "role": "Light Attack", + "year-of-variant-introduction": "1977" + }, + "UK": { + "name": "Gazelle AH.1", + "manufacturer": "Westland", + "year-of-variant-introduction": "1974" + } + }], + "SA342Mistral": [{ + "default": { + "name": "SA 342M Gazelle Mistral", + "text": "The SA342 Gazelle is a light scout/attack and transport helicopter. It was introduced in 1968 as a result of cooperation between Aérospatiale and Westland Aircraft. Operated by 23 countries, the Gazelle has served in combat operations across the world including the 1991 Gulf War, 1982 Lebanon War, Syria, and other conflicts.\n\nThe Gazelle is powered by a single turbine engine that is connected to three glass-fiber reinforced plastic main rotor blades with a bearingless main rotor developed by Bölkow GmbH. It is also the first helicopter which features the famous Fenestron tail rotor.", + "country-of-origin": "France", + "manufacturer": "Aérospatiale", + "role": "Light Attack", + "year-of-variant-introduction": "1977" + } + }], + "SH-60B": [{ + "default": { + "name": "SH-60B Seahawk", + "country-of-origin": "USA", + "manufacturer": "Sikorsky", + "role": "Transport/Anti-Ship", + "year-of-variant-introduction": "1984" + } + }], + "SpitfireLFMkIX": [{ + "default": { + "name": "Spitfire LF Mk IX", + "text": "The British Spitfire is one of the most iconic fighter aircraft of World War II. Most famous for its role in the Battle of Britain, the Spitfire served as Britain's primary fighter during the entirety of the war. The Spitfire combines graceful lines, eye-watering dogfight performance, and heavy firepower in its later variants.\n\nThe Spitfire Mk IX was originally developed as a stopgap measure as a response to the appearance of the Focke-Wulf FW 190A.\n\nThe Spitfire IX is powered by the Merlin 66. This engine produces its best performance at slightly lower altitudes than the older Merlin 61. Spitfires equipped with this engine were designated LF Mk IX. This was the most numerous version of the Mk IX, with 4,010 produced. The majority of Mk IXs of all types used the standard \"c\" wing, which would often carry two 20mm cannon and four .303in machine guns.\n\nThe Mk IX was a significant improvement on the Mk V. It had a top speed of 409 mph at 28,000 feet, an increase of 40 miles per hour. Its service ceiling rose from 36,200 feet to 43,000 feet. It could climb at 4,000 feet per minute. In July 1942, an early Mk IX was flown against a captured Fw 190A, and the two aircraft were discovered to have very similar capabilities. The RAF had its answer to the Fw 190 problem.\n\nThe Mk IX replaced the Mk V from June 1942. It allowed the RAF to go back onto the offensive in occupied Europe, and resume the \"circus\", \"ramrod\" and \"rodeo\" raids. Its first combat success came on 30 July 1942, when a Spitfire Mk IX shot down a Fw 190. Amongst other notable achievements, the Mk IX took part in the highest altitude combat of the Second World War, when it intercepted a Ju 86R at 43,000 feet over Southampton on 12 September 1942. On 5 October 1944 Spitfire Mk IXs of 401 Squadron were the first allied aircraft to shoot down an Me 262 Jet. The Mk IX remained in service until the end of the war, even after the appearance of the Griffon powered Mk XIV.", + "country-of-origin": "UK", + "manufacturer": "Supermarine", + "role": "Fighter", + "year-of-variant-introduction": "1943" + } + }], + "SpitfireLFMkIXCW": [{ + "default": { + "name": "Spitfire LF Mk IX (Clipped Wings)", + "text": "The British Spitfire is one of the most iconic fighter aircraft of World War II. Most famous for its role in the Battle of Britain, the Spitfire served as Britain's primary fighter during the entirety of the war. The Spitfire combines graceful lines, eye-watering dogfight performance, and heavy firepower in its later variants.\n\nThe Spitfire Mk IX was originally developed as a stopgap measure as a response to the appearance of the Focke-Wulf FW 190A.\n\nThe Spitfire IX is powered by the Merlin 66. This engine produces its best performance at slightly lower altitudes than the older Merlin 61. Spitfires equipped with this engine were designated LF Mk IX. This was the most numerous version of the Mk IX, with 4,010 produced. The majority of Mk IXs of all types used the standard \"c\" wing, which would often carry two 20mm cannon and four .303in machine guns.\n\nThe Mk IX was a significant improvement on the Mk V. It had a top speed of 409 mph at 28,000 feet, an increase of 40 miles per hour. Its service ceiling rose from 36,200 feet to 43,000 feet. It could climb at 4,000 feet per minute. In July 1942, an early Mk IX was flown against a captured Fw 190A, and the two aircraft were discovered to have very similar capabilities. The RAF had its answer to the Fw 190 problem.\n\nThe Mk IX replaced the Mk V from June 1942. It allowed the RAF to go back onto the offensive in occupied Europe, and resume the \"circus\", \"ramrod\" and \"rodeo\" raids. Its first combat success came on 30 July 1942, when a Spitfire Mk IX shot down a Fw 190. Amongst other notable achievements, the Mk IX took part in the highest altitude combat of the Second World War, when it intercepted a Ju 86R at 43,000 feet over Southampton on 12 September 1942. On 5 October 1944 Spitfire Mk IXs of 401 Squadron were the first allied aircraft to shoot down an Me 262 Jet. The Mk IX remained in service until the end of the war, even after the appearance of the Griffon powered Mk XIV.", + "country-of-origin": "UK", + "manufacturer": "Supermarine", + "role": "Fighter", + "year-of-variant-introduction": "1943" + } + }], + "Su-17M4": [{ + "default": { + "name": "Su-17M4 Fitter-K", + "country-of-origin": "USSR/Russia", + "manufacturer": "Sukhoi", + "role": "Fighter-Bomber", + "year-of-variant-introduction": "1981" + }, + "Iran": { + "name": "Su-22M4 Fitter-K", + "year-of-variant-introduction": "1983" + }, + "Iraq": { + "name": "Su-22M4 Fitter-K", + "year-of-variant-introduction": "1983" + }, + "Poland": { + "name": "Su-22M4 Fitter-K", + "year-of-variant-introduction": "1983" + } + }], + "Su-24M": [{ + "default": { + "name": "Su-24M Fencer-D", + "country-of-origin": "USSR/Russia", + "manufacturer": "Sukhoi", + "role": "Attack", + "year-of-variant-introduction": "1983" + }, + "Iran": { + "name": "Su-24MK Fencer-D", + "year-of-variant-introduction": "1988" + } + }], + "Su-25": [{ + "default": { + "name": "Su-25 Frogfoot", + "text": "The Su-25 'Grach' (Rook), NATO callsigned 'Frogfoot', is a dedicated strike attack aircraft designed for the close air support and anti-tank roles. The Su-25 has seen combat in several conflicts during its more than 30 years in service. The Su-25 combines excellent pilot protection and high speed compared to most dedicated attack aircraft. It can be armed with a variety of weapon systems including guided missiles, bombs, rockets, and its internal 30mm cannon.", + "country-of-origin": "USSR/Russia", + "manufacturer": "Sukhoi", + "role": "Close Air Support/Attack", + "year-of-variant-introduction": "1981" + } + }], + "Su-25T": [{ + "default": { + "name": "Su-25T Frogfoot", + "text": "The Su-25 'Grach' (Rook), NATO callsigned 'Frogfoot', is a dedicated strike attack aircraft designed for the close air support and anti-tank roles. The Su-25 has seen combat in several conflicts during its more than 30 years in service. The Su-25 combines excellent pilot protection and high speed compared to most dedicated attack aircraft. It can be armed with a variety of weapon systems including guided missiles, bombs, rockets, and its internal 30mm cannon.", + "country-of-origin": "USSR/Russia", + "manufacturer": "Sukhoi", + "role": "Close Air Support/Attack", + "year-of-variant-introduction": "1990" + } + }], + "Su-27": [{ + "default": { + "name": "Su-27 Flanker-B", + "text": "The Su-27, NATO codename Flanker, is one of the pillars of modern-day Russian combat aviation. Built to counter the American F-15 Eagle, the Flanker is a twin-engine, supersonic, highly manoeuvrable air superiority fighter. The Flanker is equally capable of engaging targets well beyond visual range as it is in a dogfight given its amazing slow speed and high angle attack manoeuvrability. Using its radar and stealthy infrared search and track system, the Flanker can employ a wide array of radar and infrared guided missiles. The Flanker also includes a helmet-mounted sight that allows you to simply look at a target to lock it up! In addition to its powerful air-to-air capabilities, the Flanker can also be armed with bombs and unguided rockets to fulfil a secondary ground attack role.", + "country-of-origin": "USSR/Russia", + "manufacturer": "Sukhoi", + "role": "Air-Superiority Fighter", + "year-of-variant-introduction": "1985" + } + }], + "Su-30": [{ + "default": { + "name": "Su-30 Flanker-C", + "country-of-origin": "USSR/Russia", + "manufacturer": "Sukhoi", + "role": "Multirole Fighter", + "year-of-variant-introduction": "1996" + }, + "China": { + "name": "Su-30MKK Flanker-G", + "country-of-origin": "USSR/Russia", + "manufacturer": "Sukhoi", + "role": "Multirole Fighter", + "year-of-variant-introduction": "2000" + } + }], + "Su-33": [{ + "default": { + "name": "Su-33 Flanker-D", + "text": "The Su-33 has been the backbone of Russian aircraft carrier aviation since the late 1990s and is an all-weather fighter capable of engaging both air and surface targets. Based on the powerful Su-27 \"Flanker\", the Su-33 is a navalized version suited for operations aboard the Admiral Kuznetsov aircraft carrier. Changes to the Su-33 include strengthened landing gear, folding wings, more powerful engines, and the very visible canards.\n\nThe Su-33 is equipped with a powerful pulse doppler radar and an Infrared Search and Track (IRST) for engaging aerial targets with a wide range of radar- and infrared-guided missiles and its internal cannon. For air-to-surface attack, the Su-33 can be armed with many types of unguided bombs, rockets, and cluster munitions.\n\nDespite its large size, the Su-33 is very capable in a dogfight when combined with its integral helmet-mounted sight and off-boresight missiles.", + "country-of-origin": "USSR/Russia", + "manufacturer": "Sukhoi", + "role": "Carrier-based Multirole Fighter", + "year-of-variant-introduction": "1998" + }, + "China": { + "name": "J-15 Flanker X-2", + "country-of-origin": "China", + "manufacturer": "Shenyang", + "role": "Carrier-based Multirole Fighter", + "year-of-variant-introduction": "2013" + } + }], + "Su-34": [{ + "default": { + "name": "Su-34 Fullback", + "country-of-origin": "USSR/Russia", + "manufacturer": "Sukhoi", + "role": "Fighter-Bomber/Strike Fighter", + "year-of-variant-introduction": "2014" + } + }], + "Su-57": [{ + "default": { + "name": "Su-57 Felon", + "country-of-origin": "Russia", + "manufacturer": "Sukhoi", + "role": "Stealth Air-Superiority Fighter", + "year-of-variant-introduction": "2020" + } + }], + "Tornado GR4": [{ + "default": { + "name": "Tornado GR4", + "country-of-origin": "UK/Italy/West Germany", + "manufacturer": "Panavia", + "role": "Strike Fighter", + "year-of-variant-introduction": "1996" + } + }], + "Tornado IDS": [{ + "default": { + "name": "Tornado IDS", + "country-of-origin": "UK/Italy/West Germany", + "manufacturer": "Panavia", + "role": "Strike Fighter", + "year-of-variant-introduction": "1979" + } + }], + "Tu-22M3": [{ + "default": { + "name": "Tu-22M3 Backfire-C", + "country-of-origin": "USSR/Russia", + "manufacturer": "Tupolev", + "role": "Strategic/Maritime Strike Bomber", + "year-of-variant-introduction": "1983" + } + }], + "Tu-95MS": [{ + "default": { + "name": "Tu-95MS Bear-H", + "country-of-origin": "USSR/Russia", + "manufacturer": "Tupolev", + "role": "Strategic Bomber", + "year-of-variant-introduction": "1981" + } + }], + "Tu-142": [{ + "default": { + "name": "Tu-142 Bear-F", + "country-of-origin": "USSR/Russia", + "manufacturer": "Tupolev", + "role": "Maritime Patrol/Anti-Ship", + "year-of-variant-introduction": "1972" + } + }], + "Tu-160": [{ + "default": { + "name": "Tu-160 Blackjack", + "country-of-origin": "USSR/Russia", + "manufacturer": "Tupolev", + "role": "Supersonic Strategic Bomber", + "year-of-variant-introduction": "1987" + } + }], + "UH-1H": [{ + "default": { + "name": "UH-1H Iroquois", + "text": "The UH-1 Iroquois, better known as the Huey, is one of the most iconic helicopters in the world. Indispensable in the Vietnam War, the Huey continues to serve in both military and civilian roles around the globe today.", + "country-of-origin": "USA", + "manufacturer": "Bell", + "role": "Transport/Light Attack", + "year-of-variant-introduction": "1967" + }, + "Germany": { + "name": "UH-1D Iroquois" + } + }], + "AAA 8,8cm Flak 18": [{ + "default": { + "name": "8.8 cm Flak 18", + "country-of-origin": "Germany", + "manufacturer": "Krupp/Rheinmetall", + "role": "Anti-Aircraft Gun/Anti-Tank Gun", + "year-of-variant-introduction": "1936" + } + }], + "AAA 8,8cm Flak 36": [{ + "default": { + "name": "8.8 cm Flak 36", + "country-of-origin": "Germany", + "manufacturer": "Krupp/Rheinmetall", + "role": "Anti-Aircraft Gun/Anti-Tank Gun", + "year-of-variant-introduction": "1936" + } + }], + "AAA 8,8cm Flak 37": [{ + "default": { + "name": "8.8 cm Flak 37", + "country-of-origin": "Germany", + "manufacturer": "Krupp/Rheinmetall", + "role": "Anti-Aircraft Gun/Anti-Tank Gun", + "year-of-variant-introduction": "1936" + } + }], + "AAA 8,8cm Flak 41": [{ + "default": { + "name": "8.8 cm Flak 41", + "country-of-origin": "Germany", + "manufacturer": "Krupp/Rheinmetall", + "role": "Anti-Aircraft Gun/Anti-Tank Gun", + "year-of-variant-introduction": "1943" + } + }], + "AAA Bofors 40mm": [{ + "default": { + "name": "Bofors 40 mm gun", + "country-of-origin": "Sweden", + "manufacturer": "Bofors", + "role": "Anti-Aircraft Gun", + "year-of-variant-introduction": "1934" + }, + "UK": { + "name": "QF 40 mm Mark III", + "year-of-variant-introduction": "1939" + } + }], + "AAA Flak 38": [{ + "default": { + "name": "2 cm Flak 38", + "country-of-origin": "Germany", + "manufacturer": "Mauser", + "role": "Anti-Aircraft Gun", + "year-of-variant-introduction": "1934" + } + }], + "AAA Flak-Vierling 38": [{ + "default": { + "name": "2 cm Flakvierling 38", + "country-of-origin": "Germany", + "manufacturer": "Mauser", + "role": "Anti-Aircraft Gun", + "year-of-variant-introduction": "1934" + } + }], + "AAA M45 Quadmount": [{ + "default": { + "name": "M45 Quadmount", + "country-of-origin": "USA", + "manufacturer": "W. L. Maxson Corporation", + "role": "Anti-Aircraft Gun", + "year-of-variant-introduction": "1943" + } + }], + "AAA M1 37mm": [{ + "default": { + "name": "M1 37mm Gun", + "country-of-origin": "USA", + "manufacturer": "Colt", + "role": "Anti-Aircraft Gun", + "year-of-variant-introduction": "1939" + } + }], + "AAA Vulcan M163": [{ + "default": { + "name": "M163 Vulcan Air Defense System", + "country-of-origin": "USA", + "manufacturer": "General Electric", + "role": "Self-Propelled Anti-Aircraft Gun", + "year-of-variant-introduction": "1989" + } + }], + "AAA ZSU-57-2": [{ + "default": { + "name": "ZSU-57-2 'Sparka'", + "country-of-origin": "USSR/Russia", + "manufacturer": "Omsk Works", + "role": "Self-Propelled Anti-Aircraft Gun", + "year-of-variant-introduction": "1955" + } + }], + "AAA ZU-23 on Ural-375": [{ + "default": { + "name": "ZU-23 on Ural-375", + "country-of-origin": "USSR/Russia", + "manufacturer": "KBP/Ural", + "role": "Self-Propelled Anti-Aircraft Gun", + "year-of-variant-introduction": "1961" + } + }], + "AAA ZU-23 Insurgent on Ural-375": [{ + "default": { + "name": "ZU-23 on Ural-375", + "country-of-origin": "USSR/Russia", + "manufacturer": "KBP/Ural", + "role": "Self-Propelled Anti-Aircraft Gun", + "year-of-variant-introduction": "1961" + } + }], + "AA gun QF 3,7\"": [{ + "default": { + "name": "QF 3.7-inch AA Gun", + "country-of-origin": "UK", + "manufacturer": "Vickers", + "role": "Anti-Aircraft Gun", + "year-of-variant-introduction": "1937" + } + }], + "AC Sd.Kfz.234/2 Puma": [{ + "default": { + "name": "Sd.Kfz.234/2 Puma", + "country-of-origin": "Germany", + "manufacturer": "Tatra/Büssing/Daimler-Benz/Schichau", + "role": "Amphibious Armoured Personnel Carrier", + "year-of-variant-introduction": "1943" + } + }], + "APC AAV-7": [{ + "default": { + "name": "AAVP-7A1 'Amtrac'", + "country-of-origin": "USA", + "manufacturer": "United Defense", + "role": "Amphibious Armoured Personnel Carrier", + "year-of-variant-introduction": "1971" + } + }], + "APC BTR-80": [{ + "default": { + "name": "BTR-80", + "country-of-origin": "USSR/Russia", + "manufacturer": "Arzamas", + "role": "Amphibious Armoured Personnel Carrier", + "year-of-variant-introduction": "1986" + } + }], + "APC BTR-82A": [{ + "default": { + "name": "BTR-82A", + "country-of-origin": "Russia", + "manufacturer": "Arzamas", + "role": "Amphibious Armoured Personnel Carrier", + "year-of-variant-introduction": "2013" + } + }], + "APC Cobra": [{ + "default": { + "name": "Cobra", + "country-of-origin": "Turkey", + "manufacturer": "Otokar", + "role": "Armoured Car", + "year-of-variant-introduction": "1997" + } + }], + "APC M2A1": [{ + "default": { + "name": "M2A1 Half-track", + "country-of-origin": "USA", + "manufacturer": "White Motor Company", + "role": "Armoured Personnel Carrier", + "year-of-variant-introduction": "1940" + } + }], + "APC M113": [{ + "default": { + "name": "M113", + "country-of-origin": "USA", + "manufacturer": "Food Machinery Corp", + "role": "Armoured Personnel Carrier", + "year-of-variant-introduction": "1960" + } + }], + "APC M1043 HMMWV Armament": [{ + "default": { + "name": "M1043 HMMWV (M2 HMG)", + "country-of-origin": "USA", + "manufacturer": "AM General", + "role": "Armoured Car", + "year-of-variant-introduction": "1983" + } + }], + "APC M1126 Stryker ICV": [{ + "default": { + "name": "M1126 Stryker ICV (M2 HMG)", + "country-of-origin": "USA", + "manufacturer": "General Dynamics", + "role": "Armoured Personnel Carrier", + "year-of-variant-introduction": "2002" + } + }], + "APC MTLB": [{ + "default": { + "name": "MT-LB", + "country-of-origin": "USSR/Russia", + "manufacturer": "Kharkiv", + "role": "Armoured Personnel Carrier", + "year-of-variant-introduction": "1958" + } + }], + "APC Sd.Kfz.251": [{ + "default": { + "name": "Sd.Kfz.251 \"Hanomag\"", + "country-of-origin": "Germany", + "manufacturer": "Hanomag", + "role": "Armoured Personnel Carrier", + "year-of-variant-introduction": "1939" + } + }], + "ARV BRDM-2": [{ + "default": { + "name": "BRDM-2", + "country-of-origin": "USSR/Russia", + "manufacturer": "GAZ", + "role": "Amphibious Armoured Car", + "year-of-variant-introduction": "1962" + } + }], + "ARV BTR-RD": [{ + "default": { + "name": "BTR-D", + "country-of-origin": "USSR/Russia", + "manufacturer": "Volgograd", + "role": "Airborne Amphibious Armoured Personnel Carrier", + "year-of-variant-introduction": "1974" + } + }], + "ATGM M1045 HMMWV TOW": [{ + "default": { + "name": "M1045 HMMWV (BGM-71 TOW)", + "country-of-origin": "USA", + "manufacturer": "AM General", + "role": "Armoured Car", + "year-of-variant-introduction": "1983" + } + }], + "ATGM M1134 Stryker": [{ + "default": { + "name": "M1134 Stryker ATGM (BGM-71 TOW)", + "country-of-origin": "USA", + "manufacturer": "General Dynamics", + "role": "Armoured Car", + "year-of-variant-introduction": "2002" + } + }], + "CT Centaur IV": [{ + "default": { + "name": "A27L Cruiser Tank MK VIII Centaur IV", + "country-of-origin": "UK", + "manufacturer": "Leyland", + "role": "Cruiser Tank", + "year-of-variant-introduction": "1944" + } + }], + "CT Cromwell IV": [{ + "default": { + "name": "A27M Cruiser Tank MK VIII Cromwell IV", + "country-of-origin": "UK", + "manufacturer": "Birmingham Railway Carriage and Wagon Company", + "role": "Cruiser Tank", + "year-of-variant-introduction": "1944" + } + }], + "Daimler Armoured Car": [{ + "default": { + "name": "Daimler Armoured Car Mk I", + "country-of-origin": "UK", + "manufacturer": "Daimler", + "role": "Armoured Car", + "year-of-variant-introduction": "1941" + } + }], + "HIT Churchill_VII": [{ + "default": { + "name": "A22 Infantry Tank MK IV Churchill VII", + "country-of-origin": "UK", + "manufacturer": "Vauxhall Motors", + "role": "Infantry Tank", + "year-of-variant-introduction": "1944" + } + }], + "HT Pz.Kpfw.VI Tiger I": [{ + "default": { + "name": "Panzerkampfwagen VI Tiger Ausf. E", + "country-of-origin": "Germany", + "manufacturer": "Henschel", + "role": "Heavy Tank", + "year-of-variant-introduction": "1942" + } + }], + "HT Pz.Kpfw.VI Ausf. B Tiger II": [{ + "default": { + "name": "Panzerkampfwagen Tiger Ausf. B Tiger II", + "country-of-origin": "Germany", + "manufacturer": "Henschel/Krupp", + "role": "Heavy Tank", + "year-of-variant-introduction": "1944" + } + }], + "IFV BMD-1": [{ + "default": { + "name": "BMD-1", + "country-of-origin": "USSR/Russia", + "manufacturer": "Volgograd", + "role": "Airborne Amphibious Infantry Fighting Vehicle", + "year-of-variant-introduction": "1969" + } + }], + "IFV BMP-1": [{ + "default": { + "name": "BMP-1", + "country-of-origin": "USSR/Russia", + "manufacturer": "Kurganmashzavod", + "role": "Amphibious Infantry Fighting Vehicle", + "year-of-variant-introduction": "1966" + } + }], + "IFV BMP-2": [{ + "default": { + "name": "BMP-2", + "country-of-origin": "USSR/Russia", + "manufacturer": "Kurganmashzavod", + "role": "Amphibious Infantry Fighting Vehicle", + "year-of-variant-introduction": "1980" + } + }], + "IFV BMP-3": [{ + "default": { + "name": "BMP-3", + "country-of-origin": "USSR/Russia", + "manufacturer": "Kurganmashzavod", + "role": "Amphibious Infantry Fighting Vehicle", + "year-of-variant-introduction": "1987" + } + }], + "IFV LAV-25": [{ + "default": { + "name": "LAV-25", + "country-of-origin": "USA", + "manufacturer": "General Dynamics", + "role": "Amphibious Armoured Car", + "year-of-variant-introduction": "1983" + } + }], + "IFV M2A2 Bradley": [{ + "default": { + "name": "M2A2 Bradley", + "country-of-origin": "USA", + "manufacturer": "United Defense", + "role": "Infantry Fighting Vehicle", + "year-of-variant-introduction": "1988" + } + }], + "IFV Marder": [{ + "default": { + "name": "Marder 1A3", + "country-of-origin": "Germany", + "manufacturer": "Rheinmetall Landsysteme", + "role": "Infantry Fighting Vehicle", + "year-of-variant-introduction": "1988" + } + }], + "IFV MCV-80": [{ + "default": { + "name": "FV510 Warrior", + "country-of-origin": "UK", + "manufacturer": "GKN Sankey", + "role": "Infantry Fighting Vehicle", + "year-of-variant-introduction": "1984" + } + }], + "LAC M8 Greyhound": [{ + "default": { + "name": "M8 Greyhound Light Armored Car", + "country-of-origin": "USA", + "manufacturer": "Ford Motor Company", + "role": "Light Armoured Car", + "year-of-variant-introduction": "1943" + } + }], + "LT Mk VII Tetrarch": [{ + "default": { + "name": "A17 Light Tank Mk VII Tetrarch", + "country-of-origin": "UK", + "manufacturer": "Vickers-Armstrongs", + "role": "Airborne Light Tank", + "year-of-variant-introduction": "1938" + } + }], + "M4 Tractor": [{ + "default": { + "name": "M4 High-Speed Tractor", + "country-of-origin": "USA", + "manufacturer": "Allis-Chalmers", + "role": "Tracked Cargo Transporter", + "year-of-variant-introduction": "1943" + } + }], + "M30 Cargo Carrier": [{ + "default": { + "name": "M30 Cargo Carrier", + "country-of-origin": "USA", + "manufacturer": "Pressed Steel Car Company", + "role": "Tracked Cargo Transporter", + "year-of-variant-introduction": "1942" + } + }], + "MBT Challenger II": [{ + "default": { + "name": "FV4034 Challenger 2", + "country-of-origin": "UK", + "manufacturer": "Vickers Defence Systems", + "role": "Main Battle Tank", + "year-of-variant-introduction": "1998" + } + }], + "M12 GMC": [{ + "default": { + "name": "M12 Gun Motor Carriage", + "country-of-origin": "USA", + "manufacturer": "Pressed Steel Car Company", + "role": "Self-Propelled Gun", + "year-of-variant-introduction": "1942" + } + }], + "MBT Leclerc": [{ + "default": { + "name": "Leclerc Séries 2", + "country-of-origin": "France", + "manufacturer": "GIAT Industries", + "role": "Main Battle Tank", + "year-of-variant-introduction": "1993" + } + }], + "MBT Leopard 1A3": [{ + "default": { + "name": "Leopard 1A3", + "country-of-origin": "Germany", + "manufacturer": "Krauss-Maffei", + "role": "Main Battle Tank", + "year-of-variant-introduction": "1974" + } + }], + "MBT Leopard-2": [{ + "default": { + "name": "Leopard 2A6", + "country-of-origin": "Germany", + "manufacturer": "Krauss-Maffei", + "role": "Main Battle Tank", + "year-of-variant-introduction": "2001" + } + }], + "MBT M1A2 Abrams": [{ + "default": { + "name": "M1A2 Abrams", + "country-of-origin": "USA", + "manufacturer": "General Dynamics", + "role": "Main Battle Tank", + "year-of-variant-introduction": "1992" + } + }], + "MBT M60A3 Patton": [{ + "default": { + "name": "M60A3 \"Patton\"", + "country-of-origin": "USA", + "manufacturer": "General Dynamics", + "role": "Main Battle Tank", + "year-of-variant-introduction": "1960" + } + }], + "MBT Merkava Mk. 4": [{ + "default": { + "name": "Merkava Mk IV", + "country-of-origin": "Israel", + "manufacturer": "MANTAK", + "role": "Main Battle Tank", + "year-of-variant-introduction": "2004" + } + }], + "MBT T-55": [{ + "default": { + "name": "T-55A", + "country-of-origin": "USSR/Russia", + "manufacturer": "Kharkiv/UralVagonZavod", + "role": "Main Battle Tank", + "year-of-variant-introduction": "1963" + } + }], + "MBT T-72B": [{ + "default": { + "name": "T-72B with Kontakt-1 ERA", + "country-of-origin": "USSR/Russia", + "manufacturer": "UralVagonZavod", + "role": "Main Battle Tank", + "year-of-variant-introduction": "1985" + } + }], + "MBT T-72B3": [{ + "default": { + "name": "T-72B3 model 2011", + "country-of-origin": "USSR/Russia", + "manufacturer": "UralVagonZavod", + "role": "Main Battle Tank", + "year-of-variant-introduction": "2010" + } + }], + "MBT T-80U": [{ + "default": { + "name": "T-80U", + "country-of-origin": "USSR/Russia", + "manufacturer": "Omsk Transmash", + "role": "Main Battle Tank", + "year-of-variant-introduction": "1985" + } + }], + "MBT T-90": [{ + "default": { + "name": "T-90A", + "country-of-origin": "Russia", + "manufacturer": "UralVagonZavod", + "role": "Main Battle Tank", + "year-of-variant-introduction": "2004" + } + }], + "MLRS 9A52 Smerch": [{ + "default": { + "name": "BM-30 Smerch (9M55K Cluster Rockets)", + "country-of-origin": "USSR/Russia", + "manufacturer": "Splav", + "role": "Multiple-Launch Rocket System", + "year-of-variant-introduction": "1989" + } + }], + "MLRS 9A52 Smerch HE": [{ + "default": { + "name": "BM-30 Smerch (9M55K5 HE Rockets)", + "country-of-origin": "USSR/Russia", + "manufacturer": "Splav", + "role": "Multiple-Launch Rocket System", + "year-of-variant-introduction": "1989" + } + }], + "MLRS 9K57 Uragan BM-27": [{ + "default": { + "name": "BM-27 Uragan", + "country-of-origin": "USSR/Russia", + "manufacturer": "Splav", + "role": "Multiple-Launch Rocket System", + "year-of-variant-introduction": "1975" + } + }], + "MLRS BM-21 Grad": [{ + "default": { + "name": "BM-21 Grad", + "country-of-origin": "USSR/Russia", + "manufacturer": "Splav", + "role": "Multiple-Launch Rocket System", + "year-of-variant-introduction": "1963" + } + }], + "MLRS FDDM": [{ + "default": { + "name": "M1043 HMMWV with Fire Direction Data Manager", + "country-of-origin": "USA", + "role": "Fire Control Vehicle", + "year-of-variant-introduction": "1993" + } + }], + "MLRS M270": [{ + "default": { + "name": "M270 Multiple Launch Rocket System", + "country-of-origin": "USA", + "manufacturer": "Vought", + "role": "Multiple-Launch Rocket System", + "year-of-variant-introduction": "1983" + } + }], + "MT M4 Sherman": [{ + "default": { + "name": "M4A2(75) Sherman", + "country-of-origin": "USA", + "manufacturer": "Fisher", + "role": "Medium Tank", + "year-of-variant-introduction": "1942" + }, + "UK": { + "name": "Sherman III" + } + }], + "MT M4A4 Sherman Firefly": [{ + "default": { + "name": "M4A4 Sherman Firefly", + "country-of-origin": "USA/UK", + "manufacturer": "Chrysler", + "role": "Medium Tank", + "year-of-variant-introduction": "1943" + }, + "UK": { + "name": "Sherman Firefly VC" + } + }], + "MT Pz.Kpfw.IV Ausf.H": [{ + "default": { + "name": "Panzerkampfwagen IV Ausf. H", + "country-of-origin": "Germany", + "manufacturer": "Krupp-Gruson/Vomag/Nibelungenwerke", + "role": "Medium Tank", + "year-of-variant-introduction": "1943" + } + }], + "MT Pz.Kpfw.V Panther Ausf.G": [{ + "default": { + "name": "Panzerkampfwagen V Panther Ausf. G", + "country-of-origin": "Germany", + "manufacturer": "MAN/Daimler-Benz/MNH", + "role": "Medium Tank", + "year-of-variant-introduction": "1944" + } + }], + "SAM Avenger M1097": [{ + "default": { + "name": "M1097 Heavy HMMWV Avenger", + "country-of-origin": "USA", + "manufacturer": "Boeing", + "role": "Self-Propelled Surface-to-Air Missile Launcher", + "year-of-variant-introduction": "1990" + } + }], + "SAM Chaparral M48": [{ + "default": { + "name": "M48 Chaparral", + "country-of-origin": "USA", + "manufacturer": "Ford Motor Company", + "role": "Self-Propelled Surface-to-Air Missile Launcher", + "year-of-variant-introduction": "1969" + } + }], + "SAM Linebacker M6": [{ + "default": { + "name": "M6 Linebacker", + "country-of-origin": "USA", + "manufacturer": "United Defense", + "role": "Self-Propelled Anti-Aircraft System", + "year-of-variant-introduction": "1997" + } + }], + "SAM Roland ADS": [{ + "default": { + "name": "Roland 2 (Marder Chassis)", + "country-of-origin": "France/Germany", + "manufacturer": "Euromissile", + "role": "Self-Propelled Surface-to-Air Missile Launcher", + "year-of-variant-introduction": "1981" + } + }], + "SAM SA-13 Strela-10M3 9A35M3": [{ + "default": { + "name": "SA-13 Gopher (9K35 Strela-10M3)", + "country-of-origin": "USSR/Russia", + "manufacturer": "Ulyanovsk", + "role": "Self-Propelled Surface-to-Air Missile Launcher", + "year-of-variant-introduction": "1981" + } + }], + "SAM SA-19 Tunguska 2S6": [{ + "default": { + "name": "SA-19 Grison (2K22 Tunguska)", + "country-of-origin": "USSR/Russia", + "manufacturer": "Ulyanovsk", + "role": "Self-Propelled Anti-Aircraft System", + "year-of-variant-introduction": "1982" + } + }], + "Sd.Kfz.184 Elefant": [{ + "default": { + "name": "Sd.Kfz.184 Elefant", + "country-of-origin": "Germany", + "manufacturer": "Porsche/Nibelungenwerk", + "role": "Tank Destroyer", + "year-of-variant-introduction": "1944" + } + }], + "SPAAA Gepard": [{ + "default": { + "name": "Flakpanzer Gepard", + "country-of-origin": "West Germany", + "manufacturer": "Krauss-Maffei", + "role": "Self-Propelled Anti-Aircraft Gun", + "year-of-variant-introduction": "1976" + } + }], + "SPAAA ZSU-23-4 Shilka": [{ + "default": { + "name": "ZSU-23-4 Shilka", + "country-of-origin": "USSR/Russia", + "manufacturer": "MMZ", + "role": "Self-Propelled Anti-Aircraft Gun", + "year-of-variant-introduction": "1960" + } + }], + "SPG M1128 Stryker MGS": [{ + "default": { + "name": "M1128 Stryker Mobile Gun System", + "country-of-origin": "USA", + "manufacturer": "General Dynamics", + "role": "Armoured Car", + "year-of-variant-introduction": "2006" + } + }], + "SpGH Dana": [{ + "default": { + "name": "SpGH DANA", + "country-of-origin": "Czechoslovakia", + "manufacturer": "ZTS", + "role": "Self-Propelled Gun", + "year-of-variant-introduction": "1980" + } + }], + "SPH 2S9 Nona": [{ + "default": { + "name": "2S9 Nona-S", + "country-of-origin": "USSR/Russia", + "manufacturer": "Motovilikha", + "role": "Self-Propelled Mortar", + "year-of-variant-introduction": "1981" + } + }], + "SPH 2S3 Akatsia": [{ + "default": { + "name": "2S3 Akatsiya", + "country-of-origin": "USSR/Russia", + "manufacturer": "Uraltransmash", + "role": "Self-Propelled Gun", + "year-of-variant-introduction": "1971" + } + }], + "SPH 2S1 Gvozdika": [{ + "default": { + "name": "2S1 Gvozdika", + "country-of-origin": "USSR/Russia", + "manufacturer": "Kharkiv", + "role": "Self-Propelled Gun", + "year-of-variant-introduction": "1972" + } + }], + "SPH 2S19 Msta": [{ + "default": { + "name": "2S19 Msta-S", + "country-of-origin": "USSR/Russia", + "manufacturer": "Uraltransmash", + "role": "Self-Propelled Gun", + "year-of-variant-introduction": "1989" + } + }], + "SPH M109 Paladin": [{ + "default": { + "name": "M109A6 Paladin", + "country-of-origin": "USA", + "manufacturer": "United Defense", + "role": "Self-Propelled Gun", + "year-of-variant-introduction": "1994" + } + }], + "StuG III Ausf. G": [{ + "default": { + "name": "Sturmgeschütz III Ausf. G", + "country-of-origin": "Germany", + "manufacturer": "Alkett/MIAG", + "role": "Assault Gun/Tank Destroyer", + "year-of-variant-introduction": "1942" + } + }], + "StuG IV": [{ + "default": { + "name": "Sturmgeschütz IV", + "country-of-origin": "Germany", + "manufacturer": "Krupp", + "role": "Assault Gun/Tank Destroyer", + "year-of-variant-introduction": "1943" + } + }], + "Sturmpanzer IV Brummbär": [{ + "default": { + "name": "Sturmpanzer IV Brummbär", + "country-of-origin": "Germany", + "manufacturer": "Vienna Arsenal", + "role": "Self-Propelled Gun", + "year-of-variant-introduction": "1943" + } + }], + "TD Jagdpanther G1": [{ + "default": { + "name": "Jagdpanther G1", + "country-of-origin": "Germany", + "manufacturer": "MIAG/MNH/MBA", + "role": "Tank Destroyer", + "year-of-variant-introduction": "1944" + } + }], + "TD Jagdpanzer IV": [{ + "default": { + "name": "Jagdpanzer IV", + "country-of-origin": "Germany", + "manufacturer": "Vomag", + "role": "Tank Destroyer", + "year-of-variant-introduction": "1944" + } + }], + "TD M10 GMC": [{ + "default": { + "name": "M10 3-inch Gun Motor Carriage", + "country-of-origin": "USA", + "manufacturer": "Fisher/Ford Motor Company", + "role": "Tank Destroyer", + "year-of-variant-introduction": "1942" + }, + "UK": { + "name": "3in SPM M10 Achilles Mk II" + } + }], + "TPz Fuchs": [{ + "default": { + "name": "TPz Fuchs", + "country-of-origin": "Germany", + "manufacturer": "Rheinstahl Wehrtechnik", + "role": "Armoured Personnel Carrier", + "year-of-variant-introduction": "1979" + } + }], + "ZTZ-96B": [{ + "default": { + "name": "Type 96B (ZTZ-96B)", + "country-of-origin": "China", + "manufacturer": "First Inner Mongolia Machinery Factory", + "role": "Main Battle Tank", + "year-of-variant-introduction": "1979" + } + }], + "ZBD-04A": [{ + "default": { + "name": "Type 04A (ZBD-04A)", + "country-of-origin": "China", + "manufacturer": "Norinco", + "role": "Main Battle Tank", + "year-of-variant-introduction": "1999" + } + }] +}