Compare commits

...

414 Commits
2.3.0 ... 2.4.0

Author SHA1 Message Date
Malakhit
c69e5e05a3 Merge pull request #841 from Khopa/develop_2_4_x
2.4.0 Release
2021-02-05 18:46:23 +00:00
Dan Albert
845e7fb956 Highlight major areas of change in the changelog.
The list is getting too long to be easily scannable for big changes :)
2021-01-31 14:52:16 -08:00
Khopa
f5cc2c3a37 Removed Western georgia campaign 2021-01-31 23:44:49 +01:00
Dan Albert
d56c2f7a50 Allow editing the arrival/diver airfield.
Fixes https://github.com/Khopa/dcs_liberation/issues/810
2021-01-31 14:26:14 -08:00
Dan Albert
5c47a8f7e1 Fix flyover waypoint generation.
Fixes https://github.com/Khopa/dcs_liberation/issues/820
2021-01-31 13:56:48 -08:00
Simon Clark
fb724e0150 Missed the bofors.
Also removes cargo carrier from factions.
2021-01-31 21:46:26 +00:00
Dan Albert
94ef47c89d Remove incomplete feature from changelog.
https://github.com/Khopa/dcs_liberation/issues/511 isn't done yet, and
as-is the system does barely anything, so not worth mentioning.
2021-01-31 13:26:29 -08:00
Dan Albert
7e415b3fd7 Fix carrier packages at beginning of campaign.
Fixes https://github.com/Khopa/dcs_liberation/issues/819
2021-01-31 13:21:34 -08:00
Simon Clark
fee497219e FInal set of vehicles, I think? 2021-01-31 20:23:58 +00:00
Simon Clark
7624f09f98 A-20G is good at anti-ship. 2021-01-31 17:09:19 +00:00
Simon Clark
946fe0a94c Added a lot of tanks to the unit info list. A lot of tanks. 2021-01-31 16:55:17 +00:00
Khopa
c7e1699546 SEAD/DEAD loadout for Harrier is using 4 AGM65-F + 2 Sidearm 2021-01-31 17:14:40 +01:00
Khopa
de4a617743 Fixed AV8B Harrier SEAD loadout contains invalid ordnance (AGM-65G) Replace by AGM-65F 2021-01-31 17:11:24 +01:00
Simon Clark
228d62dd32 Duplicates. 2021-01-31 12:15:51 +00:00
Simon Clark
51f8a60096 Add strategic bombers to DEAD/Strike lists. 2021-01-31 12:15:17 +00:00
Simon Clark
83f1a95966 Tweak the offset heading to 2 degrees, one degree doesn't always seem to be enough. 2021-01-31 10:54:29 +00:00
Dan Albert
646ba94d10 Make UI update faster on TOT change.
Fixes https://github.com/Khopa/dcs_liberation/issues/815
2021-01-30 20:25:34 -08:00
Dan Albert
8c5c08d678 Don't show income for dead buildings on the map. 2021-01-30 18:27:07 -08:00
Dan Albert
3d0b47a181 Fix exception in building debrief.
There are always a bunch of integer dead ground units. Not sure what
they are.
2021-01-30 18:24:25 -08:00
Dan Albert
ea7bece3b8 Display the TGO's income, not the CPs. 2021-01-30 17:23:18 -08:00
Dan Albert
944a8e9cd6 Allow selecting start type when creating flights. 2021-01-30 16:16:04 -08:00
Simon Clark
88f7d1d572 Changelog. 2021-01-31 00:14:53 +00:00
Simon Clark
6d78c1e302 Round budget display income figure to 2DP.
Fixes #809
2021-01-31 00:13:10 +00:00
Simon Clark
791aa8b6d1 Fix the powerplant object template.
For some reason, the large building with the tank on is rotated 180 degrees when put into an actual mission. Therefore, I've just spun it around to counter this.
2021-01-30 23:46:15 +00:00
Dan Albert
a078b67b36 Show arrival airfield in the flight list.
Fixes https://github.com/Khopa/dcs_liberation/issues/798.
2021-01-30 15:14:05 -08:00
Dan Albert
34d4ecd4e6 Add an option for default start type.
Changing this completely breaks OCA/Aircraft missions, but if the player
doesn't care about those this can reduce airfield congestion. The UI
warns about this.

This also makes the AI start type selectable in the flight UI.

Fixes https://github.com/Khopa/dcs_liberation/issues/387
Fixes https://github.com/Khopa/dcs_liberation/issues/729
2021-01-30 15:04:23 -08:00
Dan Albert
5047b535c4 Make ASAP a checkbox and maintain ASAP on changes.
Fixes https://github.com/Khopa/dcs_liberation/issues/642
2021-01-30 13:33:39 -08:00
Simon Clark
768d239840 Adds missile sites to the do-not-cull list.
Contributes-to: #700
2021-01-30 18:34:57 +00:00
Simon Clark
89392553bd Adds a new icon for AA sites with no threat.
Also adds the logic to check for this state.

Contributes-to: #239
2021-01-30 18:08:17 +00:00
Simon Clark
9a41217a59 Removes control point name dash->space replace.
This was breaking runway destruction. This seems to have been a legacy change from 1c67a2e4cf (diff-4c34a7844594e282145481e501ff81d198e869aad558b3589fbc1e140625ea03R20)

Fixes #741
2021-01-30 15:50:13 +00:00
Simon Clark
f6557e4980 Changelog. 2021-01-30 15:03:17 +00:00
Simon Clark
cc3cd95e2d Test for unit_name + " object" in the debrief.
This is because all building objects have " object" appended to the end in the unit map, but not in the state.json. Therefore, we should now find destroyed building objects in the unit map.

Should fix #793.
2021-01-30 15:00:40 +00:00
Simon Clark
8fb02136bb Changelog. 2021-01-30 13:38:55 +00:00
Simon Clark
1c86585f03 Make certain stances use slightly offset headings.
There's a DCS bug where vehicles sometimes don't move if they don't have to change heading. This is a bit of a hacky workaround for that.

Should fix #797.
2021-01-30 13:35:22 +00:00
Simon Clark
242085966b Changelog. 2021-01-30 01:14:11 +00:00
Simon Clark
6217075adc Add mission planning docs link to TOT field.
Completes: #696
2021-01-30 01:12:59 +00:00
Simon Clark
883f233c09 Adds WIP handling for default payload display with restricted date feature enabled. 2021-01-30 01:00:40 +00:00
Khopa
e4cc749180 Changelog update 2021-01-30 01:33:36 +01:00
Khopa
9780798b75 Merge remote-tracking branch 'khopa/develop' into develop 2021-01-30 01:30:00 +01:00
Khopa
225325cc29 High Digit SAM mod support added with an example factions (Russia 2010 High Digit SAM) 2021-01-30 01:28:47 +01:00
Simon Clark
2e3307065c Changelog. 2021-01-30 00:22:37 +00:00
Simon Clark
f6a4316093 Merge branch 'develop' of https://github.com/Khopa/dcs_liberation into develop 2021-01-30 00:19:38 +00:00
Simon Clark
b80aad7449 Link custom theater/faction/loadout docs in UI.
Contributes-to: #614
2021-01-30 00:19:29 +00:00
Khopa
5dbd1d093b High digit sam pydcs export update 2021-01-30 00:33:26 +01:00
Simon Clark
64e2e1109e Add the two Redfor factions from @RobertPeary.
Contributes-to: #488
2021-01-29 21:42:34 +00:00
Simon Clark
9c60140cec Changelog and Hercules radio fix.
Contributes-to: #782
2021-01-29 20:43:19 +00:00
Simon Clark
6099b664ac Updates Hercules Cargo file.
Fixes #782.
2021-01-29 20:36:05 +00:00
Simon Clark
33d084eff0 Add required mod link for USA C-130 faction.
Fixes #803.
2021-01-29 20:32:59 +00:00
Simon Clark
07eb14eaa6 Final aircraft banner until I download some mods. 2021-01-29 20:25:24 +00:00
Simon Clark
b2710fafd4 More banners.
Fixed a few typos.
2021-01-29 20:16:10 +00:00
Simon Clark
8f44b4571a Another batch of banners.
Also removes unarmed trainers.
2021-01-29 19:17:27 +00:00
Simon Clark
169f010fae Bunch more banner images. 2021-01-29 17:33:19 +00:00
Khopa
3e1547e0da Harrier default CAS loadout has APKWS 2021-01-27 22:29:43 +01:00
Khopa
825b9935ee Merge remote-tracking branch 'khopa/develop' into develop 2021-01-27 22:26:14 +01:00
Khopa
2f2e086fbb Using new pydcs export 2021-01-27 22:25:26 +01:00
Simon Clark
2f6e0c15fe Unit detail updates 2021-01-26 16:14:58 +00:00
Simon Clark
09e0c3dd63 Made the description text message a bit clearer. 2021-01-24 17:18:45 +00:00
Simon Clark
4529ac9b92 More banner images. 2021-01-24 16:43:08 +00:00
Dan Albert
8dac4eca55 Don't plan strikes against non-strike targets.
FOB strucutres can't be repaired and the player can't target them.

SAMs are targeted by DEAD already, so considering them for strike double
plans the mission.

Fixes https://github.com/Khopa/dcs_liberation/issues/686
2021-01-23 14:29:10 -08:00
Dan Albert
bf56091dc7 Fix inverted condition for building waypoints.
Fixes https://github.com/Khopa/dcs_liberation/issues/548
2021-01-23 14:10:25 -08:00
Khopa
f10f580f1c Fixed factions descriptions not being updated after auto-selection of factions. 2021-01-23 00:33:07 +01:00
Khopa
c09861d1ca Changelog update 2021-01-22 23:51:17 +01:00
Khopa
8e8df2b846 Changelog update 2021-01-22 23:51:03 +01:00
Khopa
d4a1d5bb9e Added campaign "Exercise Vegas Nerve" by Starfire 2021-01-22 23:49:03 +01:00
Khopa
84145aa7a7 Added Black Sea Lite campaign by Starfire. 2021-01-22 23:42:48 +01:00
Khopa
25f32a4776 New game wizard : Added default factions in description 2021-01-22 23:41:12 +01:00
Khopa
097c42d1dd New game wizard : Added performance information about the selected campaigns. 2021-01-22 23:07:17 +01:00
Khopa
91ac368a19 Changelog update 2021-01-22 22:11:49 +01:00
Khopa
f959dd0519 Campaign have a recommended defaults faction setup to ease campaign setup 2021-01-22 22:09:55 +01:00
Simon Clark
444605920f Adds F-14 to the DEAD list (TALD pods are cool) 2021-01-22 15:41:42 +00:00
Simon Clark
a102d8b39f Change case and add CR2. 2021-01-21 18:47:26 +00:00
Simon Clark
22eb861d28 More banner images. 2021-01-21 18:41:19 +00:00
Simon Clark
779b36bf7b The M163 Vulcan is now American again. 2021-01-20 18:07:16 +00:00
Simon Clark
f36336403b A few more ground units. The list goes on forever.
Next one to do is the TPz Fuchs.
2021-01-19 21:32:11 +00:00
Simon Clark
b7fbade968 More work on the unit info screen. 2021-01-19 17:53:55 +00:00
Simon Clark
0535b20db7 Another batch of screenshots. 2021-01-19 12:05:08 +00:00
Simon Clark
ddd91e3078 _24ed again. 2021-01-18 22:47:06 +00:00
Simon Clark
97f734b8fc Rename the B-1 image. 2021-01-18 22:46:12 +00:00
Simon Clark
38941f02a8 Add some initial images for the unit info pages.
This was pretty fun! DCS is very photogenic given the right conditions...
2021-01-18 22:44:34 +00:00
Simon Clark
84e09be199 Add aircraft manufacturer and role to info window. 2021-01-18 20:13:51 +00:00
Simon Clark
995a89d370 Add initial version of the unit info window.
DCS features a massive range of aircraft and land vehicles, and not all of them make their role(s) clear just from the name alone. What this commit does is add an "information" button (and resultant window) to the recruitment section. This should allow new players to understand what each unit is/does.

Current state - every aircraft has a country of origin and an introduction date for that variant. Some also have a small placeholder description, taken from ED's store page for that aircraft. There is also a placeholder picture (taken from a rejected image from my own personal photography) that will, in time, show a banner image of each unit.

Todo - add appropriate screenshots for each aircraft's banner, replace the placeholder text for each aircraft (this will take a while...) and add more data points for each unit type, such as a unit role (i.e. "air-superiority fighter", "multirole fighter", etc) or perhaps a list of weapons carried. I also haven't made a start on the huge number of ground units yet.
2021-01-18 19:27:54 +00:00
Simon Clark
6f11a269bc Add a few more pretty names. 2021-01-18 12:27:55 +00:00
Simon Clark
24a212a987 Make the C-130 work. For real this time.
Also separate out SEAD and DEAD taskings. Some aircraft can DEAD but not SEAD.

Also make the recruitment menu use the pretty names in the alphabetical sort func.
2021-01-17 21:41:02 +00:00
Simon Clark
3282ba0302 Hercules mod has AC-130s, so CAS makes sense too. 2021-01-17 21:10:14 +00:00
Simon Clark
a4db443f93 Add the Hercules to strike tasks.
It's not strike capable per se, but can transport objects for a strike.
2021-01-17 14:56:36 +00:00
Dan Albert
f8276f7e59 Don't defend against air threats from FOBs.
FOBs (and broken runways) do not need to be considered as sources of air
threats.

Fixes https://github.com/Khopa/dcs_liberation/issues/778
2021-01-17 01:17:33 -08:00
Dan Albert
b545634d87 Revert accidental display options default change. 2021-01-16 15:30:54 -08:00
Dan Albert
5da4cace94 Don't plan BARCAPs so aggressively.
Limit the commit range of a BARCAP to halfway to the closest enemy
airbase so that they don't become offensive missions.

This has the side effect of largely reducing long retreats to hold
points from front line airfields, since the package can get much closer
without being at risk of engagement by an enemy BARCAP.

Fixes https://github.com/Khopa/dcs_liberation/issues/742.
2021-01-16 15:26:25 -08:00
Dan Albert
1a2475dc25 Add display option for BARCAP commit range. 2021-01-16 15:15:40 -08:00
Dan Albert
2374239238 Fix doctrine inversion in threat zones.
The threat zone is the zone project by the given coaltition, not against
it.
2021-01-16 14:01:53 -08:00
Simon Clark
f84d77d334 Fix startup faction error. 2021-01-14 15:30:08 +00:00
Simon Clark
5a275e6153 Missed some subs. 2021-01-13 20:45:59 +00:00
Simon Clark
5f07069f1d Remove submarines for the time being.
They definitely feel too much like placeholders at this point.
They should be removed until they're in a better state.

https://forums.eagle.ru/topic/251238-submarines is a good read on the topic.
2021-01-13 19:52:35 +00:00
Simon Clark
1e1cebc3fc Display a "No aircraft available" message.
If in the create flight dialog, there are no suitable aircraft for a task, or no aircraft left at all, a suitable message is now shown that prevents the user from creating a flight.

Also adds in a quick "remember what plane the user had selected last" feature.
2021-01-13 12:11:53 +00:00
Simon Clark
c40ad75fa2 Add DEAD to the selectable aircraft list. 2021-01-13 10:37:05 +00:00
Simon Clark
c80db72bf7 Changelog. 2021-01-12 19:03:02 +00:00
Simon Clark
727ec6bc28 Filter mission types by aircraft.
First stab at implementing #392.
You can now only select aircraft types that can do the selected task.
2021-01-12 18:58:29 +00:00
Simon Clark
9ebad734a9 Changelog. 2021-01-12 17:30:15 +00:00
Simon Clark
0094628f6b Add the S-3B Viking into the game.
It's a carrier-based strike/anti-ship platform. What's not to love?

Completes #759.
2021-01-12 14:57:29 +00:00
Simon Clark
abbb046566 Add the SH-60B Seahawk into the game.
It was requested in #759, and also carries the Penguin AShM, which could be fun.
2021-01-12 12:27:31 +00:00
Dan Albert
a1136953d0 Fix custom waypoints. 2021-01-11 20:16:23 -08:00
Dan Albert
3298a5c6ad Improve front line flight plans.
Fixes https://github.com/Khopa/dcs_liberation/issues/462
2021-01-10 15:03:42 -08:00
Simon Clark
5e24fe9bb1 Poland is now Poland again.
The Mission Editor can be tricked into giving factions any unit.
Therefore, Poland can be Poland again, and not CJTFB.
2021-01-10 13:16:40 +00:00
Simon Clark
bc5b32ddef Changelog. 2021-01-10 13:09:28 +00:00
Simon Clark
7269dbb79d Edit Flight window is now dynamically sized.
This means that the waypoint names no longer get cut off.

Contributes-to: Khopa/dcs_liberation#754
2021-01-10 13:07:02 +00:00
Simon Clark
6f5bb6ffa2 Fix HQ7 generation error. 2021-01-10 11:46:58 +00:00
root0fall
7d0b738918 fix 588 - long waypoint descriptor in kneeboard generation 2021-01-09 23:35:47 -08:00
Dan Albert
1539d9c7ed Add a 1985 East German faction. 2021-01-09 16:02:51 -08:00
Dan Albert
4ae95e06ef Add a 1985 French faction. 2021-01-09 16:02:51 -08:00
Dan Albert
fa1166d014 Limit reserve ground units to 10.
Allowing these to grow infinitely leads to some really weird behaviors
when the enemy has been buying reserves long enough, where capturing a
base might result in 80 enemy vehicles suddenly at the gates.

This is just an interim fix. Ideally these units would be reinforcing
the front line as needed
(https://github.com/Khopa/dcs_liberation/issues/382), and a CP could be
lost without needing to completely destroy the defender.
2021-01-09 13:10:21 -08:00
Dan Albert
2d9e5fe984 Fix handling of destroyed buildings. 2021-01-08 19:21:06 -08:00
Simon Clark
7ae934e940 Make new flight comboboxes auto-adjust their size.
Content was previously being cut off when the first airport selected had a shorter name than the one the player wanted.
2021-01-08 22:52:21 +00:00
Simon Clark
64b2eeface Stop loadout resetting each time the editor opens. 2021-01-08 10:22:56 +00:00
Simon Clark
558dc591a3 Made the payload editor more user-friendly.
A user can now modify the existing default loadout, rather than starting from scratch.

Also adds WIP handling for removed pylons, which looks like it may need some PyDCS work.

Also fixes the F-14B default loadouts for everything OTHER than fighter sweep again.
2021-01-08 09:42:09 +00:00
Simon Clark
0bfd766a0b Make the base capture cheat also toggleable.
Also changelog.
2021-01-07 23:04:33 +00:00
Simon Clark
7741713a7c Makes the base intel window scrollable.
Contributes-to: Khopa/dcs_liberation#691
2021-01-07 22:56:04 +00:00
Khopa
454b540bce 2.3.4 changelog hadn't been added on develop branch. 2021-01-07 21:56:07 +01:00
Simon Clark
591c62b6d5 Make frontline advance/retreat cheats optional.
This can now be toggled on and off in the cheats menu.
2021-01-07 17:46:06 +00:00
Simon Clark
fdb4a7b055 Try to prevent objectives getting the same name.
Fixes Khopa/dcs_liberation#745
2021-01-07 17:22:43 +00:00
Simon Clark
f845ad9b31 Move the OCA payload type from runway->aircraft. 2021-01-07 15:56:00 +00:00
Simon Clark
f5f33ec865 Fixes the F-14B fighter sweep loadout.
The default one just picked two external tanks, so using the CAP one.

Fixes Khopa/dcs_liberation#663
2021-01-07 15:52:24 +00:00
Simon Clark
d35faf15d7 Tuples require a comma after a single item. 2021-01-07 13:42:21 +00:00
Simon Clark
7085bce6d4 Add support for some extra loadout types.
Also fixes the bug that @Starfire13 spotted in Khopa/dcs_liberation#744.
2021-01-07 13:37:16 +00:00
Simon Clark
a81890e844 Changelog. 2021-01-07 12:35:08 +00:00
Simon Clark
8dbec21b02 Adds Iran 1988 Faction.
Also adds the SA342 Gazelle to Iraq, as they used them.

Adds associated pretty names.
2021-01-07 12:34:35 +00:00
Simon Clark
a654c8229a Make the default loadout code more resilient.
It can now cope with the payload overrides defined in the db.
2021-01-07 11:31:46 +00:00
Simon Clark
f2e35c185b Displays the default loadout in the payload UI.
The default loadout is now displayed whenever the custom loadout checkbox is unchecked.

The custom loadout is reset when the custom loadout is checked, to force the user to be prescriptive about the loadout.

Contributes-to: Khopa/dcs_liberation#725
2021-01-07 01:45:45 -08:00
Simon Clark
062c2643ad Adds rockets/hellfires to Apaches where sensible. 2021-01-06 17:18:13 -08:00
Simon Clark
088c7b35ba Adds more strategic & maritime bombers for Russia.
Also adds the prettified name for the Tu-142.
2021-01-06 17:16:03 -08:00
walterroach
ef439a6c42 Merge pull request #736 from SimonC6R/fix-carrier-sam-rings
Carrier group threat rings move with the carrier.
2021-01-06 17:24:15 -06:00
Dan Albert
40956a4042 Merge pull request #738 from SimonC6R/remove-pyotr-velikiy
Remove Pyotr Velikiy from the generator for now.
2021-01-06 12:43:59 -08:00
Simon Clark
8680e90e3b Changelog. 2021-01-06 20:42:10 +00:00
Simon Clark
fac770424c Remove Pyotr Velikiy from the generator for now.
It's just too strong to be killed by a reasonable number of Harpoons. DCS needs a fixed ship damage model first.

Contributes-to: Khopa/dcs_liberation#567
2021-01-06 20:39:50 +00:00
Simon Clark
042be9da6d Carrier group threat rings move with the carrier.
Previously, the individual units in the group were moved, but the ground_unit object was not.

Fixes: Khopa/dcs_liberation#735
2021-01-06 18:58:20 +00:00
Khopa
81b0ea1eef Updated AIM-7 config to account for the Belly and Shoulder versions of the AIM-7 used on the F-14. Also Downgrade AIM-54C to AIM-54A before 86 on the Tomcat. 2021-01-06 01:27:53 +01:00
Khopa
52289d1283 More fallbacks weapons for popular precision air to ground weapons 2021-01-06 00:56:20 +01:00
Khopa
1843d23203 Merge remote-tracking branch 'khopa/develop' into develop 2021-01-06 00:54:42 +01:00
Khopa
1a32fef987 More fallbacks weapons for popular precision air to ground weapons 2021-01-06 00:53:55 +01:00
Simon Clark
c740c8304b Adds prettier user-facing aircraft names. (#726)
This makes the names of the aircraft displayed to the player in the UI more verbose and readable.

It allows allows specific countries to display an aircraft's name differently. An example of this would be the JF-17 Thunder, which is known in China as the FC-1 Fierce Dragon - this now displays correctly in the Liberation UI.
2021-01-05 13:21:38 -08:00
Khopa
c3401d478b Added R-73 -> R60 to weapons fallback data 2021-01-05 21:40:20 +01:00
Khopa
cf583bcd55 Merge branch 'master' into develop
# Conflicts:
#	changelog.md
#	game/db.py
#	game/game.py
#	game/income.py
#	game/theater/theatergroundobject.py
#	game/version.py
#	qt_ui/windows/finances/QFinancesMenu.py
2021-01-05 19:50:29 +01:00
Simon Clark
ef143a7ebb Changelog update. 2021-01-05 03:34:10 -08:00
SimonC6R
7aec483e73 Also add the E-2C to Israel and Japan.
Israel only got it in 1981 but it's that or the E-3A they never used.

Japan never used the E-3A, but did buy the E-2.
2021-01-05 02:36:08 -08:00
SimonC6R
db6a3b9849 Use E-2C Hawkeye for US faction between 1964-1977. 2021-01-05 02:36:08 -08:00
Dan Albert
657c5e1f52 Merge pull request #712 from SimonC6R/add-greece-2005
Adds a 2005-ish Greece faction.
2021-01-05 02:34:52 -08:00
Simon Clark
3b0466d7cb Adds a 2010-ish Poland faction. (#713)
The SU-17M4 represents the remaining Su-22s that Poland still flies.
I've used the Stryker as a replacement for the KTO Rosomak, which was in service as far back as 2007.
There were a few Molniyas still in service as of 2010, namely the Metalowiec and Rolnik, as well as a singular Kilo class.
2021-01-05 02:31:49 -08:00
Dan Albert
8a9177b459 Merge pull request #716 from SimonC6R/sold-aircraft-state
Adds a buffer for sold aircraft/vehicles.
2021-01-05 02:12:49 -08:00
Simon Clark
a0e63511d6 Update Greece faction with comments. 2021-01-05 10:12:46 +00:00
Simon Clark
7c3f7d4b8e Addresses review comments. 2021-01-05 09:42:05 +00:00
Dan Albert
be1062c373 Update era-specific loadout release notes. 2021-01-05 00:54:28 -08:00
Dan Albert
2bd5ab06a7 More weapon data.
https://github.com/Khopa/dcs_liberation/issues/490
2021-01-05 00:43:59 -08:00
Dan Albert
e174c1b147 Fix bug in weapon downgrading.
We want the *first* valid replacement, not the last one :)

https://github.com/Khopa/dcs_liberation/issues/490
2021-01-05 00:26:19 -08:00
Dan Albert
7ef191be2a Add more weapon data.
Finishes AIM-120 and AIM-9X.

https://github.com/Khopa/dcs_liberation/issues/490
2021-01-05 00:25:37 -08:00
SimonC6R
03a29aeedf Type annotations. 2021-01-05 00:31:54 +00:00
SimonC6R
d10b4c1e13 Re-add whitespace. 2021-01-05 00:27:26 +00:00
SimonC6R
ab2046a2c2 Refactor the sell unit changes as requested.
It works more simply now, and also doesn't immediately sell the unit.

Also adds a matching UI dialog popup for selling too many ground units.
2021-01-05 00:26:40 +00:00
Dan Albert
bc6b2e0f3e Add warning for missing weapon data.
These are disabled by default because it would otherwise warn about
nearly every piece of equipment in the game currently.
2021-01-04 15:57:37 -08:00
Dan Albert
746c99ebd6 Update Skynet to 2.0.1.
Fixes https://github.com/Khopa/dcs_liberation/issues/717
2021-01-04 15:36:18 -08:00
Dan Albert
34945e7eba Add date-based loadout restriction.
Follow up work:

* Data entry. I plan to do the air-to-air missiles in the near term. I
  covered some variants of the AIM-120, AIM-7, and AIM-9 here, but there
  are variants of those weapons for each mounting rack that need to be
  done still, as well as all the non-US weapons.
* Arbitrary start dates.

https://github.com/Khopa/dcs_liberation/issues/490
2021-01-04 15:13:40 -08:00
Dan Albert
507b217065 Clean up custom loadout interface.
Wraps the pydcs data in a real type so we don't need to spread the
reflection all over.
2021-01-04 15:13:40 -08:00
Simon Clark
e222f17199 Adds payload and EWRS info for J-11A. (#715)
No loadout had been specified for the J-11A, so I've added one. The J-11A carries a similar loadout to the Su-27, but with R-77s instead of R-27ERs.

Also looks like it was missing from the EWRS details, so I've added it there too.

Addresses Khopa/dcs_liberation#610
Addresses Khopa/dcs_liberation#671
2021-01-04 13:07:07 -08:00
Simon Clark
144cfecc0f Adds the E-2C Hawkeye to the game. (#714)
* Adds the E-2C Hawkeye to the game.

It wasn't being imported from pydcs, and thus wasn't in the list of AWACS aircraft or prices.

Also adds it to the 1985 US Navy list, as that makes sense to do.

Updates .gitignore to ignore my VS Code settings file.

Addresses Khopa/dcs_liberation#709
2021-01-04 13:04:11 -08:00
Simon Clark
64066bfc90 Merge branch 'develop' of https://github.com/Khopa/dcs_liberation into develop 2021-01-04 19:27:36 +00:00
Simon Clark
366ac4ee14 Adds a buffer for sold aircraft/vehicles.
This feature allows you to cancel the sales of aircraft or ground vehicles if needed.

Upon clicking the minus button, a count of sold units will be appended to the unit count. This count responds to both further presses of the minus button, and also to presses of the plus button. No further units will be requested for the next turn until all sold units have been re-bought.

I've tested a bunch of different scenarios with this:

- Selling and rebuying a unit - the budget increases and decreases as expected.

- Selling one unit, buying a unit worth the new player budget, and then trying to rebuy the old unit - the old unit cannot be rebought until the budget has been freed up for it.

- Closing the base window and re-opening it - the sold unit count is retained.

- Ending the turn - the sold unit count is reset back to 0 as expected.

Contributes to Khopa/dcs_liberation#365
2021-01-04 19:27:29 +00:00
walterroach
851984ee66 Revert TGO ID reset - fixes #708 2021-01-04 11:36:40 -06:00
Dan Albert
34bdc0e80b Fix new game creation. 2021-01-01 16:15:43 -08:00
Dan Albert
61d7d5e041 Update minimum version of pyside2.
Fixes https://github.com/Khopa/dcs_liberation/issues/416
2021-01-01 15:50:01 -08:00
Dan Albert
b5278550e7 Retreat air units from captured bases.
If there are not airbases withing ferry range with available parking for
the aircraft then the aircraft will be captured and sold. Otherwise the
aircraft will retreat to the closest available airbase.

Fixes https://github.com/Khopa/dcs_liberation/issues/693
2021-01-01 15:19:16 -08:00
Dan Albert
461635c001 Note pending unit refund in changelog. 2021-01-01 14:46:51 -08:00
Dan Albert
fcb1d8e104 Retreat ground forces from captured bases.
If the captured base has no connection to other friendly objectives (the
base was encircled) then the enemy equipment will be captured and sold.
Otherwise the units will retreat toward connected bases.

Partial fix for https://github.com/Khopa/dcs_liberation/issues/693.
Still need to fix this for air units.
2021-01-01 14:45:43 -08:00
Dan Albert
6cbc2b707a Fix type of Game.budget.
This *is* a float, since the income multiplier is a float. The type
annotations were wrong.
2021-01-01 13:56:33 -08:00
Dan Albert
9671542bdf Decouple unit deliveries and conflict events.
Fixes https://github.com/Khopa/dcs_liberation/issues/692 and lets us
clean up the interface quite a bit.
2021-01-01 13:48:23 -08:00
Dan Albert
de325c1208 Refunding unfulfilled orders on capture.
Fixes https://github.com/Khopa/dcs_liberation/issues/682
2021-01-01 13:23:58 -08:00
Dan Albert
802eff1faa Fix some airfield data in Persian Gulf.
Not sure if this changed or has just always been wrong.
2021-01-01 13:22:33 -08:00
C. Perreau
068f9e42d7 Merge pull request #698 from Khopa/develop_2_3_x
Release 2.3.4
2020-12-31 14:07:30 +01:00
Khopa
4a483c3b27 Changelog 2020-12-31 13:52:02 +01:00
Khopa
e3bd958069 Fixed possible AttributeError when generating missile site fire tasks 2020-12-31 13:28:21 +01:00
Khopa
900cf0a9d0 Update preview version number 2020-12-31 13:16:09 +01:00
C. Perreau
64c424b9a6 Merge pull request #649 from Khopa/develop_2_3_x
Release 2.3.3
2020-12-31 01:26:19 +01:00
Dan Albert
5734c29312 Remove wrong precondition in aircraft procurement.
Those task types aren't correct here (that whole dict probably serves
little purpose now), and the actual unit pool is handled in
_affordable_aircraft_of_types.
2020-12-28 16:06:54 -08:00
walterroach
362caa6ac1 Merge pull request #689 from walterroach/naming
Naming
2020-12-28 17:22:42 -06:00
Dan Albert
b620976b70 Flesh out Desert War IADS, add carriers. 2020-12-28 13:16:39 -08:00
walterroach
daba4ef09e Fix aircraft group IDs not being reproducible. 2020-12-28 14:04:19 -06:00
walterroach
c697a34239 Correctly reset ANIMALS 2020-12-28 11:31:04 -06:00
walterroach
09b7cb3d85 Change naming to static class
This ensures all generators are using the same ID set.
2020-12-28 11:22:28 -06:00
walterroach
d7e48662e0 Add custom flight names
Mildly breaks save compat with 2.3; All existing flight dialogs will be
broken, passing the turn or recreating all the flights in the UI will
allow you to continue
2020-12-28 10:52:25 -06:00
walterroach
9fd5c6f230 Add package info to aircraft names
Makes it easier to identify aircraft for client flights
2020-12-28 09:51:37 -06:00
walterroach
aa7825d4aa Reproducible unit naming
Splits infantry and other unit IDs.

Resets IDs to start from zero at each press of "Takeoff"

Direct access to the the `Game` class IDs is done on the `Operation` class to
preserve save compat
2020-12-28 09:27:33 -06:00
Dan Albert
436725b38e Don't award base capture bonuses.
If anything we should be granting the loser extra income so they can
recover. For now just remove.
2020-12-27 19:52:42 -08:00
Dan Albert
922d935bc1 Tweak default budget and turn 0 allocation.
AI wasn't buying many ground vehicles. Keep the turn 0 air budget about
where it was but bump up the ground budget from ~120M to ~600M.
2020-12-27 19:48:03 -08:00
Dan Albert
3716395453 Don't show FOB structure as a target.
This isn't perfect because the auto planner might still target it. We
need a larger refactoring for target iteration so we don't need to
remember all the special rules at each call site. For now, this restores
the 2.3.2 behavior.

Fixes https://github.com/Khopa/dcs_liberation/issues/681

(cherry picked from commit 69833f66e3)
2020-12-27 19:35:45 -08:00
Dan Albert
69833f66e3 Don't show FOB structure as a target.
This isn't perfect because the auto planner might still target it. We
need a larger refactoring for target iteration so we don't need to
remember all the special rules at each call site. For now, this restores
the 2.3.2 behavior.

Fixes https://github.com/Khopa/dcs_liberation/issues/681
2020-12-27 19:34:10 -08:00
Dan Albert
ec787b913c Use exact name matching when picking targets.
Inexact name matching targets the first group that partially matches the
given group name. aa|71 will match aa|7.

The inexact match that was here was only needed for an early attempt to
use skynet where group names were not used consistently. That's no
longer a problem so we don't need this workaround.

Fixes https://github.com/Khopa/dcs_liberation/issues/676

(cherry picked from commit 89f313295e)
2020-12-27 13:50:55 -08:00
Dan Albert
89f313295e Use exact name matching when picking targets.
Inexact name matching targets the first group that partially matches the
given group name. aa|71 will match aa|7.

The inexact match that was here was only needed for an early attempt to
use skynet where group names were not used consistently. That's no
longer a problem so we don't need this workaround.

Fixes https://github.com/Khopa/dcs_liberation/issues/676
2020-12-27 13:45:53 -08:00
Dan Albert
7bc7a44c72 Allow managing disbanded sites in CPs.
Fixes https://github.com/Khopa/dcs_liberation/issues/679

(cherry picked from commit 317a882386)
2020-12-27 13:09:15 -08:00
Dan Albert
317a882386 Allow managing disbanded sites in CPs.
Fixes https://github.com/Khopa/dcs_liberation/issues/679
2020-12-27 13:08:59 -08:00
Dan Albert
3a9f585b6b Plan multiple CAP rounds per turn.
On station time for CAP is only 30 minutes, so plan three cycles to give
~90 minutes of CAP coverage.

Default starting budget has increased significantly to account for the
greatly increased aircraft needs on turn 1.

Fixes https://github.com/Khopa/dcs_liberation/issues/673
2020-12-26 17:26:07 -08:00
Dan Albert
7bbb1c0822 Don't show repaired TGOs as dead.
We were never resetting the dead state for repaired SAMs. Rather than
tracking that manually, determine liveness from the number of units left
alive.

For building objectives where the group is not assigned to the TGO until
*mission* generation time we still need manual tracking.

(cherry picked from commit d3b1f6110f)
2020-12-26 16:21:31 -08:00
Dan Albert
d3b1f6110f Don't show repaired TGOs as dead.
We were never resetting the dead state for repaired SAMs. Rather than
tracking that manually, determine liveness from the number of units left
alive.

For building objectives where the group is not assigned to the TGO until
*mission* generation time we still need manual tracking.
2020-12-26 16:19:41 -08:00
Dan Albert
a6dc3d2aff Handle threat/detection per group.
Some SAMs have multiple groups (such as an SA-10 group with its
accompanying SA-15 and SA-19 groups). This shows each group's threat and
detection separately on the map, and also makes it so that an SA-10 with
dead radars will no longer contribute to the threat zone just because
the shilka next to it still has a functioning radar.

https://github.com/Khopa/dcs_liberation/issues/647
Fixes https://github.com/Khopa/dcs_liberation/issues/672
2020-12-26 15:52:36 -08:00
Dan Albert
d946a9e526 Don't count blind SAMs as threats.
https://github.com/Khopa/dcs_liberation/issues/647
2020-12-26 15:40:38 -08:00
Dan Albert
17dd1b193e Remove aa_ranges in favor of using the TGO data. 2020-12-26 15:25:23 -08:00
Dan Albert
d634fd3236 Adjust income based on control point type.
* Navies and off map spawns generate no income
* FOBs generate 10 instead of 20

Fixes https://github.com/Khopa/dcs_liberation/issues/662
2020-12-26 15:08:57 -08:00
Dan Albert
e861e5b3d6 Even out player and opfor income rules.
Should help fix the anemic enemy forces after the first few turns.
2020-12-26 14:35:57 -08:00
Dan Albert
6045f4dd91 Fix New Package for naval control points.
Also reordered the tasks so ship-specific tasks appear first.

Fixes https://github.com/Khopa/dcs_liberation/issues/628

(cherry picked from commit 8be2841bdf)
2020-12-26 14:30:34 -08:00
Dan Albert
8be2841bdf Fix New Package for naval control points.
Also reordered the tasks so ship-specific tasks appear first.

Fixes https://github.com/Khopa/dcs_liberation/issues/628
2020-12-26 14:30:26 -08:00
Dan Albert
b6e37b9e67 Exclude non-AA groups from SAM threat zones.
Fixes https://github.com/Khopa/dcs_liberation/issues/666
2020-12-26 14:00:31 -08:00
walterroach
0d0d582bd8 Add F-14A-135-GR Icon 2020-12-26 13:24:45 -06:00
walterroach
0c42227e5e Add F-14A-135-GR Icon 2020-12-26 13:23:45 -06:00
Dan Albert
98ac4bd5c8 Fix generator -> group conversion when buying SAM.
Fixes https://github.com/Khopa/dcs_liberation/issues/664
Fixes https://github.com/Khopa/dcs_liberation/issues/665
2020-12-26 02:23:15 -08:00
Dan Albert
a43b100781 Purchase reserves for CAP/CAS.
Next turn's defenses should be planned in preference to expanding
offensive capabilities.

Fixes https://github.com/Khopa/dcs_liberation/issues/511
2020-12-25 19:15:31 -08:00
Dan Albert
c7f9bfbb43 Sell off incomplete opfor squadrons.
Short term fix for https://github.com/Khopa/dcs_liberation/issues/41.
2020-12-25 18:45:58 -08:00
Dan Albert
b5f8e6925b Rank aircraft purchase preferences.
Rather than randomly selecting compatible aircraft for missions, perfer
the *best* aircraft for the job. This removes the "preferred" lists in
favor of sorting the capable lists in priority order. To maintain some
amount of variety the procurer has a 50/50 chance of buying when it
finds a match.

Fixes https://github.com/Khopa/dcs_liberation/issues/510
2020-12-25 18:22:50 -08:00
Dan Albert
993e59413a Add pretty-print for AircraftProcurementRequest. 2020-12-25 17:31:26 -08:00
Dan Albert
9f2fab78a1 Flesh out intel displays.
* Add enemy air and ground unit reports.
* Changes the summary to be a comparison of relative strengths rather
  than raw enemy numbers.

Fixes https://github.com/Khopa/dcs_liberation/issues/658

(cherry picked from commit 3bdf1377c0)
2020-12-25 16:08:10 -08:00
Dan Albert
3bdf1377c0 Flesh out intel displays.
* Add enemy air and ground unit reports.
* Changes the summary to be a comparison of relative strengths rather
  than raw enemy numbers.

Fixes https://github.com/Khopa/dcs_liberation/issues/658
2020-12-25 16:08:02 -08:00
Dan Albert
8f24cf07be Improve layout of intel window.
(cherry picked from commit 1f4516b954)
2020-12-25 16:05:04 -08:00
Dan Albert
17c40234e9 Add basic intel window.
Currently only shows the enemy's economic information.

https://github.com/Khopa/dcs_liberation/issues/658
(cherry picked from commit 1d76ee4871)
2020-12-25 16:04:54 -08:00
Dan Albert
4cecddcdd0 Add basic enemy intel widget.
https://github.com/Khopa/dcs_liberation/issues/658
(cherry picked from commit b53cac4c7a)
2020-12-25 16:04:17 -08:00
Dan Albert
1f4516b954 Improve layout of intel window. 2020-12-25 14:42:04 -08:00
Dan Albert
1d76ee4871 Add basic intel window.
Currently only shows the enemy's economic information.

https://github.com/Khopa/dcs_liberation/issues/658
2020-12-25 03:24:33 -08:00
Dan Albert
b53cac4c7a Add basic enemy intel widget.
https://github.com/Khopa/dcs_liberation/issues/658
2020-12-25 01:38:14 -08:00
Dan Albert
29a0644719 Never generate empty ship groups.
Fixes https://github.com/Khopa/dcs_liberation/issues/391

(cherry picked from commit c833078e71)
2020-12-25 01:35:24 -08:00
Dan Albert
c833078e71 Never generate empty ship groups.
Fixes https://github.com/Khopa/dcs_liberation/issues/391
2020-12-25 01:32:54 -08:00
Dan Albert
e4cba8d19f Mention Skynet PD in changelog. 2020-12-24 19:32:31 -08:00
Dan Albert
cd6620712f Configure skynet point defenses.
Fixes https://github.com/Khopa/dcs_liberation/issues/470
2020-12-24 17:10:32 -08:00
Dan Albert
85619b156d Support groups for SAM templates.
It's only possible to control emissions for the group as a whole, so
Skynet needs PDs to be in separate groups from the main part of the SAM
for PD to operate correctly.

https://github.com/Khopa/dcs_liberation/issues/429
https://github.com/Khopa/dcs_liberation/issues/470
2020-12-24 16:09:42 -08:00
Dan Albert
10debbc286 Constrain front lines better.
Holes in the inclusion zone are defined by exclusion zones, not by holes
in the inclusion zone. Add a cached property for the inclusion zone that
is not also sea or exclusion zone and use that boundary instead.
2020-12-24 13:50:29 -08:00
Khopa
dcac5b488a Changelog update 2020-12-24 16:11:12 +01:00
Khopa
e1009bdafa Fixed ships group that could be replaced by sam site. Removed the possibility to disband ship groups for now. 2020-12-24 16:09:13 +01:00
Khopa
38ce842ca8 Merge remote-tracking branch 'khopa/master' into develop_2_3_x
# Conflicts:
#	changelog.md
#	game/version.py
#	gen/ground_forces/ai_ground_planner.py
#	pydcs
#	resources/factions/iraq_1991.json
#	resources/factions/russia_2010.json
2020-12-24 13:32:14 +01:00
Dan Albert
aafd09569c Fix mypy error. 2020-12-24 02:20:49 -08:00
Dan Albert
67a9df686e Add fast path for NavPoint equality.
Hot method and the FFI costs for comparing the points are not cheap.
2020-12-24 02:06:08 -08:00
Dan Albert
9a374711fd Don't access point coordinates when hashing.
For some reason this is crazy expensive. Turn time goes from 1.7 seconds
to 1 second with this change.
2020-12-24 01:49:07 -08:00
Dan Albert
b9138acbc8 Use shapely projection instead of brute force.
Converts the landmap to use MultiPolygon instead of a collection of
polygons, since Shapely has explicit support for this.

Because we've done that, we can use a single projection from a line
instead of brute forcing the extent of the front line.

This makes turn processing ~66% faster (3 seconds to 1.8).

There are probably other places this should be used.
2020-12-24 01:20:15 -08:00
Dan Albert
2a65916f7c Log time taken in turn processing. 2020-12-24 00:53:36 -08:00
Dan Albert
6aa1f1cca0 Prefer buying aircraft at safe airbases.
Fixes https://github.com/Khopa/dcs_liberation/issues/652
2020-12-23 22:25:47 -08:00
Dan Albert
8c1ebfda02 Show ASAP TOT for debug view flights. 2020-12-23 21:42:13 -08:00
Dan Albert
81af5d7497 Use navmesh to plan strike-like flight plans.
The cases where the target is extremely close to the origin point still
use the old flight plan pattern. This is probably fine.

https://github.com/Khopa/dcs_liberation/issues/292
2020-12-23 21:30:36 -08:00
Khopa
368bf08ade Fixed mypy error 2020-12-24 03:18:53 +01:00
Dan Albert
d95f623ca9 Use navmesh to plan sweep missions.
https://github.com/Khopa/dcs_liberation/issues/292
2020-12-23 18:08:58 -08:00
Dan Albert
2856fbc42b Improve display of nav points in kneeboard. 2020-12-23 17:52:14 -08:00
Dan Albert
ac59e15bd9 Use navmesh to plan CAS and BARCAP.
https://github.com/Khopa/dcs_liberation/issues/292
2020-12-23 17:40:59 -08:00
Dan Albert
91d9bbdc97 Add visual debugging for other fligth plans. 2020-12-23 17:31:04 -08:00
Dan Albert
575f4e1786 Clean up debug display options. 2020-12-23 17:19:57 -08:00
Dan Albert
bff905fae5 Use navmeshes to improve TARCAP flight plans.
Started with TARCAP because they're simple, but will follow and extend
this to the other flight plans next.

This works by building navigation meshes (navmeshes) of the theater
based on the threat regions. A navmesh is created for each faction to
allow the unique pathing around each side's threats. Navmeshes are built
such that there are nav edges around threat zones to allow the planner
to pick waypoints that (slightly) route around threats before
approaching the target.

Using the navmesh, routes are found using A*. Performance appears
adequate, and could probably be improved with a cache if needed since
the small number of origin points means many flights will share portions
of their flight paths.

This adds a few visual debugging tools to the map. They're disabled by
default, but changing the local `debug` variable in `DisplayOptions` to
`True` will make them appear in the display options menu. These are:

* Display navmeshes (red and blue). Displaying either navmesh will draw
  each navmesh polygon on the map view and highlight the mesh that
  contains the cursor. Neighbors are indicated by a small yellow line
  pointing from the center of the polygon's edge/vertext that is shared
  with its neighbor toward the centroid of the zone.
* Shortest path from control point to mouse location. The first control
  point for the selected faction is arbitrarily selected, and the
  shortest path from that control point to the mouse cursor will be
  drawn on the map.
* TARCAP plan near mouse location. A TARCAP will be planned from the
  faction's first control point to the target nearest the mouse cursor.

https://github.com/Khopa/dcs_liberation/issues/292
2020-12-23 17:09:34 -08:00
Khopa
c0fa135bf6 Artillery groups would retreat in the wrong direction - fixed (parameters of the find_retreat_point function are a bit confusing 😕 ) 2020-12-24 02:03:12 +01:00
Khopa
86394d8f19 Artillery groups would retreat in the wrong direction - fixed (parameters of the retreat point function are a bit confusing 😕 ) 2020-12-24 02:02:17 +01:00
Khopa
72c233cb0d Fixed possible assertion error when redeploying units which would lead to ground units not being redeployed. 2020-12-24 01:47:44 +01:00
Khopa
04e2c02eff SCUD missile sites will fire on nearest enemy airport by default 2020-12-24 01:26:00 +01:00
Khopa
7362744df2 Changelog update 2020-12-23 22:15:56 +01:00
Khopa
01951b5c32 Reworked emirates campaign 2020-12-23 21:58:39 +01:00
Khopa
f2f52771bd Removed "broken" midgame setting 2020-12-23 21:37:59 +01:00
Khopa
b59167d3ca Changelog update, WW2 factions can recruit AA/AT guns for frontlines. 2020-12-23 18:21:13 +01:00
Khopa
88e466562c Infantry squads can contain a mortar. 2020-12-23 17:53:52 +01:00
Khopa
1f85e5d7f8 Changelog update 2020-12-23 17:25:15 +01:00
Khopa
50471d510e Fixed and added many ground units icons 2020-12-23 17:24:20 +01:00
Khopa
8b7cf2f725 Changelog update 2020-12-23 01:35:19 +01:00
Khopa
282a5109ba Infantry group are always made of 5 units instead of a random amount. 2020-12-23 01:33:49 +01:00
Khopa
3d3b4738d9 Insurgent hard faction name fixed 2020-12-23 01:31:07 +01:00
Khopa
66149bb591 Fixed error in merge 2020-12-22 23:34:08 +01:00
Khopa
b0ad664ece Merge branch 'develop_2_3_x' into develop
# Conflicts:
#	changelog.md
#	game/procurement.py
#	resources/factions/iraq_1991.json
2020-12-22 23:32:06 +01:00
Khopa
7c29ea836c Infantry is only generated for IFV and APC groups 2020-12-22 23:24:27 +01:00
Khopa
92e9e8c56a Merge remote-tracking branch 'khopa/develop_2_3_x' into develop_2_3_x 2020-12-22 23:23:52 +01:00
Khopa
12bf26223d Added shorad units on frontline 2020-12-22 23:23:32 +01:00
Dan Albert
56d7993c8f Improve threat zone display options. 2020-12-22 13:57:05 -08:00
Dan Albert
52b63927b4 Prune escorts from packages that don't need them.
If the package is not flying into the threat zones of significant air
defenses there's no need for SEAD, and packages not near enemy airbases
do not need escorts. Prune these flights from the package to save
aircraft.
2020-12-22 13:12:04 -08:00
Dan Albert
86558bdef6 Add threat zone modeling.
Creates threat zones around airfields and non-trivial air defenses (it's
not worth dodging anything with a threat range under 3nm). These threat
zones can be used to aid mission planning and waypoint placement.

https://github.com/Khopa/dcs_liberation/issues/292
2020-12-22 13:12:04 -08:00
Dan Albert
e46262b021 Move has_radar into the TGO. 2020-12-22 12:59:29 -08:00
Dan Albert
c53feb5ccb Specify CAP engagement range in the doctrine. 2020-12-22 12:42:36 -08:00
Dan Albert
fc6d4f0990 Add EWRS plugin.
Fixes https://github.com/Khopa/dcs_liberation/issues/323
2020-12-21 21:28:27 +01:00
C. Perreau
df948bde9d Update CONTRIBUTING.md 2020-12-21 14:14:42 +01:00
C. Perreau
203a720ae1 Create CONTRIBUTING.md 2020-12-21 14:14:39 +01:00
C. Perreau
3410f08cfb Create CODE_OF_CONDUCT.md 2020-12-21 14:14:37 +01:00
C. Perreau
a553914ef4 Merge pull request #625 from Khopa/contributing
Create CONTRIBUTING.md
2020-12-21 14:13:29 +01:00
Khopa
21220141f2 pydcs submodule version update 2020-12-21 14:08:21 +01:00
Khopa
caf2d8436b Changelog update for 2.3.3 2020-12-21 13:56:00 +01:00
Khopa
4cc305fa81 Syrian civil war description updated 2020-12-21 13:55:33 +01:00
Khopa
60f837d0b9 Fixed : AI wouldn't buy artillery units 2020-12-21 13:34:56 +01:00
C. Perreau
05bd7f8e6b Update CONTRIBUTING.md 2020-12-21 13:04:16 +01:00
Emanuele Garofalo
e58ab34a15 Update NATO_Desert_Storm.json 2020-12-21 13:02:01 +01:00
Emanuele Garofalo
d960758ef3 Update iraq_1991.json 2020-12-21 13:01:58 +01:00
Emanuele Garofalo
a36ccdcc39 Update NATO_Desert_Storm.json 2020-12-21 13:01:57 +01:00
Emanuele Garofalo
d582948377 Update NATO_Desert_Storm.json 2020-12-21 03:31:19 -08:00
Emanuele Garofalo
d806e0b1c3 Update iraq_1991.json 2020-12-21 03:31:19 -08:00
Emanuele Garofalo
b9e110a7e3 Update NATO_Desert_Storm.json 2020-12-21 03:31:19 -08:00
Dan Albert
a2f218d56d Add EWRS plugin.
Fixes https://github.com/Khopa/dcs_liberation/issues/323
2020-12-21 01:19:54 -08:00
Khopa
2c475011a1 Syria terrain update + syrian civil war reworked in miz format 2020-12-21 03:19:17 +01:00
walterroach
2d7fc33726 Fix Distance being passed to pydcs methods 2020-12-20 17:19:49 -06:00
Dan Albert
0c8d1e1dc4 Remove checkbox from feature request form. 2020-12-20 14:48:54 -08:00
Khopa
bb04ce2abb Golan heights battle scenario fully migrated to miz format 2020-12-20 22:39:15 +01:00
Khopa
9850b22c0a Improved "Golan Heights Campaign Lite" for the Syria map. 2020-12-20 14:56:55 +01:00
C. Perreau
a2f65666a5 Create CONTRIBUTING.md 2020-12-20 14:20:18 +01:00
C. Perreau
7730809dbb Merge pull request #619 from Khopa/add-code-of-conduct-1
Add code of conduct
2020-12-20 13:45:25 +01:00
Dan Albert
2ac818dcdd Convert to new unit APIs, remove old APIs.
There are probably plenty of raw ints around that never used the old
conversion APIs, but we'll just need to fix those when we see them.

Fixes https://github.com/Khopa/dcs_liberation/issues/558
2020-12-19 22:08:57 -08:00
Dan Albert
113947b9f0 Add types for distance and speed.
Not converting all at once so I can prove the concept. After that we'll
want to cover all the cases where an int distance or speed is a part of
the save game (I've done one of them here with `Flight.alt`) so further
cleanups don't break save compat.

https://github.com/Khopa/dcs_liberation/issues/558
2020-12-19 21:07:55 -08:00
walterroach
44bc2d769b Merge branch 'develop_2_3_x' into develop 2020-12-19 20:27:31 -06:00
Khopa
02ecfebb85 Merge remote-tracking branch 'khopa/develop_2_3_x' into develop_2_3_x 2020-12-19 23:54:28 +01:00
Khopa
a1fed62591 Added "Golan Heights Campaign Lite" for the Syria map. 2020-12-19 23:54:02 +01:00
Dan Albert
778ed6ad91 Update 2.3 branch to 2.3.3 preview. 2020-12-19 13:48:44 -08:00
Dan Albert
7d539f5810 Remove pyproj from requirements.txt.
Not actually used.
2020-12-19 12:44:12 -08:00
Dan Albert
b407acbc07 Add section for 2.4 changelog. 2020-12-19 12:36:32 -08:00
Dan Albert
3260260dce Move more SAMs off runways in Syria Full.
(cherry picked from commit e5bca224e9)
2020-12-19 12:30:38 -08:00
Dan Albert
70c1290993 Note fixed SAM placement in changelog. 2020-12-19 12:30:38 -08:00
Dan Albert
6bae60c51e Note Iraq 1991 faction in changelog.
(cherry picked from commit 8447c563ea)
2020-12-19 12:30:38 -08:00
Emanuele Garofalo
a45adb6b3a New Faction Iraq 1991
(cherry picked from commit fd61a4b23a)
2020-12-19 12:30:38 -08:00
Dan Albert
476aaf5d3e Update changelog for #616.
(cherry picked from commit 3e4bb88089)
2020-12-19 12:30:38 -08:00
Dan Albert
58187b6969 Put back code to reserve beacon frequencies.
Fixes https://github.com/Khopa/dcs_liberation/issues/616

(cherry picked from commit 2f3f53a978)
2020-12-19 12:30:38 -08:00
Dan Albert
f3a3d81d96 Update beacon data.
(cherry picked from commit 89755b1005)
2020-12-19 12:30:38 -08:00
Dan Albert
7a40b54153 Update beacon importer to handle TheChannel.
TheChannel doesn't have message catalogs for English.

(cherry picked from commit a7203ea90a)
2020-12-19 12:30:38 -08:00
Dan Albert
9dd62d3538 Improve TOT planning.
Moves all TOT planning into the FlightPlan to clean up specialized
behavior and improve planning characteristics.

The important part of this change is that flights are now planning to
the mission time for their flight rather than the package as a whole.
For example, a TARCAP is planned based on the time from startup to the
patrol point, a sweep is planned based on the time from startup to the
sween end point, and a strike flight is planned based on the time from
startup to the target location. TOT offsets can be handled within the
flight plan.

As another benefit of theis cleanup, flights without hold points no
longer account for the hold time in their planning, so those flights are
planned to reach their targets sooner.

As a follow up TotEstimator can be removed, but I want to keep this low
impact for 2.3.2.

Fixes https://github.com/Khopa/dcs_liberation/issues/593

(cherry picked from commit 745dfc71bc)
2020-12-19 12:30:38 -08:00
walterroach
76e4a6ed83 Fix bad air defense location #617 2020-12-19 12:30:38 -08:00
Dan Albert
7a9eb06677 Add missing 2.3.2 change to changelog.
(cherry picked from commit 4eac743812)
2020-12-19 12:30:38 -08:00
Dan Albert
26f54e7619 Fix adding custom waypoints.
Fixes https://github.com/Khopa/dcs_liberation/issues/604

(cherry picked from commit 296e6e8e8f)
2020-12-19 12:30:38 -08:00
Khopa
117b7ae414 Changelog update 2020-12-19 12:30:38 -08:00
Khopa
baeac324d6 Updated version string 2020-12-19 12:30:38 -08:00
Khopa
0db0f003dc Added ZSU-57 sites 2020-12-19 12:30:38 -08:00
Khopa
2d4f341710 T72B3 and BTR-82A support 2020-12-19 12:30:38 -08:00
Khopa
b8a41dc937 pydcs version update to include new data export 2020-12-19 12:30:38 -08:00
walterroach
2f2bb0de4f Fix Ground units ... don't move forward #601 2020-12-19 12:30:38 -08:00
Dan Albert
3b76d7f47e Fail gracefully when out of radio channels.
Fixes https://github.com/Khopa/dcs_liberation/issues/598

(cherry picked from commit 498af28efb)
2020-12-19 12:30:38 -08:00
Dan Albert
10b74e507f Don't exclude BARCAP targets from culling.
Fixes https://github.com/Khopa/dcs_liberation/issues/597

(cherry picked from commit b9ade2295e)
2020-12-19 12:30:38 -08:00
Dan Albert
8a03a9462b Move more SAMs off runways in Syria Full.
(cherry picked from commit e5bca224e9)
2020-12-19 12:22:27 -08:00
Dan Albert
e5bca224e9 Move more SAMs off runways in Syria Full. 2020-12-19 12:22:08 -08:00
Dan Albert
197bf5d0cf Mark develop branch as 2.4 preview. 2020-12-19 12:07:35 -08:00
Dan Albert
d8b15ebcdb Merge branch 'develop_2_3_x' into develop 2020-12-19 12:03:21 -08:00
Dan Albert
078466241f Note fixed SAM placement in changelog. 2020-12-19 12:02:34 -08:00
Dan Albert
57c3eb5d2c Remove checkboxes from bug template.
Prevents all our bugs from looking complete or in progress because they have partially completed "tasks".
2020-12-19 11:59:28 -08:00
C. Perreau
a4876167c4 Create CODE_OF_CONDUCT.md 2020-12-19 20:57:55 +01:00
Dan Albert
a38a5654a9 Document current license.
Copied from the original shdwp repository.

https://github.com/Khopa/dcs_liberation/issues/243
2020-12-19 11:56:13 -08:00
Dan Albert
69a41879bb Note Iraq 1991 faction in changelog.
(cherry picked from commit 8447c563ea)
2020-12-19 11:53:49 -08:00
Emanuele Garofalo
e3524a506b New Faction Iraq 1991
(cherry picked from commit fd61a4b23a)
2020-12-19 11:53:46 -08:00
Dan Albert
8447c563ea Note Iraq 1991 faction in changelog. 2020-12-19 11:53:25 -08:00
Emanuele Garofalo
fd61a4b23a New Faction Iraq 1991 2020-12-19 11:52:54 -08:00
Dan Albert
9257311896 Update changelog for #616.
(cherry picked from commit 3e4bb88089)
2020-12-19 11:47:02 -08:00
Dan Albert
23e870e416 Put back code to reserve beacon frequencies.
Fixes https://github.com/Khopa/dcs_liberation/issues/616

(cherry picked from commit 2f3f53a978)
2020-12-19 11:47:01 -08:00
Dan Albert
8270b28d85 Update beacon data.
(cherry picked from commit 89755b1005)
2020-12-19 11:47:00 -08:00
Dan Albert
1a0889d3d9 Update beacon importer to handle TheChannel.
TheChannel doesn't have message catalogs for English.

(cherry picked from commit a7203ea90a)
2020-12-19 11:47:00 -08:00
Dan Albert
5382d99a94 Improve TOT planning.
Moves all TOT planning into the FlightPlan to clean up specialized
behavior and improve planning characteristics.

The important part of this change is that flights are now planning to
the mission time for their flight rather than the package as a whole.
For example, a TARCAP is planned based on the time from startup to the
patrol point, a sweep is planned based on the time from startup to the
sween end point, and a strike flight is planned based on the time from
startup to the target location. TOT offsets can be handled within the
flight plan.

As another benefit of theis cleanup, flights without hold points no
longer account for the hold time in their planning, so those flights are
planned to reach their targets sooner.

As a follow up TotEstimator can be removed, but I want to keep this low
impact for 2.3.2.

Fixes https://github.com/Khopa/dcs_liberation/issues/593

(cherry picked from commit 745dfc71bc)
2020-12-19 11:46:58 -08:00
Dan Albert
3e4bb88089 Update changelog for #616. 2020-12-19 11:46:19 -08:00
Dan Albert
2f3f53a978 Put back code to reserve beacon frequencies.
Fixes https://github.com/Khopa/dcs_liberation/issues/616
2020-12-19 11:45:33 -08:00
Dan Albert
89755b1005 Update beacon data. 2020-12-19 11:44:12 -08:00
Dan Albert
a7203ea90a Update beacon importer to handle TheChannel.
TheChannel doesn't have message catalogs for English.
2020-12-19 11:44:02 -08:00
walterroach
afb0ac14c4 Fix bad air defense location #617 2020-12-19 13:31:31 -06:00
Dan Albert
745dfc71bc Improve TOT planning.
Moves all TOT planning into the FlightPlan to clean up specialized
behavior and improve planning characteristics.

The important part of this change is that flights are now planning to
the mission time for their flight rather than the package as a whole.
For example, a TARCAP is planned based on the time from startup to the
patrol point, a sweep is planned based on the time from startup to the
sween end point, and a strike flight is planned based on the time from
startup to the target location. TOT offsets can be handled within the
flight plan.

As another benefit of theis cleanup, flights without hold points no
longer account for the hold time in their planning, so those flights are
planned to reach their targets sooner.

As a follow up TotEstimator can be removed, but I want to keep this low
impact for 2.3.2.

Fixes https://github.com/Khopa/dcs_liberation/issues/593
2020-12-19 11:20:16 -08:00
Dan Albert
82d9689d1b Remove links from bug report template.
Links don't render here.
2020-12-18 13:39:48 -08:00
Dan Albert
6afaef1654 Add issue templates. 2020-12-18 13:38:07 -08:00
Dan Albert
bb42d86012 Add more information to the readme.
Adds information about filing bugs/feature requests, finding the roadmap, and finding the preview builds.
2020-12-18 13:10:05 -08:00
Dan Albert
5b44580061 Add missing 2.3.2 change to changelog.
(cherry picked from commit 4eac743812)
2020-12-17 16:28:17 -08:00
Dan Albert
4eac743812 Add missing 2.3.2 change to changelog. 2020-12-17 16:27:54 -08:00
Dan Albert
ed8ab37bd5 Fix adding custom waypoints.
Fixes https://github.com/Khopa/dcs_liberation/issues/604

(cherry picked from commit 296e6e8e8f)
2020-12-17 16:26:19 -08:00
Dan Albert
563c3f0f1b Merge branch 'develop_2_3_x' into develop 2020-12-17 16:25:18 -08:00
Dan Albert
296e6e8e8f Fix adding custom waypoints.
Fixes https://github.com/Khopa/dcs_liberation/issues/604
2020-12-17 16:21:31 -08:00
Khopa
334aab2755 Changelog update 2020-12-18 01:09:14 +01:00
Khopa
419f4f3156 Updated version string 2020-12-18 01:08:34 +01:00
Khopa
ec5a26e8dd Added ZSU-57 sites 2020-12-18 01:05:06 +01:00
Khopa
2b7cd36eea T72B3 and BTR-82A support 2020-12-18 00:42:44 +01:00
Khopa
2f11731052 pydcs version update to include new data export 2020-12-18 00:25:50 +01:00
Khopa
23a0846533 Merge branch 'develop_2_3_x' into develop
# Conflicts:
#	changelog.md
#	qt_ui/windows/QLiberationWindow.py
#	resources/campaigns/syria_full_map_remastered.json
2020-12-18 00:23:53 +01:00
walterroach
666858f8e2 Fix Ground units ... don't move forward #601 2020-12-17 02:03:02 -06:00
Dan Albert
2288b7f7b2 Fail gracefully when out of radio channels.
Fixes https://github.com/Khopa/dcs_liberation/issues/598

(cherry picked from commit 498af28efb)
2020-12-16 22:24:07 -08:00
Dan Albert
498af28efb Fail gracefully when out of radio channels.
Fixes https://github.com/Khopa/dcs_liberation/issues/598
2020-12-16 19:12:18 -08:00
Khopa
3902ab3375 Fixed : BMD_1 IFV missing from db. Fixed error preventing mission generation when the role of an unit can not be determined.
(cherry picked from commit 4112a86fe9)
2020-12-16 19:08:42 -08:00
Dan Albert
6bb0bdf66e Don't exclude BARCAP targets from culling.
Fixes https://github.com/Khopa/dcs_liberation/issues/597

(cherry picked from commit b9ade2295e)
2020-12-16 18:53:17 -08:00
Dan Albert
b9ade2295e Don't exclude BARCAP targets from culling.
Fixes https://github.com/Khopa/dcs_liberation/issues/597
2020-12-16 18:52:12 -08:00
C. Perreau
44b5f5a919 Merge pull request #596 from Khopa/develop_2_3_x
Release 2.3.1
2020-12-16 21:45:30 +01:00
Khopa
17d37494c2 2.3.1 changelog 2020-12-16 21:14:25 +01:00
Khopa
f0d81e98a0 About dialog update 2020-12-16 21:12:01 +01:00
Khopa
e3b13f7b4a UX : Display a warning message when attempting to buy more aircraft at an already full airfield. 2020-12-16 21:08:48 +01:00
Khopa
ba2686630a Updated version number 2020-12-16 20:51:36 +01:00
Emanuele Garofalo
e195cfa6a0 Replaced previous Syria full map by the new version by Hawkmoon 2020-12-16 20:49:03 +01:00
Emanuele Garofalo
b9fbd1906f Add files via upload
Fixed NATO DESERT STORM FACTIO
2020-12-16 20:47:04 +01:00
Emanuele Garofalo
1f611bafef Add files via upload
New Syria Full map Remastered
2020-12-16 20:46:54 +01:00
C. Perreau
af7faa59dc Merge pull request #595 from ITAHawkmoon/develop
Syria Full map Remastered & NATO Desert Storm fixed
2020-12-16 20:43:34 +01:00
Emanuele Garofalo
0b21ee46ea Delete NATO Desert Storm.json
old faction
2020-12-16 16:23:40 +01:00
Emanuele Garofalo
f64996a350 Add files via upload
Fixed NATO DESERT STORM FACTIO
2020-12-16 16:22:37 +01:00
Emanuele Garofalo
d7cccd1980 Add files via upload
New Syria Full map Remastered
2020-12-16 16:20:45 +01:00
walterroach
b9467d9236 Fix broken Full Caucasus Map frontline 2020-12-16 09:08:13 -06:00
walterroach
69096b15ae Fix broken Full Caucasus Map frontline 2020-12-16 09:06:51 -06:00
Dan Albert
a075e62bad Fix easy going CAPs.
Fixes https://github.com/Khopa/dcs_liberation/issues/592

(cherry picked from commit 1ebe367e07)
2020-12-15 22:48:21 -08:00
Dan Albert
1ebe367e07 Fix easy going CAPs.
Fixes https://github.com/Khopa/dcs_liberation/issues/592
2020-12-15 22:48:05 -08:00
Khopa
db229f25bf Changelog update 2020-12-16 00:28:59 +01:00
C. Perreau
97ea67d01d Merge pull request #533 from walterroach/changelog_audit
WIP: 2.3.0 Changelog Audit
2020-12-15 23:19:13 +01:00
Khopa
d6376c3a91 pydcs version update 2020-12-15 19:57:04 +01:00
Dan Albert
793b356c01 Fix crash in new game generation.
Fixes https://github.com/Khopa/dcs_liberation/issues/583
2020-12-15 10:16:21 -08:00
Dan Albert
25efdd3d4f Don't cull objects near package targets.
https://github.com/Khopa/dcs_liberation/issues/578
Fixes https://github.com/Khopa/dcs_liberation/issues/262
2020-12-14 23:47:31 -08:00
Dan Albert
0b2483ea15 Don't cull flights.
This was made largely pointless in 2.2, since the AI won't plan a dozen
CAPs 300nm from the front line any more. Culling flights mostly just
confuses players and breaks the planning AI.

If we do want to limit flight counts, we need to do that by limiting
flight counts. The AI will try to put their aircraft as close to the
mission as possible, so culling will do very little to stop them from
spawning 50 flights at the front-line attached airfield.

https://github.com/Khopa/dcs_liberation/issues/578
2020-12-14 23:35:30 -08:00
Dan Albert
4d26ec0789 Add off-map spawns to the Black Sea campaign.
Fixes https://github.com/Khopa/dcs_liberation/issues/561
2020-12-14 22:36:39 -08:00
Dan Albert
7d907aac0f Fix budget update for non-base SAMs.
Just emit the signal to update the budget rather than trying to figure
out the heirarchy of the UI.

Fixes https://github.com/Khopa/dcs_liberation/issues/581
2020-12-14 17:00:56 -08:00
Khopa
d6981550a8 C-130 Plugin by Plob 2020-12-14 22:23:21 +01:00
Khopa
8b0636367b Updated Persian Gulf Full Map in new miz format by Plob. 2020-12-14 22:09:37 +01:00
Khopa
a6c9d0f9bc Hercules 130 pydcs extensions updated to version 6.0 (MOAB 💣 support) 2020-12-14 21:41:30 +01:00
Dan Albert
61ebe9780e Fix kneeboard ILS for many airfields.
Fixes https://github.com/Khopa/dcs_liberation/issues/564
2020-12-13 13:48:08 -08:00
Dan Albert
e887082501 Fix missing import for new package on enemy CVs.
Fixes https://github.com/Khopa/dcs_liberation/issues/570
2020-12-13 13:44:10 -08:00
Khopa
80778aa267 Fix usa C-130 faction name being the same as usa_2005 2020-12-13 17:21:13 +01:00
C. Perreau
445cb4f146 Merge pull request #569 from ITAHawkmoon/ITAHawkmoon-update-db
Update db and new faction.py
2020-12-13 16:43:09 +01:00
C. Perreau
95db2aa14f Merge branch 'develop' into ITAHawkmoon-update-db 2020-12-13 16:25:52 +01:00
Emanuele Garofalo
4b0fc637eb Update NATO Desert Storm.json
syntax error
2020-12-13 12:43:47 +01:00
Emanuele Garofalo
48d6b4cfa1 Add files via upload
New faction NATO desert Storm
2020-12-13 12:33:21 +01:00
Emanuele Garofalo
fc11182bbe Update db.py
added some plane missing from the db
2020-12-13 12:29:58 +01:00
Dan Albert
1848338ef7 Fix UI exception for custom flight plans. 2020-12-12 18:27:30 -08:00
Dan Albert
08ceb57c31 Fix division by zero for very close waypoints.
Fixes https://github.com/Khopa/dcs_liberation/issues/557
2020-12-12 14:51:03 -08:00
Dan Albert
affb332eb9 Fix duplication of pydcs translation keys.
We shouldn't be constructing these by hand, and the first argument is
not the value of the string. I'm not really sure why the current code
works at all. We probably do this in other places and should clean that
up, but for now this should fix Tauntaun.

Fixes https://github.com/Khopa/dcs_liberation/issues/528
2020-12-12 14:24:30 -08:00
Dan Albert
d5276c9d4a Show player waypoint timing in the briefing.
Fixes https://github.com/Khopa/dcs_liberation/issues/536
2020-12-12 14:17:48 -08:00
Dan Albert
07e5c568c4 Show takeoff time in waypoint list.
https://github.com/Khopa/dcs_liberation/issues/536
2020-12-12 13:22:23 -08:00
Khopa
31fdd24c3e Fixed possible crash in mission generation when generating a mission with a faction that doesn't have mandpad and with the disabled infantry option checked. 2020-12-12 15:27:36 +01:00
Khopa
bfa0e4ba49 Made normandy campaign less cluttered 2020-12-12 15:22:51 +01:00
Khopa
c89ff2c3d6 pydcs update 2020-12-12 14:36:40 +01:00
Khopa
4ec88d524a Fixed invasion_from_turkey inverted setup. 2020-12-12 14:25:30 +01:00
Khopa
48c218b430 Fix russia_small campaign not being invertable 2020-12-12 13:22:01 +01:00
Dan Albert
d316836e90 Set carrier speed for recovery.
https://github.com/Khopa/dcs_liberation/issues/543
2020-12-11 19:19:08 -08:00
Dan Albert
8443f61f0a Always aim the carrier into the wind.
https://github.com/Khopa/dcs_liberation/issues/543
2020-12-11 19:01:35 -08:00
walterroach
67806f3d76 Dec 10 review 2020-12-11 11:53:25 -06:00
walterroach
7744f84e85 Changelog audit 2020-12-10 14:04:48 -06:00
587 changed files with 12119 additions and 3906 deletions

37
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -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.

View File

@@ -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.

1
.gitignore vendored
View File

@@ -5,6 +5,7 @@ resources/payloads/*.lua
venv
logs.txt
.DS_Store
.vscode/settings.json
dist/**
a.py
resources/tools/a.miz

76
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at khopa.studio@gmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

26
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,26 @@
First, note that we have a code of conduct, please follow it in all your interactions with the project.
## Contributing as a non-developer
* Report bugs by opening issues here on Github.
* Help others users on Discord by answering their questions.
* Raise awareness about the project, by making a video and/or a tutorial.
Should you report a bug, 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.
## Making content for Liberation
You can create new campaigns : See [campaign creation wiki](https://github.com/Khopa/dcs_liberation/wiki/Custom-Campaigns).
You can also improve existing campaigns.
You can then submit new campaigns on the "campaigns" channel on Discord, or by making a pull request if you are comfortable with git.
## Develop new features
If you want to develop a new feature, we recommend you first open an issue describing the new feature and discuss it with us on Discord before starting development.
However, feel free to work on any existing issue.
## Pull requests
Please submit your pull requests on the **develop** branch. We expect a description of its content, and when applicable, a reference to the issue(s) it is resolving.

165
LICENSE Normal file
View File

@@ -0,0 +1,165 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
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.

View File

@@ -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/)

View File

@@ -1,16 +1,162 @@
# 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
# 2.3.3
## Features/Improvements
* **[Campaigns]** Reworked Golan Heights campaign on Syria, (Added FOB and preset locations for SAMS)
* **[Campaigns]** Added a lite version of the Golan Heights campaign
* **[Campaigns]** Reworked Syrian Civil War campaign (Added FOB and preset locations for SAMS)
* **[Campaigns]** Reworked Emirates campaign
* **[Campaigns]** AA units added to frontlines and updated all factions to include some frontline AA units.
* **[Mission Generator]** Infantry will only be generated for APC and IFV groups
* **[Mission Generator]** Infantry squads size is not randomized anymore
* **[Mission Generator]** Infantry squads can have a mortar.
* **[Mission Generator]** SCUD missiles sites will now fire on enemy controls points in range when possible
* **[Factions]** Updated Nato Desert Storm to include F-14A
* **[Factions]** Updated Iraq 1991 factions to include Zsu-57 and Mig-29A
* **[Factions]** Germany 1944, added Stug III and Stug IV
* **[Factions]** Added factions Insurgents (Hard) with better and more weapons
* **[Plugins]** [The EWRS plugin](https://github.com/Bob7heBuilder/EWRS) is now included.
* **[UI]** Added enemy intelligence summary and details window.
## Fixes:
* **[Factions]** AI would never buy artillery units for the frontline - fixed
* **[Factions]** Removed the F-111 unit from the NATO desert storm faction. (Recruiting it would cause crashes in DCS, since it is not a valid unit)
* **[Campaign]** Automatic redeployment of ground units would sometimes fail - fixed
* **[Mission Generator]** Artillery groups would retreat in the wrong direction - fixed
* **[Units]** Fixed SPG_Stryker_M1128_MGS not being in db
* **[UI]** Fixed and added many missing ground units icons
* **[UI]** Ship groups could be replaced by SAM sites in the UI, which would lead to broken mission being generated - fixed
* **[New Game Wizard]** Removed the "mid game" campaign generator option which is currently broken
* **[Mission Generator]** Empty navy groups will no longer be generated
* **[Mission Generator]** Fixed BAI, SEAD, and DEAD flights ocassionally being assigned the wrong targets.
* **[Flight Planner]** Fixed not being able to plan packages against opfor carriers
* **[UI]** Repaired SAMs no longer show as dead.
* **[UI]** Fixed not being able to manage a disbanded site after disbanding and closing the base menu.
# 2.3.2
## Features/Improvements
* **[Units]** Support for newly added BTR-82A, T-72B3
* **[Units]** Added ZSU-57 AAA sites
* **[Culling]** BARCAP missions no longer create culling exclusion zones.
* **[Flight Planner]** Improved TOT planning. Negative start times no longer occur with TARCAPs and hold times no longer affect planning for flight plans without hold points.
* **[Factions]** Added Iraq 1991 faction (thanks again to Hawkmoon!)
## Fixes:
* **[Mission Generator]** Fix mission generation error when there are too many radio frequency to setup for the Mig-21
* **[Mission Generator]** Fix ground units not moving forward
* **[Mission Generator]** Fixed assigned radio channels overlapping with beacons.
* **[Flight Planner]** Fix creation of custom waypoints.
* **[Campaigns]** Fixed many cases of SAMs spawning on the runways/taxiways in Syria Full.
# 2.3.1
## Features/Improvements
* **[UX]** Added a warning message when the player is attempting to buy more planes at an already full airbase.
* **[Campaigns]** Migrated Syria full map to new format. (Thanks to Hawkmoon)
* **[Faction]** Added NATO desert Storm faction (Thanks to Hawkmoon)
## Fixes:
* **[AI]** CAP flights will engage enemies again.
* **[Campaigns]** Fixed a missing path on the Caucasus Full Map campaign
# 2.3.0
# Features/Improvements
## Features/Improvements
* **[Campaign Map]** Overhauled the campaign model
* **[Campaign Map]** Possible to add FOB as control points
* **[Campaign Map]** Added off-map spawn locations
* **[Campaign AI]** Overhauled AI recruiting behaviour
* **[Campaign AI]** Added AI proucurement for Blue
* **[Campaign AI]** Added AI procurement for Blue
* **[Campaign]** New Campaign: "Black Sea"
* **[Mission Planner]** Possible to move carrier and tarawa on the campaign map
* **[Mission Generator]** Infantry squads on frontline can have manpads
* **[Mission Generator]** Unused aircraft now spawned to allow for OCA strikes
* **[Mission Generator]** Opfor now obeys parking limits
* **[Mission Generator]** Support for Anubis C-130 Hercules mod
* **[Flight Planner]** Added fighter sweep missions.
* **[Flight Planner]** Added BAI missions.
* **[Flight Planner]** Added anti-ship missions.
@@ -21,6 +167,7 @@
* **[QOL]** On liberation startup, your latest save game is loaded automatically
* **[Units]** Reduced starting fuel load for C101
* **[UI]** Inform the user of the weather
* **[UI]** Added toolbar buttons to change map display settings
* **[Game]** Added new Economy options for adjusting income multipliers and starting budgets.
## Fixes :
@@ -34,7 +181,7 @@
# 2.2.1
# Features/Improvements
## Features/Improvements
* **[Factions]** Added factions : Georgia 2008, USN 1985, France 2005 Frenchpack by HerrTom
* **[Factions]** Added map Persian Gulf full by Plob
* **[Flight Planner]** Player flights with start delays under ten minutes will spawn immediately.

View File

@@ -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),
)

326
game/data/weapons.py Normal file
View File

@@ -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
}

View File

@@ -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,
@@ -362,12 +421,14 @@ PRICES = {
# armor
Armor.APC_MTLB: 4,
Armor.FDDM_Grad: 5,
Armor.FDDM_Grad: 4,
Armor.ARV_BRDM_2: 6,
Armor.ARV_BTR_RD: 8,
Armor.ARV_BTR_RD: 6,
Armor.APC_BTR_80: 8,
Armor.APC_BTR_82A: 10,
Armor.MBT_T_55: 18,
Armor.MBT_T_72B: 22,
Armor.MBT_T_72B: 20,
Armor.MBT_T_72B3: 25,
Armor.MBT_T_80U: 25,
Armor.MBT_T_90: 30,
Armor.IFV_BMD_1: 8,
@@ -383,6 +444,7 @@ PRICES = {
Armor.ATGM_M1045_HMMWV_TOW: 8,
Armor.IFV_M2A2_Bradley: 12,
Armor.APC_M1126_Stryker_ICV: 10,
Armor.SPG_M1128_Stryker_MGS: 14,
Armor.ATGM_M1134_Stryker: 12,
Armor.MBT_M60A3_Patton: 16,
Armor.MBT_M1A2_Abrams: 25,
@@ -406,6 +468,8 @@ PRICES = {
Artillery.MLRS_BM_21_Grad: 15,
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,
@@ -434,6 +498,7 @@ PRICES = {
Armor.LAC_M8_Greyhound: 8,
Armor.TD_M10_GMC: 14,
Armor.StuG_III_Ausf__G: 12,
Armor.StuG_IV: 14,
Artillery.M12_GMC: 10,
Artillery.Sturmpanzer_IV_Brummbär: 10,
Armor.Daimler_Armoured_Car: 8,
@@ -454,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,
@@ -476,16 +541,16 @@ 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,
AirDefence.SPAAA_ZSU_23_4_Shilka: 10,
AirDefence.AAA_ZSU_57_2: 12,
AirDefence.AAA_ZU_23_Closed: 6,
AirDefence.AAA_ZU_23_Emplacement: 6,
AirDefence.AAA_ZU_23_on_Ural_375: 8,
AirDefence.AAA_ZU_23_on_Ural_375: 7,
AirDefence.AAA_ZU_23_Insurgent_Closed: 6,
AirDefence.AAA_ZU_23_Insurgent_on_Ural_375: 8,
AirDefence.AAA_ZU_23_Insurgent_on_Ural_375: 7,
AirDefence.AAA_ZU_23_Insurgent: 6,
AirDefence.SAM_SA_18_Igla_MANPADS: 10,
AirDefence.SAM_SA_18_Igla_comm: 8,
@@ -493,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,
@@ -558,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,
}
"""
@@ -568,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
@@ -627,6 +741,7 @@ UNIT_BY_TASK = {
A_10C_2,
A_20G,
B_17G,
F_111F,
B_1B,
B_52H,
F_117A,
@@ -649,6 +764,7 @@ UNIT_BY_TASK = {
RQ_1A_Predator,
Rafale_A_S,
Rafale_B,
S_3B,
SA342L,
SA342M,
SA342Minigun,
@@ -663,7 +779,8 @@ UNIT_BY_TASK = {
Tu_160,
Tu_22M3,
Tu_95MS,
UH_1H,
UH_1H,
SH_60B,
WingLoong_I,
Hercules
],
@@ -679,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,
@@ -704,6 +827,8 @@ UNIT_BY_TASK = {
Armor.APC_BTR_80,
Armor.APC_BTR_80,
Armor.APC_BTR_80,
Armor.APC_BTR_82A,
Armor.APC_BTR_82A,
Armor.IFV_BMP_1,
Armor.IFV_BMP_1,
Armor.IFV_BMP_1,
@@ -720,6 +845,8 @@ UNIT_BY_TASK = {
Armor.MBT_T_55,
Armor.MBT_T_72B,
Armor.MBT_T_72B,
Armor.MBT_T_72B3,
Armor.MBT_T_72B3,
Armor.MBT_T_80U,
Armor.MBT_T_80U,
Armor.MBT_T_90,
@@ -748,6 +875,7 @@ UNIT_BY_TASK = {
Armor.APC_M1126_Stryker_ICV,
Armor.APC_M1126_Stryker_ICV,
Armor.APC_M1126_Stryker_ICV,
Armor.SPG_M1128_Stryker_MGS,
Armor.IFV_MCV_80,
Armor.IFV_MCV_80,
Armor.IFV_MCV_80,
@@ -812,6 +940,7 @@ UNIT_BY_TASK = {
Armor.TD_M10_GMC,
Armor.TD_M10_GMC,
Armor.StuG_III_Ausf__G,
Armor.StuG_IV,
Artillery.M12_GMC,
Artillery.Sturmpanzer_IV_Brummbär,
Armor.Daimler_Armoured_Car,
@@ -827,9 +956,34 @@ 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,
AirDefence.AAA_ZU_23_on_Ural_375,
AirDefence.AAA_ZU_23_Insurgent_on_Ural_375,
AirDefence.AAA_ZSU_57_2,
AirDefence.SPAAA_ZSU_23_4_Shilka,
AirDefence.SAM_SA_8_Osa_9A33,
AirDefence.SAM_SA_9_Strela_1_9P31,
AirDefence.SAM_SA_13_Strela_10M3_9A35M3,
AirDefence.SAM_SA_15_Tor_9A331,
AirDefence.SAM_SA_19_Tunguska_2S6,
AirDefence.SPAAA_Gepard,
AirDefence.AAA_Vulcan_M163,
AirDefence.SAM_Linebacker_M6,
AirDefence.SAM_Chaparral_M48,
AirDefence.SAM_Avenger_M1097,
AirDefence.SAM_Roland_ADS,
AirDefence.HQ_7_Self_Propelled_LN,
AirDefence.AAA_8_8cm_Flak_18,
AirDefence.AAA_8_8cm_Flak_36,
AirDefence.AAA_8_8cm_Flak_37,
AirDefence.AAA_8_8cm_Flak_41,
AirDefence.AAA_Bofors_40mm,
AirDefence.AAA_M1_37mm,
AirDefence.AA_gun_QF_3_7,
frenchpack.DIM__TOYOTA_BLUE,
frenchpack.DIM__TOYOTA_DESERT,
frenchpack.DIM__TOYOTA_GREEN,
@@ -855,23 +1009,6 @@ UNIT_BY_TASK = {
],
AirDefence: [
# those are listed multiple times here to balance prioritization more into lower tier AAs
AirDefence.AAA_Vulcan_M163,
AirDefence.AAA_Vulcan_M163,
AirDefence.AAA_Vulcan_M163,
AirDefence.SAM_Linebacker_M6,
AirDefence.SPAAA_ZSU_23_4_Shilka,
AirDefence.AAA_ZU_23_Closed,
AirDefence.SAM_SA_9_Strela_1_9P31,
AirDefence.SAM_SA_8_Osa_9A33,
AirDefence.SAM_SA_19_Tunguska_2S6,
AirDefence.SAM_SA_6_Kub_LN_2P25,
AirDefence.SAM_SA_3_S_125_LN_5P73,
AirDefence.SAM_Hawk_PCP,
AirDefence.SAM_SA_2_LN_SM_90,
AirDefence.SAM_SA_11_Buk_LN_9A310M1,
],
Reconnaissance: [Unarmed.Transport_M818, Unarmed.Transport_Ural_375, Unarmed.Transport_UAZ_469],
Nothing: [Infantry.Infantry_M4, Infantry.Soldier_AK, ],
@@ -979,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]] = {
@@ -997,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",
@@ -1007,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",
@@ -1022,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,
@@ -1047,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,
@@ -1084,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: {
@@ -1151,9 +1319,6 @@ REWARDS = {
"derrick": 8
}
# Base post-turn bonus value
PLAYER_BUDGET_BASE = 20
CARRIER_CAPABLE = [
FA_18C_hornet,
F_14A_135_GR,
@@ -1162,6 +1327,7 @@ CARRIER_CAPABLE = [
Su_33,
A_4E_C,
Rafale_M,
S_3B,
UH_1H,
Mi_8MT,
@@ -1169,6 +1335,7 @@ CARRIER_CAPABLE = [
AH_1W,
OH_58D,
UH_60A,
SH_60B,
SA342L,
SA342M,
@@ -1185,6 +1352,7 @@ LHA_CAPABLE = [
AH_1W,
OH_58D,
UH_60A,
SH_60B,
SA342L,
SA342M,
@@ -1255,6 +1423,7 @@ INFANTRY: List[VehicleType] = [
Infantry.Soldier_RPG,
Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4,
Infantry.Soldier_M249,
Artillery._2B11_mortar,
Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK,
Infantry.Paratrooper_RPG_16,
Infantry.Georgian_soldier_with_M4, Infantry.Georgian_soldier_with_M4, Infantry.Georgian_soldier_with_M4,
@@ -1288,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:

View File

@@ -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)

View File

@@ -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()
@@ -152,12 +145,10 @@ class Event:
loss.group.units.remove(loss.unit)
loss.group.units_losts.append(loss.unit)
if not loss.ground_object.alive_unit_count:
loss.ground_object.is_dead = True
def commit_building_losses(self, debriefing: Debriefing) -> None:
for loss in debriefing.building_losses:
loss.ground_object.is_dead = True
loss.ground_object.kill()
self.game.informations.append(Information(
"Building destroyed",
f"{loss.ground_object.dcs_identifier} has been destroyed at "
@@ -305,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
@@ -349,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 = {}

View File

@@ -1,9 +1,10 @@
import itertools
import logging
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
@@ -11,7 +12,6 @@ from dcs.task import CAP, CAS, PinpointStrike
from dcs.vehicles import AirDefence
from game import db
from game.db import PLAYER_BUDGET_BASE, REWARDS
from game.inventory import GlobalAircraftInventory
from game.models.game_stats import GameStats
from game.plugins import LuaPluginManager
@@ -19,16 +19,21 @@ from gen.ato import AirTaskingOrder
from gen.conflictgen import Conflict
from gen.flights.ai_flight_planner import CoalitionMissionPlanner
from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.flights.flight import FlightType
from gen.ground_forces.ai_ground_planner import GroundPlanner
from . import persistency
from .debriefing import Debriefing
from .event.event import Event, UnitsDeliveryEvent
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
@@ -65,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
@@ -106,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()
@@ -124,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)
@@ -149,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
@@ -165,39 +189,20 @@ class Game:
front_line.control_point_a,
front_line.control_point_b)
@property
def budget_reward_amount(self) -> int:
reward = PLAYER_BUDGET_BASE * len(self.theater.player_points())
for cp in self.theater.player_points():
for g in cp.ground_objects:
if g.category in REWARDS.keys() and not g.is_dead:
reward += REWARDS[g.category]
return int(reward * self.settings.player_income_multiplier)
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 += self.budget_reward_amount
self.budget += Income(self, player=True).total
def process_enemy_income(self):
# TODO: Clean up save compat.
if not hasattr(self, "enemy_budget"):
self.enemy_budget = 0
production = 0.0
for enemy_point in self.theater.enemy_points():
for g in enemy_point.ground_objects:
if g.category in REWARDS.keys() and not g.is_dead:
production = production + REWARDS[g.category]
self.enemy_budget += production * self.settings.enemy_income_multiplier
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
self.enemy_budget += Income(self, player=False).total
def initiate_event(self, event: Event) -> UnitMap:
#assert event in self.events
@@ -207,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)
@@ -225,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()
@@ -278,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
@@ -288,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()
@@ -308,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(
@@ -323,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:
@@ -351,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
@@ -367,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)
@@ -391,9 +418,16 @@ class Game:
if cpoint is not None:
points.append(cpoint)
for package in self.blue_ato.packages:
points.append(package.target.position)
for package in self.red_ato.packages:
packages = itertools.chain(self.blue_ato.packages,
self.red_ato.packages)
for package in packages:
if package.primary_task is FlightType.BARCAP:
# BARCAPs will be planned at most locations on smaller theaters,
# rendering culling fairly useless. BARCAP packages don't really
# need the ground detail since they're defensive. SAMs nearby
# are only interesting if there are enemies in the area, and if
# there are they won't be culled because of the enemy's mission.
continue
points.append(package.target.position)
# Else 0,0, since we need a default value

55
game/income.py Normal file
View File

@@ -0,0 +1,55 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
from game.db import REWARDS
if TYPE_CHECKING:
from game import Game
@dataclass(frozen=True)
class BuildingIncome:
name: str
category: str
number: int
income_per_building: int
@property
def income(self) -> int:
return self.number * self.income_per_building
class Income:
def __init__(self, game: Game, player: bool) -> None:
if player:
self.multiplier = game.settings.player_income_multiplier
else:
self.multiplier = game.settings.enemy_income_multiplier
self.control_points = []
self.buildings = []
names = set()
for cp in game.theater.control_points_for(player):
if cp.income_per_turn:
self.control_points.append(cp)
for tgo in cp.ground_objects:
names.add(tgo.obj_name)
for name in names:
count = 0
tgos = game.theater.find_ground_objects_by_obj_name(name)
category = tgos[0].category
if category not in REWARDS:
continue
for tgo in tgos:
if not tgo.is_dead:
count += 1
self.buildings.append(BuildingIncome(name, category, count,
REWARDS[category]))
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)

270
game/navmesh.py Normal file
View File

@@ -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)

View File

@@ -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
@@ -199,10 +199,14 @@ class Operation:
@classmethod
def create_radio_registries(cls) -> None:
unique_map_frequencies = set() # type: Set[RadioFrequency]
unique_map_frequencies: Set[RadioFrequency] = set()
cls._create_tacan_registry(unique_map_frequencies)
cls._create_radio_registry(unique_map_frequencies)
assert cls.radio_registry is not None
for frequency in unique_map_frequencies:
cls.radio_registry.reserve(frequency)
@classmethod
def assign_channels_to_flights(cls, flights: List[FlightData],
air_support: AirSupport) -> None:
@@ -256,8 +260,8 @@ class Operation:
unique_map_frequencies.add(data.atc.vhf_fm)
unique_map_frequencies.add(data.atc.vhf_am)
unique_map_frequencies.add(data.atc.uhf)
# No need to reserve ILS or TACAN because those are in the
# beacon list.
# No need to reserve ILS or TACAN because those are in the
# beacon list.
@classmethod
def _generate_ground_units(cls):
@@ -291,7 +295,7 @@ class Operation:
heading=d["orientation"],
dead=True,
)
@classmethod
def generate(cls) -> UnitMap:
"""Build the final Mission to be exported"""
@@ -345,7 +349,7 @@ class Operation:
cls.jtacs,
cls.airgen
)
cls.reset_naming_ids()
return cls.unit_map
@classmethod
@@ -407,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,

View File

@@ -1,22 +1,20 @@
from __future__ import annotations
from dataclasses import dataclass
import math
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, UnitType, VehicleType
from dcs.unittype import FlyingType, VehicleType
from game import db
from game.factions.faction import Faction
from game.theater import ControlPoint, MissionTarget
from gen.flights.ai_flight_planner_db import (
capable_aircraft_for_task,
preferred_aircraft_for_task,
)
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,15 +111,26 @@ class ProcurementAi:
return budget
def random_affordable_ground_unit(
self, budget: int) -> Optional[Type[VehicleType]]:
affordable_units = [u for u in self.faction.frontline_units if
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]
total_number_aa = cp.base.total_frontline_aa + cp.pending_frontline_aa_deliveries_count
total_non_aa = cp.base.total_armor + cp.pending_deliveries_count - total_number_aa
max_aa = math.ceil(total_non_aa/8)
# Limit the number of AA units the AI will buy
if not total_number_aa < max_aa:
for unit in [u for u in affordable_units if u in TYPE_SHORAD]:
affordable_units.remove(unit)
if not affordable_units:
return None
return random.choice(affordable_units)
def reinforce_front_line(self, budget: int) -> int:
if not self.faction.frontline_units:
def reinforce_front_line(self, budget: float) -> float:
if not self.faction.frontline_units and not self.faction.artillery_units:
return budget
while budget > 0:
@@ -91,49 +139,44 @@ class ProcurementAi:
break
cp = random.choice(candidates)
unit = self.random_affordable_ground_unit(budget)
unit = self.random_affordable_ground_unit(budget, cp)
if unit is None:
# Can't afford any more units.
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)
@@ -146,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
@@ -164,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
@@ -171,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 = []
@@ -179,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:
@@ -187,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

View File

@@ -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

View File

@@ -9,6 +9,8 @@ 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
PLANES_SCRAMBLE_MIN_BASE = 2
@@ -36,6 +38,20 @@ 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])
@property
def total_aa(self) -> int:
return sum(self.aa.values())
@@ -98,11 +114,11 @@ class Base:
self.armor = {k: v for k, v in self.armor.items() if k in applicable_units}
def commision_units(self, units: typing.Dict[typing.Any, int]):
for value in units.values():
assert value > 0
assert value == math.floor(value)
for unit_type, unit_count in units.items():
if unit_count <= 0:
continue
for_task = db.unit_task(unit_type)
target_dict = None

View File

@@ -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,14 +483,14 @@ 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 = None # type: Optional[geometry.Point]
nearest_point = None # type: Optional[geometry.Point]
for pt in nearest_points:
min_distance = point.distance(nearest_points[0]) # type: geometry.Point
nearest_point = nearest_points[0] # type: geometry.Point
for pt in nearest_points[1:]:
distance = point.distance(pt)
if not min_distance or distance < min_distance:
if distance < min_distance:
min_distance = distance
nearest_point = pt
assert isinstance(nearest_point, geometry.Point)
@@ -503,8 +502,13 @@ class ConflictTheater:
)
return new_point
def control_points_for(self, player: bool) -> Iterator[ControlPoint]:
for point in self.controlpoints:
if point.captured == player:
yield point
def player_points(self) -> List[ControlPoint]:
return [point for point in self.controlpoints if point.captured]
return list(self.control_points_for(player=True))
def conflicts(self, from_player=True) -> Iterator[FrontLine]:
for cp in [x for x in self.controlpoints if x.captured == from_player]:
@@ -512,7 +516,7 @@ class ConflictTheater:
yield FrontLine(cp, connected_point, self)
def enemy_points(self) -> List[ControlPoint]:
return [point for point in self.controlpoints if not point.captured]
return list(self.control_points_for(player=False))
def closest_control_point(self, point: Point) -> ControlPoint:
closest = self.controlpoints[0]
@@ -523,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]:
"""

View File

@@ -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,23 +22,27 @@ from dcs.terrain.terrain import Airport, ParkingSlot
from dcs.unittype import FlyingType
from game import db
from gen.runways import RunwayAssigner, RunwayData
from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.ground_forces.ai_ground_planner_db import TYPE_SHORAD
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):
@@ -190,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
@@ -207,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
@@ -228,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
@@ -363,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:
@@ -372,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()
@@ -401,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):
@@ -438,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()
@@ -452,11 +561,51 @@ 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
u.position.y = u.position.y + delta.y
@property
def pending_frontline_aa_deliveries_count(self):
"""
Get number of pending frontline aa units
"""
if self.pending_unit_deliveries:
return sum([v for k,v in self.pending_unit_deliveries.units.items() if k in TYPE_SHORAD])
else:
return 0
@property
def pending_deliveries_count(self):
"""
Get number of pending units
"""
if self.pending_unit_deliveries:
return sum([v for k, v in self.pending_unit_deliveries.units.items()])
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):
@@ -521,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):
@@ -529,7 +682,6 @@ class NavalControlPoint(ControlPoint, ABC):
return True
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
yield from super().mission_types(for_player)
from gen.flights.flight import FlightType
if self.is_friendly(for_player):
yield from [
@@ -540,6 +692,7 @@ class NavalControlPoint(ControlPoint, ABC):
]
else:
yield FlightType.ANTISHIP
yield from super().mission_types(for_player)
@property
def heading(self) -> int:
@@ -720,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

View File

@@ -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))

View File

@@ -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:

View File

@@ -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,10 +90,12 @@ class TheaterGroundObject(MissionTarget):
self.dcs_identifier = dcs_identifier
self.airbase_group = airbase_group
self.sea_object = sea_object
self.is_dead = False
# TODO: There is never more than one group.
self.groups: List[Group] = []
@property
def is_dead(self) -> bool:
return self.alive_unit_count == 0
@property
def units(self) -> List[Unit]:
"""
@@ -144,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,
@@ -161,6 +208,9 @@ class BuildingGroundObject(TheaterGroundObject):
sea_object=False
)
self.object_id = object_id
# Other TGOs track deadness based on the number of alive units, but
# buildings don't have groups assigned to the TGO.
self._dead = False
@property
def group_name(self) -> str:
@@ -171,6 +221,15 @@ class BuildingGroundObject(TheaterGroundObject):
def waypoint_name(self) -> str:
return f"{super().waypoint_name} #{self.object_id}"
@property
def is_dead(self) -> bool:
if not hasattr(self, "_dead"):
self._dead = False
return self._dead
def kill(self) -> None:
self._dead = True
class NavalGroundObject(TheaterGroundObject):
def mission_types(self, for_player: bool) -> Iterator[FlightType]:

158
game/threatzones.py Normal file
View File

@@ -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)

View File

@@ -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)

View File

@@ -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)
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)

View File

@@ -2,7 +2,7 @@ from pathlib import Path
def _build_version_string() -> str:
components = ["2.3.0"]
components = ["2.4"]
build_number_path = Path("resources/buildnumber")
if build_number_path.exists():
with build_number_path.open("r") as build_number_file:

View File

@@ -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)
)

View File

@@ -20,20 +20,20 @@ from dcs.planes import (
B_17G,
B_52H,
Bf_109K_4,
C_101EB,
C_101CC,
C_101EB,
FW_190A8,
FW_190D9,
F_14B,
I_16,
JF_17,
Ju_88A4,
PlaneType,
P_47D_30,
P_47D_30bl1,
P_47D_40,
P_51D,
P_51D_30_NA,
PlaneType,
SpitfireLFMkIX,
SpitfireLFMkIXCW,
Su_33,
@@ -59,22 +59,22 @@ from dcs.task import (
OptReactOnThreat,
OptRestrictJettison,
OrbitAction,
PinpointStrike,
RunwayAttack,
SEAD,
StartCommand,
Targets,
Task,
WeaponType,
PinpointStrike,
)
from dcs.terrain.terrain import Airport, NoParkingSlotError
from dcs.translation import String
from dcs.triggers import Event, TriggerOnce, TriggerRule
from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup
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 (
@@ -86,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
@@ -99,7 +99,6 @@ from gen.flights.flight import (
)
from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
from gen.runways import RunwayData
from .conflictgen import Conflict
from .flights.flightplan import (
CasFlightPlan,
LoiterFlightPlan,
@@ -108,17 +107,14 @@ from .flights.flightplan import (
)
from .flights.traveltime import GroundSpeed, TotEstimator
from .naming import namegen
from .runways import RunwayAssigner
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
@@ -266,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.
@@ -303,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],
@@ -312,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
@@ -782,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),
@@ -835,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)
@@ -870,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
@@ -887,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
@@ -902,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:
@@ -966,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,
@@ -1036,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,
@@ -1057,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,
@@ -1070,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)
@@ -1315,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:
@@ -1349,7 +1374,7 @@ class AircraftConflictGenerator:
# And setting *our* waypoint TOT causes the takeoff time to show up in
# the player's kneeboard.
waypoint.tot = estimator.takeoff_time_for_flight(flight)
waypoint.tot = flight.flight_plan.takeoff_time()
# And finally assign it to the FlightData info so it shows correctly in
# the briefing.
self.flights[-1].departure_delay = start_time
@@ -1383,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)
@@ -1477,10 +1506,7 @@ class BaiIngressBuilder(PydcsWaypointBuilder):
target_group = self.package.target
if isinstance(target_group, TheaterGroundObject):
# Match search is used due to TheaterGroundObject.name not matching
# the Mission group name because of SkyNet prefixes.
tgroup = self.mission.find_group(target_group.group_name,
search="match")
tgroup = self.mission.find_group(target_group.group_name)
if tgroup is not None:
task = AttackGroup(tgroup.id, weapon_type=WeaponType.Auto)
task.params["attackQtyLimit"] = False
@@ -1503,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,
@@ -1514,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,
@@ -1530,10 +1556,7 @@ class DeadIngressBuilder(PydcsWaypointBuilder):
target_group = self.package.target
if isinstance(target_group, TheaterGroundObject):
# Match search is used due to TheaterGroundObject.name not matching
# the Mission group name because of SkyNet prefixes.
tgroup = self.mission.find_group(target_group.group_name,
search="match")
tgroup = self.mission.find_group(target_group.group_name)
if tgroup is not None:
task = AttackGroup(tgroup.id, weapon_type=WeaponType.Guided)
task.params["expend"] = "All"
@@ -1563,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
@@ -1596,14 +1619,11 @@ class SeadIngressBuilder(PydcsWaypointBuilder):
target_group = self.package.target
if isinstance(target_group, TheaterGroundObject):
# Match search is used due to TheaterGroundObject.name not matching
# the Mission group name because of SkyNet prefixes.
tgroup = self.mission.find_group(target_group.group_name,
search="match")
tgroup = self.mission.find_group(target_group.group_name)
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,
])
@@ -1685,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
@@ -1728,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]
)))
@@ -1749,22 +1769,20 @@ 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.")
return waypoint
racetrack = ControlledTask(OrbitAction(
altitude=waypoint.alt,
pattern=OrbitAction.OrbitPattern.RaceTrack
))
self.set_waypoint_tot(
waypoint, self.flight.flight_plan.patrol_start_time)
racetrack.stop_after_time(
int(self.flight.flight_plan.patrol_end_time.total_seconds()))
waypoint.add_task(racetrack)
# NB: It's important that the engage task comes before the orbit task.
# Though they're on the same waypoint, if the orbit task comes first it
# is their first priority and they will not engage any targets because
# they're fully focused on orbiting. If the STE task is first, they will
# engage targets if available and orbit if they find nothing to shoot.
# TODO: Move the properties of this task into the flight plan?
# CAP is the only current user of this so it's not a big deal, but might
@@ -1772,8 +1790,19 @@ 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, flight_plan.patrol_start_time)
racetrack.stop_after_time(
int(flight_plan.patrol_end_time.total_seconds()))
waypoint.add_task(racetrack)
return waypoint

View File

@@ -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(

View File

@@ -50,6 +50,8 @@ FIGHT_DISTANCE = 3500
RANDOM_OFFSET_ATTACK = 250
INFANTRY_GROUP_SIZE = 5
@dataclass(frozen=True)
class JtacInfo:
@@ -206,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,
@@ -220,18 +222,18 @@ 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,
move_formation=PointAction.OffRoad)
for i in range(random.randint(3, 10)):
for i in range(INFANTRY_GROUP_SIZE):
u = random.choice(possible_infantry_units)
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,
@@ -281,7 +283,7 @@ class GroundConflictGenerator:
# Hold position
dcs_group.points[1].tasks.append(Hold())
retreat = self.find_retreat_point(dcs_group, heading_sum(forward_heading, 180), (int)(RETREAT_DISTANCE/3))
retreat = self.find_retreat_point(dcs_group, forward_heading, (int)(RETREAT_DISTANCE/3))
dcs_group.add_waypoint(dcs_group.position.point_from_heading(forward_heading, 1), PointAction.OffRoad)
dcs_group.points[2].tasks.append(Hold())
dcs_group.add_waypoint(retreat, PointAction.OffRoad)
@@ -350,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)
@@ -365,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
@@ -675,12 +687,14 @@ class GroundConflictGenerator:
else:
g.set_skill(self.game.settings.enemy_vehicle_skill)
positioned_groups.append((g, group))
self.gen_infantry_group_for_group(
g,
is_player,
self.mission.country(country),
opposite_heading(spawn_heading)
)
if group.role in [CombatGroupRole.APC, CombatGroupRole.IFV]:
self.gen_infantry_group_for_group(
g,
is_player,
self.mission.country(country),
opposite_heading(spawn_heading)
)
else:
logging.warning(f"Unable to get valid position for {group}")

View File

@@ -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)

View File

@@ -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]:

View File

@@ -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:

View File

@@ -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,10 +26,8 @@ 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_frigate = True
if include_frigate:
self.add_unit(Type_054A_Frigate, "FF1", self.position.x + 1200, self.position.y + 900, self.heading)
@@ -41,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

View File

@@ -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
)
@@ -35,6 +34,9 @@ class RussianNavyGroupGenerator(ShipGroupGenerator):
else:
include_cc = False
if not any([include_frigate, include_dd, include_cc]):
include_frigate = True
if include_frigate:
frigate_type = random.choice([FFL_1124_4_Grisha, FSG_1241_1MP_Molniya])
self.add_unit(frigate_type, "FF1", self.position.x + 1200, self.position.y + 900, self.heading)
@@ -46,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

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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}"

View File

@@ -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
@@ -73,13 +74,20 @@ class FlightPlan:
"""Iterates over all waypoints in the flight plan, in order."""
raise NotImplementedError
@property
def edges(self) -> Iterator[Tuple[FlightWaypoint, FlightWaypoint]]:
def edges(
self, until: Optional[FlightWaypoint] = None
) -> Iterator[Tuple[FlightWaypoint, FlightWaypoint]]:
"""A list of all paths between waypoints, in order."""
return zip(self.waypoints, self.waypoints[1:])
waypoints = self.waypoints
if until is None:
last_index = len(waypoints)
else:
last_index = waypoints.index(until) + 1
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:
@@ -98,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
@@ -119,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
@@ -137,15 +145,15 @@ class FlightPlan:
"""Joker fuel value for the 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:
@@ -156,26 +164,18 @@ class FlightPlan:
"""
return timedelta()
# Not cached because changes to the package might alter the formation speed.
@property
def travel_time_to_target(self) -> Optional[timedelta]:
"""The estimated time between the first waypoint and the target."""
if self.tot_waypoint is None:
return None
return self._travel_time_to_waypoint(self.tot_waypoint)
def _travel_time_to_waypoint(
self, destination: FlightWaypoint) -> timedelta:
total = timedelta()
for previous_waypoint, waypoint in self.edges:
total += self.travel_time_between_waypoints(previous_waypoint,
waypoint)
if waypoint == destination:
break
else:
if destination not in self.waypoints:
raise PlanningError(
f"Did not find destination waypoint {destination} in "
f"waypoints for {self.flight}")
for previous_waypoint, waypoint in self.edges(until=destination):
total += self.travel_time_between_waypoints(previous_waypoint,
waypoint)
return total
def travel_time_between_waypoints(self, a: FlightWaypoint,
@@ -196,10 +196,64 @@ class FlightPlan:
def dismiss_escort_at(self) -> Optional[FlightWaypoint]:
return None
def takeoff_time(self) -> Optional[timedelta]:
tot_waypoint = self.tot_waypoint
if tot_waypoint is None:
return None
time = self.tot_for_waypoint(tot_waypoint)
if time is None:
return None
time += self.tot_offset
return time - self._travel_time_to_waypoint(tot_waypoint)
def startup_time(self) -> Optional[timedelta]:
takeoff_time = self.takeoff_time()
if takeoff_time is None:
return None
start_time = (takeoff_time - self.estimate_startup() -
self.estimate_ground_ops())
# In case FP math has given us some barely below zero time, round to
# zero.
if math.isclose(start_time.total_seconds(), 0):
return timedelta()
# Trim microseconds. DCS doesn't handle sub-second resolution for tasks,
# and they're not interesting from a mission planning perspective so we
# don't want them in the UI.
#
# Round down so *barely* above zero start times are just zero.
return timedelta(seconds=math.floor(start_time.total_seconds()))
def estimate_startup(self) -> timedelta:
if self.flight.start_type == "Cold":
if self.flight.client_count:
return timedelta(minutes=10)
else:
# The AI doesn't seem to have a real startup procedure.
return timedelta(minutes=2)
return timedelta()
def estimate_ground_ops(self) -> timedelta:
if self.flight.start_type in ("Runway", "In Flight"):
return timedelta()
if self.flight.from_cp.is_fleet:
return timedelta(minutes=2)
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):
hold: FlightWaypoint
hold_duration: timedelta
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
raise NotImplementedError
@@ -221,6 +275,17 @@ class LoiterFlightPlan(FlightPlan):
return self.push_time
return None
def travel_time_between_waypoints(self, a: FlightWaypoint,
b: FlightWaypoint) -> timedelta:
travel_time = super().travel_time_between_waypoints(a, b)
if a != self.hold:
return travel_time
try:
return travel_time + self.hold_duration
except AttributeError:
# Save compat for 2.3.
return travel_time + timedelta(minutes=5)
@dataclass(frozen=True)
class FormationFlightPlan(LoiterFlightPlan):
@@ -245,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
@@ -254,14 +319,14 @@ class FormationFlightPlan(LoiterFlightPlan):
all of its formation waypoints.
"""
speeds = []
for previous_waypoint, waypoint in self.edges:
for previous_waypoint, waypoint in self.edges():
if waypoint in self.package_speed_waypoints:
speeds.append(self.best_speed_between_waypoints(
previous_waypoint, waypoint))
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.
@@ -297,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
@@ -339,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):
@@ -347,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
@@ -365,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
@@ -390,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
@@ -428,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
@@ -461,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:
@@ -479,14 +566,15 @@ 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:
"""The estimated time between the first waypoint and the target."""
destination = self.tot_waypoint
total = timedelta()
for previous_waypoint, waypoint in self.edges:
for previous_waypoint, waypoint in self.edges():
if waypoint == self.tot_waypoint:
# For anything strike-like the TOT waypoint is the *flight's*
# mission target, but to synchronize with the rest of the
@@ -504,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
@@ -546,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
@@ -602,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):
@@ -632,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."""
@@ -652,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,
@@ -697,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,
@@ -710,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.
@@ -804,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),
@@ -830,32 +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:
@@ -870,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,
@@ -882,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
@@ -901,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
@@ -911,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.
@@ -924,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(
@@ -945,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),
@@ -1040,21 +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)),
join=builder.join(self.package.waypoints.join),
hold=hold,
hold_duration=timedelta(minutes=5),
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)
)
@@ -1077,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),
@@ -1128,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
@@ -1145,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)
@@ -1157,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,
@@ -1171,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(
@@ -1179,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:
@@ -1191,17 +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)),
join=builder.join(self.package.waypoints.join),
hold=hold,
hold_duration=timedelta(minutes=5),
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)
)
@@ -1211,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(
@@ -1242,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:
@@ -1277,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

View File

@@ -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,130 +34,50 @@ 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.
class TotEstimator:
# An extra five minutes given as wiggle room. Expected to be spent at the
# hold point performing any last minute configuration.
HOLD_TIME = timedelta(minutes=5)
def __init__(self, package: Package) -> None:
self.package = package
def mission_start_time(self, flight: Flight) -> timedelta:
takeoff_time = self.takeoff_time_for_flight(flight)
if takeoff_time is None:
@staticmethod
def mission_start_time(flight: Flight) -> timedelta:
startup_time = flight.flight_plan.startup_time()
if startup_time is None:
# Could not determine takeoff time, probably due to a custom flight
# plan. Start immediately.
return timedelta()
startup_time = self.estimate_startup(flight)
ground_ops_time = self.estimate_ground_ops(flight)
start_time = takeoff_time - startup_time - ground_ops_time
# In case FP math has given us some barely below zero time, round to
# zero.
if math.isclose(start_time.total_seconds(), 0):
return timedelta()
# Trim microseconds. DCS doesn't handle sub-second resolution for tasks,
# and they're not interesting from a mission planning perspective so we
# don't want them in the UI.
#
# Round down so *barely* above zero start times are just zero.
return timedelta(seconds=math.floor(start_time.total_seconds()))
def takeoff_time_for_flight(self, flight: Flight) -> Optional[timedelta]:
travel_time = self.travel_time_to_rendezvous_or_target(flight)
if travel_time is None:
from gen.flights.flightplan import CustomFlightPlan
if not isinstance(flight.flight_plan, CustomFlightPlan):
logging.warning(
"Found no rendezvous or target point. Cannot estimate "
f"takeoff time takeoff time for {flight}.")
return None
from gen.flights.flightplan import FormationFlightPlan
if isinstance(flight.flight_plan, FormationFlightPlan):
tot = flight.flight_plan.tot_for_waypoint(
flight.flight_plan.join)
if tot is None:
logging.warning(
"Could not determine the TOT of the join point. Takeoff "
f"time for {flight} will be immediate.")
return None
else:
tot_waypoint = flight.flight_plan.tot_waypoint
if tot_waypoint is None:
tot = self.package.time_over_target
else:
tot = flight.flight_plan.tot_for_waypoint(tot_waypoint)
if tot is None:
logging.error(f"TOT waypoint for {flight} has no TOT")
tot = self.package.time_over_target
return tot - travel_time - self.HOLD_TIME
return startup_time
def earliest_tot(self) -> timedelta:
earliest_tot = max((
self.earliest_tot_for_flight(f) for f in self.package.flights
)) + self.HOLD_TIME
))
# Trim microseconds. DCS doesn't handle sub-second resolution for tasks,
# and they're not interesting from a mission planning perspective so we
@@ -159,7 +86,8 @@ class TotEstimator:
# Round up so we don't get negative start times.
return timedelta(seconds=math.ceil(earliest_tot.total_seconds()))
def earliest_tot_for_flight(self, flight: Flight) -> timedelta:
@staticmethod
def earliest_tot_for_flight(flight: Flight) -> timedelta:
"""Estimate fastest time from mission start to the target position.
For BARCAP flights, this is time to race track start. This ensures that
@@ -175,51 +103,18 @@ class TotEstimator:
The earliest possible TOT for the given flight in seconds. Returns 0
if an ingress point cannot be found.
"""
time_to_target = self.travel_time_to_target(flight)
if time_to_target is None:
# Clear the TOT, calculate the startup time. Negating the result gives
# the earliest possible start time.
orig_tot = flight.package.time_over_target
try:
flight.package.time_over_target = timedelta()
time = flight.flight_plan.startup_time()
finally:
flight.package.time_over_target = orig_tot
if time is None:
logging.warning(f"Cannot estimate TOT for {flight}")
# Return 0 so this flight's travel time does not affect the rest
# of the package.
return timedelta()
# Account for TOT offsets for the flight plan. An offset of -2 minutes
# means the flight's TOT is 2 minutes ahead of the package's so it needs
# an extra two minutes.
offset = -flight.flight_plan.tot_offset
startup = self.estimate_startup(flight)
ground_ops = self.estimate_ground_ops(flight)
return startup + ground_ops + time_to_target + offset
@staticmethod
def estimate_startup(flight: Flight) -> timedelta:
if flight.start_type == "Cold":
if flight.client_count:
return timedelta(minutes=10)
else:
# The AI doesn't seem to have a real startup procedure.
return timedelta(minutes=2)
return timedelta()
@staticmethod
def estimate_ground_ops(flight: Flight) -> timedelta:
if flight.start_type in ("Runway", "In Flight"):
return timedelta()
if flight.from_cp.is_fleet:
return timedelta(minutes=2)
else:
return timedelta(minutes=5)
@staticmethod
def travel_time_to_target(flight: Flight) -> Optional[timedelta]:
if flight.flight_plan is None:
return None
return flight.flight_plan.travel_time_to_target
@staticmethod
def travel_time_to_rendezvous_or_target(
flight: Flight) -> Optional[timedelta]:
if flight.flight_plan is None:
return None
from gen.flights.flightplan import FormationFlightPlan
if isinstance(flight.flight_plan, FormationFlightPlan):
return flight.flight_plan.travel_time_to_rendezvous
return flight.flight_plan.travel_time_to_target
return -time

View File

@@ -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)

View File

@@ -3,178 +3,14 @@ from enum import Enum
from typing import Dict, List
from dcs.unittype import VehicleType
from dcs.vehicles import Armor, Artillery, Infantry, Unarmed
import pydcs_extensions.frenchpack.frenchpack as frenchpack
from game.theater import ControlPoint
from gen.ground_forces.ai_ground_planner_db import *
from gen.ground_forces.combat_stance import CombatStance
TYPE_TANKS = [
Armor.MBT_T_55,
Armor.MBT_T_72B,
Armor.MBT_T_80U,
Armor.MBT_T_90,
Armor.MBT_Leopard_2,
Armor.MBT_Leopard_1A3,
Armor.MBT_Leclerc,
Armor.MBT_Challenger_II,
Armor.MBT_M1A2_Abrams,
Armor.MBT_M60A3_Patton,
Armor.MBT_Merkava_Mk__4,
Armor.ZTZ_96B,
# WW2
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
Armor.MT_Pz_Kpfw_IV_Ausf_H,
Armor.HT_Pz_Kpfw_VI_Tiger_I,
Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II,
Armor.MT_M4_Sherman,
Armor.MT_M4A4_Sherman_Firefly,
Armor.StuG_IV,
Armor.CT_Centaur_IV,
Armor.CT_Cromwell_IV,
Armor.HIT_Churchill_VII,
Armor.LT_Mk_VII_Tetrarch,
# Mods
frenchpack.DIM__TOYOTA_BLUE,
frenchpack.DIM__TOYOTA_GREEN,
frenchpack.DIM__TOYOTA_DESERT,
frenchpack.DIM__KAMIKAZE,
frenchpack.AMX_10RCR,
frenchpack.AMX_10RCR_SEPAR,
frenchpack.AMX_30B2,
frenchpack.Leclerc_Serie_XXI,
]
TYPE_ATGM = [
Armor.ATGM_M1045_HMMWV_TOW,
Armor.ATGM_M1134_Stryker,
Armor.IFV_BMP_2,
# WW2 (Tank Destroyers)
Armor.M30_Cargo_Carrier,
Armor.TD_Jagdpanzer_IV,
Armor.TD_Jagdpanther_G1,
Armor.TD_M10_GMC,
# Mods
frenchpack.VBAE_CRAB_MMP,
frenchpack.VAB_MEPHISTO,
frenchpack.TRM_2000_PAMELA,
]
TYPE_IFV = [
Armor.IFV_BMP_3,
Armor.IFV_BMP_2,
Armor.IFV_BMP_1,
Armor.IFV_Marder,
Armor.IFV_MCV_80,
Armor.IFV_LAV_25,
Armor.AC_Sd_Kfz_234_2_Puma,
Armor.IFV_M2A2_Bradley,
Armor.IFV_BMD_1,
Armor.ZBD_04A,
# WW2
Armor.AC_Sd_Kfz_234_2_Puma,
Armor.LAC_M8_Greyhound,
Armor.Daimler_Armoured_Car,
# Mods
frenchpack.ERC_90,
frenchpack.VBAE_CRAB,
frenchpack.VAB_T20_13
]
TYPE_APC = [
Armor.APC_M1043_HMMWV_Armament,
Armor.APC_M1126_Stryker_ICV,
Armor.APC_M113,
Armor.APC_BTR_80,
Armor.APC_MTLB,
Armor.APC_M2A1,
Armor.APC_Cobra,
Armor.APC_Sd_Kfz_251,
Armor.APC_AAV_7,
Armor.TPz_Fuchs,
Armor.ARV_BRDM_2,
Armor.ARV_BTR_RD,
Armor.FDDM_Grad,
# WW2
Armor.APC_M2A1,
Armor.APC_Sd_Kfz_251,
# Mods
frenchpack.VAB__50,
frenchpack.VBL__50,
frenchpack.VBL_AANF1,
]
TYPE_ARTILLERY = [
Artillery.MLRS_9A52_Smerch,
Artillery.SPH_2S1_Gvozdika,
Artillery.SPH_2S3_Akatsia,
Artillery.MLRS_BM_21_Grad,
Artillery.MLRS_9K57_Uragan_BM_27,
Artillery.SPH_M109_Paladin,
Artillery.MLRS_M270,
Artillery.SPH_2S9_Nona,
Artillery.SpGH_Dana,
Artillery.SPH_2S19_Msta,
Artillery.MLRS_FDDM,
# WW2
Artillery.Sturmpanzer_IV_Brummbär,
Artillery.M12_GMC
]
TYPE_LOGI = [
Unarmed.Transport_M818,
Unarmed.Transport_KAMAZ_43101,
Unarmed.Transport_Ural_375,
Unarmed.Transport_GAZ_66,
Unarmed.Transport_GAZ_3307,
Unarmed.Transport_GAZ_3308,
Unarmed.Transport_Ural_4320_31_Armored,
Unarmed.Transport_Ural_4320T,
Unarmed.Blitz_3_6_6700A,
Unarmed.Kübelwagen_82,
Unarmed.Sd_Kfz_7,
Unarmed.Sd_Kfz_2,
Unarmed.Willys_MB,
Unarmed.Land_Rover_109_S3,
Unarmed.Land_Rover_101_FC,
# Mods
frenchpack.VBL,
frenchpack.VAB,
]
TYPE_INFANTRY = [
Infantry.Infantry_Soldier_Insurgents,
Infantry.Soldier_AK,
Infantry.Infantry_M1_Garand,
Infantry.Infantry_Mauser_98,
Infantry.Infantry_SMLE_No_4_Mk_1,
Infantry.Georgian_soldier_with_M4,
Infantry.Infantry_Soldier_Rus,
Infantry.Paratrooper_AKS,
Infantry.Paratrooper_RPG_16,
Infantry.Soldier_M249,
Infantry.Infantry_M4,
Infantry.Soldier_RPG,
]
MAX_COMBAT_GROUP_PER_CP = 10
class CombatGroupRole(Enum):
TANK = 1
APC = 2
@@ -222,6 +58,7 @@ class CombatGroup:
s += "UNITS " + self.units[0].name + " * " + str(len(self.units))
return s
class GroundPlanner:
def __init__(self, cp:ControlPoint, game):
@@ -241,7 +78,6 @@ class GroundPlanner:
self.units_per_cp[cp.id] = []
self.reserve: List[CombatGroup] = []
def plan_groundwar(self):
if hasattr(self.cp, 'stance'):
@@ -273,6 +109,9 @@ class GroundPlanner:
elif key in TYPE_ATGM:
collection = self.atgm_group
role = CombatGroupRole.ATGM
elif key in TYPE_SHORAD:
collection = self.shorad_groups
role = CombatGroupRole.SHORAD
else:
print("Warning unit type not handled by ground generator")
print(key)
@@ -280,12 +119,16 @@ class GroundPlanner:
available = self.cp.base.armor[key]
while available > 0:
n = random.choice(group_size_choice)
if n > available:
if available >= 2:
n = 2
else:
n = 1
if role == CombatGroupRole.SHORAD:
n = 1
else:
n = random.choice(group_size_choice)
if n > available:
if available >= 2:
n = 2
else:
n = 1
available -= n
group = CombatGroup(role)

View File

@@ -0,0 +1,199 @@
from dcs.vehicles import AirDefence, Infantry, Unarmed, Artillery, Armor
from pydcs_extensions.frenchpack import frenchpack
TYPE_TANKS = [
Armor.MBT_T_55,
Armor.MBT_T_72B,
Armor.MBT_T_72B3,
Armor.MBT_T_80U,
Armor.MBT_T_90,
Armor.MBT_Leopard_2,
Armor.MBT_Leopard_1A3,
Armor.MBT_Leclerc,
Armor.MBT_Challenger_II,
Armor.MBT_M1A2_Abrams,
Armor.MBT_M60A3_Patton,
Armor.MBT_Merkava_Mk__4,
Armor.ZTZ_96B,
# WW2
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
Armor.MT_Pz_Kpfw_IV_Ausf_H,
Armor.HT_Pz_Kpfw_VI_Tiger_I,
Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II,
Armor.MT_M4_Sherman,
Armor.MT_M4A4_Sherman_Firefly,
Armor.StuG_IV,
Armor.CT_Centaur_IV,
Armor.CT_Cromwell_IV,
Armor.HIT_Churchill_VII,
Armor.LT_Mk_VII_Tetrarch,
# Mods
frenchpack.DIM__TOYOTA_BLUE,
frenchpack.DIM__TOYOTA_GREEN,
frenchpack.DIM__TOYOTA_DESERT,
frenchpack.DIM__KAMIKAZE,
frenchpack.AMX_10RCR,
frenchpack.AMX_10RCR_SEPAR,
frenchpack.AMX_30B2,
frenchpack.Leclerc_Serie_XXI,
]
TYPE_ATGM = [
Armor.ATGM_M1045_HMMWV_TOW,
Armor.ATGM_M1134_Stryker,
Armor.IFV_BMP_2,
# WW2 (Tank Destroyers)
Armor.M30_Cargo_Carrier,
Armor.TD_Jagdpanzer_IV,
Armor.TD_Jagdpanther_G1,
Armor.TD_M10_GMC,
# Mods
frenchpack.VBAE_CRAB_MMP,
frenchpack.VAB_MEPHISTO,
frenchpack.TRM_2000_PAMELA,
]
TYPE_IFV = [
Armor.IFV_BMP_3,
Armor.IFV_BMP_2,
Armor.IFV_BMP_1,
Armor.IFV_Marder,
Armor.IFV_MCV_80,
Armor.IFV_LAV_25,
Armor.SPG_M1128_Stryker_MGS,
Armor.AC_Sd_Kfz_234_2_Puma,
Armor.IFV_M2A2_Bradley,
Armor.IFV_BMD_1,
Armor.ZBD_04A,
# WW2
Armor.AC_Sd_Kfz_234_2_Puma,
Armor.LAC_M8_Greyhound,
Armor.Daimler_Armoured_Car,
# Mods
frenchpack.ERC_90,
frenchpack.VBAE_CRAB,
frenchpack.VAB_T20_13
]
TYPE_APC = [
Armor.APC_M1043_HMMWV_Armament,
Armor.APC_M1126_Stryker_ICV,
Armor.APC_M113,
Armor.APC_BTR_80,
Armor.APC_BTR_82A,
Armor.APC_MTLB,
Armor.APC_M2A1,
Armor.APC_Cobra,
Armor.APC_Sd_Kfz_251,
Armor.APC_AAV_7,
Armor.TPz_Fuchs,
Armor.ARV_BRDM_2,
Armor.ARV_BTR_RD,
Armor.FDDM_Grad,
# WW2
Armor.APC_M2A1,
Armor.APC_Sd_Kfz_251,
# Mods
frenchpack.VAB__50,
frenchpack.VBL__50,
frenchpack.VBL_AANF1,
]
TYPE_ARTILLERY = [
Artillery.MLRS_9A52_Smerch,
Artillery.SPH_2S1_Gvozdika,
Artillery.SPH_2S3_Akatsia,
Artillery.MLRS_BM_21_Grad,
Artillery.MLRS_9K57_Uragan_BM_27,
Artillery.SPH_M109_Paladin,
Artillery.MLRS_M270,
Artillery.SPH_2S9_Nona,
Artillery.SpGH_Dana,
Artillery.SPH_2S19_Msta,
Artillery.MLRS_FDDM,
# WW2
Artillery.Sturmpanzer_IV_Brummbär,
Artillery.M12_GMC
]
TYPE_LOGI = [
Unarmed.Transport_M818,
Unarmed.Transport_KAMAZ_43101,
Unarmed.Transport_Ural_375,
Unarmed.Transport_GAZ_66,
Unarmed.Transport_GAZ_3307,
Unarmed.Transport_GAZ_3308,
Unarmed.Transport_Ural_4320_31_Armored,
Unarmed.Transport_Ural_4320T,
Unarmed.Blitz_3_6_6700A,
Unarmed.Kübelwagen_82,
Unarmed.Sd_Kfz_7,
Unarmed.Sd_Kfz_2,
Unarmed.Willys_MB,
Unarmed.Land_Rover_109_S3,
Unarmed.Land_Rover_101_FC,
# Mods
frenchpack.VBL,
frenchpack.VAB,
]
TYPE_INFANTRY = [
Infantry.Infantry_Soldier_Insurgents,
Infantry.Soldier_AK,
Infantry.Infantry_M1_Garand,
Infantry.Infantry_Mauser_98,
Infantry.Infantry_SMLE_No_4_Mk_1,
Infantry.Georgian_soldier_with_M4,
Infantry.Infantry_Soldier_Rus,
Infantry.Paratrooper_AKS,
Infantry.Paratrooper_RPG_16,
Infantry.Soldier_M249,
Infantry.Infantry_M4,
Infantry.Soldier_RPG,
]
TYPE_SHORAD = [
AirDefence.AAA_ZU_23_on_Ural_375,
AirDefence.AAA_ZU_23_Insurgent_on_Ural_375,
AirDefence.AAA_ZSU_57_2,
AirDefence.SPAAA_ZSU_23_4_Shilka,
AirDefence.SAM_SA_8_Osa_9A33,
AirDefence.SAM_SA_9_Strela_1_9P31,
AirDefence.SAM_SA_13_Strela_10M3_9A35M3,
AirDefence.SAM_SA_15_Tor_9A331,
AirDefence.SAM_SA_19_Tunguska_2S6,
AirDefence.SPAAA_Gepard,
AirDefence.AAA_Vulcan_M163,
AirDefence.SAM_Linebacker_M6,
AirDefence.SAM_Chaparral_M48,
AirDefence.SAM_Avenger_M1097,
AirDefence.SAM_Roland_ADS,
AirDefence.HQ_7_Self_Propelled_LN,
AirDefence.AAA_8_8cm_Flak_18,
AirDefence.AAA_8_8cm_Flak_36,
AirDefence.AAA_8_8cm_Flak_37,
AirDefence.AAA_8_8cm_Flak_41,
AirDefence.AAA_Bofors_40mm,
AirDefence.AAA_M1_37mm,
AirDefence.AA_gun_QF_3_7,
]

View File

@@ -9,20 +9,21 @@ from __future__ import annotations
import logging
import random
from typing import Dict, Iterator, Optional, TYPE_CHECKING, Type
from typing import Dict, Iterator, Optional, TYPE_CHECKING, Type, List
from dcs import Mission
from dcs import Mission, Point
from dcs.country import Country
from dcs.statics import fortification_map, warehouse_map
from dcs.task import (
ActivateBeaconCommand,
ActivateICLSCommand,
EPLRS,
OptAlarmState,
OptAlarmState, FireAtPoint,
)
from dcs.unit import Ship, Unit, Vehicle
from dcs.unitgroup import Group, ShipGroup, StaticGroup, VehicleGroup
from dcs.unittype import StaticType, UnitType
from dcs.vehicles import vehicle_map
from game import db
from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID
@@ -31,10 +32,10 @@ from game.theater import ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import (
BuildingGroundObject, CarrierGroundObject,
GenericCarrierGroundObject,
LhaGroundObject, ShipGroundObject,
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
@@ -50,7 +51,7 @@ AA_CP_MIN_DISTANCE = 40000
class GenericGroundObjectGenerator:
"""An unspecialized ground object generator.
Currently used only for SAM and missile (V1/V2) sites.
Currently used only for SAM
"""
def __init__(self, ground_object: TheaterGroundObject, country: Country,
game: Game, mission: Mission, unit_map: UnitMap) -> None:
@@ -111,6 +112,58 @@ class GenericGroundObjectGenerator:
persistence_group, miz_group)
class MissileSiteGenerator(GenericGroundObjectGenerator):
def generate(self) -> None:
super(MissileSiteGenerator, self).generate()
# Note : Only the SCUD missiles group can fire (V1 site cannot fire in game right now)
# TODO : Should be pre-planned ?
# TODO : Add delay to task to spread fire task over mission duration ?
for group in self.ground_object.groups:
vg = self.m.find_group(group.name)
if vg is not None:
targets = self.possible_missile_targets(vg)
if targets:
target = random.choice(targets)
real_target = target.point_from_heading(random.randint(0, 360), random.randint(0, 2500))
vg.points[0].add_task(FireAtPoint(real_target))
logging.info("Set up fire task for missile group.")
else:
logging.info("Couldn't setup missile site to fire, no valid target in range.")
else:
logging.info("Couldn't setup missile site to fire, group was not generated.")
def possible_missile_targets(self, vg: Group) -> List[Point]:
"""
Find enemy control points in range
:param vg: Vehicle group we are searching a target for (There is always only oe group right now)
:return: List of possible missile targets
"""
targets: List[Point] = []
for cp in self.game.theater.controlpoints:
if cp.captured != self.ground_object.control_point.captured:
distance = cp.position.distance_to_point(vg.position)
if distance < self.missile_site_range:
targets.append(cp.position)
return targets
@property
def missile_site_range(self) -> int:
"""
Get the missile site range
:return: Missile site range
"""
site_range = 0
for group in self.ground_object.groups:
vg = self.m.find_group(group.name)
if vg is not None:
for u in vg.units:
if u.type in vehicle_map:
if vehicle_map[u.type].threat_range > site_range:
site_range = vehicle_map[u.type].threat_range
return site_range
class BuildingSiteGenerator(GenericGroundObjectGenerator):
"""Generator for building sites.
@@ -247,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
@@ -421,8 +474,11 @@ class GroundObjectsGenerator:
generator = ShipObjectGenerator(
ground_object, country, self.game, self.m,
self.unit_map)
elif isinstance(ground_object, MissileSiteGroundObject):
generator = MissileSiteGenerator(
ground_object, country, self.game, self.m,
self.unit_map)
else:
generator = GenericGroundObjectGenerator(
ground_object, country, self.game, self.m,
self.unit_map)

View File

@@ -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:

View File

@@ -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

View File

@@ -1,5 +1,6 @@
"""Radio frequency types and allocators."""
import itertools
import logging
from dataclasses import dataclass
from typing import Dict, Iterator, List, Set
@@ -71,12 +72,9 @@ class Radio:
self.minimum.hertz, self.maximum.hertz, self.step.hertz
))
class OutOfChannelsError(RuntimeError):
"""Raised when all channels usable by this radio have been allocated."""
def __init__(self, radio: Radio) -> None:
super().__init__(f"No available channels for {radio}")
@property
def last_channel(self) -> RadioFrequency:
return RadioFrequency(self.maximum.hertz - self.step.hertz)
class ChannelInUseError(RuntimeError):
@@ -215,7 +213,13 @@ class RadioRegistry:
self.reserve(channel)
return channel
except StopIteration:
raise OutOfChannelsError(radio)
# In the event of too many channel users, fail gracefully by reusing
# the last channel.
# https://github.com/Khopa/dcs_liberation/issues/598
channel = radio.last_channel
logging.warning(
f"No more free channels for {radio.name}. Reusing {channel}.")
return channel
def alloc_uhf(self) -> RadioFrequency:
"""Allocates a UHF radio channel suitable for inter-flight comms.

34
gen/sam/aaa_ks19.py Normal file
View File

@@ -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

25
gen/sam/aaa_zsu57.py Normal file
View File

@@ -0,0 +1,25 @@
from dcs.vehicles import AirDefence
from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
class ZSU57Generator(AirDefenseGroupGenerator):
"""
This generate a Zsu 57 group
"""
name = "ZSU-57-2 Group"
price = 60
def generate(self):
num_launchers = 5
positions = self.get_circular_position(num_launchers, launcher_distance=110, coverage=360)
for i, position in enumerate(positions):
self.add_unit(AirDefence.AAA_ZSU_57_2, "SPAA#" + str(i), position[0], position[1], position[2])
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Short

View File

@@ -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:

View File

@@ -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):

View File

@@ -11,7 +11,9 @@ 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
from gen.sam.airdefensegroupgenerator import (
AirDefenseGroupGenerator,
@@ -47,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
@@ -98,7 +101,16 @@ SAM_MAP: Dict[str, Type[AirDefenseGroupGenerator]] = {
"ColdWarFlakGenerator": ColdWarFlakGenerator,
"EarlyColdWarFlakGenerator": EarlyColdWarFlakGenerator,
"FreyaGenerator": FreyaGenerator,
"AllyWW2FlakGenerator": AllyWW2FlakGenerator
"AllyWW2FlakGenerator": AllyWW2FlakGenerator,
"ZSU57Generator": ZSU57Generator,
"KS19Generator": KS19Generator,
"SA10BGenerator": SA10BGenerator,
"SA12Generator": SA12Generator,
"SA17Generator": SA17Generator,
"SA20Generator": SA20Generator,
"SA20BGenerator": SA20BGenerator,
"SA23Generator": SA23Generator,
}
@@ -169,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.
@@ -210,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,

View File

@@ -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)

View File

@@ -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:

View File

@@ -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:

View File

@@ -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

30
gen/sam/sam_sa17.py Normal file
View File

@@ -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

2
pydcs

Submodule pydcs updated: c9751f54e0...84f116c358

View File

@@ -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
},
},
}

View File

@@ -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

View File

@@ -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
]

View File

@@ -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
)

View File

@@ -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

View File

@@ -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,

View File

@@ -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."""

View File

@@ -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))

View File

@@ -1,6 +1,7 @@
from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QPushButton
import qt_ui.uiconstants as CONST
from game.income import Income
from qt_ui.windows.finances.QFinancesMenu import QFinancesMenu
@@ -34,14 +35,14 @@ 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:
return
self.game = game
self.setBudget(self.game.budget, self.game.budget_reward_amount)
self.setBudget(self.game.budget, Income(self.game, player=True).total)
self.finances.setEnabled(True)
def openFinances(self):

View File

@@ -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)

107
qt_ui/widgets/QIntelBox.py Normal file
View File

@@ -0,0 +1,107 @@
from typing import Optional
from PySide2.QtWidgets import (
QGridLayout,
QGroupBox,
QHBoxLayout,
QLabel,
QPushButton,
)
from game import Game
from game.income import Income
from qt_ui.windows.intel import IntelWindow
class QIntelBox(QGroupBox):
def __init__(self, game: Game) -> None:
super().__init__("Intel")
self.setProperty("style", "IntelSummary")
self.game = game
columns = QHBoxLayout()
self.setLayout(columns)
summary = QGridLayout()
columns.addLayout(summary)
summary.addWidget(QLabel("Air superiority:"), 0, 0)
self.air_strength = QLabel()
summary.addWidget(self.air_strength, 0, 1)
summary.addWidget(QLabel("Front line:"), 1, 0)
self.ground_strength = QLabel()
summary.addWidget(self.ground_strength, 1, 1)
summary.addWidget(QLabel("Economic strength:"), 2, 0)
self.economic_strength = QLabel()
summary.addWidget(self.economic_strength, 2, 1)
details = QPushButton("Details")
columns.addWidget(details)
details.clicked.connect(self.open_details_window)
self.update_summary()
self.details_window: Optional[IntelWindow] = None
def set_game(self, game: Optional[Game]) -> None:
self.game = game
self.update_summary()
@staticmethod
def forces_strength_text(own: int, enemy: int) -> str:
if not enemy:
return "enemy eliminated"
ratio = own / enemy
if ratio < 0.6:
return "outnumbered"
if ratio < 0.8:
return "slightly outnumbered"
if ratio < 1.2:
return "evenly matched"
if ratio < 1.4:
return "slight advantage"
return "strong advantage"
def economic_strength_text(self) -> str:
assert self.game is not None
own = Income(self.game, player=True).total
enemy = Income(self.game, player=False).total
if not enemy:
return "enemy economy ruined"
ratio = own / enemy
if ratio < 0.6:
return "strong disadvantage"
if ratio < 0.8:
return "slight disadvantage"
if ratio < 1.2:
return "evenly matched"
if ratio < 1.4:
return "slight advantage"
return "strong advantage"
def update_summary(self) -> None:
if self.game is None:
self.air_strength.setText("no data")
self.ground_strength.setText("no data")
self.economic_strength.setText("no data")
return
data = self.game.game_stats.data_per_turn[-1]
self.air_strength.setText(self.forces_strength_text(
data.allied_units.aircraft_count,
data.enemy_units.aircraft_count))
self.ground_strength.setText(self.forces_strength_text(
data.allied_units.vehicles_count,
data.enemy_units.vehicles_count))
self.economic_strength.setText(self.economic_strength_text())
def open_details_window(self) -> None:
self.details_window = IntelWindow(self.game)
self.details_window.show()

View File

@@ -1,3 +1,6 @@
import logging
import timeit
from datetime import timedelta
from typing import List, Optional
from PySide2.QtWidgets import (
@@ -16,6 +19,7 @@ from gen.flights.traveltime import TotEstimator
from qt_ui.models import GameModel
from qt_ui.widgets.QBudgetBox import QBudgetBox
from qt_ui.widgets.QFactionsInfos import QFactionsInfos
from qt_ui.widgets.QIntelBox import QIntelBox
from qt_ui.widgets.clientslots import MaxPlayerCount
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.QWaitingForMissionResultWindow import \
@@ -71,6 +75,8 @@ class QTopPanel(QFrame):
self.statistics.setProperty("style", "btn-primary")
self.statistics.clicked.connect(self.openStatisticsWindow)
self.intel_box = QIntelBox(self.game)
self.buttonBox = QGroupBox("Misc")
self.buttonBoxLayout = QHBoxLayout()
self.buttonBoxLayout.addWidget(self.settings)
@@ -90,6 +96,7 @@ class QTopPanel(QFrame):
self.layout.addWidget(self.factionsInfos)
self.layout.addWidget(self.conditionsWidget)
self.layout.addWidget(self.budgetBox)
self.layout.addWidget(self.intel_box)
self.layout.addWidget(self.buttonBox)
self.layout.addStretch(1)
self.layout.addWidget(self.proceedBox)
@@ -106,6 +113,7 @@ class QTopPanel(QFrame):
self.statistics.setEnabled(True)
self.conditionsWidget.setCurrentTurn(game.turn, game.conditions)
self.intel_box.set_game(game)
self.budgetBox.setGame(game)
self.factionsInfos.setGame(game)
@@ -125,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 = []

View File

@@ -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,

View File

@@ -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)

View File

@@ -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:

View File

@@ -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

View File

@@ -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())

View File

@@ -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:

View File

@@ -4,7 +4,7 @@ from PySide2.QtGui import QColor, QPainter
from PySide2.QtWidgets import QAction, QMenu
import qt_ui.uiconstants as const
from game.theater import ControlPoint
from game.theater import ControlPoint, NavalControlPoint
from qt_ui.models import GameModel
from qt_ui.windows.basemenu.QBaseMenu2 import QBaseMenu2
from .QMapObject import QMapObject
@@ -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
@@ -108,7 +108,8 @@ class QMapControlPoint(QMapObject):
def open_new_package_dialog(self) -> None:
"""Extends the default packagedialog to redirect to base menu for red air base."""
if not self.control_point.captured:
self.on_click()
else:
super(QMapControlPoint, self).open_new_package_dialog()
is_navy = isinstance(self.control_point, NavalControlPoint)
if self.control_point.captured or is_navy:
super().open_new_package_dialog()
return
self.on_click()

View File

@@ -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"])

View File

@@ -47,7 +47,7 @@ class QMapObject(QGraphicsRectItem):
object_details_action.triggered.connect(self.on_click)
menu.addAction(object_details_action)
# Not all locations have valid objetives. Off-map spawns, for example,
# Not all locations have valid objectives. Off-map spawns, for example,
# have no mission types.
if list(self.mission_target.mission_types(for_player=True)):
new_package_action = QAction(f"New package")

View File

@@ -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:

View File

@@ -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)
@@ -284,7 +285,7 @@ class QLiberationWindow(QMainWindow):
"<h4>Authors</h4>" + \
"<p>DCS Liberation was originally developed by <b>shdwp</b>, DCS Liberation 2.0 is a partial rewrite based on this work by <b>Khopa</b>." \
"<h4>Contributors</h4>" + \
"shdwp, Khopa, ColonelPanic, Roach, Wrycu, calvinmorrow, JohanAberg, Deus, root0fall, Captain Cody, steveveepee, pedromagueija, parithon, bwRavencl, davidp57, Plob" + \
"shdwp, Khopa, ColonelPanic, Roach, Wrycu, calvinmorrow, JohanAberg, Deus, root0fall, Captain Cody, steveveepee, pedromagueija, parithon, bwRavencl, davidp57, Plob, Hawkmoon" + \
"<h4>Special Thanks :</h4>" \
"<b>rp-</b> <i>for the pydcs framework</i><br/>"\
"<b>Grimes (mrSkortch)</b> & <b>Speed</b> <i>for the MIST framework</i><br/>"\

View File

@@ -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"<b>Name:</b> {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"<b>Country of Origin:</b> {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"<b>Role:</b> {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"<b>Variant Introduction:</b> {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"<b>In-Game Tasks:</b> {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]

View File

@@ -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:

View File

@@ -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("<b>" + db.unit_type_name_2(unit_type) + "</b>")
unitName = QLabel("<b>" + db.unit_get_expanded_info(self.game_model.game.player_country, unit_type, 'name') + "</b>")
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

View File

@@ -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,
@@ -88,8 +88,17 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
if self.maximum_units > 0:
if self.cp.unclaimed_parking(self.game_model.game) <= 0:
logging.debug(f"No space for additional aircraft at {self.cp}.")
QMessageBox.warning(
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()

View File

@@ -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")

View File

@@ -7,7 +7,8 @@ from PySide2.QtWidgets import (
QWidget,
)
from game.theater import Airport, ControlPoint
from game.theater import Airport, ControlPoint, Fob
from game.theater.theatergroundobject import BuildingGroundObject
from qt_ui.windows.basemenu.base_defenses.QBaseDefenseGroupInfo import \
QBaseDefenseGroupInfo
@@ -30,9 +31,18 @@ class QBaseInformation(QFrame):
scroll_content.setLayout(task_box_layout)
for g in self.cp.ground_objects:
if g.airbase_group and len(g.groups) > 0:
group_info = QBaseDefenseGroupInfo(self.cp, g, self.game)
task_box_layout.addWidget(group_info)
# Airbase groups are the objects that are hidden on the map because
# they're shown in the base menu.
if not g.airbase_group:
continue
# Of these, we need to ignore the FOB structure itself since that's
# not supposed to be targetable.
if isinstance(self.cp, Fob) and isinstance(g, BuildingGroundObject):
continue
group_info = QBaseDefenseGroupInfo(self.cp, g, self.game)
task_box_layout.addWidget(group_info)
scroll_content.setLayout(task_box_layout)
scroll = QScrollArea()

View File

@@ -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)

View File

@@ -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("<b>" + db.unit_type_name(unit_type) + "</b>"), row, 0)
groupLayout.addWidget(QLabel("<b>" + db.unit_get_expanded_info(self.game.enemy_country, unit_type, 'name') + "</b>"), 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)

View File

@@ -1,19 +1,94 @@
from PySide2.QtWidgets import QDialog, QGridLayout, QLabel, QFrame, QSizePolicy
import itertools
from typing import Optional
from PySide2.QtWidgets import (
QDialog,
QFrame,
QGridLayout,
QLabel,
QSizePolicy,
)
import qt_ui.uiconstants as CONST
from game.db import REWARDS, PLAYER_BUDGET_BASE
from game.game import Game
from game.income import BuildingIncome, Income
from game.theater import ControlPoint
class QHorizontalSeparationLine(QFrame):
def __init__(self):
super().__init__()
self.setMinimumWidth(1)
self.setFixedHeight(20)
self.setFrameShape(QFrame.HLine)
self.setFrameShadow(QFrame.Sunken)
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum)
def __init__(self):
super().__init__()
self.setMinimumWidth(1)
self.setFixedHeight(20)
self.setFrameShape(QFrame.HLine)
self.setFrameShadow(QFrame.Sunken)
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum)
class FinancesLayout(QGridLayout):
def __init__(self, game: Game, player: bool) -> None:
super().__init__()
self.row = itertools.count(0)
income = Income(game, player)
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.add_line()
buildings = reversed(sorted(income.buildings, key=lambda b: b.income))
for building in buildings:
self.add_building(building)
self.add_line()
self.add_row(middle=f"Income multiplier: {income.multiplier:.1f}",
right=f"<b>{income.total}M</b>")
if player:
budget = game.budget
else:
budget = game.enemy_budget
self.add_row(middle="Balance", right=f"<b>{budget}M</b>")
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"<b>{control_point.name}</b>",
right=f"{control_point.income_per_turn}M")
def add_building(self, building: BuildingIncome) -> None:
row = next(self.row)
self.addWidget(
QLabel(f"<b>{building.category.upper()} [{building.name}]</b>"),
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):
@@ -26,49 +101,4 @@ class QFinancesMenu(QDialog):
self.setWindowIcon(CONST.ICONS["Money"])
self.setMinimumSize(450, 200)
reward = PLAYER_BUDGET_BASE * len(self.game.theater.player_points())
layout = QGridLayout()
layout.addWidget(QLabel("<b>Control Points</b>"), 0, 0)
layout.addWidget(QLabel(str(len(self.game.theater.player_points())) + " bases x " + str(PLAYER_BUDGET_BASE) + "M"), 0, 1)
layout.addWidget(QLabel(str(reward) + "M"), 0, 2)
layout.addWidget(QHorizontalSeparationLine(), 1, 0, 1, 3)
i = 2
for cp in self.game.theater.player_points():
obj_names = []
[obj_names.append(ground_object.obj_name) for ground_object in cp.ground_objects if ground_object.obj_name not in obj_names]
for obj_name in obj_names:
reward = 0
g = None
cat = None
number = 0
for ground_object in cp.ground_objects:
if ground_object.obj_name != obj_name or ground_object.is_dead:
continue
else:
if g is None:
g = ground_object
cat = g.category
if cat in REWARDS.keys():
number = number + 1
reward += REWARDS[cat]
if g is not None and cat in REWARDS.keys():
layout.addWidget(QLabel("<b>" + g.category.upper() + " [" + obj_name + "]</b>"), i, 0)
layout.addWidget(QLabel(str(number) + " buildings x " + str(REWARDS[cat]) + "M"), i, 1)
rlabel = QLabel(str(reward) + "M")
rlabel.setProperty("style", "green")
layout.addWidget(rlabel, i, 2)
i = i + 1
self.setLayout(layout)
layout.addWidget(QHorizontalSeparationLine(), i + 1, 0, 1, 3)
layout.addWidget(QLabel(
f"Income multiplier: {game.settings.player_income_multiplier:.1f}"),
i + 2, 1
)
layout.addWidget(
QLabel("<b>" + str(self.game.budget_reward_amount) + "M </b>"),
i + 2, 2)
self.setLayout(FinancesLayout(game, player=True))

View File

@@ -21,6 +21,7 @@ from game import Game, db
from game.data.building_data import FORTIFICATION_BUILDINGS
from game.db import PRICES, PinpointStrike, REWARDS, unit_type_of
from game.theater import ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import NavalGroundObject
from gen.defenses.armor_group_generator import \
generate_armor_group_of_type_and_size
from gen.sam.sam_group_generator import get_faction_possible_sams_generator
@@ -81,9 +82,10 @@ class QGroundObjectMenu(QDialog):
self.buy_replace.clicked.connect(self.buy_group)
self.buy_replace.setProperty("style", "btn-success")
if self.total_value > 0:
self.actionLayout.addWidget(self.sell_all_button)
self.actionLayout.addWidget(self.buy_replace)
if not isinstance(self.ground_object, NavalGroundObject):
if self.total_value > 0:
self.actionLayout.addWidget(self.sell_all_button)
self.actionLayout.addWidget(self.buy_replace)
if self.cp.captured and self.ground_object.dcs_identifier == "AA":
self.mainLayout.addLayout(self.actionLayout)
@@ -355,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)

147
qt_ui/windows/intel.py Normal file
View File

@@ -0,0 +1,147 @@
import itertools
from PySide2.QtWidgets import (
QDialog,
QFrame,
QGridLayout,
QLabel,
QLayout,
QScrollArea,
QSizePolicy,
QSpacerItem,
QTabWidget,
QVBoxLayout,
QWidget,
)
from game.game import Game, db
from qt_ui.uiconstants import ICONS
from qt_ui.windows.finances.QFinancesMenu import FinancesLayout
class ScrollingFrame(QFrame):
def __init__(self) -> None:
super().__init__()
widget = QWidget()
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setWidget(widget)
self.scrolling_layout = QVBoxLayout()
widget.setLayout(self.scrolling_layout)
self.setLayout(QVBoxLayout())
self.layout().addWidget(scroll_area)
def addWidget(self, widget: QWidget, *args, **kwargs) -> None:
self.scrolling_layout.addWidget(widget, *args, **kwargs)
def addLayout(self, layout: QLayout, *args, **kwargs) -> None:
self.scrolling_layout.addLayout(layout, *args, **kwargs)
class EconomyIntelTab(ScrollingFrame):
def __init__(self, game: Game) -> None:
super().__init__()
self.addLayout(FinancesLayout(game, player=False))
class IntelTableLayout(QGridLayout):
def __init__(self) -> None:
super().__init__()
self.row = itertools.count(0)
def add_header(self, text: str) -> None:
self.addWidget(QLabel(f"<b>{text}</b>"), next(self.row), 0)
def add_spacer(self) -> None:
self.addItem(
QSpacerItem(0, 0, QSizePolicy.Preferred, QSizePolicy.Expanding),
next(self.row), 0)
def add_row(self, text: str, count: int) -> None:
row = next(self.row)
self.addWidget(QLabel(text), row, 0)
self.addWidget(QLabel(str(count)), row, 1)
class AircraftIntelLayout(IntelTableLayout):
def __init__(self, game: Game, player: bool) -> None:
super().__init__()
total = 0
for control_point in game.theater.control_points_for(player):
base = control_point.base
total += base.total_aircraft
if not base.total_aircraft:
continue
self.add_header(control_point.name)
for airframe, count in base.aircraft.items():
if not count:
continue
self.add_row(db.unit_get_expanded_info(game.enemy_country, airframe, 'name'), count)
self.add_spacer()
self.add_row("<b>Total</b>", total)
class AircraftIntelTab(ScrollingFrame):
def __init__(self, game: Game) -> None:
super().__init__()
self.addLayout(AircraftIntelLayout(game, player=False))
class ArmyIntelLayout(IntelTableLayout):
def __init__(self, game: Game, player: bool) -> None:
super().__init__()
total = 0
for control_point in game.theater.control_points_for(player):
base = control_point.base
total += base.total_armor
if not base.total_armor:
continue
self.add_header(control_point.name)
for vehicle, count in base.armor.items():
if not count:
continue
self.add_row(vehicle.id, count)
self.add_spacer()
self.add_row("<b>Total</b>", total)
class ArmyIntelTab(ScrollingFrame):
def __init__(self, game: Game) -> None:
super().__init__()
self.addLayout(ArmyIntelLayout(game, player=False))
class IntelTabs(QTabWidget):
def __init__(self, game: Game):
super().__init__()
self.addTab(EconomyIntelTab(game), "Economy")
self.addTab(AircraftIntelTab(game), "Air forces")
self.addTab(ArmyIntelTab(game), "Ground forces")
class IntelWindow(QDialog):
def __init__(self, game: Game):
super().__init__()
self.game = game
self.setModal(True)
self.setWindowTitle("Intelligence")
self.setWindowIcon(ICONS["Statistics"])
self.setMinimumSize(600, 500)
layout = QVBoxLayout()
self.setLayout(layout)
layout.addWidget(IntelTabs(game), stretch=1)

View File

@@ -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)

View File

@@ -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("<a href=\"https://github.com/Khopa/dcs_liberation/wiki/Mission-planning\"><span style=\"color:#FFFFFF;\">Help</span></a>")
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()

View File

@@ -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<br />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:

View File

@@ -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")

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