Compare commits

...

176 Commits
2.4.1 ... 2.5.1

Author SHA1 Message Date
Dan Albert
a0e5a707fb Merge pull request #1053 from Khopa/develop_2_5_x
Release 2.5.1.
2021-05-02 13:28:45 -07:00
Dan Albert
4555a4968d Update changelog for 2.5.1. 2021-05-02 13:17:35 -07:00
SnappyComebacks
ae34e4749b Move base EWRs into their own category.
Without this we're sometimes spawning base EWRs at points far outside the base perimeter.
2021-04-28 21:07:22 -07:00
Khopa
635eee9590 Fixed ai_flight_planner for maps lacking frontlines (such as battle of britain on The Channel map) 2021-04-24 00:11:53 +02:00
Khopa
f0558c4c1e Fixed ai_flight_planner for maps lacking frontlines (such as battle of britain on The Channel map) 2021-04-23 23:45:14 +02:00
Dan Albert
637ca8fbca Stop projecting threat zones from front lines.
This is an interim improvement since we should probably be pushing the
BARCAPs into TARCAP roles when the front line is so close. This does
regress flight pathing for anything that should route around the front
(to avoid getting shot at by SHORADS and TARCAPs), but for now it's one
or the other and this is the one everyone's complaining about.

(cherry picked from commit e474748f4d)
2021-04-22 18:23:12 -07:00
Dan Albert
e4e65df976 Generalize commit range display for all patrols.
Fixes https://github.com/Khopa/dcs_liberation/issues/890

(cherry picked from commit 132ba905c7)
2021-04-22 17:55:49 -07:00
Dan Albert
29579a2aec Remove missed merge conflict marker. 2021-04-22 17:49:34 -07:00
Dan Albert
e32b43cffb Show BARCAP commit ranges by default.
BARCAP placement confuses a lot of people but this should make it more
clear.

(cherry picked from commit 208d1b82b5)
2021-04-22 17:46:29 -07:00
C. Perreau
de2e5f861b Merge pull request #1007 from Khopa/develop_2_5_x
Release 2.5.0
2021-04-22 00:08:42 +02:00
Khopa
b27a7fc71b Fixed Lint issue 2021-04-21 22:54:48 +02:00
Khopa
5861ce6146 Fixed error with Ramat David frequency (typo) 2021-04-21 22:38:08 +02:00
Khopa
c732ed556f Fixed airfields frequency on Persian Gulf 2021-04-21 22:30:08 +02:00
Khopa
be1a75e520 Fixed airfields frequency on Syria 2021-04-21 22:14:18 +02:00
Khopa
c41d10c581 Pydcs update to latest version 2021-04-21 12:57:19 +02:00
Dan Albert
157a59e3c4 Fix UI crash when unchecking default loadout.
This was throwing because it was being called with the wrong number of
arguments, preventing the UI from actually updating back to the default.
2021-04-18 13:05:22 -07:00
Khopa
d24c65c3aa Fixed airfield data airport name for Persian Gulf map 2021-04-18 20:10:52 +02:00
Khopa
d4d441ff9b Fixed some factions errors that weren't caught yet. 2021-04-18 18:11:00 +02:00
Khopa
f43fb1223f Fix : Fixed duplicate units on cold war flak site. 2021-04-18 15:16:17 +02:00
Khopa
3db275414d Allow 0 income multiplier in game settings windows (this was already possible in new game wizard) 2021-04-18 01:20:32 +02:00
Khopa
6e0ff6c805 pydcs update 2021-04-18 01:13:50 +02:00
Dan Albert
9c359efbff Note Litening -> ATFLIR change. 2021-04-17 16:03:56 -07:00
Dan Albert
c5cc1ea8e8 Make the F/A-18C strike loadout less silly.
Instead of 4xMk83 and 4xGBU-38, 2 bags and 2 GBU-31. ATFLIR added for
TOO/BDA.
2021-04-17 15:51:46 -07:00
Dan Albert
afb6a33131 Replace Litening II with ATFLIR in Honet loadouts.
https://github.com/pydcs/dcs/pull/120
2021-04-17 15:43:49 -07:00
Khopa
539a11f54d Added icons for new units 2021-04-18 00:15:10 +02:00
Khopa
9324e549e6 Updated changelog 2021-04-18 00:13:43 +02:00
Khopa
c8f6b6df87 Fixed lint issue 2021-04-18 00:11:06 +02:00
Dan Albert
38f632097e Add support for DCS 2.7 weather generation.
https://github.com/Khopa/dcs_liberation/issues/981
2021-04-17 15:06:17 -07:00
Khopa
e63743f537 Improved FOB support : new custom banner for FOB menu and do not display aircrafts menu on first page. 2021-04-17 23:49:49 +02:00
Khopa
ce13295cf0 pydcs repo now pointing on temporary branch 2-7-temp on https://github.com/Khopa/dcs for new weather development 2021-04-17 23:06:48 +02:00
Khopa
23c02a3510 Updated airfields data for the Channel map 2021-04-17 17:50:41 +02:00
Khopa
01ea7b9ee1 Updated airfields metadata for Syria 2021-04-17 17:37:15 +02:00
Khopa
6fed1284a1 Updated airfields metadata for Syria 2021-04-17 17:35:40 +02:00
Khopa
5574d849bd Unit support : S-60 added to Syria faction 2021-04-17 13:11:58 +02:00
Khopa
c2ce3a6992 Fixed Lint issue 2021-04-17 13:11:26 +02:00
Khopa
b61d15fdf4 Unit support : Added support for the PLZ-05, new artillery unit from the Chinese Asset Pack 2021-04-17 11:28:36 +02:00
Khopa
ad5cc83fb3 Unit support : now using the new unit S-60 57mm AA Gun units. 2021-04-17 11:23:00 +02:00
Ronny Röhricht
2f53edd775 Add plugin for exporting RED and BLUE threat circles to LotATC.
Implemented as a plugin because LotATC needs actual lat/lon, and the only APIs for those are in lua.

Fixes https://github.com/Khopa/dcs_liberation/issues/956.
2021-04-17 00:55:06 -07:00
Khopa
923459c88b Pydcs update to the good commit reference 2021-04-17 02:35:34 +02:00
Khopa
1192d26448 Fixed lint issue 2021-04-17 02:27:42 +02:00
Khopa
2d5e827417 Pydcs update to master repo 2021-04-17 02:26:31 +02:00
Khopa
a30d9276b8 Merge remote-tracking branch 'khopa/develop' into develop 2021-04-17 02:22:56 +02:00
Khopa
b963c2272f More naming fixes 2021-04-17 02:21:19 +02:00
Khopa
221cb8709b Ran black formatter 2021-04-17 02:15:49 +02:00
Khopa
648857fc44 Removed deprecated faction 2021-04-17 02:15:02 +02:00
Khopa
8091051bb4 Fixed weapons names in pdcs extensions, removed deprecated rafale mod, fixed many other compilation issues with pydcs 2.7+ 2021-04-17 02:13:52 +02:00
Khopa
1e468cd3e0 Fixed weapons fallback db names with new pydcs version 2021-04-17 01:23:08 +02:00
Khopa
15d2a5bb2b Updated units name in liberation 2021-04-16 23:33:22 +02:00
Khopa
5c76229ee5 Referencing pydcs new version 2021-04-16 23:31:03 +02:00
Dan Albert
0cd088122e Remove WIP status of AEW&C missions. 2021-04-15 21:35:25 -07:00
Dan Albert
b6f3467a89 Update changelog. 2021-04-15 21:34:31 -07:00
SnappyComebacks
52ce1a5959 Add support for additional EWR sites in campaigns.
* A Bluefor EWR 55GS in the campaign miz defines an optional EWR site. There is no distinction between how close or far it is to a base, so it's possible that there will be many EWRs within an airbase.
* A Redfor EWR 1L13 in the campaign miz defines a required EWR site.

It would be a good future idea to limit the amount of EWRs within a certain distance from an airbase. That way there's no chance of 5 EWRs all at the same airbase. Even better if there were something preventing any two EWRs from being right next to each other.

No campaigns take advantage of this yet.

Fixes https://github.com/Khopa/dcs_liberation/issues/524
2021-04-15 21:23:27 -07:00
Khopa
7ce05762f5 Possible to add additional helipad to any control point in campaign file. (WIP) 2021-04-14 00:00:25 +02:00
Dan Albert
cce736bc16 Note the font crash fix in the changelog. 2021-04-11 13:27:39 -07:00
Hanninho
2a1127e637 Force the basic layout engine when generating the kneeboard.
The libraqm backed layout engine causes crashes on some machines.

Fixes #531.
2021-04-11 13:24:31 -07:00
Dan Albert
8ca68b3d7a Set win/loss status for functioning airfields.
"Fixes" https://github.com/Khopa/dcs_liberation/issues/833. The crash is
still present, but we're at least telling the player that the game is
over so they shouldn't try to play. The UX for this sucks
(https://github.com/Khopa/dcs_liberation/issues/978), but it's the same
as other end-game states.
2021-04-10 15:44:57 -07:00
Dan Albert
0f76d893b8 Note date fix in the changelog. 2021-04-10 15:22:27 -07:00
Dan Albert
ab746b5195 Fix date given to the conditions generator.
`Game.date` is actually the start date, not the current date. Not renaming to
avoid breaking save compat.

This fix won't have any effect on existing saves until they pass the turn
because this is encoded into the conditions generated at the start of the turn,
but it will fix on the next turn.

Fixes https://github.com/Khopa/dcs_liberation/issues/973
2021-04-10 15:20:54 -07:00
Khopa
828c87df39 Game settings / new game wizard : Allowed a 0% income multiplier. 2021-04-07 19:33:23 +02:00
C. Perreau
888aeb621d Merge pull request #955 from Hornet2041/Integrate-splash-damage-script-plugin
Integrate splash damage plugin script
2021-04-05 20:33:17 +02:00
Khopa
ac2fddf87e Changelog update 2021-03-31 00:28:52 +02:00
Khopa
614304cc81 Added F86 Sabre loadout by Starfire. 2021-03-31 00:20:19 +02:00
Khopa
f363d66aac F_86F Sabre payloads can now be customized. 2021-03-31 00:17:21 +02:00
Khopa
1706c42695 Fix : Added Mig-19P to CAS capable aircraft list 2021-03-31 00:00:44 +02:00
Khopa
2b44b2fc0b Ran formatter to fix lint issue 2021-03-29 23:53:09 +02:00
C. Perreau
25986aa15c Merge pull request #928 from Mustang-25/patch-1
Add GAR-8 restriction date and fallback info
2021-03-29 23:51:21 +02:00
C. Perreau
264eb01afc Merge pull request #947 from SnappyComebacks/add-e2c-to-more-factions
Add E-2C to more factions
2021-03-29 23:49:27 +02:00
Khopa
6db3c3f9f1 Updated changelog 2021-03-29 23:46:44 +02:00
SnappyComebacks
714992bdcb ARMADILLLO to ARMADILLO. 2021-03-27 13:02:16 -07:00
SnappyComebacks
ca7a86b6d7 Merge branch 'develop' into add-e2c-to-more-factions 2021-03-27 11:04:13 -06:00
GvonH
49e729e9ec Add dark kneeboard option for night missions (#951) 2021-03-22 19:41:54 -07:00
Hornet2041
7f0a690c7b Add files via upload
referencing the original upload in Discord here: https://discord.com/channels/595702951800995872/768226890158702654/809571449979142174

and wheelyjoe's github repository here: https://github.com/wheelyjoe/DCS-Scripts

"Improves splash damage modelling by pulling weapon warhead info (where available) and using this to create explosions (only way to apply damage) to units in more sensible range." Makes using non-precision weaponry actually viable for ground targets.
2021-03-22 09:43:42 -04:00
C. Perreau
d07afc603b Merge pull request #950 from Khopa/dependabot/pip/pillow-8.1.1
Bump pillow from 7.2.0 to 8.1.1
2021-03-21 19:09:21 +01:00
Khopa
5bd4c00257 Merge branch 'develop_2_4_x' into develop
# Conflicts:
#	changelog.md
#	game/db.py
#	game/navmesh.py
#	game/operation/operation.py
#	game/theater/conflicttheater.py
#	game/theater/controlpoint.py
#	game/theater/start_generator.py
#	game/theater/theatergroundobject.py
#	game/threatzones.py
#	game/version.py
#	gen/aircraft.py
#	gen/airsupportgen.py
#	gen/fleet/carrier_group.py
#	gen/flights/ai_flight_planner.py
#	gen/flights/ai_flight_planner_db.py
#	gen/flights/flightplan.py
#	gen/flights/waypointbuilder.py
#	gen/groundobjectsgen.py
#	gen/kneeboard.py
#	pydcs
#	pydcs_extensions/f22a/f22a.py
#	qt_ui/uiconstants.py
#	qt_ui/widgets/combos/QAircraftTypeSelector.py
#	qt_ui/widgets/map/QLiberationMap.py
#	qt_ui/windows/QUnitInfoWindow.py
#	qt_ui/windows/mission/flight/payload/QPylonEditor.py
#	qt_ui/windows/settings/QSettingsWindow.py
2021-03-21 18:50:50 +01:00
dependabot[bot]
13272aa280 Bump pillow from 7.2.0 to 8.1.1
Bumps [pillow](https://github.com/python-pillow/Pillow) from 7.2.0 to 8.1.1.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/7.2.0...8.1.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-19 15:06:06 +00:00
Simon Krüger
260358c5fb AEW&C kneeboard + actually do AEW&C (#922)
* AEW&C will now do AEW&C
* AEW&C gets a frequency
* AEW&C is added to kneeboard (Frequency, Depature, Depature Time, Arrival Time)
2021-03-13 14:07:19 -08:00
Dan Albert
0f07b2c095 Increase size of navmesh to avoid planning issues.
The tradeoff is that any flights that might have previously routed
_around_ a threat near the edge of the map may no longer do so as the
zones at the edge are significantly larger now.

Fixes https://github.com/Khopa/dcs_liberation/issues/903
2021-03-13 13:41:57 -08:00
Dan Albert
b2fafc22dd Fix flight path debugging. 2021-03-13 13:34:49 -08:00
Dan Albert
6200ec8e0e Don't plan BAI targets at dead subgroups.
Fixes https://github.com/Khopa/dcs_liberation/issues/900
2021-03-13 13:16:10 -08:00
Dan Albert
831516c5f5 Ask for state.json in the bug template. 2021-03-13 12:56:33 -08:00
Dan Albert
fb49d9e6ae Project threat zone from front lines.
Fixes https://github.com/Khopa/dcs_liberation/issues/940
2021-03-13 12:53:10 -08:00
SnappyComebacks
e660726828 Added E-2C elements for UI. 2021-03-12 14:19:50 -07:00
SnappyComebacks
4519f47b19 Added US Aggressors. 2021-03-12 11:16:27 -07:00
SnappyComebacks
47174bbb4d Added E-2C to USA, France, Bluefor. 2021-03-12 11:12:29 -07:00
Mustang-25
de18ce3e7b Add GAR-8 restriction date and fallback info 2021-02-27 15:22:26 -08:00
Dan Albert
b5934633fa Improve wording of "never delay players" option. 2021-02-27 14:16:41 -08:00
Khopa
f314c08216 Improved map scale display 2021-02-27 00:29:23 +01:00
Khopa
6704cded2d Fixed unit info windows crashing when banner not found (variable referenced before assignment error) 2021-02-27 00:10:57 +01:00
Khopa
f11918fc41 Fixed F-22A invalid radio frequency issues for player flights (F-22 mod only allow 100-156Mhz frequency range)
Added F-22A icon and banner.
2021-02-27 00:10:06 +01:00
Khopa
58d5aa9944 Fixes : Missing weapons names would cause flight edition window to crash while setting up default loadout UI, preventing the user from editing flights. 2021-02-26 23:30:23 +01:00
Khopa
60af2ad0a4 Preparing 2.4.4 2021-02-26 23:12:59 +01:00
Dan Albert
4ec8994c38 Avoid warning in GitHub workflow. 2021-02-25 17:32:33 -08:00
Simon Krüger
49d6cece50 Merge pull request #896 from siKruger/aewc_no_threat_zone
if aewc is in threat zone move it further away
2021-02-24 22:14:22 +01:00
sikruger
03251d5bd5 some improvements 2021-02-24 22:06:42 +01:00
BenBenBeartrax
a6e03184bc fictional Korea factions 2021-02-22 16:52:53 -08:00
Khopa
54f2a6f1b5 Release 2.4.3 2021-02-22 21:58:56 +01:00
Khopa
7eed7ae6ba Release 2.4.3 2021-02-22 21:44:11 +01:00
C. Perreau
b3d642fdf5 Merge pull request #913 from Khopa/develop_2_4_x
Release 2.4.3    (fixed)
2021-02-22 21:41:48 +01:00
Khopa
35dd9427a5 Added possibility to set up custom date in new game wizard. 2021-02-22 21:34:14 +01:00
C. Perreau
bf1087df3c Merge pull request #912 from Khopa/develop_2_4_x
2.4.3 Release
2021-02-22 21:04:23 +01:00
Khopa
523ef08697 Ran black reformater on 2.4x branch 2021-02-22 20:55:51 +01:00
Khopa
c2310453d8 Updated version String to 2.4.3 2021-02-22 20:51:53 +01:00
Khopa
b4a6a5dc26 Changelog update for 4.2.3 2021-02-22 20:51:24 +01:00
Khopa
20ceb440fa black 2021-02-22 20:28:54 +01:00
Khopa
4ee9c66524 Updated Hercules cargo script to latest version. 2021-02-22 20:28:52 +01:00
Khopa
9d04abd3af Updated C130J Hercules pydcs data 2021-02-22 20:28:51 +01:00
Khopa
a562345876 Updated credits in about dialog. 2021-02-22 20:28:34 +01:00
Dan Albert
2e7daceb3c Increase file log level to debug.
Fixes https://github.com/Khopa/dcs_liberation/issues/907
2021-02-22 20:27:57 +01:00
Khopa
a9aba3484a F-22 default loadout replaced weapons in internal bay by their "no-drag" versions 2021-02-22 20:24:27 +01:00
Khopa
1b9bbb8eb6 Updated F22 mod 2021-02-22 20:24:19 +01:00
BenBenBeartrax
25c44723a9 customized_payloads: M-2000C add Eclair counter meassures pod to all loadouts 2021-02-22 20:18:24 +01:00
Khopa
688a20c312 Changelog update 2021-02-21 23:01:05 +01:00
Khopa
4c1b34461e Merge remote-tracking branch 'khopa/develop' into develop 2021-02-21 22:57:32 +01:00
Khopa
c6939e7194 Added possibility to set up custom date in new game wizard. 2021-02-21 22:57:21 +01:00
BenBenBeartrax
cd9432e395 kneeboard: custom flight name in title (#911)
If a flight has a custom flight name set it gets appended to the title in the kneeboard.
Partially addresses #862 .
2021-02-21 13:29:31 -08:00
Khopa
7d5244a5bc black 2021-02-21 17:47:51 +01:00
Khopa
1aa5d4f7de Updated Hercules cargo script to latest version. 2021-02-21 17:33:34 +01:00
Khopa
ad74204fe4 Updated C130J Hercules pydcs data 2021-02-21 17:31:58 +01:00
Khopa
5cb1a47ed3 Updated credits in about dialog. 2021-02-21 17:24:15 +01:00
Khopa
fff22d3cd1 F-22 default loadout replaced weapons in internal bay by their "no-drag" versions 2021-02-21 17:20:51 +01:00
Khopa
665cd7b996 Merge remote-tracking branch 'khopa/develop' into develop 2021-02-21 17:15:54 +01:00
Khopa
61173196d2 Updated F22 mod 2021-02-21 17:15:29 +01:00
Dan Albert
03e98ba562 Increase file log level to debug.
Fixes https://github.com/Khopa/dcs_liberation/issues/907
2021-02-20 15:38:52 -08:00
BenBenBeartrax
6195290adf customized_payloads: M-2000C add Eclair counter meassures pod to all loadouts 2021-02-20 12:49:08 -08:00
sikruger
4f1b0055e1 new point generation 2021-02-19 20:40:58 +01:00
sikruger
dd9fe87ff4 new point generation 2021-02-18 16:08:23 +01:00
sikruger
5bda4abfce if aewc is in threat zone move it further away 2021-02-17 16:47:24 +01:00
Dan Albert
24f98aede5 Undo unintentional change.
Not sure how the extra whitespace got there, but this gets overwritten
on every launch.

(cherry picked from commit 2ffe3bf722)
2021-02-13 13:09:47 -08:00
Dan Albert
f8ae1e9076 Merge pull request #885 from DanAlbert/black
Set up black.
2021-02-12 20:21:41 -08:00
Dan Albert
16b0dcad71 Add black workflow. 2021-02-12 20:13:47 -08:00
Dan Albert
9c1265d50d Add pre-commit configuration for black.
To set up, run `pre-commit install`.
2021-02-12 20:11:41 -08:00
Dan Albert
8c0e781c94 Ignore reformating in blame. 2021-02-12 20:11:36 -08:00
Dan Albert
a47bef1f13 Blacken. 2021-02-12 20:10:45 -08:00
Dan Albert
053663bd76 Merge branch 'develop_2_4_x' into develop 2021-02-12 19:23:13 -08:00
Dan Albert
e40b916b07 Merge pull request #884 from Khopa/develop_2_4_x
Release 2.4.2.
2021-02-12 19:05:44 -08:00
Dan Albert
2ffe3bf722 Undo unintentional change.
Not sure how the extra whitespace got there, but this gets overwritten
on every launch.
2021-02-12 18:59:48 -08:00
Dan Albert
1bc994c102 Fix version number of the release. 2021-02-12 16:18:01 -08:00
Dan Albert
21be4d38e1 Mention new start dates in the changelog.
(cherry picked from commit f7889b785d)
2021-02-12 16:16:36 -08:00
Mustang-25
3b58c571b3 Add a mid-90s campaign date option
1995 is a good date to pick if you want to date restrict all GPS weapons but still have all the laser guided options.

(cherry picked from commit 27829a024a)
2021-02-12 16:16:35 -08:00
Mustang-25
e0430cf607 Adjust HARM Weapon Restriction Date
Official Navy docs have the 88A's IOC date in 1983. Also left a note on the B and C IOC dates if DCS ever adds the older models.

(cherry picked from commit a0fda2552f)
2021-02-12 16:16:31 -08:00
Dan Albert
f7889b785d Mention new start dates in the changelog. 2021-02-12 16:16:13 -08:00
Mustang-25
27829a024a Add a mid-90s campaign date option
1995 is a good date to pick if you want to date restrict all GPS weapons but still have all the laser guided options.
2021-02-12 16:15:20 -08:00
Mustang-25
a0fda2552f Adjust HARM Weapon Restriction Date
Official Navy docs have the 88A's IOC date in 1983. Also left a note on the B and C IOC dates if DCS ever adds the older models.
2021-02-12 16:14:54 -08:00
Simon Krüger
65c185ebd2 Add an option for disabling the legacy AEW&C aircraft.
Using the legacy AEW&C aircraft is still the default until
https://github.com/Khopa/dcs_liberation/issues/844 is fixed.
2021-02-12 14:20:26 -08:00
Dan Albert
fb425d3524 Fix rounding of budget in recruitment menu.
Fixes https://github.com/Khopa/dcs_liberation/issues/861.

(cherry picked from commit 5792eb354c)
2021-02-12 14:01:34 -08:00
Dan Albert
5792eb354c Fix rounding of budget in recruitment menu.
Fixes https://github.com/Khopa/dcs_liberation/issues/861.
2021-02-12 14:00:58 -08:00
Dan Albert
45300b64c5 Mention weapon data in changelog.
(cherry picked from commit dce7d91511)
2021-02-12 13:54:51 -08:00
Dan Albert
dce7d91511 Mention weapon data in changelog. 2021-02-12 13:54:30 -08:00
Brandon Danyluk
4b7ef46f82 Add weapon era restrictions for USA/Russia/UK/France (#860)
(cherry picked from commit 61f1e11a48)
2021-02-12 13:49:40 -08:00
Dan Albert
b67e6d20f1 Move 2.4.2 fix to the correct section.
(cherry picked from commit 3d1afa74d4)
2021-02-12 13:48:02 -08:00
Dan Albert
3d1afa74d4 Move 2.4.2 fix to the correct section. 2021-02-12 13:47:14 -08:00
Dan Albert
0ae6575087 Transfer pending purchases forward along capture.
Fixes https://github.com/Khopa/dcs_liberation/issues/828.

(cherry picked from commit d8c94f5ece)
2021-02-12 13:46:08 -08:00
Dan Albert
d8c94f5ece Transfer pending purchases forward along capture.
Fixes https://github.com/Khopa/dcs_liberation/issues/828.
2021-02-12 13:45:01 -08:00
Brandon Danyluk
61f1e11a48 Add weapon era restrictions for USA/Russia/UK/France (#860) 2021-02-10 22:30:56 -08:00
Simon Krüger
98249b1aca Bugfix: Blue AEW&C above Red CV (#872) 2021-02-10 11:49:49 -08:00
Simon Krüger
8e51b7fc1d Carrier strike group (#863)
Generate a Carrier Group which comes close the the real Carrier Strike Group 8.

Under carrier_names in the faction simply add "Carrier Strike Group 8" as the first and only entry and enable super carrier.

* TRU as TACAN name
* Harry S. Truman CV
* 4x Arleigh Burke
* 1x Ticonderoga
* CV in the middle, Ticonderoga in a radius of 2 miles, Arleigh Burkes forming a rectangle
2021-02-09 14:17:46 -08:00
Simon Krüger
71914b8a8b Aew&c ai planning.
AI will generate AWE&C

* Only one flight per turn
* Takes the airfield farthest away from the frontline
* Prefers CV over any airfield
2021-02-09 12:35:47 -08:00
Khopa
7a077a0d21 Merge remote-tracking branch 'khopa/develop' into develop 2021-02-07 21:16:04 +01:00
Khopa
d23e4665e7 Fixed possible cheat by selling SA-10 SAMs site for more money than what they were bought for.
+ Fixed SA-10 sites having the same name in game UI.
2021-02-07 21:15:48 +01:00
C. Perreau
df12b54856 Merge pull request #858 from benedikt-wegmann/develop_kneeboard_briefing_cleanup
kneeboard: slight cleanup in Jinja
2021-02-07 18:48:47 +01:00
Khopa
ae12053d74 Pydcs update 2021-02-07 18:34:48 +01:00
Khopa
0e7695f2d6 pydcs update repo 2021-02-07 17:49:01 +01:00
BenBenBeartrax
38cee87ee9 kneeboard: slight cleanup in Jinja by not rendering sections that are empty anyway (JTAC, Carriers, AWACS etc.) 2021-02-07 16:44:41 +01:00
Simon Clark
c64a6083e2 Merge branch 'develop' of https://github.com/Khopa/dcs_liberation into develop 2021-02-07 11:41:04 +00:00
Simon Krüger
e0501e46e3 Initial implementation of AEW&C missions.
Still a work in progress (the missions don't actually perform their task, just orbit). Currently:

* AEW&C aircraft can be bought.
* AEW&C missions can be planned at any control point and at front lines.
* AEW&C will return after 4H or Bingo.
2021-02-07 11:39:22 +00:00
Khopa
4a0ccc4c2f Fixed error detected by mypy 2021-02-07 11:39:21 +00:00
Khopa
c92b7240eb Increased number of launchers on Silkworms sites 2021-02-07 11:39:21 +00:00
Khopa
6a74c3faeb Added coastal defenses sites generator for Iran and China. 2021-02-07 11:39:21 +00:00
BenBenBeartrax
c5ae872787 waypointbuilder: low altitude AGL for helos 2021-02-07 11:39:21 +00:00
Dan Albert
2e9ab0a9d7 Move develop to 2.5. 2021-02-07 11:39:20 +00:00
Simon Krüger
a004f62fe8 Initial implementation of AEW&C missions.
Still a work in progress (the missions don't actually perform their task, just orbit). Currently:

* AEW&C aircraft can be bought.
* AEW&C missions can be planned at any control point and at front lines.
* AEW&C will return after 4H or Bingo.
2021-02-05 17:18:50 -08:00
Khopa
07ce20fd29 Fixed error detected by mypy 2021-02-05 00:48:35 +01:00
Khopa
df74f7c6c7 Increased number of launchers on Silkworms sites 2021-02-05 00:25:42 +01:00
Khopa
2a6e2d470d Added coastal defenses sites generator for Iran and China. 2021-02-05 00:15:06 +01:00
BenBenBeartrax
643dd65113 waypointbuilder: low altitude AGL for helos 2021-02-02 14:30:28 -08:00
Dan Albert
cee0532b85 Move develop to 2.5. 2021-01-31 15:42:13 -08:00
353 changed files with 15775 additions and 9476 deletions

2
.git-blame-ignore-revs Normal file
View File

@@ -0,0 +1,2 @@
# Black
a47bef1f1336fd264d0b175f4421758339a30acb

View File

@@ -28,7 +28,8 @@ We will usually need more information for debugging. Include as much of the foll
- 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`). - 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`). - 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`). - A tacview track file, especially when demonstrating an issue with AI behavior. By default these are located in your Tacview tracks directory (`%USERPROFILE%/Documents/Tacview`).
- The state.json file from the finished mission when the problem is related to results processing. By default these are located in your Liberation install directory.
**Version information (please complete the following information):** **Version information (please complete the following information):**
- DCS Liberation [e.g. 2.3.1]: - DCS Liberation [e.g. 2.3.1]:

13
.github/workflows/black.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
name: Lint
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: psf/black@stable
with:
args: ". --check"

1
.gitignore vendored
View File

@@ -11,6 +11,7 @@ a.py
resources/tools/a.miz resources/tools/a.miz
# User-specific stuff # User-specific stuff
.idea/ .idea/
.env
/kneeboards /kneeboards
/liberation_preferences.json /liberation_preferences.json

6
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,6 @@
repos:
- repo: https://github.com/psf/black
rev: 20.8b1
hooks:
- id: black
language_version: python3

View File

@@ -1,3 +1,74 @@
# 2.5.1
## Features/Improvements
* **[UI]** Engagement ranges are now displayed by default.
* **[UI]** Engagement range display generalized to work for all patrolling flight plans (BARCAP, TARCAP, and CAS).
* **[Flight Planner]** Front lines no longer project threat zones to avoid pushing BARCAPs back so much. TARCAPs will be forcibly planned but strike packages will not route around front lines even if it is reasonable to do so.
## Fixes
* **[Campaigns]** EWRs associated with a base will now only be generated near the base.
* **[Flight Planner]** Fixed error when generating AEW&C flight plans in campaigns with no front lines.
# 2.5.0
Saves from 2.4 are not compatible with 2.5.
## Features/Improvements
* **[Engine]** DCS 2.7 Support
* **[UI]** Improved FOB menu, added a custom banner, and do not display aircraft recruitment menu
* **[Flight Planner]** Added AEW&C missions. (by siKruger)
* **[Kneeboard]** Added dark kneeboard option (by GvonH)
* **[Campaigns]** Multiple EWR sites may now be generated, and EWR sites may be generated outside bases (by SnappyComebacks)
* **[Mission Generation]** Cloudy and rainy (but not thunderstorm) weather will use the cloud presets from DCS 2.7.
* **[Plugins]** Added LotATC export plugin (by drsoran)
* **[Plugins]** Added Splash Damage Plugin (by Wheelijoe)
* **[Loadouts]** Replaced Litening with ATFLIR for all default F/A-18C loadouts.
## Fixes
* **[Flight Planner]** Front lines now project threat zones, so TARCAP/escorts will not be pruned for flights near the front. Packages may also route around the front line when practical.
* **[Flight Planner]** Fixed error when planning BAI at SAMs with dead subgroups.
* **[Flight Planner]** Mig-19 was not allowed for CAS roles fixed
* **[Flight Planner]** Increased size of navigation planning area to avoid plannign failures with distant waypoints.
* **[Flight Planner]** Fixed UI refresh when unchecking the "default loadout" box in the loadout editor.
* **[Objective names]** Fixed typos in objective name : ARMADILLLO -> ARMADILLO (by SnappyComebacks)
* **[Payloads]** F-86 Sabre was missing a custom payload
* **[Payloads]** Added GAR-8 period restrictions (by Mustang-25)
* **[Campaign]** Date now progresses.
* **[Campaign]** Added game over message when a coalition runs out of functioning airbases.
* **[Mission Generation]** Fixed "invalid face handle" error in kneeboard generation that occurred on some machines.
## Regressions
* **[Mod Support]** Stopped support for 2.5.5 Rafale Mode, and removed factions that were using it
* **[Mod Support]** Su-57 mod support might be out of date
# 2.4.3
## Features/Improvements
* **[New Game Wizard]** Added the possibility to setup custom start date
## Fixes
* **[Mods]** Updated C-130J mod data to version 6.4
* **[Mods]** Updated F-22A mod to latest version
# 2.4.2
## Features/Improvements
* **[Factions]** Introduction dates and fallback weapons added for US, Russian, UK, and French weapons. Huge thanks to @TheCandianVendingMachine for the massive amount of data entry!
* **[Campaigns]** Added 1995 start dates.
## Fixes
* **[Economy]** Pending ground unit purchases will also be transferred when a connected base is captured.
* **[UI]** Fixed rounding of budget in recruitment menu.
# 2.4.1 # 2.4.1
## Fixes ## Fixes

View File

@@ -2,20 +2,21 @@ from dcs.vehicles import AirDefence
AAA_UNITS = [ AAA_UNITS = [
AirDefence.SPAAA_Gepard, AirDefence.SPAAA_Gepard,
AirDefence.SPAAA_ZSU_23_4_Shilka, AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
AirDefence.AAA_Vulcan_M163, AirDefence.SPAAA_Vulcan_M163,
AirDefence.AAA_ZU_23_Closed, AirDefence.AAA_ZU_23_Closed_Emplacement,
AirDefence.AAA_ZU_23_Emplacement, AirDefence.AAA_ZU_23_Emplacement,
AirDefence.AAA_ZU_23_on_Ural_375, AirDefence.SPAAA_ZU_23_2_Mounted_Ural_375,
AirDefence.AAA_ZU_23_Insurgent_Closed, AirDefence.AAA_ZU_23_Closed_Emplacement_Insurgent,
AirDefence.AAA_ZU_23_Insurgent_on_Ural_375, AirDefence.SPAAA_ZU_23_2_Insurgent_Mounted_Ural_375,
AirDefence.AAA_ZU_23_Insurgent, AirDefence.AAA_ZU_23_Insurgent,
AirDefence.AAA_8_8cm_Flak_18, AirDefence.AAA_8_8cm_Flak_18,
AirDefence.AAA_Flak_38, AirDefence.AAA_Flak_38_20mm,
AirDefence.AAA_8_8cm_Flak_36, AirDefence.AAA_8_8cm_Flak_36,
AirDefence.AAA_8_8cm_Flak_37, AirDefence.AAA_8_8cm_Flak_37,
AirDefence.AAA_Flak_Vierling_38, AirDefence.AAA_Flak_Vierling_38_Quad_20mm,
AirDefence.AAA_Kdo_G_40, AirDefence.AAA_SP_Kdo_G_40,
AirDefence.AAA_8_8cm_Flak_41, AirDefence.AAA_8_8cm_Flak_41,
AirDefence.AAA_Bofors_40mm AirDefence.AAA_40mm_Bofors,
] AirDefence.AAA_S_60_57mm,
]

View File

@@ -1,17 +1,70 @@
import inspect import inspect
import dcs import dcs
DEFAULT_AVAILABLE_BUILDINGS = ['fuel', 'ammo', 'comms', 'oil', 'ware', 'farp', 'fob', 'power', 'factory', 'derrick'] DEFAULT_AVAILABLE_BUILDINGS = [
"fuel",
"ammo",
"comms",
"oil",
"ware",
"farp",
"fob",
"power",
"factory",
"derrick",
]
WW2_FREE = ['fuel', 'factory', 'ware', 'fob'] WW2_FREE = ["fuel", "factory", "ware", "fob"]
WW2_GERMANY_BUILDINGS = ['fuel', 'factory', 'ww2bunker', 'ww2bunker', 'ww2bunker', 'allycamp', 'allycamp', 'fob'] WW2_GERMANY_BUILDINGS = [
WW2_ALLIES_BUILDINGS = ['fuel', 'factory', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'fob'] "fuel",
"factory",
"ww2bunker",
"ww2bunker",
"ww2bunker",
"allycamp",
"allycamp",
"fob",
]
WW2_ALLIES_BUILDINGS = [
"fuel",
"factory",
"allycamp",
"allycamp",
"allycamp",
"allycamp",
"allycamp",
"fob",
]
FORTIFICATION_BUILDINGS = ['Siegfried Line', 'Concertina wire', 'Concertina Wire', 'Czech hedgehogs 1', 'Czech hedgehogs 2', FORTIFICATION_BUILDINGS = [
'Dragonteeth 1', 'Dragonteeth 2', 'Dragonteeth 3', 'Dragonteeth 4', 'Dragonteeth 5', "Siegfried Line",
'Haystack 1', 'Haystack 2', 'Haystack 3', 'Haystack 4', 'Hemmkurvenvenhindernis', "Concertina wire",
'Log posts 1', 'Log posts 2', 'Log posts 3', 'Log ramps 1', 'Log ramps 2', 'Log ramps 3', "Concertina Wire",
'Belgian Gate', 'Container white'] "Czech hedgehogs 1",
"Czech hedgehogs 2",
"Dragonteeth 1",
"Dragonteeth 2",
"Dragonteeth 3",
"Dragonteeth 4",
"Dragonteeth 5",
"Haystack 1",
"Haystack 2",
"Haystack 3",
"Haystack 4",
"Hemmkurvenvenhindernis",
"Log posts 1",
"Log posts 2",
"Log posts 3",
"Log ramps 1",
"Log ramps 2",
"Log ramps 3",
"Belgian Gate",
"Container white",
]
FORTIFICATION_UNITS = [c for c in vars(dcs.vehicles.Fortification).values() if inspect.isclass(c)] FORTIFICATION_UNITS = [
FORTIFICATION_UNITS_ID = [c.id for c in vars(dcs.vehicles.Fortification).values() if inspect.isclass(c)] c for c in vars(dcs.vehicles.Fortification).values() if inspect.isclass(c)
]
FORTIFICATION_UNITS_ID = [
c.id for c in vars(dcs.vehicles.Fortification).values() if inspect.isclass(c)
]

View File

@@ -16,7 +16,7 @@ from dcs.planes import (
P_51D, P_51D,
P_51D_30_NA, P_51D_30_NA,
SpitfireLFMkIX, SpitfireLFMkIX,
SpitfireLFMkIXCW SpitfireLFMkIXCW,
) )
from pydcs_extensions.a4ec.a4ec import A_4E_C from pydcs_extensions.a4ec.a4ec import A_4E_C
@@ -26,7 +26,6 @@ This list contains the aircraft that do not use the guns as the last resort weap
They'll RTB when they don't have gun ammo left They'll RTB when they don't have gun ammo left
""" """
GUNFIGHTERS = [ GUNFIGHTERS = [
# Cold War # Cold War
MiG_15bis, MiG_15bis,
MiG_19P, MiG_19P,
@@ -34,11 +33,9 @@ GUNFIGHTERS = [
F_86F_Sabre, F_86F_Sabre,
A_4E_C, A_4E_C,
F_5E_3, F_5E_3,
# Trainers # Trainers
C_101CC, C_101CC,
L_39ZA, L_39ZA,
# WW2 # WW2
P_51D_30_NA, P_51D_30_NA,
P_51D, P_51D,
@@ -51,5 +48,4 @@ GUNFIGHTERS = [
FW_190D9, FW_190D9,
FW_190A8, FW_190A8,
I_16, I_16,
]
]

View File

@@ -1,6 +1,6 @@
from dcs.ships import ( from dcs.ships import (
CGN_1144_2_Pyotr_Velikiy, Battlecruiser_1144_2_Pyotr_Velikiy,
CG_1164_Moskva, Cruiser_1164_Moskva,
CVN_70_Carl_Vinson, CVN_70_Carl_Vinson,
CVN_71_Theodore_Roosevelt, CVN_71_Theodore_Roosevelt,
CVN_72_Abraham_Lincoln, CVN_72_Abraham_Lincoln,
@@ -8,67 +8,65 @@ from dcs.ships import (
CVN_74_John_C__Stennis, CVN_74_John_C__Stennis,
CV_1143_5_Admiral_Kuznetsov, CV_1143_5_Admiral_Kuznetsov,
CV_1143_5_Admiral_Kuznetsov_2017, CV_1143_5_Admiral_Kuznetsov_2017,
FFG_11540_Neustrashimy, Frigate_11540_Neustrashimy,
FFL_1124_4_Grisha, Corvette_1124_4_Grisha,
FF_1135M_Rezky, Frigate_1135M_Rezky,
FSG_1241_1MP_Molniya, Corvette_1241_1_Molniya,
LHA_1_Tarawa, LHA_1_Tarawa,
Oliver_Hazzard_Perry_class, FFG_Oliver_Hazzard_Perry,
Ticonderoga_class, CG_Ticonderoga,
Type_052B_Destroyer, Type_052B_Destroyer,
Type_052C_Destroyer, Type_052C_Destroyer,
Type_054A_Frigate, Type_054A_Frigate,
USS_Arleigh_Burke_IIa, DDG_Arleigh_Burke_IIa,
) )
from dcs.vehicles import AirDefence from dcs.vehicles import AirDefence
UNITS_WITH_RADAR = [ UNITS_WITH_RADAR = [
# Radars # Radars
AirDefence.SAM_SA_15_Tor_9A331, AirDefence.SAM_SA_15_Tor_Gauntlet,
AirDefence.SAM_SA_11_Buk_CC_9S470M1, AirDefence.SAM_SA_11_Buk_Gadfly_C2,
AirDefence.SAM_Patriot_AMG_AN_MRC_137, AirDefence.SAM_Patriot_CR__AMG_AN_MRC_137,
AirDefence.SAM_Patriot_ECS_AN_MSQ_104, AirDefence.SAM_Patriot_ECS,
AirDefence.SPAAA_Gepard, AirDefence.SPAAA_Gepard,
AirDefence.AAA_Vulcan_M163, AirDefence.SPAAA_Vulcan_M163,
AirDefence.SPAAA_ZSU_23_4_Shilka, AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
AirDefence.EWR_1L13, AirDefence.EWR_1L13,
AirDefence.SAM_SA_6_Kub_STR_9S91, AirDefence.SAM_SA_6_Kub_Long_Track_STR,
AirDefence.SAM_SA_10_S_300PS_TR_30N6, AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR,
AirDefence.SAM_SA_10_S_300PS_SR_5N66M, AirDefence.SAM_SA_10_S_300_Grumble_Clam_Shell_SR,
AirDefence.EWR_55G6, AirDefence.EWR_55G6,
AirDefence.SAM_SA_10_S_300PS_SR_64H6E, AirDefence.SAM_SA_10_S_300_Grumble_Big_Bird_SR,
AirDefence.SAM_SA_11_Buk_SR_9S18M1, AirDefence.SAM_SA_11_Buk_Gadfly_Snow_Drift_SR,
AirDefence.CP_9S80M1_Sborka, AirDefence.MCC_SR_Sborka_Dog_Ear_SR,
AirDefence.SAM_Hawk_TR_AN_MPQ_46, AirDefence.SAM_Hawk_TR__AN_MPQ_46,
AirDefence.SAM_Hawk_SR_AN_MPQ_50, AirDefence.SAM_Hawk_SR__AN_MPQ_50,
AirDefence.SAM_Patriot_STR_AN_MPQ_53, AirDefence.SAM_Patriot_STR,
AirDefence.SAM_Hawk_CWAR_AN_MPQ_55, AirDefence.SAM_Hawk_CWAR_AN_MPQ_55,
AirDefence.SAM_SR_P_19, AirDefence.SAM_P19_Flat_Face_SR__SA_2_3,
AirDefence.SAM_Roland_EWR, AirDefence.SAM_Roland_EWR,
AirDefence.SAM_SA_3_S_125_TR_SNR, AirDefence.SAM_SA_3_S_125_Low_Blow_TR,
AirDefence.SAM_SA_2_TR_SNR_75_Fan_Song, AirDefence.SAM_SA_2_S_75_Fan_Song_TR,
AirDefence.HQ_7_Self_Propelled_STR, AirDefence.HQ_7_Self_Propelled_STR,
# Ships # Ships
CVN_70_Carl_Vinson, CVN_70_Carl_Vinson,
Oliver_Hazzard_Perry_class, FFG_Oliver_Hazzard_Perry,
Ticonderoga_class, CG_Ticonderoga,
FFL_1124_4_Grisha, Corvette_1124_4_Grisha,
CV_1143_5_Admiral_Kuznetsov, CV_1143_5_Admiral_Kuznetsov,
FSG_1241_1MP_Molniya, Corvette_1241_1_Molniya,
CG_1164_Moskva, Cruiser_1164_Moskva,
FFG_11540_Neustrashimy, Frigate_11540_Neustrashimy,
CGN_1144_2_Pyotr_Velikiy, Battlecruiser_1144_2_Pyotr_Velikiy,
FF_1135M_Rezky, Frigate_1135M_Rezky,
CV_1143_5_Admiral_Kuznetsov_2017, CV_1143_5_Admiral_Kuznetsov_2017,
CVN_74_John_C__Stennis, CVN_74_John_C__Stennis,
CVN_71_Theodore_Roosevelt, CVN_71_Theodore_Roosevelt,
CVN_72_Abraham_Lincoln, CVN_72_Abraham_Lincoln,
CVN_73_George_Washington, CVN_73_George_Washington,
USS_Arleigh_Burke_IIa, DDG_Arleigh_Burke_IIa,
LHA_1_Tarawa, LHA_1_Tarawa,
Type_052B_Destroyer, Type_052B_Destroyer,
Type_054A_Frigate, Type_054A_Frigate,
Type_052C_Destroyer Type_052C_Destroyer,
] ]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -96,13 +96,14 @@ class StateData:
# them when they've already dead. Dedup. # them when they've already dead. Dedup.
killed_ground_units=list(set(data["killed_ground_units"])), killed_ground_units=list(set(data["killed_ground_units"])),
destroyed_statics=data["destroyed_objects_positions"], destroyed_statics=data["destroyed_objects_positions"],
base_capture_events=data["base_capture_events"] base_capture_events=data["base_capture_events"],
) )
class Debriefing: class Debriefing:
def __init__(self, state_data: Dict[str, Any], game: Game, def __init__(
unit_map: UnitMap) -> None: self, state_data: Dict[str, Any], game: Game, unit_map: UnitMap
) -> None:
self.state_data = StateData.from_json(state_data) self.state_data = StateData.from_json(state_data)
self.unit_map = unit_map self.unit_map = unit_map
@@ -135,12 +136,9 @@ class Debriefing:
yield from self.ground_losses.enemy_airfields yield from self.ground_losses.enemy_airfields
def casualty_count(self, control_point: ControlPoint) -> int: def casualty_count(self, control_point: ControlPoint) -> int:
return len( return len([x for x in self.front_line_losses if x.origin == control_point])
[x for x in self.front_line_losses if x.origin == control_point]
)
def front_line_losses_by_type( def front_line_losses_by_type(self, player: bool) -> Dict[Type[UnitType], int]:
self, player: bool) -> Dict[Type[UnitType], int]:
losses_by_type: Dict[Type[UnitType], int] = defaultdict(int) losses_by_type: Dict[Type[UnitType], int] = defaultdict(int)
if player: if player:
losses = self.ground_losses.player_front_line losses = self.ground_losses.player_front_line
@@ -221,8 +219,10 @@ class Debriefing:
# deaths, so we expect to see quite a few unclaimed dead ground # deaths, so we expect to see quite a few unclaimed dead ground
# units. We should start tracking those and covert this to a # units. We should start tracking those and covert this to a
# warning. # warning.
logging.debug(f"Death of untracked ground unit {unit_name} will " logging.debug(
"have no effect. This may be normal behavior.") f"Death of untracked ground unit {unit_name} will "
"have no effect. This may be normal behavior."
)
return losses return losses
@@ -234,15 +234,16 @@ class Debriefing:
for idx, base in enumerate(i.split("||")[0] for i in reversed_captures): for idx, base in enumerate(i.split("||")[0] for i in reversed_captures):
if base not in [x[1] for x in last_base_cap_indexes]: if base not in [x[1] for x in last_base_cap_indexes]:
last_base_cap_indexes.append((idx, base)) last_base_cap_indexes.append((idx, base))
return [reversed_captures[idx[0]] for idx in last_base_cap_indexes] return [reversed_captures[idx[0]] for idx in last_base_cap_indexes]
class PollDebriefingFileThread(threading.Thread): class PollDebriefingFileThread(threading.Thread):
"""Thread class with a stop() method. The thread itself has to check """Thread class with a stop() method. The thread itself has to check
regularly for the stopped() condition.""" regularly for the stopped() condition."""
def __init__(self, callback: Callable[[Debriefing], None], def __init__(
game: Game, unit_map: UnitMap) -> None: self, callback: Callable[[Debriefing], None], game: Game, unit_map: UnitMap
) -> None:
super().__init__() super().__init__()
self._stop_event = threading.Event() self._stop_event = threading.Event()
self.callback = callback self.callback = callback
@@ -261,7 +262,10 @@ class PollDebriefingFileThread(threading.Thread):
else: else:
last_modified = 0 last_modified = 0
while not self.stopped(): while not self.stopped():
if os.path.isfile("state.json") and os.path.getmtime("state.json") > last_modified: if (
os.path.isfile("state.json")
and os.path.getmtime("state.json") > last_modified
):
with open("state.json", "r") as json_file: with open("state.json", "r") as json_file:
json_data = json.load(json_file) json_data = json.load(json_file)
debriefing = Debriefing(json_data, self.game, self.unit_map) debriefing = Debriefing(json_data, self.game, self.unit_map)
@@ -270,8 +274,9 @@ class PollDebriefingFileThread(threading.Thread):
time.sleep(5) time.sleep(5)
def wait_for_debriefing(callback: Callable[[Debriefing], None], def wait_for_debriefing(
game: Game, unit_map) -> PollDebriefingFileThread: callback: Callable[[Debriefing], None], game: Game, unit_map
) -> PollDebriefingFileThread:
thread = PollDebriefingFileThread(callback, game, unit_map) thread = PollDebriefingFileThread(callback, game, unit_map)
thread.start() thread.start()
return thread return thread

View File

@@ -6,7 +6,7 @@ from typing import Dict, Iterator, List, TYPE_CHECKING, Tuple, Type
from dcs.mapping import Point from dcs.mapping import Point
from dcs.task import Task from dcs.task import Task
from dcs.unittype import UnitType from dcs.unittype import UnitType, VehicleType
from game import persistency from game import persistency
from game.debriefing import AirLosses, Debriefing from game.debriefing import AirLosses, Debriefing
@@ -37,7 +37,15 @@ class Event:
to_cp = None # type: ControlPoint to_cp = None # type: ControlPoint
difficulty = 1 # type: int difficulty = 1 # type: int
def __init__(self, game, from_cp: ControlPoint, target_cp: ControlPoint, location: Point, attacker_name: str, defender_name: str): def __init__(
self,
game,
from_cp: ControlPoint,
target_cp: ControlPoint,
location: Point,
attacker_name: str,
defender_name: str,
):
self.game = game self.game = game
self.from_cp = from_cp self.from_cp = from_cp
self.to_cp = target_cp self.to_cp = target_cp
@@ -57,12 +65,14 @@ class Event:
Operation.prepare(self.game) Operation.prepare(self.game)
unit_map = Operation.generate() unit_map = Operation.generate()
Operation.current_mission.save( Operation.current_mission.save(
persistency.mission_path_for("liberation_nextturn.miz")) persistency.mission_path_for("liberation_nextturn.miz")
)
return unit_map return unit_map
@staticmethod @staticmethod
def _transfer_aircraft(ato: AirTaskingOrder, losses: AirLosses, def _transfer_aircraft(
for_player: bool) -> None: ato: AirTaskingOrder, losses: AirLosses, for_player: bool
) -> None:
for package in ato.packages: for package in ato.packages:
for flight in package.flights: for flight in package.flights:
# No need to transfer to the same location. # No need to transfer to the same location.
@@ -77,13 +87,16 @@ class Event:
if flight.arrival.captured != for_player: if flight.arrival.captured != for_player:
logging.info( logging.info(
f"Not transferring {flight} because {flight.arrival} " f"Not transferring {flight} because {flight.arrival} "
"was captured") "was captured"
)
continue continue
transfer_count = losses.surviving_flight_members(flight) transfer_count = losses.surviving_flight_members(flight)
if transfer_count < 0: if transfer_count < 0:
logging.error(f"{flight} had {flight.count} aircraft but " logging.error(
f"{transfer_count} losses were recorded.") f"{flight} had {flight.count} aircraft but "
f"{transfer_count} losses were recorded."
)
continue continue
aircraft = flight.unit_type aircraft = flight.unit_type
@@ -91,7 +104,8 @@ class Event:
if available < transfer_count: if available < transfer_count:
logging.error( logging.error(
f"Found killed {aircraft} from {flight.departure} but " f"Found killed {aircraft} from {flight.departure} but "
f"that airbase has only {available} available.") f"that airbase has only {available} available."
)
continue continue
flight.departure.base.aircraft[aircraft] -= transfer_count flight.departure.base.aircraft[aircraft] -= transfer_count
@@ -101,10 +115,12 @@ class Event:
flight.arrival.base.aircraft[aircraft] += transfer_count flight.arrival.base.aircraft[aircraft] += transfer_count
def complete_aircraft_transfers(self, debriefing: Debriefing) -> None: def complete_aircraft_transfers(self, debriefing: Debriefing) -> None:
self._transfer_aircraft(self.game.blue_ato, debriefing.air_losses, self._transfer_aircraft(
for_player=True) self.game.blue_ato, debriefing.air_losses, for_player=True
self._transfer_aircraft(self.game.red_ato, debriefing.air_losses, )
for_player=False) self._transfer_aircraft(
self.game.red_ato, debriefing.air_losses, for_player=False
)
@staticmethod @staticmethod
def commit_air_losses(debriefing: Debriefing) -> None: def commit_air_losses(debriefing: Debriefing) -> None:
@@ -115,7 +131,8 @@ class Event:
if available <= 0: if available <= 0:
logging.error( logging.error(
f"Found killed {aircraft} from {cp} but that airbase has " f"Found killed {aircraft} from {cp} but that airbase has "
"none available.") "none available."
)
continue continue
logging.info(f"{aircraft} destroyed from {cp}") logging.info(f"{aircraft} destroyed from {cp}")
@@ -130,7 +147,8 @@ class Event:
if available <= 0: if available <= 0:
logging.error( logging.error(
f"Found killed {unit_type} from {control_point} but that " f"Found killed {unit_type} from {control_point} but that "
"airbase has none available.") "airbase has none available."
)
continue continue
logging.info(f"{unit_type} destroyed from {control_point}") logging.info(f"{unit_type} destroyed from {control_point}")
@@ -149,11 +167,14 @@ class Event:
def commit_building_losses(self, debriefing: Debriefing) -> None: def commit_building_losses(self, debriefing: Debriefing) -> None:
for loss in debriefing.building_losses: for loss in debriefing.building_losses:
loss.ground_object.kill() loss.ground_object.kill()
self.game.informations.append(Information( self.game.informations.append(
"Building destroyed", Information(
f"{loss.ground_object.dcs_identifier} has been destroyed at " "Building destroyed",
f"location {loss.ground_object.obj_name}", self.game.turn f"{loss.ground_object.dcs_identifier} has been destroyed at "
)) f"location {loss.ground_object.obj_name}",
self.game.turn,
)
)
@staticmethod @staticmethod
def commit_damaged_runways(debriefing: Debriefing) -> None: def commit_damaged_runways(debriefing: Debriefing) -> None:
@@ -171,9 +192,9 @@ class Event:
# ------------------------------ # ------------------------------
# Captured bases # Captured bases
#if self.game.player_country in db.BLUEFOR_FACTIONS: # if self.game.player_country in db.BLUEFOR_FACTIONS:
coalition = 2 # Value in DCS mission event for BLUE coalition = 2 # Value in DCS mission event for BLUE
#else: # else:
# coalition = 1 # Value in DCS mission event for RED # coalition = 1 # Value in DCS mission event for RED
for captured in debriefing.base_capture_events: for captured in debriefing.base_capture_events:
@@ -187,12 +208,22 @@ class Event:
if cp.captured and new_owner_coalition != coalition: if cp.captured and new_owner_coalition != coalition:
for_player = False for_player = False
info = Information(cp.name + " lost !", "The ennemy took control of " + cp.name + "\nShame on us !", self.game.turn) info = Information(
cp.name + " lost !",
"The ennemy took control of "
+ cp.name
+ "\nShame on us !",
self.game.turn,
)
self.game.informations.append(info) self.game.informations.append(info)
captured_cps.append(cp) captured_cps.append(cp)
elif not(cp.captured) and new_owner_coalition == coalition: elif not (cp.captured) and new_owner_coalition == coalition:
for_player = True for_player = True
info = Information(cp.name + " captured !", "We took control of " + cp.name + "! Great job !", self.game.turn) info = Information(
cp.name + " captured !",
"We took control of " + cp.name + "! Great job !",
self.game.turn,
)
self.game.informations.append(info) self.game.informations.append(info)
captured_cps.append(cp) captured_cps.append(cp)
else: else:
@@ -218,7 +249,12 @@ class Event:
for cp in self.game.theater.player_points(): for cp in self.game.theater.player_points():
enemy_cps = [e for e in cp.connected_points if not e.captured] enemy_cps = [e for e in cp.connected_points if not e.captured]
for enemy_cp in enemy_cps: for enemy_cp in enemy_cps:
print("Compute frontline progression for : " + cp.name + " to " + enemy_cp.name) print(
"Compute frontline progression for : "
+ cp.name
+ " to "
+ enemy_cp.name
)
delta = 0.0 delta = 0.0
player_won = True player_won = True
@@ -234,7 +270,11 @@ class Event:
ratio = (1.0 + enemy_casualties) / (1.0 + ally_casualties) ratio = (1.0 + enemy_casualties) / (1.0 + ally_casualties)
player_aggresive = cp.stances[enemy_cp.id] in [CombatStance.AGGRESSIVE, CombatStance.ELIMINATION, CombatStance.BREAKTHROUGH] player_aggresive = cp.stances[enemy_cp.id] in [
CombatStance.AGGRESSIVE,
CombatStance.ELIMINATION,
CombatStance.BREAKTHROUGH,
]
if ally_units_alive == 0: if ally_units_alive == 0:
player_won = False player_won = False
@@ -259,11 +299,17 @@ class Event:
delta = DEFEAT_INFLUENCE delta = DEFEAT_INFLUENCE
elif ally_casualties > enemy_casualties: elif ally_casualties > enemy_casualties:
if ally_units_alive > 2*enemy_units_alive and player_aggresive: if (
ally_units_alive > 2 * enemy_units_alive
and player_aggresive
):
# Even with casualties if the enemy is overwhelmed, they are going to lose ground # Even with casualties if the enemy is overwhelmed, they are going to lose ground
player_won = True player_won = True
delta = MINOR_DEFEAT_INFLUENCE delta = MINOR_DEFEAT_INFLUENCE
elif ally_units_alive > 3*enemy_units_alive and player_aggresive: elif (
ally_units_alive > 3 * enemy_units_alive
and player_aggresive
):
player_won = True player_won = True
delta = STRONG_DEFEAT_INFLUENCE delta = STRONG_DEFEAT_INFLUENCE
else: else:
@@ -275,7 +321,10 @@ class Event:
delta = STRONG_DEFEAT_INFLUENCE delta = STRONG_DEFEAT_INFLUENCE
# No progress with defensive strategies # No progress with defensive strategies
if player_won and cp.stances[enemy_cp.id] in [CombatStance.DEFENSIVE, CombatStance.AMBUSH]: if player_won and cp.stances[enemy_cp.id] in [
CombatStance.DEFENSIVE,
CombatStance.AMBUSH,
]:
print("Defensive stance, progress is limited") print("Defensive stance, progress is limited")
delta = MINOR_DEFEAT_INFLUENCE delta = MINOR_DEFEAT_INFLUENCE
@@ -283,62 +332,95 @@ class Event:
print(cp.name + " won ! factor > " + str(delta)) print(cp.name + " won ! factor > " + str(delta))
cp.base.affect_strength(delta) cp.base.affect_strength(delta)
enemy_cp.base.affect_strength(-delta) enemy_cp.base.affect_strength(-delta)
info = Information("Frontline Report", info = Information(
"Our ground forces from " + cp.name + " are making progress toward " + enemy_cp.name, "Frontline Report",
self.game.turn) "Our ground forces from "
+ cp.name
+ " are making progress toward "
+ enemy_cp.name,
self.game.turn,
)
self.game.informations.append(info) self.game.informations.append(info)
else: else:
print(cp.name + " lost ! factor > " + str(delta)) print(cp.name + " lost ! factor > " + str(delta))
enemy_cp.base.affect_strength(delta) enemy_cp.base.affect_strength(delta)
cp.base.affect_strength(-delta) cp.base.affect_strength(-delta)
info = Information("Frontline Report", info = Information(
"Our ground forces from " + cp.name + " are losing ground against the enemy forces from " + enemy_cp.name, "Frontline Report",
self.game.turn) "Our ground forces from "
+ cp.name
+ " are losing ground against the enemy forces from "
+ enemy_cp.name,
self.game.turn,
)
self.game.informations.append(info) self.game.informations.append(info)
def redeploy_units(self, cp): def redeploy_units(self, cp: ControlPoint) -> None:
"""" """ "
Auto redeploy units to newly captured base Auto redeploy units to newly captured base
""" """
ally_connected_cps = [ocp for ocp in cp.connected_points if cp.captured == ocp.captured] ally_connected_cps = [
enemy_connected_cps = [ocp for ocp in cp.connected_points if cp.captured != ocp.captured] ocp for ocp in cp.connected_points if cp.captured == ocp.captured
]
enemy_connected_cps = [
ocp for ocp in cp.connected_points if cp.captured != ocp.captured
]
# If the newly captured cp does not have enemy connected cp, # If the newly captured cp does not have enemy connected cp,
# then it is not necessary to redeploy frontline units there. # then it is not necessary to redeploy frontline units there.
if len(enemy_connected_cps) == 0: if len(enemy_connected_cps) == 0:
return return
# From each ally cp, send reinforcements
for ally_cp in ally_connected_cps:
self.redeploy_between(cp, ally_cp)
def redeploy_between(self, destination: ControlPoint, source: ControlPoint) -> None:
total_units_redeployed = 0
moved_units = {}
if source.has_active_frontline or not destination.captured:
# If there are still active front lines to defend at the
# transferring CP we should not transfer all units.
#
# Opfor also does not transfer all of their units.
# TODO: Balance the CPs rather than moving half from everywhere.
move_factor = 0.5
else: else:
# From each ally cp, send reinforcements # Otherwise we can move everything.
for ally_cp in ally_connected_cps: move_factor = 1
total_units_redeployed = 0
own_enemy_cp = [ocp for ocp in ally_cp.connected_points if ally_cp.captured != ocp.captured]
moved_units = {} for frontline_unit, count in source.base.armor.items():
moved_units[frontline_unit] = int(count * move_factor)
total_units_redeployed = total_units_redeployed + int(count * move_factor)
# If the connected base, does not have any more enemy cp connected. destination.base.commision_units(moved_units)
# Or if it is not the opponent redeploying forces there (enemy AI will never redeploy all their forces at once) source.base.commit_losses(moved_units)
if len(own_enemy_cp) > 0 or not cp.captured:
for frontline_unit, count in ally_cp.base.armor.items():
moved_units[frontline_unit] = int(count/2)
total_units_redeployed = total_units_redeployed + int(count/2)
else: # So if the old base, does not have any more enemy cp connected, or if it is an enemy base
for frontline_unit, count in ally_cp.base.armor.items():
moved_units[frontline_unit] = count
total_units_redeployed = total_units_redeployed + count
cp.base.commision_units(moved_units) # Also transfer pending deliveries.
ally_cp.base.commit_losses(moved_units) for unit_type, count in source.pending_unit_deliveries.units.items():
if not issubclass(unit_type, VehicleType):
continue
if count <= 0:
# Don't transfer *sales*...
continue
move_count = int(count * move_factor)
source.pending_unit_deliveries.sell({unit_type: move_count})
destination.pending_unit_deliveries.order({unit_type: move_count})
total_units_redeployed += move_count
if total_units_redeployed > 0: if total_units_redeployed > 0:
info = Information("Units redeployed", "", self.game.turn) text = (
info.text = str(total_units_redeployed) + " units have been redeployed from " + ally_cp.name + " to " + cp.name f"{total_units_redeployed} units have been redeployed from "
self.game.informations.append(info) f"{source.name} to {destination.name}"
logging.info(info.text) )
info = Information("Units redeployed", text, self.game.turn)
self.game.informations.append(info)
logging.info(text)
class UnitsDeliveryEvent: class UnitsDeliveryEvent:
def __init__(self, control_point: ControlPoint) -> None: def __init__(self, control_point: ControlPoint) -> None:
self.to_cp = control_point self.to_cp = control_point
self.units: Dict[Type[UnitType], int] = {} self.units: Dict[Type[UnitType], int] = {}
@@ -366,8 +448,7 @@ class UnitsDeliveryEvent:
logging.error(f"Could not refund {unit_type.id}, price unknown") logging.error(f"Could not refund {unit_type.id}, price unknown")
continue continue
logging.info( logging.info(f"Refunding {count} {unit_type.id} at {self.to_cp.name}")
f"Refunding {count} {unit_type.id} at {self.to_cp.name}")
game.adjust_budget(price * count, player=self.to_cp.captured) game.adjust_budget(price * count, player=self.to_cp.captured)
def available_next_turn(self, unit_type: Type[UnitType]) -> int: def available_next_turn(self, unit_type: Type[UnitType]) -> int:
@@ -385,13 +466,13 @@ class UnitsDeliveryEvent:
aircraft = unit_type.id aircraft = unit_type.id
name = self.to_cp.name name = self.to_cp.name
if count >= 0: if count >= 0:
bought_units[unit_type] = count bought_units[unit_type] = count
game.message( game.message(
f"{coalition} reinforcements: {aircraft} x {count} at {name}") f"{coalition} reinforcements: {aircraft} x {count} at {name}"
)
else: else:
sold_units[unit_type] = -count sold_units[unit_type] = -count
game.message( game.message(f"{coalition} sold: {aircraft} x {-count} at {name}")
f"{coalition} sold: {aircraft} x {-count} at {name}")
self.to_cp.base.commision_units(bought_units) self.to_cp.base.commision_units(bought_units)
self.to_cp.base.commit_losses(sold_units) self.to_cp.base.commit_losses(sold_units)
self.units = {} self.units = {}

View File

@@ -7,5 +7,6 @@ class FrontlineAttackEvent(Event):
Currently the same as its parent, but here for legacy compatibility as well as to allow for Currently the same as its parent, but here for legacy compatibility as well as to allow for
future unique Event handling future unique Event handling
""" """
def __str__(self): def __str__(self):
return "Frontline attack" return "Frontline attack"

View File

@@ -10,8 +10,18 @@ from dcs.planes import plane_map
from dcs.unittype import FlyingType, ShipType, VehicleType, UnitType from dcs.unittype import FlyingType, ShipType, VehicleType, UnitType
from dcs.vehicles import Armor, Unarmed, Infantry, Artillery, AirDefence from dcs.vehicles import Armor, Unarmed, Infantry, Artillery, AirDefence
from game.data.building_data import WW2_ALLIES_BUILDINGS, DEFAULT_AVAILABLE_BUILDINGS, WW2_GERMANY_BUILDINGS, WW2_FREE from game.data.building_data import (
from game.data.doctrine import Doctrine, MODERN_DOCTRINE, COLDWAR_DOCTRINE, WWII_DOCTRINE WW2_ALLIES_BUILDINGS,
DEFAULT_AVAILABLE_BUILDINGS,
WW2_GERMANY_BUILDINGS,
WW2_FREE,
)
from game.data.doctrine import (
Doctrine,
MODERN_DOCTRINE,
COLDWAR_DOCTRINE,
WWII_DOCTRINE,
)
from pydcs_extensions.mod_units import MODDED_VEHICLES, MODDED_AIRPLANES from pydcs_extensions.mod_units import MODDED_VEHICLES, MODDED_AIRPLANES
@@ -60,6 +70,9 @@ class Faction:
# Possible Missile site generators for this faction # Possible Missile site generators for this faction
missiles: List[str] = field(default_factory=list) missiles: List[str] = field(default_factory=list)
# Possible costal site generators for this faction
coastal_defenses: List[str] = field(default_factory=list)
# Required mods or asset packs # Required mods or asset packs
requirements: Dict[str, str] = field(default_factory=dict) requirements: Dict[str, str] = field(default_factory=dict)
@@ -90,6 +103,9 @@ class Faction:
# How many missiles group should we try to generate per CP on startup for this faction # How many missiles group should we try to generate per CP on startup for this faction
missiles_group_count: int = field(default=1) missiles_group_count: int = field(default=1)
# How many coastal group should we try to generate per CP on startup for this faction
coastal_group_count: int = field(default=1)
# Whether this faction has JTAC access # Whether this faction has JTAC access
has_jtac: bool = field(default=False) has_jtac: bool = field(default=False)
@@ -103,8 +119,7 @@ class Faction:
building_set: List[str] = field(default_factory=list) building_set: List[str] = field(default_factory=list)
# List of default livery overrides # List of default livery overrides
liveries_overrides: Dict[Type[UnitType], List[str]] = field( liveries_overrides: Dict[Type[UnitType], List[str]] = field(default_factory=dict)
default_factory=dict)
#: Set to True if the faction should force the "Unrestricted satnav" option #: Set to True if the faction should force the "Unrestricted satnav" option
#: for the mission. This option enables GPS for capable aircraft regardless #: for the mission. This option enables GPS for capable aircraft regardless
@@ -122,7 +137,11 @@ class Faction:
faction.country = json.get("country", "/") faction.country = json.get("country", "/")
if faction.country not in [c.name for c in country_dict.values()]: if faction.country not in [c.name for c in country_dict.values()]:
raise AssertionError("Faction's country (\"{}\") is not a valid DCS country ID".format(faction.country)) raise AssertionError(
'Faction\'s country ("{}") is not a valid DCS country ID'.format(
faction.country
)
)
faction.name = json.get("name", "") faction.name = json.get("name", "")
if not faction.name: if not faction.name:
@@ -135,14 +154,10 @@ class Faction:
faction.awacs = load_all_aircraft(json.get("awacs", [])) faction.awacs = load_all_aircraft(json.get("awacs", []))
faction.tankers = load_all_aircraft(json.get("tankers", [])) faction.tankers = load_all_aircraft(json.get("tankers", []))
faction.frontline_units = load_all_vehicles( faction.frontline_units = load_all_vehicles(json.get("frontline_units", []))
json.get("frontline_units", [])) faction.artillery_units = load_all_vehicles(json.get("artillery_units", []))
faction.artillery_units = load_all_vehicles( faction.infantry_units = load_all_vehicles(json.get("infantry_units", []))
json.get("artillery_units", [])) faction.logistics_units = load_all_vehicles(json.get("logistics_units", []))
faction.infantry_units = load_all_vehicles(
json.get("infantry_units", []))
faction.logistics_units = load_all_vehicles(
json.get("logistics_units", []))
faction.ewrs = json.get("ewrs", []) faction.ewrs = json.get("ewrs", [])
@@ -153,16 +168,14 @@ class Faction:
faction.air_defenses.extend(json.get("shorads", [])) faction.air_defenses.extend(json.get("shorads", []))
faction.missiles = json.get("missiles", []) faction.missiles = json.get("missiles", [])
faction.coastal_defenses = json.get("coastal_defenses", [])
faction.requirements = json.get("requirements", {}) faction.requirements = json.get("requirements", {})
faction.carrier_names = json.get("carrier_names", []) faction.carrier_names = json.get("carrier_names", [])
faction.helicopter_carrier_names = json.get( faction.helicopter_carrier_names = json.get("helicopter_carrier_names", [])
"helicopter_carrier_names", [])
faction.navy_generators = json.get("navy_generators", []) faction.navy_generators = json.get("navy_generators", [])
faction.aircraft_carrier = load_all_ships( faction.aircraft_carrier = load_all_ships(json.get("aircraft_carrier", []))
json.get("aircraft_carrier", [])) faction.helicopter_carrier = load_all_ships(json.get("helicopter_carrier", []))
faction.helicopter_carrier = load_all_ships(
json.get("helicopter_carrier", []))
faction.destroyers = load_all_ships(json.get("destroyers", [])) faction.destroyers = load_all_ships(json.get("destroyers", []))
faction.cruisers = load_all_ships(json.get("cruisers", [])) faction.cruisers = load_all_ships(json.get("cruisers", []))
faction.has_jtac = json.get("has_jtac", False) faction.has_jtac = json.get("has_jtac", False)
@@ -173,6 +186,7 @@ class Faction:
faction.jtac_unit = None faction.jtac_unit = None
faction.navy_group_count = int(json.get("navy_group_count", 1)) faction.navy_group_count = int(json.get("navy_group_count", 1))
faction.missiles_group_count = int(json.get("missiles_group_count", 0)) faction.missiles_group_count = int(json.get("missiles_group_count", 0))
faction.coastal_group_count = int(json.get("coastal_group_count", 0))
# Load doctrine # Load doctrine
doctrine = json.get("doctrine", "modern") doctrine = json.get("doctrine", "modern")
@@ -212,13 +226,18 @@ class Faction:
@property @property
def units(self) -> List[Type[UnitType]]: def units(self) -> List[Type[UnitType]]:
return (self.infantry_units + self.aircrafts + self.awacs + return (
self.artillery_units + self.frontline_units + self.infantry_units
self.tankers + self.logistics_units) + self.aircrafts
+ self.awacs
+ self.artillery_units
+ self.frontline_units
+ self.tankers
+ self.logistics_units
)
def unit_loader( def unit_loader(unit: str, class_repository: List[Any]) -> Optional[Type[UnitType]]:
unit: str, class_repository: List[Any]) -> Optional[Type[UnitType]]:
""" """
Find unit by name Find unit by name
:param unit: Unit name as string :param unit: Unit name as string
@@ -242,9 +261,10 @@ def unit_loader(
def load_aircraft(name: str) -> Optional[Type[FlyingType]]: def load_aircraft(name: str) -> Optional[Type[FlyingType]]:
return cast(Optional[FlyingType], unit_loader( return cast(
name, [dcs.planes, dcs.helicopters, MODDED_AIRPLANES] Optional[FlyingType],
)) unit_loader(name, [dcs.planes, dcs.helicopters, MODDED_AIRPLANES]),
)
def load_all_aircraft(data) -> List[Type[FlyingType]]: def load_all_aircraft(data) -> List[Type[FlyingType]]:
@@ -257,9 +277,12 @@ def load_all_aircraft(data) -> List[Type[FlyingType]]:
def load_vehicle(name: str) -> Optional[Type[VehicleType]]: def load_vehicle(name: str) -> Optional[Type[VehicleType]]:
return cast(Optional[FlyingType], unit_loader( return cast(
name, [Infantry, Unarmed, Armor, AirDefence, Artillery, MODDED_VEHICLES] Optional[FlyingType],
)) unit_loader(
name, [Infantry, Unarmed, Armor, AirDefence, Artillery, MODDED_VEHICLES]
),
)
def load_all_vehicles(data) -> List[Type[VehicleType]]: def load_all_vehicles(data) -> List[Type[VehicleType]]:

View File

@@ -78,10 +78,16 @@ class TurnState(Enum):
class Game: class Game:
def __init__(self, player_name: str, enemy_name: str, def __init__(
theater: ConflictTheater, start_date: datetime, self,
settings: Settings, player_budget: float, player_name: str,
enemy_budget: float) -> None: enemy_name: str,
theater: ConflictTheater,
start_date: datetime,
settings: Settings,
player_budget: float,
enemy_budget: float,
) -> None:
self.settings = settings self.settings = settings
self.events: List[Event] = [] self.events: List[Event] = []
self.theater = theater self.theater = theater
@@ -90,6 +96,7 @@ class Game:
self.enemy_name = enemy_name self.enemy_name = enemy_name
self.enemy_country = db.FACTIONS[enemy_name].country self.enemy_country = db.FACTIONS[enemy_name].country
self.turn = 0 self.turn = 0
# NB: This is the *start* date. It is never updated.
self.date = date(start_date.year, start_date.month, start_date.day) self.date = date(start_date.year, start_date.month, start_date.day)
self.game_stats = GameStats() self.game_stats = GameStats()
self.game_stats.update(self) self.game_stats.update(self)
@@ -112,9 +119,7 @@ class Game:
self.blue_ato = AirTaskingOrder() self.blue_ato = AirTaskingOrder()
self.red_ato = AirTaskingOrder() self.red_ato = AirTaskingOrder()
self.aircraft_inventory = GlobalAircraftInventory( self.aircraft_inventory = GlobalAircraftInventory(self.theater.controlpoints)
self.theater.controlpoints
)
self.sanitize_sides() self.sanitize_sides()
@@ -147,8 +152,9 @@ class Game:
self.on_load() self.on_load()
def generate_conditions(self) -> Conditions: def generate_conditions(self) -> Conditions:
return Conditions.generate(self.theater, self.date, return Conditions.generate(
self.current_turn_time_of_day, self.settings) self.theater, self.current_day, self.current_turn_time_of_day, self.settings
)
def sanitize_sides(self): def sanitize_sides(self):
""" """
@@ -184,13 +190,24 @@ class Game:
return random.randint(1, 100) <= prob * mult return random.randint(1, 100) <= prob * mult
def _generate_player_event(self, event_class, player_cp, enemy_cp): def _generate_player_event(self, event_class, player_cp, enemy_cp):
self.events.append(event_class(self, player_cp, enemy_cp, enemy_cp.position, self.player_name, self.enemy_name)) self.events.append(
event_class(
self,
player_cp,
enemy_cp,
enemy_cp.position,
self.player_name,
self.enemy_name,
)
)
def _generate_events(self): def _generate_events(self):
for front_line in self.theater.conflicts(True): for front_line in self.theater.conflicts(True):
self._generate_player_event(FrontlineAttackEvent, self._generate_player_event(
front_line.control_point_a, FrontlineAttackEvent,
front_line.control_point_b) front_line.control_point_a,
front_line.control_point_b,
)
def adjust_budget(self, amount: float, player: bool) -> None: def adjust_budget(self, amount: float, player: bool) -> None:
if player: if player:
@@ -208,7 +225,7 @@ class Game:
self.enemy_budget += Income(self, player=False).total self.enemy_budget += Income(self, player=False).total
def initiate_event(self, event: Event) -> UnitMap: def initiate_event(self, event: Event) -> UnitMap:
#assert event in self.events # assert event in self.events
logging.info("Generating {} (regular)".format(event)) logging.info("Generating {} (regular)".format(event))
return event.generate() return event.generate()
@@ -223,7 +240,11 @@ class Game:
def is_player_attack(self, event): def is_player_attack(self, event):
if isinstance(event, Event): if isinstance(event, Event):
return event and event.attacker_name and event.attacker_name == self.player_name return (
event
and event.attacker_name
and event.attacker_name == self.player_name
)
else: else:
raise RuntimeError(f"{event} was passed when an Event type was expected") raise RuntimeError(f"{event} was passed when an Event type was expected")
@@ -235,7 +256,9 @@ class Game:
def pass_turn(self, no_action: bool = False) -> None: def pass_turn(self, no_action: bool = False) -> None:
logging.info("Pass turn") logging.info("Pass turn")
self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0)) self.informations.append(
Information("End of turn #" + str(self.turn), "-" * 40, 0)
)
self.turn += 1 self.turn += 1
for control_point in self.theater.controlpoints: for control_point in self.theater.controlpoints:
@@ -261,11 +284,18 @@ class Game:
persistency.autosave(self) persistency.autosave(self)
def check_win_loss(self): def check_win_loss(self):
captured_states = {i.captured for i in self.theater.controlpoints} player_airbases = {
if True not in captured_states: cp for cp in self.theater.player_points() if cp.runway_is_operational()
}
if not player_airbases:
return TurnState.LOSS return TurnState.LOSS
if False not in captured_states:
enemy_airbases = {
cp for cp in self.theater.enemy_points() if cp.runway_is_operational()
}
if not enemy_airbases:
return TurnState.WIN return TurnState.WIN
return TurnState.CONTINUE return TurnState.CONTINUE
def initialize_turn(self) -> None: def initialize_turn(self) -> None:
@@ -281,7 +311,7 @@ class Game:
# Check for win or loss condition # Check for win or loss condition
turn_state = self.check_win_loss() turn_state = self.check_win_loss()
if turn_state in (TurnState.LOSS,TurnState.WIN): if turn_state in (TurnState.LOSS, TurnState.WIN):
return self.process_win_loss(turn_state) return self.process_win_loss(turn_state)
# Plan flights & combat for next turn # Plan flights & combat for next turn
@@ -305,8 +335,11 @@ class Game:
self.plan_procurement(blue_planner, red_planner) self.plan_procurement(blue_planner, red_planner)
def plan_procurement(self, blue_planner: CoalitionMissionPlanner, def plan_procurement(
red_planner: CoalitionMissionPlanner) -> None: self,
blue_planner: CoalitionMissionPlanner,
red_planner: CoalitionMissionPlanner,
) -> None:
# The first turn needs to buy a *lot* of aircraft to fill CAPs, so it # 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 # gets much more of the budget that turn. Otherwise budget (after
# repairs) is split evenly between air and ground. For the default # repairs) is split evenly between air and ground. For the default
@@ -320,7 +353,7 @@ class Game:
manage_runways=self.settings.automate_runway_repair, manage_runways=self.settings.automate_runway_repair,
manage_front_line=self.settings.automate_front_line_reinforcements, 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 front_line_budget_share=ground_portion,
).spend_budget(self.budget, blue_planner.procurement_requests) ).spend_budget(self.budget, blue_planner.procurement_requests)
self.enemy_budget = ProcurementAi( self.enemy_budget = ProcurementAi(
@@ -330,7 +363,7 @@ class Game:
manage_runways=True, manage_runways=True,
manage_front_line=True, manage_front_line=True,
manage_aircraft=True, manage_aircraft=True,
front_line_budget_share=ground_portion front_line_budget_share=ground_portion,
).spend_budget(self.enemy_budget, red_planner.procurement_requests) ).spend_budget(self.enemy_budget, red_planner.procurement_requests)
def message(self, text: str) -> None: def message(self, text: str) -> None:
@@ -361,10 +394,12 @@ class Game:
def compute_threat_zones(self) -> None: def compute_threat_zones(self) -> None:
self.blue_threat_zone = ThreatZones.for_faction(self, player=True) self.blue_threat_zone = ThreatZones.for_faction(self, player=True)
self.red_threat_zone = ThreatZones.for_faction(self, player=False) self.red_threat_zone = ThreatZones.for_faction(self, player=False)
self.blue_navmesh = NavMesh.from_threat_zones(self.red_threat_zone, self.blue_navmesh = NavMesh.from_threat_zones(
self.theater) self.red_threat_zone, self.theater
self.red_navmesh = NavMesh.from_threat_zones(self.blue_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: def threat_zone_for(self, player: bool) -> ThreatZones:
if player: if player:
@@ -386,9 +421,9 @@ class Game:
# By default, use the existing frontline conflict position # By default, use the existing frontline conflict position
for front_line in self.theater.conflicts(): for front_line in self.theater.conflicts():
position = Conflict.frontline_position(front_line.control_point_a, position = Conflict.frontline_position(
front_line.control_point_b, front_line.control_point_a, front_line.control_point_b, self.theater
self.theater) )
zones.append(position[0]) zones.append(position[0])
zones.append(front_line.control_point_a.position) zones.append(front_line.control_point_a.position)
zones.append(front_line.control_point_b.position) zones.append(front_line.control_point_b.position)
@@ -413,7 +448,10 @@ class Game:
d = cp.position.distance_to_point(cp2.position) d = cp.position.distance_to_point(cp2.position)
if d < min_distance: if d < min_distance:
min_distance = d min_distance = d
cpoint = Point((cp.position.x + cp2.position.x) / 2, (cp.position.y + cp2.position.y) / 2) cpoint = Point(
(cp.position.x + cp2.position.x) / 2,
(cp.position.y + cp2.position.y) / 2,
)
zones.append(cp.position) zones.append(cp.position)
zones.append(cp2.position) zones.append(cp2.position)
break break
@@ -422,8 +460,7 @@ class Game:
if cpoint is not None: if cpoint is not None:
zones.append(cpoint) zones.append(cpoint)
packages = itertools.chain(self.blue_ato.packages, packages = itertools.chain(self.blue_ato.packages, self.red_ato.packages)
self.red_ato.packages)
for package in packages: for package in packages:
if package.primary_task is FlightType.BARCAP: if package.primary_task is FlightType.BARCAP:
# BARCAPs will be planned at most locations on smaller theaters, # BARCAPs will be planned at most locations on smaller theaters,
@@ -460,7 +497,10 @@ class Game:
return False return False
else: else:
for z in self.__culling_zones: for z in self.__culling_zones:
if z.distance_to_point(pos) < self.settings.perf_culling_distance * 1000: if (
z.distance_to_point(pos)
< self.settings.perf_culling_distance * 1000
):
return False return False
for p in self.__culling_points: for p in self.__culling_points:
if p.distance_to_point(pos) < 2500: if p.distance_to_point(pos) < 2500:
@@ -502,6 +542,10 @@ class Game:
def process_win_loss(self, turn_state: TurnState): def process_win_loss(self, turn_state: TurnState):
if turn_state is TurnState.WIN: if turn_state is TurnState.WIN:
return self.message("Congratulations, you are victorious! Start a new campaign to continue.") return self.message(
"Congratulations, you are victorious! Start a new campaign to continue."
)
elif turn_state is TurnState.LOSS: elif turn_state is TurnState.LOSS:
return self.message("Game Over, you lose. Start a new campaign to continue.") return self.message(
"Game Over, you lose. Start a new campaign to continue."
)

View File

@@ -46,10 +46,10 @@ class Income:
for tgo in tgos: for tgo in tgos:
if not tgo.is_dead: if not tgo.is_dead:
count += 1 count += 1
self.buildings.append(BuildingIncome(name, category, count, self.buildings.append(
REWARDS[category])) BuildingIncome(name, category, count, REWARDS[category])
)
self.from_bases = sum(cp.income_per_turn for cp in self.control_points) self.from_bases = sum(cp.income_per_turn for cp in self.control_points)
self.total_buildings = sum(b.income for b in self.buildings) self.total_buildings = sum(b.income for b in self.buildings)
self.total = ((self.total_buildings + self.from_bases) * self.total = (self.total_buildings + self.from_bases) * self.multiplier
self.multiplier)

View File

@@ -1,7 +1,7 @@
import datetime import datetime
class Information():
class Information:
def __init__(self, title="", text="", turn=0): def __init__(self, title="", text="", turn=0):
self.title = title self.title = title
self.text = text self.text = text
@@ -9,9 +9,11 @@ class Information():
self.timestamp = datetime.datetime.now() self.timestamp = datetime.datetime.now()
def __str__(self): def __str__(self):
return '[{}][{}] {} {}'.format( return "[{}][{}] {} {}".format(
self.timestamp.strftime("%Y-%m-%d %H:%M:%S") if self.timestamp is not None else '', self.timestamp.strftime("%Y-%m-%d %H:%M:%S")
if self.timestamp is not None
else "",
self.turn, self.turn,
self.title, self.title,
self.text self.text,
) )

View File

@@ -79,6 +79,7 @@ class ControlPointAircraftInventory:
class GlobalAircraftInventory: class GlobalAircraftInventory:
"""Game-wide aircraft inventory.""" """Game-wide aircraft inventory."""
def __init__(self, control_points: Iterable[ControlPoint]) -> None: def __init__(self, control_points: Iterable[ControlPoint]) -> None:
self.inventories: Dict[ControlPoint, ControlPointAircraftInventory] = { self.inventories: Dict[ControlPoint, ControlPointAircraftInventory] = {
cp: ControlPointAircraftInventory(cp) for cp in control_points cp: ControlPointAircraftInventory(cp) for cp in control_points
@@ -100,8 +101,8 @@ class GlobalAircraftInventory:
inventory.add_aircraft(aircraft, count) inventory.add_aircraft(aircraft, count)
def for_control_point( def for_control_point(
self, self, control_point: ControlPoint
control_point: ControlPoint) -> ControlPointAircraftInventory: ) -> ControlPointAircraftInventory:
"""Returns the inventory specific to the given control point.""" """Returns the inventory specific to the given control point."""
return self.inventories[control_point] return self.inventories[control_point]

View File

@@ -7,8 +7,7 @@ class DestroyedUnit:
y: int y: int
name: str name: str
def __init__(self, x , y, name): def __init__(self, x, y, name):
self.x = x self.x = x
self.y = y self.y = y
self.name = name self.name = name

View File

@@ -6,7 +6,7 @@ class FrontlineData:
This Data structure will store information about an existing frontline This Data structure will store information about an existing frontline
""" """
def __init__(self, from_cp:ControlPoint, to_cp: ControlPoint): def __init__(self, from_cp: ControlPoint, to_cp: ControlPoint):
self.to_cp = to_cp self.to_cp = to_cp
self.from_cp = from_cp self.from_cp = from_cp
self.enemy_units_position = [] self.enemy_units_position = []

View File

@@ -1,5 +1,6 @@
from typing import List from typing import List
class FactionTurnMetadata: class FactionTurnMetadata:
""" """
Store metadata about a faction Store metadata about a faction
@@ -20,8 +21,8 @@ class GameTurnMetadata:
Store metadata about a game turn Store metadata about a game turn
""" """
allied_units:FactionTurnMetadata allied_units: FactionTurnMetadata
enemy_units:FactionTurnMetadata enemy_units: FactionTurnMetadata
def __init__(self): def __init__(self):
self.allied_units = FactionTurnMetadata() self.allied_units = FactionTurnMetadata()
@@ -53,4 +54,3 @@ class GameStats:
turn_data.enemy_units.vehicles_count += sum(cp.base.armor.values()) turn_data.enemy_units.vehicles_count += sum(cp.base.armor.values())
self.data_per_turn.append(turn_data) self.data_per_turn.append(turn_data)

View File

@@ -21,6 +21,10 @@ from game.threatzones import ThreatZones
from game.utils import nautical_miles from game.utils import nautical_miles
class NavMeshError(RuntimeError):
pass
class NavMeshPoly: class NavMeshPoly:
def __init__(self, ident: int, poly: Polygon, threatened: bool) -> None: def __init__(self, ident: int, poly: Polygon, threatened: bool) -> None:
self.ident = ident self.ident = ident
@@ -114,16 +118,18 @@ class NavMesh:
return self.travel_cost(a, b) return self.travel_cost(a, b)
@staticmethod @staticmethod
def reconstruct_path(came_from: Dict[NavPoint, Optional[NavPoint]], def reconstruct_path(
origin: NavPoint, came_from: Dict[NavPoint, Optional[NavPoint]],
destination: NavPoint) -> List[Point]: origin: NavPoint,
destination: NavPoint,
) -> List[Point]:
current = destination current = destination
path: List[Point] = [] path: List[Point] = []
while current != origin: while current != origin:
path.append(current.world_point) path.append(current.world_point)
previous = came_from[current] previous = came_from[current]
if previous is None: if previous is None:
raise RuntimeError( raise NavMeshError(
f"Could not reconstruct path to {destination} from {origin}" f"Could not reconstruct path to {destination} from {origin}"
) )
current = previous current = previous
@@ -138,19 +144,19 @@ class NavMesh:
def shortest_path(self, origin: Point, destination: Point) -> List[Point]: def shortest_path(self, origin: Point, destination: Point) -> List[Point]:
origin_poly = self.localize(origin) origin_poly = self.localize(origin)
if origin_poly is None: if origin_poly is None:
raise ValueError(f"Origin point {origin} is outside the navmesh") raise NavMeshError(f"Origin point {origin} is outside the navmesh")
destination_poly = self.localize(destination) destination_poly = self.localize(destination)
if destination_poly is None: if destination_poly is None:
raise ValueError( raise NavMeshError(
f"Origin point {destination} is outside the navmesh") f"Destination point {destination} is outside the navmesh"
)
return self._shortest_path( return self._shortest_path(
NavPoint(self.dcs_to_shapely_point(origin), origin_poly), NavPoint(self.dcs_to_shapely_point(origin), origin_poly),
NavPoint(self.dcs_to_shapely_point(destination), destination_poly) NavPoint(self.dcs_to_shapely_point(destination), destination_poly),
) )
def _shortest_path(self, origin: NavPoint, def _shortest_path(self, origin: NavPoint, destination: NavPoint) -> List[Point]:
destination: NavPoint) -> List[Point]:
# Adapted from # Adapted from
# https://www.redblobgames.com/pathfinding/a-star/implementation.py. # https://www.redblobgames.com/pathfinding/a-star/implementation.py.
frontier = NavFrontier() frontier = NavFrontier()
@@ -167,9 +173,7 @@ class NavMesh:
if current.poly == destination.poly: if current.poly == destination.poly:
# Made it to the correct nav poly. Add the leg from the border # Made it to the correct nav poly. Add the leg from the border
# to the target. # to the target.
cost = best_known[current] + self.travel_cost( cost = best_known[current] + self.travel_cost(current, destination)
current, destination
)
if cost < best_known[destination]: if cost < best_known[destination]:
best_known[destination] = cost best_known[destination] = cost
estimated = cost estimated = cost
@@ -185,14 +189,10 @@ class NavMesh:
raise RuntimeError raise RuntimeError
_, neighbor_point = nearest_points(current.point, boundary) _, neighbor_point = nearest_points(current.point, boundary)
neighbor_nav = NavPoint(neighbor_point, neighbor) neighbor_nav = NavPoint(neighbor_point, neighbor)
cost = best_known[current] + self.travel_cost( cost = best_known[current] + self.travel_cost(current, neighbor_nav)
current, neighbor_nav
)
if cost < best_known[neighbor_nav]: if cost < best_known[neighbor_nav]:
best_known[neighbor_nav] = cost best_known[neighbor_nav] = cost
estimated = cost + self.travel_heuristic( estimated = cost + self.travel_heuristic(neighbor_nav, destination)
neighbor_nav, destination
)
frontier.push(neighbor_nav, estimated) frontier.push(neighbor_nav, estimated)
came_from[neighbor_nav] = current came_from[neighbor_nav] = current
@@ -209,13 +209,16 @@ class NavMesh:
# threatened airbases at the map edges have room to retreat from the # threatened airbases at the map edges have room to retreat from the
# threat without running off the navmesh. # threat without running off the navmesh.
return box(*LineString(points).bounds).buffer( return box(*LineString(points).bounds).buffer(
nautical_miles(100).meters, resolution=1) nautical_miles(200).meters, resolution=1
)
@staticmethod @staticmethod
def create_navpolys(polys: List[Polygon], def create_navpolys(
threat_zones: ThreatZones) -> List[NavMeshPoly]: polys: List[Polygon], threat_zones: ThreatZones
return [NavMeshPoly(i, p, threat_zones.threatened(p)) ) -> List[NavMeshPoly]:
for i, p in enumerate(polys)] return [
NavMeshPoly(i, p, threat_zones.threatened(p)) for i, p in enumerate(polys)
]
@staticmethod @staticmethod
def associate_neighbors(polys: List[NavMeshPoly]) -> None: def associate_neighbors(polys: List[NavMeshPoly]) -> None:
@@ -234,8 +237,7 @@ class NavMesh:
point = (int(x), int(y)) point = (int(x), int(y))
neighbors = {} neighbors = {}
for potential_neighbor in points_map[point]: for potential_neighbor in points_map[point]:
intersection = navpoly.poly.intersection( intersection = navpoly.poly.intersection(potential_neighbor.poly)
potential_neighbor.poly)
if not intersection.is_empty: if not intersection.is_empty:
potential_neighbor.neighbors[navpoly] = intersection potential_neighbor.neighbors[navpoly] = intersection
neighbors[potential_neighbor] = intersection neighbors[potential_neighbor] = intersection
@@ -243,8 +245,9 @@ class NavMesh:
points_map[point].add(navpoly) points_map[point].add(navpoly)
@classmethod @classmethod
def from_threat_zones(cls, threat_zones: ThreatZones, def from_threat_zones(
theater: ConflictTheater) -> NavMesh: cls, threat_zones: ThreatZones, theater: ConflictTheater
) -> NavMesh:
# Simplify the threat poly to reduce the number of nav zones. Increase # 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 # 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. # error margin. This will create a simpler poly around the threat zone.

View File

@@ -41,6 +41,7 @@ if TYPE_CHECKING:
class Operation: class Operation:
"""Static class for managing the final Mission generation""" """Static class for managing the final Mission generation"""
current_mission = None # type: Mission current_mission = None # type: Mission
airgen = None # type: AircraftConflictGenerator airgen = None # type: AircraftConflictGenerator
triggersgen = None # type: TriggersGenerator triggersgen = None # type: TriggersGenerator
@@ -84,7 +85,7 @@ class Operation:
cls.game.enemy_name, cls.game.enemy_name,
cls.game.player_country, cls.game.player_country,
cls.game.enemy_country, cls.game.enemy_country,
frontline.position frontline.position,
) )
@classmethod @classmethod
@@ -93,7 +94,7 @@ class Operation:
player_cp, enemy_cp = cls.game.theater.closest_opposing_control_points() player_cp, enemy_cp = cls.game.theater.closest_opposing_control_points()
mid_point = player_cp.position.point_from_heading( mid_point = player_cp.position.point_from_heading(
player_cp.position.heading_between_point(enemy_cp.position), player_cp.position.heading_between_point(enemy_cp.position),
player_cp.position.distance_to_point(enemy_cp.position) / 2 player_cp.position.distance_to_point(enemy_cp.position) / 2,
) )
return Conflict( return Conflict(
cls.game.theater, cls.game.theater,
@@ -103,7 +104,7 @@ class Operation:
cls.game.enemy_name, cls.game.enemy_name,
cls.game.player_country, cls.game.player_country,
cls.game.enemy_country, cls.game.enemy_country,
mid_point mid_point,
) )
@classmethod @classmethod
@@ -118,9 +119,11 @@ class Operation:
p_country = cls.game.player_country p_country = cls.game.player_country
e_country = cls.game.enemy_country e_country = cls.game.enemy_country
cls.current_mission.coalition["blue"].add_country( cls.current_mission.coalition["blue"].add_country(
country_dict[db.country_id_from_name(p_country)]()) country_dict[db.country_id_from_name(p_country)]()
)
cls.current_mission.coalition["red"].add_country( cls.current_mission.coalition["red"].add_country(
country_dict[db.country_id_from_name(e_country)]()) country_dict[db.country_id_from_name(e_country)]()
)
@classmethod @classmethod
def inject_lua_trigger(cls, contents: str, comment: str) -> None: def inject_lua_trigger(cls, contents: str, comment: str) -> None:
@@ -133,12 +136,11 @@ class Operation:
cls.plugin_scripts.append(mnemonic) cls.plugin_scripts.append(mnemonic)
@classmethod @classmethod
def inject_plugin_script(cls, plugin_mnemonic: str, script: str, def inject_plugin_script(
script_mnemonic: str) -> None: cls, plugin_mnemonic: str, script: str, script_mnemonic: str
) -> None:
if script_mnemonic in cls.plugin_scripts: if script_mnemonic in cls.plugin_scripts:
logging.debug( logging.debug(f"Skipping already loaded {script} for {plugin_mnemonic}")
f"Skipping already loaded {script} for {plugin_mnemonic}"
)
else: else:
cls.plugin_scripts.append(script_mnemonic) cls.plugin_scripts.append(script_mnemonic)
@@ -146,15 +148,12 @@ class Operation:
script_path = Path(plugin_path, script) script_path = Path(plugin_path, script)
if not script_path.exists(): if not script_path.exists():
logging.error( logging.error(f"Cannot find {script_path} for plugin {plugin_mnemonic}")
f"Cannot find {script_path} for plugin {plugin_mnemonic}"
)
return return
trigger = TriggerStart(comment=f"Load {script_mnemonic}") trigger = TriggerStart(comment=f"Load {script_mnemonic}")
filename = script_path.resolve() filename = script_path.resolve()
fileref = cls.current_mission.map_resource.add_resource_file( fileref = cls.current_mission.map_resource.add_resource_file(filename)
filename)
trigger.add_action(DoScriptFile(fileref)) trigger.add_action(DoScriptFile(fileref))
cls.current_mission.triggerrules.triggers.append(trigger) cls.current_mission.triggerrules.triggers.append(trigger)
@@ -166,11 +165,11 @@ class Operation:
jtacs: List[JtacInfo], jtacs: List[JtacInfo],
airgen: AircraftConflictGenerator, airgen: AircraftConflictGenerator,
): ):
"""Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings) """Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings)"""
"""
gens: List[MissionInfoGenerator] = [ gens: List[MissionInfoGenerator] = [
KneeboardGenerator(cls.current_mission, cls.game), KneeboardGenerator(cls.current_mission, cls.game),
BriefingGenerator(cls.current_mission, cls.game) BriefingGenerator(cls.current_mission, cls.game),
] ]
for gen in gens: for gen in gens:
for dynamic_runway in groundobjectgen.runways.values(): for dynamic_runway in groundobjectgen.runways.values():
@@ -179,9 +178,8 @@ class Operation:
for tanker in airsupportgen.air_support.tankers: for tanker in airsupportgen.air_support.tankers:
gen.add_tanker(tanker) gen.add_tanker(tanker)
if cls.player_awacs_enabled: for aewc in airsupportgen.air_support.awacs:
for awacs in airsupportgen.air_support.awacs: gen.add_awacs(aewc)
gen.add_awacs(awacs)
for jtac in jtacs: for jtac in jtacs:
gen.add_jtac(jtac) gen.add_jtac(jtac)
@@ -208,8 +206,9 @@ class Operation:
cls.radio_registry.reserve(frequency) cls.radio_registry.reserve(frequency)
@classmethod @classmethod
def assign_channels_to_flights(cls, flights: List[FlightData], def assign_channels_to_flights(
air_support: AirSupport) -> None: cls, flights: List[FlightData], air_support: AirSupport
) -> None:
"""Assigns preset radio channels for client flights.""" """Assigns preset radio channels for client flights."""
for flight in flights: for flight in flights:
if not flight.client_units: if not flight.client_units:
@@ -217,8 +216,7 @@ class Operation:
cls.assign_channels_to_flight(flight, air_support) cls.assign_channels_to_flight(flight, air_support)
@staticmethod @staticmethod
def assign_channels_to_flight(flight: FlightData, def assign_channels_to_flight(flight: FlightData, air_support: AirSupport) -> None:
air_support: AirSupport) -> None:
"""Assigns preset radio channels for a client flight.""" """Assigns preset radio channels for a client flight."""
airframe = flight.aircraft_type airframe = flight.aircraft_type
@@ -234,7 +232,9 @@ class Operation:
) )
@classmethod @classmethod
def _create_tacan_registry(cls, unique_map_frequencies: Set[RadioFrequency]) -> None: def _create_tacan_registry(
cls, unique_map_frequencies: Set[RadioFrequency]
) -> None:
""" """
Dedup beacon/radio frequencies, since some maps have some frequencies Dedup beacon/radio frequencies, since some maps have some frequencies
used multiple times. used multiple times.
@@ -246,13 +246,14 @@ class Operation:
unique_map_frequencies.add(beacon.frequency) unique_map_frequencies.add(beacon.frequency)
if beacon.is_tacan: if beacon.is_tacan:
if beacon.channel is None: if beacon.channel is None:
logging.error( logging.error(f"TACAN beacon has no channel: {beacon.callsign}")
f"TACAN beacon has no channel: {beacon.callsign}")
else: else:
cls.tacan_registry.reserve(beacon.tacan_channel) cls.tacan_registry.reserve(beacon.tacan_channel)
@classmethod @classmethod
def _create_radio_registry(cls, unique_map_frequencies: Set[RadioFrequency]) -> None: def _create_radio_registry(
cls, unique_map_frequencies: Set[RadioFrequency]
) -> None:
cls.radio_registry = RadioRegistry() cls.radio_registry = RadioRegistry()
for data in AIRFIELD_DATA.values(): for data in AIRFIELD_DATA.values():
if data.theater == cls.game.theater.terrain.name and data.atc: if data.theater == cls.game.theater.terrain.name and data.atc:
@@ -270,7 +271,7 @@ class Operation:
cls.game, cls.game,
cls.radio_registry, cls.radio_registry,
cls.tacan_registry, cls.tacan_registry,
cls.unit_map cls.unit_map,
) )
cls.groundobjectgen.generate() cls.groundobjectgen.generate()
@@ -284,10 +285,13 @@ class Operation:
continue continue
pos = Point(d["x"], d["z"]) pos = Point(d["x"], d["z"])
if utype is not None and not cls.game.position_culled(pos) and cls.game.settings.perf_destroyed_units: if (
utype is not None
and not cls.game.position_culled(pos)
and cls.game.settings.perf_destroyed_units
):
cls.current_mission.static_group( cls.current_mission.static_group(
country=cls.current_mission.country( country=cls.current_mission.country(cls.game.player_country),
cls.game.player_country),
name="", name="",
_type=utype, _type=utype,
hidden=True, hidden=True,
@@ -302,13 +306,13 @@ class Operation:
cls.create_unit_map() cls.create_unit_map()
cls.create_radio_registries() cls.create_radio_registries()
# Set mission time and weather conditions. # Set mission time and weather conditions.
EnvironmentGenerator(cls.current_mission, EnvironmentGenerator(cls.current_mission, cls.game.conditions).generate()
cls.game.conditions).generate()
cls._generate_ground_units() cls._generate_ground_units()
cls._generate_destroyed_units() cls._generate_destroyed_units()
cls._generate_air_units() cls._generate_air_units()
cls.assign_channels_to_flights(cls.airgen.flights, cls.assign_channels_to_flights(
cls.airsupportgen.air_support) cls.airgen.flights, cls.airsupportgen.air_support
)
cls._generate_ground_conflicts() cls._generate_ground_conflicts()
# Triggers # Triggers
@@ -317,14 +321,16 @@ class Operation:
# Setup combined arms parameters # Setup combined arms parameters
cls.current_mission.groundControl.pilot_can_control_vehicles = cls.ca_slots > 0 cls.current_mission.groundControl.pilot_can_control_vehicles = cls.ca_slots > 0
if cls.game.player_country in [country.name for country in cls.current_mission.coalition["blue"].countries.values()]: if cls.game.player_country in [
country.name
for country in cls.current_mission.coalition["blue"].countries.values()
]:
cls.current_mission.groundControl.blue_tactical_commander = cls.ca_slots cls.current_mission.groundControl.blue_tactical_commander = cls.ca_slots
else: else:
cls.current_mission.groundControl.red_tactical_commander = cls.ca_slots cls.current_mission.groundControl.red_tactical_commander = cls.ca_slots
# Options # Options
forcedoptionsgen = ForcedOptionsGenerator( forcedoptionsgen = ForcedOptionsGenerator(cls.current_mission, cls.game)
cls.current_mission, cls.game)
forcedoptionsgen.generate() forcedoptionsgen.generate()
# Generate Visuals Smoke Effects # Generate Visuals Smoke Effects
@@ -341,13 +347,11 @@ class Operation:
plugin.inject_scripts(cls) plugin.inject_scripts(cls)
plugin.inject_configuration(cls) plugin.inject_configuration(cls)
cls.assign_channels_to_flights(cls.airgen.flights, cls.assign_channels_to_flights(
cls.airsupportgen.air_support) cls.airgen.flights, cls.airsupportgen.air_support
)
cls.notify_info_generators( cls.notify_info_generators(
cls.groundobjectgen, cls.groundobjectgen, cls.airsupportgen, cls.jtacs, cls.airgen
cls.airsupportgen,
cls.jtacs,
cls.airgen
) )
cls.reset_naming_ids() cls.reset_naming_ids()
return cls.unit_map return cls.unit_map
@@ -359,29 +363,40 @@ class Operation:
# Air Support (Tanker & Awacs) # Air Support (Tanker & Awacs)
assert cls.radio_registry and cls.tacan_registry assert cls.radio_registry and cls.tacan_registry
cls.airsupportgen = AirSupportConflictGenerator( cls.airsupportgen = AirSupportConflictGenerator(
cls.current_mission, cls.air_conflict(), cls.game, cls.radio_registry, cls.current_mission,
cls.tacan_registry) cls.air_conflict(),
cls.game,
cls.radio_registry,
cls.tacan_registry,
)
cls.airsupportgen.generate() cls.airsupportgen.generate()
# Generate Aircraft Activity on the map # Generate Aircraft Activity on the map
cls.airgen = AircraftConflictGenerator( cls.airgen = AircraftConflictGenerator(
cls.current_mission, cls.game.settings, cls.game, cls.current_mission,
cls.radio_registry, cls.unit_map) cls.game.settings,
cls.game,
cls.radio_registry,
cls.unit_map,
air_support=cls.airsupportgen.air_support,
)
cls.airgen.clear_parking_slots() cls.airgen.clear_parking_slots()
cls.airgen.generate_flights( cls.airgen.generate_flights(
cls.current_mission.country(cls.game.player_country), cls.current_mission.country(cls.game.player_country),
cls.game.blue_ato, cls.game.blue_ato,
cls.groundobjectgen.runways cls.groundobjectgen.runways,
) )
cls.airgen.generate_flights( cls.airgen.generate_flights(
cls.current_mission.country(cls.game.enemy_country), cls.current_mission.country(cls.game.enemy_country),
cls.game.red_ato, cls.game.red_ato,
cls.groundobjectgen.runways cls.groundobjectgen.runways,
) )
cls.airgen.spawn_unused_aircraft( cls.airgen.spawn_unused_aircraft(
cls.current_mission.country(cls.game.player_country), cls.current_mission.country(cls.game.player_country),
cls.current_mission.country(cls.game.enemy_country)) cls.current_mission.country(cls.game.enemy_country),
)
@classmethod @classmethod
def _generate_ground_conflicts(cls) -> None: def _generate_ground_conflicts(cls) -> None:
@@ -396,17 +411,19 @@ class Operation:
cls.current_mission.country(cls.game.enemy_country), cls.current_mission.country(cls.game.enemy_country),
player_cp, player_cp,
enemy_cp, enemy_cp,
cls.game.theater cls.game.theater,
) )
# Generate frontline ops # Generate frontline ops
player_gp = cls.game.ground_planners[player_cp.id].units_per_cp[enemy_cp.id] player_gp = cls.game.ground_planners[player_cp.id].units_per_cp[enemy_cp.id]
enemy_gp = cls.game.ground_planners[enemy_cp.id].units_per_cp[player_cp.id] enemy_gp = cls.game.ground_planners[enemy_cp.id].units_per_cp[player_cp.id]
ground_conflict_gen = GroundConflictGenerator( ground_conflict_gen = GroundConflictGenerator(
cls.current_mission, cls.current_mission,
conflict, cls.game, conflict,
player_gp, enemy_gp, cls.game,
player_gp,
enemy_gp,
player_cp.stances[enemy_cp.id], player_cp.stances[enemy_cp.id],
cls.unit_map cls.unit_map,
) )
ground_conflict_gen.generate() ground_conflict_gen.generate()
cls.jtacs.extend(ground_conflict_gen.jtacs) cls.jtacs.extend(ground_conflict_gen.jtacs)
@@ -416,9 +433,12 @@ class Operation:
namegen.reset_numbers() namegen.reset_numbers()
@classmethod @classmethod
def generate_lua(cls, airgen: AircraftConflictGenerator, def generate_lua(
airsupportgen: AirSupportConflictGenerator, cls,
jtacs: List[JtacInfo]) -> None: airgen: AircraftConflictGenerator,
airsupportgen: AirSupportConflictGenerator,
jtacs: List[JtacInfo],
) -> None:
# TODO: Refactor this # TODO: Refactor this
luaData = { luaData = {
"AircraftCarriers": {}, "AircraftCarriers": {},
@@ -426,6 +446,8 @@ class Operation:
"AWACs": {}, "AWACs": {},
"JTACs": {}, "JTACs": {},
"TargetPoints": {}, "TargetPoints": {},
"RedAA": {},
"BlueAA": {},
} # type: ignore } # type: ignore
for tanker in airsupportgen.air_support.tankers: for tanker in airsupportgen.air_support.tankers:
@@ -434,7 +456,7 @@ class Operation:
"callsign": tanker.callsign, "callsign": tanker.callsign,
"variant": tanker.variant, "variant": tanker.variant,
"radio": tanker.freq.mhz, "radio": tanker.freq.mhz,
"tacan": str(tanker.tacan.number) + tanker.tacan.band.name "tacan": str(tanker.tacan.number) + tanker.tacan.band.name,
} }
if airsupportgen.air_support.awacs: if airsupportgen.air_support.awacs:
@@ -442,7 +464,7 @@ class Operation:
luaData["AWACs"][awacs.callsign] = { luaData["AWACs"][awacs.callsign] = {
"dcsGroupName": awacs.dcsGroupName, "dcsGroupName": awacs.dcsGroupName,
"callsign": awacs.callsign, "callsign": awacs.callsign,
"radio": awacs.freq.mhz "radio": awacs.freq.mhz,
} }
for jtac in jtacs: for jtac in jtacs:
@@ -451,14 +473,16 @@ class Operation:
"callsign": jtac.callsign, "callsign": jtac.callsign,
"zone": jtac.region, "zone": jtac.region,
"dcsUnit": jtac.unit_name, "dcsUnit": jtac.unit_name,
"laserCode": jtac.code "laserCode": jtac.code,
} }
for flight in airgen.flights: for flight in airgen.flights:
if flight.friendly and flight.flight_type in [FlightType.ANTISHIP, if flight.friendly and flight.flight_type in [
FlightType.DEAD, FlightType.ANTISHIP,
FlightType.SEAD, FlightType.DEAD,
FlightType.STRIKE]: FlightType.SEAD,
FlightType.STRIKE,
]:
flightType = str(flight.flight_type) flightType = str(flight.flight_type)
flightTarget = flight.package.target flightTarget = flight.package.target
if flightTarget: if flightTarget:
@@ -466,23 +490,47 @@ class Operation:
flightTargetType = None flightTargetType = None
if isinstance(flightTarget, TheaterGroundObject): if isinstance(flightTarget, TheaterGroundObject):
flightTargetName = flightTarget.obj_name flightTargetName = flightTarget.obj_name
flightTargetType = flightType + \ flightTargetType = (
f" TGT ({flightTarget.category})" flightType + f" TGT ({flightTarget.category})"
elif hasattr(flightTarget, 'name'): )
elif hasattr(flightTarget, "name"):
flightTargetName = flightTarget.name flightTargetName = flightTarget.name
flightTargetType = flightType + " TGT (Airbase)" flightTargetType = flightType + " TGT (Airbase)"
luaData["TargetPoints"][flightTargetName] = { luaData["TargetPoints"][flightTargetName] = {
"name": flightTargetName, "name": flightTargetName,
"type": flightTargetType, "type": flightTargetType,
"position": {"x": flightTarget.position.x, "position": {
"y": flightTarget.position.y} "x": flightTarget.position.x,
"y": flightTarget.position.y,
},
} }
for cp in cls.game.theater.controlpoints:
for ground_object in cp.ground_objects:
if ground_object.might_have_aa and not ground_object.is_dead:
for g in ground_object.groups:
threat_range = ground_object.threat_range(g)
if not threat_range:
continue
faction = "BlueAA" if cp.captured else "RedAA"
luaData[faction][g.name] = {
"name": ground_object.name,
"range": threat_range.meters,
"position": {
"x": ground_object.position.x,
"y": ground_object.position.y,
},
}
# set a LUA table with data from Liberation that we want to set # set a LUA table with data from Liberation that we want to set
# at the moment it contains Liberation's install path, and an overridable definition for the JTACAutoLase function # at the moment it contains Liberation's install path, and an overridable definition for the JTACAutoLase function
# later, we'll add data about the units and points having been generated, in order to facilitate the configuration of the plugin lua scripts # later, we'll add data about the units and points having been generated, in order to facilitate the configuration of the plugin lua scripts
state_location = "[[" + os.path.abspath(".") + "]]" state_location = "[[" + os.path.abspath(".") + "]]"
lua = """ lua = (
"""
-- setting configuration table -- setting configuration table
env.info("DCSLiberation|: setting configuration table") env.info("DCSLiberation|: setting configuration table")
@@ -490,9 +538,12 @@ class Operation:
dcsLiberation = {} dcsLiberation = {}
-- the base location for state.json; if non-existent, it'll be replaced with LIBERATION_EXPORT_DIR, TEMP, or DCS working directory -- the base location for state.json; if non-existent, it'll be replaced with LIBERATION_EXPORT_DIR, TEMP, or DCS working directory
dcsLiberation.installPath=""" + state_location + """ dcsLiberation.installPath="""
+ state_location
+ """
""" """
)
# Process the tankers # Process the tankers
lua += """ lua += """
@@ -566,7 +617,33 @@ class Operation:
-- list the aircraft carriers generated by Liberation -- list the aircraft carriers generated by Liberation
-- dcsLiberation.Carriers = {} -- dcsLiberation.Carriers = {}
-- later, we'll add more data to the table -- list the Red AA generated by Liberation
dcsLiberation.RedAA = {
"""
for key in luaData["RedAA"]:
data = luaData["RedAA"][key]
name = data["name"]
radius = data["range"]
positionX = data["position"]["x"]
positionY = data["position"]["y"]
lua += f" {{dcsGroupName='{key}', name='{name}', range='{radius}', positionX='{positionX}', positionY='{positionY}' }}, \n"
lua += "}"
lua += """
-- list the Blue AA generated by Liberation
dcsLiberation.BlueAA = {
"""
for key in luaData["BlueAA"]:
data = luaData["BlueAA"][key]
name = data["name"]
radius = data["range"]
positionX = data["position"]["x"]
positionY = data["position"]["y"]
lua += f" {{dcsGroupName='{key}', name='{name}', range='{radius}', positionX='{positionX}', positionY='{positionY}' }}, \n"
lua += "}"
lua += """
""" """

View File

@@ -67,4 +67,3 @@ def autosave(game) -> bool:
except Exception: except Exception:
logging.exception("Could not save game") logging.exception("Could not save game")
return False return False

View File

@@ -14,9 +14,9 @@ if TYPE_CHECKING:
class LuaPluginWorkOrder: class LuaPluginWorkOrder:
def __init__(
def __init__(self, parent_mnemonic: str, filename: str, mnemonic: str, self, parent_mnemonic: str, filename: str, mnemonic: str, disable: bool
disable: bool) -> None: ) -> None:
self.parent_mnemonic = parent_mnemonic self.parent_mnemonic = parent_mnemonic
self.filename = filename self.filename = filename
self.mnemonic = mnemonic self.mnemonic = mnemonic
@@ -26,8 +26,9 @@ class LuaPluginWorkOrder:
if self.disable: if self.disable:
operation.bypass_plugin_script(self.mnemonic) operation.bypass_plugin_script(self.mnemonic)
else: else:
operation.inject_plugin_script(self.parent_mnemonic, self.filename, operation.inject_plugin_script(
self.mnemonic) self.parent_mnemonic, self.filename, self.mnemonic
)
class PluginSettings: class PluginSettings:
@@ -45,8 +46,7 @@ class PluginSettings:
# Plugin options are saved in the game's Settings, but it's possible for # Plugin options are saved in the game's Settings, but it's possible for
# plugins to change across loads. If new plugins are added or new # plugins to change across loads. If new plugins are added or new
# options added to those plugins, initialize the new settings. # options added to those plugins, initialize the new settings.
self.settings.initialize_plugin_option(self.identifier, self.settings.initialize_plugin_option(self.identifier, self.enabled_by_default)
self.enabled_by_default)
@property @property
def enabled(self) -> bool: def enabled(self) -> bool:
@@ -57,8 +57,7 @@ class PluginSettings:
class LuaPluginOption(PluginSettings): class LuaPluginOption(PluginSettings):
def __init__(self, identifier: str, name: str, def __init__(self, identifier: str, name: str, enabled_by_default: bool) -> None:
enabled_by_default: bool) -> None:
super().__init__(identifier, enabled_by_default) super().__init__(identifier, enabled_by_default)
self.name = name self.name = name
@@ -80,24 +79,34 @@ class LuaPluginDefinition:
options = [] options = []
for option in data.get("specificOptions"): for option in data.get("specificOptions"):
option_id = option["mnemonic"] option_id = option["mnemonic"]
options.append(LuaPluginOption( options.append(
identifier=f"{name}.{option_id}", LuaPluginOption(
name=option.get("nameInUI", name), identifier=f"{name}.{option_id}",
enabled_by_default=option.get("defaultValue") name=option.get("nameInUI", name),
)) enabled_by_default=option.get("defaultValue"),
)
)
work_orders = [] work_orders = []
for work_order in data.get("scriptsWorkOrders"): for work_order in data.get("scriptsWorkOrders"):
work_orders.append(LuaPluginWorkOrder( work_orders.append(
name, work_order.get("file"), work_order["mnemonic"], LuaPluginWorkOrder(
work_order.get("disable", False) name,
)) work_order.get("file"),
work_order["mnemonic"],
work_order.get("disable", False),
)
)
config_work_orders = [] config_work_orders = []
for work_order in data.get("configurationWorkOrders"): for work_order in data.get("configurationWorkOrders"):
config_work_orders.append(LuaPluginWorkOrder( config_work_orders.append(
name, work_order.get("file"), work_order["mnemonic"], LuaPluginWorkOrder(
work_order.get("disable", False) name,
)) work_order.get("file"),
work_order["mnemonic"],
work_order.get("disable", False),
)
)
return cls( return cls(
identifier=name, identifier=name,
@@ -106,16 +115,14 @@ class LuaPluginDefinition:
enabled_by_default=data.get("defaultValue", False), enabled_by_default=data.get("defaultValue", False),
options=options, options=options,
work_orders=work_orders, work_orders=work_orders,
config_work_orders=config_work_orders config_work_orders=config_work_orders,
) )
class LuaPlugin(PluginSettings): class LuaPlugin(PluginSettings):
def __init__(self, definition: LuaPluginDefinition) -> None: def __init__(self, definition: LuaPluginDefinition) -> None:
self.definition = definition self.definition = definition
super().__init__(self.definition.identifier, super().__init__(self.definition.identifier, self.definition.enabled_by_default)
self.definition.enabled_by_default)
@property @property
def name(self) -> str: def name(self) -> str:
@@ -155,12 +162,12 @@ class LuaPlugin(PluginSettings):
for option in self.options: for option in self.options:
enabled = str(option.enabled).lower() enabled = str(option.enabled).lower()
name = option.identifier name = option.identifier
option_decls.append( option_decls.append(f" dcsLiberation.plugins.{name} = {enabled}")
f" dcsLiberation.plugins.{name} = {enabled}")
joined_options = "\n".join(option_decls) joined_options = "\n".join(option_decls)
lua = textwrap.dedent(f"""\ lua = textwrap.dedent(
f"""\
-- {self.identifier} plugin configuration. -- {self.identifier} plugin configuration.
if dcsLiberation then if dcsLiberation then
@@ -171,10 +178,10 @@ class LuaPlugin(PluginSettings):
{joined_options} {joined_options}
end end
""") """
)
operation.inject_lua_trigger( operation.inject_lua_trigger(lua, f"{self.identifier} plugin configuration")
lua, f"{self.identifier} plugin configuration")
for work_order in self.definition.config_work_orders: for work_order in self.definition.config_work_orders:
work_order.work(operation) work_order.work(operation)

View File

@@ -27,7 +27,8 @@ class LuaPluginManager:
if not plugin_path.exists(): if not plugin_path.exists():
raise RuntimeError( raise RuntimeError(
f"Invalid plugin configuration: required plugin {name} " f"Invalid plugin configuration: required plugin {name} "
f"does not exist at {plugin_path}") f"does not exist at {plugin_path}"
)
logging.info(f"Loading plugin {name} from {plugin_path}") logging.info(f"Loading plugin {name} from {plugin_path}")
plugin = LuaPlugin.from_json(name, plugin_path) plugin = LuaPlugin.from_json(name, plugin_path)
if plugin is not None: if plugin is not None:

View File

@@ -0,0 +1,15 @@
from dcs import Point
class PointWithHeading(Point):
def __init__(self):
super(PointWithHeading, self).__init__(0, 0)
self.heading = 0
@staticmethod
def from_point(point: Point, heading: int):
p = PointWithHeading()
p.x = point.x
p.y = point.y
p.heading = heading
return p

View File

@@ -35,9 +35,16 @@ class AircraftProcurementRequest:
class ProcurementAi: class ProcurementAi:
def __init__(self, game: Game, for_player: bool, faction: Faction, def __init__(
manage_runways: bool, manage_front_line: bool, self,
manage_aircraft: bool, front_line_budget_share: float) -> None: game: Game,
for_player: bool,
faction: Faction,
manage_runways: bool,
manage_front_line: bool,
manage_aircraft: bool,
front_line_budget_share: float,
) -> None:
if front_line_budget_share > 1.0: if front_line_budget_share > 1.0:
raise ValueError raise ValueError
@@ -51,8 +58,8 @@ class ProcurementAi:
self.threat_zones = self.game.threat_zone_for(not self.is_player) self.threat_zones = self.game.threat_zone_for(not self.is_player)
def spend_budget( def spend_budget(
self, budget: float, self, budget: float, aircraft_requests: List[AircraftProcurementRequest]
aircraft_requests: List[AircraftProcurementRequest]) -> float: ) -> float:
if self.manage_runways: if self.manage_runways:
budget = self.repair_runways(budget) budget = self.repair_runways(budget)
if self.manage_front_line: if self.manage_front_line:
@@ -100,25 +107,30 @@ class ProcurementAi:
budget -= db.RUNWAY_REPAIR_COST budget -= db.RUNWAY_REPAIR_COST
if self.is_player: if self.is_player:
self.game.message( self.game.message(
"OPFOR has begun repairing the runway at " "OPFOR has begun repairing the runway at " f"{control_point}"
f"{control_point}"
) )
else: else:
self.game.message( self.game.message(
"We have begun repairing the runway at " "We have begun repairing the runway at " f"{control_point}"
f"{control_point}"
) )
return budget return budget
def random_affordable_ground_unit( def random_affordable_ground_unit(
self, budget: float, self, budget: float, cp: ControlPoint
cp: ControlPoint) -> Optional[Type[VehicleType]]: ) -> Optional[Type[VehicleType]]:
affordable_units = [u for u in self.faction.frontline_units + self.faction.artillery_units if affordable_units = [
db.PRICES[u] <= budget] 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_number_aa = (
total_non_aa = cp.base.total_armor + cp.pending_deliveries_count - total_number_aa cp.base.total_frontline_aa + cp.pending_frontline_aa_deliveries_count
max_aa = math.ceil(total_non_aa/8) )
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 # Limit the number of AA units the AI will buy
if not total_number_aa < max_aa: if not total_number_aa < max_aa:
@@ -150,8 +162,12 @@ class ProcurementAi:
return budget return budget
def _affordable_aircraft_of_types( def _affordable_aircraft_of_types(
self, types: List[Type[FlyingType]], airbase: ControlPoint, self,
number: int, max_price: float) -> Optional[Type[FlyingType]]: types: List[Type[FlyingType]],
airbase: ControlPoint,
number: int,
max_price: float,
) -> Optional[Type[FlyingType]]:
best_choice: Optional[Type[FlyingType]] = None best_choice: Optional[Type[FlyingType]] = None
for unit in [u for u in self.faction.aircrafts if u in types]: for unit in [u for u in self.faction.aircrafts if u in types]:
if db.PRICES[unit] * number > max_price: if db.PRICES[unit] * number > max_price:
@@ -168,15 +184,15 @@ class ProcurementAi:
return best_choice return best_choice
def affordable_aircraft_for( def affordable_aircraft_for(
self, request: AircraftProcurementRequest, self, request: AircraftProcurementRequest, airbase: ControlPoint, budget: float
airbase: ControlPoint, budget: float) -> Optional[Type[FlyingType]]: ) -> Optional[Type[FlyingType]]:
return self._affordable_aircraft_of_types( return self._affordable_aircraft_of_types(
aircraft_for_task(request.task_capability), aircraft_for_task(request.task_capability), airbase, request.number, budget
airbase, request.number, budget) )
def purchase_aircraft( def purchase_aircraft(
self, budget: float, self, budget: float, aircraft_requests: List[AircraftProcurementRequest]
aircraft_requests: List[AircraftProcurementRequest]) -> float: ) -> float:
for request in aircraft_requests: for request in aircraft_requests:
for airbase in self.best_airbases_for(request): for airbase in self.best_airbases_for(request):
unit = self.affordable_aircraft_for(request, airbase, budget) unit = self.affordable_aircraft_for(request, airbase, budget)
@@ -201,11 +217,9 @@ class ProcurementAi:
return self.game.theater.enemy_points() return self.game.theater.enemy_points()
def best_airbases_for( def best_airbases_for(
self, self, request: AircraftProcurementRequest
request: AircraftProcurementRequest) -> Iterator[ControlPoint]: ) -> Iterator[ControlPoint]:
distance_cache = ObjectiveDistanceCache.get_closest_airfields( distance_cache = ObjectiveDistanceCache.get_closest_airfields(request.near)
request.near
)
threatened = [] threatened = []
for cp in distance_cache.airfields_within(request.range): for cp in distance_cache.airfields_within(request.range):
if not cp.is_friendly(self.is_player): if not cp.is_friendly(self.is_player):

View File

@@ -30,6 +30,8 @@ class Settings:
automate_front_line_reinforcements: bool = False automate_front_line_reinforcements: bool = False
automate_aircraft_reinforcements: bool = False automate_aircraft_reinforcements: bool = False
restrict_weapons_by_date: bool = False restrict_weapons_by_date: bool = False
disable_legacy_aewc: bool = False
generate_dark_kneeboard: bool = False
# Performance oriented # Performance oriented
perf_red_alert_state: bool = True perf_red_alert_state: bool = True
@@ -58,8 +60,7 @@ class Settings:
def plugin_settings_key(identifier: str) -> str: def plugin_settings_key(identifier: str) -> str:
return f"plugins.{identifier}" return f"plugins.{identifier}"
def initialize_plugin_option(self, identifier: str, def initialize_plugin_option(self, identifier: str, default_value: bool) -> None:
default_value: bool) -> None:
try: try:
self.plugin_option(identifier) self.plugin_option(identifier)
except KeyError: except KeyError:

View File

@@ -4,7 +4,7 @@ import math
import typing import typing
from typing import Dict, Type from typing import Dict, Type
from dcs.task import CAP, CAS, Embarking, PinpointStrike, Task from dcs.task import AWACS, CAP, CAS, Embarking, PinpointStrike, Task
from dcs.unittype import FlyingType, UnitType, VehicleType from dcs.unittype import FlyingType, UnitType, VehicleType
from dcs.vehicles import AirDefence, Armor from dcs.vehicles import AirDefence, Armor
@@ -22,7 +22,6 @@ BASE_MIN_STRENGTH = 0
class Base: class Base:
def __init__(self): def __init__(self):
self.aircraft: Dict[Type[FlyingType], int] = {} self.aircraft: Dict[Type[FlyingType], int] = {}
self.armor: Dict[Type[VehicleType], int] = {} self.armor: Dict[Type[VehicleType], int] = {}
@@ -57,23 +56,43 @@ class Base:
return sum(self.aa.values()) return sum(self.aa.values())
def total_units(self, task: Task) -> int: def total_units(self, task: Task) -> int:
return sum([c for t, c in itertools.chain(self.aircraft.items(), self.armor.items(), self.aa.items()) if t in db.UNIT_BY_TASK[task]]) return sum(
[
c
for t, c in itertools.chain(
self.aircraft.items(), self.armor.items(), self.aa.items()
)
if t in db.UNIT_BY_TASK[task]
]
)
def total_units_of_type(self, unit_type) -> int: def total_units_of_type(self, unit_type) -> int:
return sum([c for t, c in itertools.chain(self.aircraft.items(), self.armor.items(), self.aa.items()) if t == unit_type]) return sum(
[
c
for t, c in itertools.chain(
self.aircraft.items(), self.armor.items(), self.aa.items()
)
if t == unit_type
]
)
@property @property
def all_units(self): def all_units(self):
return itertools.chain(self.aircraft.items(), self.armor.items(), self.aa.items()) return itertools.chain(
self.aircraft.items(), self.armor.items(), self.aa.items()
)
def _find_best_unit(self, available_units: Dict[UnitType, int], def _find_best_unit(
for_type: Task, count: int) -> Dict[UnitType, int]: self, available_units: Dict[UnitType, int], for_type: Task, count: int
) -> Dict[UnitType, int]:
if count <= 0: if count <= 0:
logging.warning("{}: no units for {}".format(self, for_type)) logging.warning("{}: no units for {}".format(self, for_type))
return {} return {}
sorted_units = [key for key in available_units if sorted_units = [
key in db.UNIT_BY_TASK[for_type]] key for key in available_units if key in db.UNIT_BY_TASK[for_type]
]
sorted_units.sort(key=lambda x: db.PRICES[x], reverse=True) sorted_units.sort(key=lambda x: db.PRICES[x], reverse=True)
result: Dict[UnitType, int] = {} result: Dict[UnitType, int] = {}
@@ -94,14 +113,18 @@ class Base:
logging.info("{} for {} ({}): {}".format(self, for_type, count, result)) logging.info("{} for {} ({}): {}".format(self, for_type, count, result))
return result return result
def _find_best_planes(self, for_type: Task, count: int) -> typing.Dict[FlyingType, int]: def _find_best_planes(
self, for_type: Task, count: int
) -> typing.Dict[FlyingType, int]:
return self._find_best_unit(self.aircraft, for_type, count) return self._find_best_unit(self.aircraft, for_type, count)
def _find_best_armor(self, for_type: Task, count: int) -> typing.Dict[Armor, int]: def _find_best_armor(self, for_type: Task, count: int) -> typing.Dict[Armor, int]:
return self._find_best_unit(self.armor, for_type, count) return self._find_best_unit(self.armor, for_type, count)
def append_commision_points(self, for_type, points: float) -> int: def append_commision_points(self, for_type, points: float) -> int:
self.commision_points[for_type] = self.commision_points.get(for_type, 0) + points self.commision_points[for_type] = (
self.commision_points.get(for_type, 0) + points
)
points = self.commision_points[for_type] points = self.commision_points[for_type]
if points >= 1: if points >= 1:
self.commision_points[for_type] = points - math.floor(points) self.commision_points[for_type] = points - math.floor(points)
@@ -110,7 +133,9 @@ class Base:
return 0 return 0
def filter_units(self, applicable_units: typing.Collection): def filter_units(self, applicable_units: typing.Collection):
self.aircraft = {k: v for k, v in self.aircraft.items() if k in applicable_units} self.aircraft = {
k: v for k, v in self.aircraft.items() if k in applicable_units
}
self.armor = {k: v for k, v in self.armor.items() if k in applicable_units} 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]): def commision_units(self, units: typing.Dict[typing.Any, int]):
@@ -122,7 +147,12 @@ class Base:
for_task = db.unit_task(unit_type) for_task = db.unit_task(unit_type)
target_dict = None target_dict = None
if for_task == CAS or for_task == CAP or for_task == Embarking: if (
for_task == AWACS
or for_task == CAS
or for_task == CAP
or for_task == Embarking
):
target_dict = self.aircraft target_dict = self.aircraft
elif for_task == PinpointStrike: elif for_task == PinpointStrike:
target_dict = self.armor target_dict = self.armor
@@ -149,7 +179,7 @@ class Base:
if unit_type not in target_array: if unit_type not in target_array:
print("Base didn't find event type {}".format(unit_type)) print("Base didn't find event type {}".format(unit_type))
continue continue
target_array[unit_type] = max(target_array[unit_type] - count, 0) target_array[unit_type] = max(target_array[unit_type] - count, 0)
if target_array[unit_type] == 0: if target_array[unit_type] == 0:
del target_array[unit_type] del target_array[unit_type]
@@ -166,12 +196,20 @@ class Base:
def scramble_count(self, multiplier: float, task: Task = None) -> int: def scramble_count(self, multiplier: float, task: Task = None) -> int:
if task: if task:
count = sum([v for k, v in self.aircraft.items() if db.unit_task(k) == task]) count = sum(
[v for k, v in self.aircraft.items() if db.unit_task(k) == task]
)
else: else:
count = self.total_aircraft count = self.total_aircraft
count = int(math.ceil(count * PLANES_SCRAMBLE_FACTOR * self.strength)) count = int(math.ceil(count * PLANES_SCRAMBLE_FACTOR * self.strength))
return min(min(max(count, PLANES_SCRAMBLE_MIN_BASE), int(PLANES_SCRAMBLE_MAX_BASE * multiplier)), count) return min(
min(
max(count, PLANES_SCRAMBLE_MIN_BASE),
int(PLANES_SCRAMBLE_MAX_BASE * multiplier),
),
count,
)
def assemble_count(self): def assemble_count(self):
return int(self.total_armor * 0.5) return int(self.total_armor * 0.5)
@@ -202,4 +240,8 @@ class Base:
return self._find_best_armor(PinpointStrike, count) return self._find_best_armor(PinpointStrike, count)
def assemble_aa(self, count=None) -> typing.Dict[AirDefence, int]: def assemble_aa(self, count=None) -> typing.Dict[AirDefence, int]:
return self._find_best_unit(self.aa, AirDefence, count and min(count, self.total_aa) or self.assemble_aa_count()) return self._find_best_unit(
self.aa,
AirDefence,
count and min(count, self.total_aa) or self.assemble_aa_count(),
)

View File

@@ -23,7 +23,7 @@ from dcs.planes import F_15C
from dcs.ships import ( from dcs.ships import (
CVN_74_John_C__Stennis, CVN_74_John_C__Stennis,
LHA_1_Tarawa, LHA_1_Tarawa,
USS_Arleigh_Burke_IIa, DDG_Arleigh_Burke_IIa,
) )
from dcs.statics import Fortification from dcs.statics import Fortification
from dcs.terrain import ( from dcs.terrain import (
@@ -55,6 +55,7 @@ from .controlpoint import (
Fob, Fob,
) )
from .landmap import Landmap, load_landmap, poly_contains from .landmap import Landmap, load_landmap, poly_contains
from ..point_with_heading import PointWithHeading
from ..utils import Distance, meters, nautical_miles from ..utils import Distance, meters, nautical_miles
Numeric = Union[int, float] Numeric = Union[int, float]
@@ -71,6 +72,7 @@ IMPORTANCE_HIGH = 1.4
FRONTLINE_MIN_CP_DISTANCE = 5000 FRONTLINE_MIN_CP_DISTANCE = 5000
def pairwise(iterable): def pairwise(iterable):
""" """
itertools recipe itertools recipe
@@ -91,30 +93,33 @@ class MizCampaignLoader:
LHA_UNIT_TYPE = LHA_1_Tarawa.id LHA_UNIT_TYPE = LHA_1_Tarawa.id
FRONT_LINE_UNIT_TYPE = Armor.APC_M113.id FRONT_LINE_UNIT_TYPE = Armor.APC_M113.id
FOB_UNIT_TYPE = Unarmed.CP_SKP_11_ATC_Mobile_Command_Post.id FOB_UNIT_TYPE = Unarmed.Truck_SKP_11_Mobile_ATC.id
FARP_HELIPAD = "SINGLE_HELIPAD"
EWR_UNIT_TYPE = AirDefence.EWR_55G6.id EWR_UNIT_TYPE = AirDefence.EWR_55G6.id
SAM_UNIT_TYPE = AirDefence.SAM_SA_10_S_300PS_SR_64H6E.id SAM_UNIT_TYPE = AirDefence.SAM_SA_10_S_300_Grumble_Big_Bird_SR.id
GARRISON_UNIT_TYPE = AirDefence.SAM_SA_19_Tunguska_2S6.id GARRISON_UNIT_TYPE = AirDefence.SAM_SA_19_Tunguska_Grison.id
OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id
SHIP_UNIT_TYPE = USS_Arleigh_Burke_IIa.id SHIP_UNIT_TYPE = DDG_Arleigh_Burke_IIa.id
MISSILE_SITE_UNIT_TYPE = MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M.id MISSILE_SITE_UNIT_TYPE = MissilesSS.SSM_SS_1C_Scud_B.id
COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.SS_N_2_Silkworm.id COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.AShM_SS_N_2_Silkworm.id
# Multiple options for the required SAMs so campaign designers can more # Multiple options for the required SAMs so campaign designers can more
# accurately see the coverage of their IADS for the expected type. # accurately see the coverage of their IADS for the expected type.
REQUIRED_LONG_RANGE_SAM_UNIT_TYPES = { REQUIRED_LONG_RANGE_SAM_UNIT_TYPES = {
AirDefence.SAM_Patriot_LN_M901.id, AirDefence.SAM_Patriot_LN.id,
AirDefence.SAM_SA_10_S_300PS_LN_5P85C.id, AirDefence.SAM_SA_10_S_300_Grumble_TEL_C.id,
AirDefence.SAM_SA_10_S_300PS_LN_5P85D.id, AirDefence.SAM_SA_10_S_300_Grumble_TEL_D.id,
} }
REQUIRED_MEDIUM_RANGE_SAM_UNIT_TYPES = { REQUIRED_MEDIUM_RANGE_SAM_UNIT_TYPES = {
AirDefence.SAM_Hawk_LN_M192.id, AirDefence.SAM_Hawk_LN_M192.id,
AirDefence.SAM_SA_2_LN_SM_90.id, AirDefence.SAM_SA_2_S_75_Guideline_LN.id,
AirDefence.SAM_SA_3_S_125_LN_5P73.id, AirDefence.SAM_SA_3_S_125_Goa_LN.id,
} }
REQUIRED_EWR_UNIT_TYPE = AirDefence.EWR_1L13.id
BASE_DEFENSE_RADIUS = nautical_miles(2) BASE_DEFENSE_RADIUS = nautical_miles(2)
def __init__(self, miz: Path, theater: ConflictTheater) -> None: def __init__(self, miz: Path, theater: ConflictTheater) -> None:
@@ -156,7 +161,8 @@ class MizCampaignLoader:
def country(self, blue: bool) -> Country: def country(self, blue: bool) -> Country:
country = self.mission.country( country = self.mission.country(
self.BLUE_COUNTRY.name if blue else self.RED_COUNTRY.name) self.BLUE_COUNTRY.name if blue else self.RED_COUNTRY.name
)
# Should be guaranteed because we initialized them. # Should be guaranteed because we initialized them.
assert country assert country
return country return country
@@ -183,7 +189,7 @@ class MizCampaignLoader:
for group in self.country(blue).ship_group: for group in self.country(blue).ship_group:
if group.units[0].type == self.LHA_UNIT_TYPE: if group.units[0].type == self.LHA_UNIT_TYPE:
yield group yield group
def fobs(self, blue: bool) -> Iterator[VehicleGroup]: def fobs(self, blue: bool) -> Iterator[VehicleGroup]:
for group in self.country(blue).vehicle_group: for group in self.country(blue).vehicle_group:
if group.units[0].type == self.FOB_UNIT_TYPE: if group.units[0].type == self.FOB_UNIT_TYPE:
@@ -243,6 +249,18 @@ class MizCampaignLoader:
if group.units[0].type in self.REQUIRED_MEDIUM_RANGE_SAM_UNIT_TYPES: if group.units[0].type in self.REQUIRED_MEDIUM_RANGE_SAM_UNIT_TYPES:
yield group yield group
@property
def required_ewrs(self) -> Iterator[VehicleGroup]:
for group in self.red.vehicle_group:
if group.units[0].type in self.REQUIRED_EWR_UNIT_TYPE:
yield group
@property
def helipads(self) -> Iterator[StaticGroup]:
for group in self.blue.static_group:
if group.units[0].type == self.FARP_HELIPAD:
yield group
@cached_property @cached_property
def control_points(self) -> Dict[int, ControlPoint]: def control_points(self) -> Dict[int, ControlPoint]:
control_points = {} control_points = {}
@@ -253,22 +271,23 @@ class MizCampaignLoader:
for blue in (False, True): for blue in (False, True):
for group in self.off_map_spawns(blue): for group in self.off_map_spawns(blue):
control_point = OffMapSpawn(next(self.control_point_id), control_point = OffMapSpawn(
str(group.name), group.position) next(self.control_point_id), str(group.name), group.position
)
control_point.captured = blue control_point.captured = blue
control_point.captured_invert = group.late_activation control_point.captured_invert = group.late_activation
control_points[control_point.id] = control_point control_points[control_point.id] = control_point
for group in self.carriers(blue): for group in self.carriers(blue):
# TODO: Name the carrier. # TODO: Name the carrier.
control_point = Carrier( control_point = Carrier(
"carrier", group.position, next(self.control_point_id)) "carrier", group.position, next(self.control_point_id)
)
control_point.captured = blue control_point.captured = blue
control_point.captured_invert = group.late_activation control_point.captured_invert = group.late_activation
control_points[control_point.id] = control_point control_points[control_point.id] = control_point
for group in self.lhas(blue): for group in self.lhas(blue):
# TODO: Name the LHA. # TODO: Name the LHA.db
control_point = Lha( control_point = Lha("lha", group.position, next(self.control_point_id))
"lha", group.position, next(self.control_point_id))
control_point.captured = blue control_point.captured = blue
control_point.captured_invert = group.late_activation control_point.captured_invert = group.late_activation
control_points[control_point.id] = control_point control_points[control_point.id] = control_point
@@ -297,24 +316,24 @@ class MizCampaignLoader:
# final waypoint at the destination CP. Intermediate waypoints # final waypoint at the destination CP. Intermediate waypoints
# define the curve of the front line. # define the curve of the front line.
waypoints = [p.position for p in group.points] waypoints = [p.position for p in group.points]
origin = self.theater.closest_control_point(waypoints[0]) origin = self.theater.closest_control_point(waypoints[0])
if origin is None: if origin is None:
raise RuntimeError( raise RuntimeError(
f"No control point near the first waypoint of {group.name}") f"No control point near the first waypoint of {group.name}"
)
destination = self.theater.closest_control_point(waypoints[-1]) destination = self.theater.closest_control_point(waypoints[-1])
if destination is None: if destination is None:
raise RuntimeError( raise RuntimeError(
f"No control point near the final waypoint of {group.name}") f"No control point near the final waypoint of {group.name}"
)
# Snap the begin and end points to the control points. # Snap the begin and end points to the control points.
waypoints[0] = origin.position waypoints[0] = origin.position
waypoints[-1] = destination.position waypoints[-1] = destination.position
front_line_id = f"{origin.id}|{destination.id}" front_line_id = f"{origin.id}|{destination.id}"
front_lines[front_line_id] = ComplexFrontLine(origin, waypoints) front_lines[front_line_id] = ComplexFrontLine(origin, waypoints)
self.control_points[origin.id].connect( self.control_points[origin.id].connect(self.control_points[destination.id])
self.control_points[destination.id]) self.control_points[destination.id].connect(self.control_points[origin.id])
self.control_points[destination.id].connect(
self.control_points[origin.id])
return front_lines return front_lines
def objective_info(self, group: Group) -> Tuple[ControlPoint, Distance]: def objective_info(self, group: Group) -> Tuple[ControlPoint, Distance]:
@@ -326,49 +345,80 @@ class MizCampaignLoader:
for group in self.garrisons: for group in self.garrisons:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
if distance < self.BASE_DEFENSE_RADIUS: if distance < self.BASE_DEFENSE_RADIUS:
closest.preset_locations.base_garrisons.append(group.position) closest.preset_locations.base_garrisons.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
else: else:
logging.warning( logging.warning(f"Found garrison unit too far from base: {group.name}")
f"Found garrison unit too far from base: {group.name}")
for group in self.sams: for group in self.sams:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
if distance < self.BASE_DEFENSE_RADIUS: if distance < self.BASE_DEFENSE_RADIUS:
closest.preset_locations.base_air_defense.append(group.position) closest.preset_locations.base_air_defense.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
else: else:
closest.preset_locations.strike_locations.append(group.position) closest.preset_locations.strike_locations.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.ewrs: for group in self.ewrs:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.ewrs.append(group.position) if distance < self.BASE_DEFENSE_RADIUS:
closest.preset_locations.base_ewrs.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
else:
closest.preset_locations.ewrs.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.offshore_strike_targets: for group in self.offshore_strike_targets:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.offshore_strike_locations.append( closest.preset_locations.offshore_strike_locations.append(
group.position) PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.ships: for group in self.ships:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.ships.append(group.position) closest.preset_locations.ships.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.missile_sites: for group in self.missile_sites:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.missile_sites.append(group.position) closest.preset_locations.missile_sites.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.coastal_defenses: for group in self.coastal_defenses:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.coastal_defenses.append(group.position) closest.preset_locations.coastal_defenses.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.required_long_range_sams: for group in self.required_long_range_sams:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.required_long_range_sams.append( closest.preset_locations.required_long_range_sams.append(
group.position PointWithHeading.from_point(group.position, group.units[0].heading)
) )
for group in self.required_medium_range_sams: for group in self.required_medium_range_sams:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.required_medium_range_sams.append( closest.preset_locations.required_medium_range_sams.append(
group.position PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.required_ewrs:
closest, distance = self.objective_info(group)
closest.preset_locations.required_ewrs.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.helipads:
closest, distance = self.objective_info(group)
closest.helipads.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
) )
def populate_theater(self) -> None: def populate_theater(self) -> None:
@@ -423,8 +473,9 @@ class ConflictTheater:
logging.warning("Replacing existing frontline data") logging.warning("Replacing existing frontline data")
self._frontline_data = data self._frontline_data = data
def add_controlpoint(self, point: ControlPoint, def add_controlpoint(
connected_to: Optional[List[ControlPoint]] = None): self, point: ControlPoint, connected_to: Optional[List[ControlPoint]] = None
):
if connected_to is None: if connected_to is None:
connected_to = [] connected_to = []
for connected_point in connected_to: for connected_point in connected_to:
@@ -486,8 +537,8 @@ class ConflictTheater:
for inclusion_zone in self.landmap.inclusion_zones: for inclusion_zone in self.landmap.inclusion_zones:
nearest_pair = ops.nearest_points(point, inclusion_zone) nearest_pair = ops.nearest_points(point, inclusion_zone)
nearest_points.append(nearest_pair[1]) nearest_points.append(nearest_pair[1])
min_distance = point.distance(nearest_points[0]) # type: geometry.Point min_distance = point.distance(nearest_points[0]) # type: geometry.Point
nearest_point = nearest_points[0] # type: geometry.Point nearest_point = nearest_points[0] # type: geometry.Point
for pt in nearest_points[1:]: for pt in nearest_points[1:]:
distance = point.distance(pt) distance = point.distance(pt)
if distance < min_distance: if distance < min_distance:
@@ -498,7 +549,7 @@ class ConflictTheater:
nearest_point = Point(nearest_point.x, nearest_point.y) nearest_point = Point(nearest_point.x, nearest_point.y)
new_point = point.point_from_heading( new_point = point.point_from_heading(
point.heading_between_point(nearest_point), point.heading_between_point(nearest_point),
point.distance_to_point(nearest_point) + extend_dist point.distance_to_point(nearest_point) + extend_dist,
) )
return new_point return new_point
@@ -512,7 +563,9 @@ class ConflictTheater:
def conflicts(self, from_player=True) -> Iterator[FrontLine]: def conflicts(self, from_player=True) -> Iterator[FrontLine]:
for cp in [x for x in self.controlpoints if x.captured == from_player]: for cp in [x for x in self.controlpoints if x.captured == from_player]:
for connected_point in [x for x in cp.connected_points if x.captured != from_player]: for connected_point in [
x for x in cp.connected_points if x.captured != from_player
]:
yield FrontLine(cp, connected_point, self) yield FrontLine(cp, connected_point, self)
def enemy_points(self) -> List[ControlPoint]: def enemy_points(self) -> List[ControlPoint]:
@@ -547,7 +600,7 @@ class ConflictTheater:
closest = conflict closest = conflict
closest_distance = distance closest_distance = distance
return closest return closest
def closest_opposing_control_points(self) -> Tuple[ControlPoint, ControlPoint]: def closest_opposing_control_points(self) -> Tuple[ControlPoint, ControlPoint]:
""" """
Returns a tuple of the two nearest opposing ControlPoints in theater. Returns a tuple of the two nearest opposing ControlPoints in theater.
@@ -567,17 +620,22 @@ class ConflictTheater:
distances[cp.id] = dist distances[cp.id] = dist
closest_cp_id = min(distances, key=distances.get) # type: ignore closest_cp_id = min(distances, key=distances.get) # type: ignore
all_cp_min_distances[(control_point.id, closest_cp_id)] = distances[closest_cp_id] all_cp_min_distances[(control_point.id, closest_cp_id)] = distances[
closest_cp_id
]
closest_opposing_cps = [ closest_opposing_cps = [
self.find_control_point_by_id(i) self.find_control_point_by_id(i)
for i for i in min(
in min(all_cp_min_distances, key=all_cp_min_distances.get) # type: ignore all_cp_min_distances, key=all_cp_min_distances.get
] # type: List[ControlPoint] ) # type: ignore
] # type: List[ControlPoint]
assert len(closest_opposing_cps) == 2 assert len(closest_opposing_cps) == 2
if closest_opposing_cps[0].captured: if closest_opposing_cps[0].captured:
return cast(Tuple[ControlPoint, ControlPoint], tuple(closest_opposing_cps)) return cast(Tuple[ControlPoint, ControlPoint], tuple(closest_opposing_cps))
else: else:
return cast(Tuple[ControlPoint, ControlPoint], tuple(reversed(closest_opposing_cps))) return cast(
Tuple[ControlPoint, ControlPoint], tuple(reversed(closest_opposing_cps))
)
def find_control_point_by_id(self, id: int) -> ControlPoint: def find_control_point_by_id(self, id: int) -> ControlPoint:
for i in self.controlpoints: for i in self.controlpoints:
@@ -613,7 +671,7 @@ class ConflictTheater:
cp.captured_invert = False cp.captured_invert = False
return cp return cp
@staticmethod @staticmethod
def from_json(directory: Path, data: Dict[str, Any]) -> ConflictTheater: def from_json(directory: Path, data: Dict[str, Any]) -> ConflictTheater:
theaters = { theaters = {
@@ -649,7 +707,7 @@ class ConflictTheater:
cps[l[1]].connect(cps[l[0]]) cps[l[1]].connect(cps[l[0]])
return t return t
class CaucasusTheater(ConflictTheater): class CaucasusTheater(ConflictTheater):
terrain = caucasus.Caucasus() terrain = caucasus.Caucasus()
@@ -672,9 +730,8 @@ class PersianGulfTheater(ConflictTheater):
terrain = persiangulf.PersianGulf() terrain = persiangulf.PersianGulf()
overview_image = "persiangulf.gif" overview_image = "persiangulf.gif"
reference_points = ( reference_points = (
ReferencePoint(persiangulf.Jiroft_Airport.position, ReferencePoint(persiangulf.Jiroft.position, Point(1692, 1343)),
Point(1692, 1343)), ReferencePoint(persiangulf.Liwa_AFB.position, Point(358, 3238)),
ReferencePoint(persiangulf.Liwa_Airbase.position, Point(358, 3238)),
) )
landmap = load_landmap("resources\\gulflandmap.p") landmap = load_landmap("resources\\gulflandmap.p")
daytime_map = { daytime_map = {
@@ -722,7 +779,7 @@ class TheChannelTheater(ConflictTheater):
overview_image = "thechannel.gif" overview_image = "thechannel.gif"
reference_points = ( reference_points = (
ReferencePoint(thechannel.Abbeville_Drucat.position, Point(2005, 2390)), ReferencePoint(thechannel.Abbeville_Drucat.position, Point(2005, 2390)),
ReferencePoint(thechannel.Detling.position, Point(706, 382)) ReferencePoint(thechannel.Detling.position, Point(706, 382)),
) )
landmap = load_landmap("resources\\channellandmap.p") landmap = load_landmap("resources\\channellandmap.p")
daytime_map = { daytime_map = {
@@ -791,7 +848,7 @@ class FrontLine(MissionTarget):
self, self,
control_point_a: ControlPoint, control_point_a: ControlPoint,
control_point_b: ControlPoint, control_point_b: ControlPoint,
theater: ConflictTheater theater: ConflictTheater,
) -> None: ) -> None:
self.control_point_a = control_point_a self.control_point_a = control_point_a
self.control_point_b = control_point_b self.control_point_b = control_point_b
@@ -807,6 +864,7 @@ class FrontLine(MissionTarget):
def mission_types(self, for_player: bool) -> Iterator[FlightType]: def mission_types(self, for_player: bool) -> Iterator[FlightType]:
yield from [ yield from [
FlightType.CAS, FlightType.CAS,
FlightType.AEWC,
# TODO: FlightType.TROOP_TRANSPORT # TODO: FlightType.TROOP_TRANSPORT
# TODO: FlightType.EVAC # TODO: FlightType.EVAC
] ]
@@ -935,10 +993,9 @@ class FrontLine(MissionTarget):
) )
) )
@staticmethod @staticmethod
def load_json_frontlines( def load_json_frontlines(
theater: ConflictTheater theater: ConflictTheater,
) -> Optional[Dict[str, ComplexFrontLine]]: ) -> Optional[Dict[str, ComplexFrontLine]]:
"""Load complex frontlines from json""" """Load complex frontlines from json"""
try: try:

View File

@@ -4,7 +4,6 @@ import heapq
import itertools import itertools
import logging import logging
import random import random
import re
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import Enum from enum import Enum
@@ -28,6 +27,7 @@ from gen.ground_forces.combat_stance import CombatStance
from gen.runways import RunwayAssigner, RunwayData from gen.runways import RunwayAssigner, RunwayData
from .base import Base from .base import Base
from .missiontarget import MissionTarget from .missiontarget import MissionTarget
from game.point_with_heading import PointWithHeading
from .theatergroundobject import ( from .theatergroundobject import (
BaseDefenseGroundObject, BaseDefenseGroundObject,
EwrGroundObject, EwrGroundObject,
@@ -63,6 +63,7 @@ class LocationType(Enum):
BaseAirDefense = "base air defense" BaseAirDefense = "base air defense"
Coastal = "coastal defense" Coastal = "coastal defense"
Ewr = "EWR" Ewr = "EWR"
BaseEwr = "Base EWR"
Garrison = "garrison" Garrison = "garrison"
MissileSite = "missile site" MissileSite = "missile site"
OffshoreStrikeTarget = "offshore strike target" OffshoreStrikeTarget = "offshore strike target"
@@ -77,38 +78,44 @@ class PresetLocations:
"""Defines the preset locations loaded from the campaign mission file.""" """Defines the preset locations loaded from the campaign mission file."""
#: Locations used for spawning ground defenses for bases. #: Locations used for spawning ground defenses for bases.
base_garrisons: List[Point] = field(default_factory=list) base_garrisons: List[PointWithHeading] = field(default_factory=list)
#: Locations used for spawning air defenses for bases. Used by SAMs, AAA, #: Locations used for spawning air defenses for bases. Used by SAMs, AAA,
#: and SHORADs. #: and SHORADs.
base_air_defense: List[Point] = field(default_factory=list) base_air_defense: List[PointWithHeading] = field(default_factory=list)
#: Locations used by EWRs. #: Locations used by EWRs.
ewrs: List[Point] = field(default_factory=list) ewrs: List[PointWithHeading] = field(default_factory=list)
#: Locations used by Base EWRs.
base_ewrs: List[PointWithHeading] = field(default_factory=list)
#: Locations used by non-carrier ships. Carriers and LHAs are not random. #: Locations used by non-carrier ships. Carriers and LHAs are not random.
ships: List[Point] = field(default_factory=list) ships: List[PointWithHeading] = field(default_factory=list)
#: Locations used by coastal defenses. #: Locations used by coastal defenses.
coastal_defenses: List[Point] = field(default_factory=list) coastal_defenses: List[PointWithHeading] = field(default_factory=list)
#: Locations used by ground based strike objectives. #: Locations used by ground based strike objectives.
strike_locations: List[Point] = field(default_factory=list) strike_locations: List[PointWithHeading] = field(default_factory=list)
#: Locations used by offshore strike objectives. #: Locations used by offshore strike objectives.
offshore_strike_locations: List[Point] = field(default_factory=list) offshore_strike_locations: List[PointWithHeading] = field(default_factory=list)
#: Locations used by missile sites like scuds and V-2s. #: Locations used by missile sites like scuds and V-2s.
missile_sites: List[Point] = field(default_factory=list) missile_sites: List[PointWithHeading] = field(default_factory=list)
#: Locations of long range SAMs which should always be spawned. #: Locations of long range SAMs which should always be spawned.
required_long_range_sams: List[Point] = field(default_factory=list) required_long_range_sams: List[PointWithHeading] = field(default_factory=list)
#: Locations of medium range SAMs which should always be spawned. #: Locations of medium range SAMs which should always be spawned.
required_medium_range_sams: List[Point] = field(default_factory=list) required_medium_range_sams: List[PointWithHeading] = field(default_factory=list)
#: Locations of EWRs which should always be spawned.
required_ewrs: List[PointWithHeading] = field(default_factory=list)
@staticmethod @staticmethod
def _random_from(points: List[Point]) -> Optional[Point]: def _random_from(points: List[PointWithHeading]) -> Optional[PointWithHeading]:
"""Finds, removes, and returns a random position from the given list.""" """Finds, removes, and returns a random position from the given list."""
if not points: if not points:
return None return None
@@ -116,7 +123,7 @@ class PresetLocations:
points.remove(point) points.remove(point)
return point return point
def random_for(self, location_type: LocationType) -> Optional[Point]: def random_for(self, location_type: LocationType) -> Optional[PointWithHeading]:
"""Returns a position suitable for the given location type. """Returns a position suitable for the given location type.
The location, if found, will be claimed by the caller and not available The location, if found, will be claimed by the caller and not available
@@ -128,6 +135,8 @@ class PresetLocations:
return self._random_from(self.coastal_defenses) return self._random_from(self.coastal_defenses)
if location_type == LocationType.Ewr: if location_type == LocationType.Ewr:
return self._random_from(self.ewrs) return self._random_from(self.ewrs)
if location_type == LocationType.BaseEwr:
return self._random_from(self.base_ewrs)
if location_type == LocationType.Garrison: if location_type == LocationType.Garrison:
return self._random_from(self.base_garrisons) return self._random_from(self.base_garrisons)
if location_type == LocationType.MissileSite: if location_type == LocationType.MissileSite:
@@ -231,10 +240,17 @@ class ControlPoint(MissionTarget, ABC):
# TODO: Only airbases have IDs. # TODO: Only airbases have IDs.
# TODO: has_frontline is only reasonable for airbases. # TODO: has_frontline is only reasonable for airbases.
# TODO: cptype is obsolete. # TODO: cptype is obsolete.
def __init__(self, cp_id: int, name: str, position: Point, def __init__(
at: db.StartingPosition, size: int, self,
importance: float, has_frontline=True, cp_id: int,
cptype=ControlPointType.AIRBASE): name: str,
position: Point,
at: db.StartingPosition,
size: int,
importance: float,
has_frontline=True,
cptype=ControlPointType.AIRBASE,
):
super().__init__(name, position) super().__init__(name, position)
# TODO: Should be Airbase specific. # TODO: Should be Airbase specific.
self.id = cp_id self.id = cp_id
@@ -243,6 +259,7 @@ class ControlPoint(MissionTarget, ABC):
self.connected_objectives: List[TheaterGroundObject] = [] self.connected_objectives: List[TheaterGroundObject] = []
self.base_defenses: List[BaseDefenseGroundObject] = [] self.base_defenses: List[BaseDefenseGroundObject] = []
self.preset_locations = PresetLocations() self.preset_locations = PresetLocations()
self.helipads: List[PointWithHeading] = []
# TODO: Should be Airbase specific. # TODO: Should be Airbase specific.
self.size = size self.size = size
@@ -257,17 +274,17 @@ class ControlPoint(MissionTarget, ABC):
# TODO: Should be Airbase specific. # TODO: Should be Airbase specific.
self.stances: Dict[int, CombatStance] = {} self.stances: Dict[int, CombatStance] = {}
from ..event import UnitsDeliveryEvent from ..event import UnitsDeliveryEvent
self.pending_unit_deliveries = UnitsDeliveryEvent(self) self.pending_unit_deliveries = UnitsDeliveryEvent(self)
self.target_position: Optional[Point] = None self.target_position: Optional[Point] = None
def __repr__(self): def __repr__(self):
return f"<{__class__}: {self.name}>" return f"<{__class__}: {self.name}>"
@property @property
def ground_objects(self) -> List[TheaterGroundObject]: def ground_objects(self) -> List[TheaterGroundObject]:
return list( return list(itertools.chain(self.connected_objectives, self.base_defenses))
itertools.chain(self.connected_objectives, self.base_defenses))
@property @property
@abstractmethod @abstractmethod
@@ -342,15 +359,18 @@ class ControlPoint(MissionTarget, ABC):
Get the carrier group name if the airbase is a carrier Get the carrier group name if the airbase is a carrier
:return: Carrier group name :return: Carrier group name
""" """
if self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, if self.cptype in [
ControlPointType.LHA_GROUP]: ControlPointType.AIRCRAFT_CARRIER_GROUP,
ControlPointType.LHA_GROUP,
]:
for g in self.ground_objects: for g in self.ground_objects:
if g.dcs_identifier == "CARRIER": if g.dcs_identifier == "CARRIER":
for group in g.groups: for group in g.groups:
for u in group.units: for u in group.units:
if db.unit_type_from_name(u.type) in [ if db.unit_type_from_name(u.type) in [
CVN_74_John_C__Stennis, CVN_74_John_C__Stennis,
CV_1143_5_Admiral_Kuznetsov]: CV_1143_5_Admiral_Kuznetsov,
]:
return group.name return group.name
elif g.dcs_identifier == "LHA": elif g.dcs_identifier == "LHA":
for group in g.groups: for group in g.groups:
@@ -376,20 +396,19 @@ class ControlPoint(MissionTarget, ABC):
# TODO: Should be Airbase specific. # TODO: Should be Airbase specific.
def clear_base_defenses(self) -> None: def clear_base_defenses(self) -> None:
for base_defense in self.base_defenses: for base_defense in self.base_defenses:
p = PointWithHeading.from_point(base_defense.position, base_defense.heading)
if isinstance(base_defense, EwrGroundObject): if isinstance(base_defense, EwrGroundObject):
self.preset_locations.ewrs.append(base_defense.position) self.preset_locations.base_ewrs.append(p)
elif isinstance(base_defense, SamGroundObject): elif isinstance(base_defense, SamGroundObject):
self.preset_locations.base_air_defense.append( self.preset_locations.base_air_defense.append(p)
base_defense.position)
elif isinstance(base_defense, VehicleGroupGroundObject): elif isinstance(base_defense, VehicleGroupGroundObject):
self.preset_locations.base_garrisons.append( self.preset_locations.base_garrisons.append(p)
base_defense.position)
else: else:
logging.error( logging.error(
"Could not determine preset location type for " "Could not determine preset location type for "
f"{base_defense}. Assuming garrison type.") f"{base_defense}. Assuming garrison type."
self.preset_locations.base_garrisons.append( )
base_defense.position) self.preset_locations.base_garrisons.append(p)
self.base_defenses = [] self.base_defenses = []
def capture_equipment(self, game: Game) -> None: def capture_equipment(self, game: Game) -> None:
@@ -398,15 +417,18 @@ class ControlPoint(MissionTarget, ABC):
game.adjust_budget(total, player=not self.captured) game.adjust_budget(total, player=not self.captured)
game.message( game.message(
f"{self.name} is not connected to any friendly points. Ground " f"{self.name} is not connected to any friendly points. Ground "
f"vehicles have been captured and sold for ${total}M.") f"vehicles have been captured and sold for ${total}M."
)
def retreat_ground_units(self, game: Game): def retreat_ground_units(self, game: Game):
# When there are multiple valid destinations, deliver units to whichever # When there are multiple valid destinations, deliver units to whichever
# base is least defended first. The closest approximation of unit # base is least defended first. The closest approximation of unit
# strength we have is price # strength we have is price
destinations = [GroundUnitDestination(cp) destinations = [
for cp in self.connected_points GroundUnitDestination(cp)
if cp.captured == self.captured] for cp in self.connected_points
if cp.captured == self.captured
]
if not destinations: if not destinations:
self.capture_equipment(game) self.capture_equipment(game)
return return
@@ -419,8 +441,9 @@ class ControlPoint(MissionTarget, ABC):
destination.control_point.base.commision_units({unit_type: 1}) destination.control_point.base.commision_units({unit_type: 1})
destination = heapq.heappushpop(destinations, destination) destination = heapq.heappushpop(destinations, destination)
def capture_aircraft(self, game: Game, airframe: Type[FlyingType], def capture_aircraft(
count: int) -> None: self, game: Game, airframe: Type[FlyingType], count: int
) -> None:
try: try:
value = PRICES[airframe] * count value = PRICES[airframe] * count
except KeyError: except KeyError:
@@ -431,11 +454,12 @@ class ControlPoint(MissionTarget, ABC):
game.message( game.message(
f"No valid retreat destination in range of {self.name} for " f"No valid retreat destination in range of {self.name} for "
f"{airframe.id}. {count} aircraft have been captured and sold for " f"{airframe.id}. {count} aircraft have been captured and sold for "
f"${value}M.") f"${value}M."
)
def aircraft_retreat_destination( def aircraft_retreat_destination(
self, game: Game, self, game: Game, airframe: Type[FlyingType]
airframe: Type[FlyingType]) -> Optional[ControlPoint]: ) -> Optional[ControlPoint]:
closest = ObjectiveDistanceCache.get_closest_airfields(self) closest = ObjectiveDistanceCache.get_closest_airfields(self)
# TODO: Should be airframe dependent. # TODO: Should be airframe dependent.
max_retreat_distance = nautical_miles(200) max_retreat_distance = nautical_miles(200)
@@ -451,8 +475,9 @@ class ControlPoint(MissionTarget, ABC):
return airbase return airbase
return None return None
def _retreat_air_units(self, game: Game, airframe: Type[FlyingType], def _retreat_air_units(
count: int) -> None: self, game: Game, airframe: Type[FlyingType], count: int
) -> None:
while count: while count:
logging.debug(f"Retreating {count} {airframe.id} from {self.name}") logging.debug(f"Retreating {count} {airframe.id} from {self.name}")
destination = self.aircraft_retreat_destination(game, airframe) destination = self.aircraft_retreat_destination(game, airframe)
@@ -485,6 +510,7 @@ class ControlPoint(MissionTarget, ABC):
self.clear_base_defenses() self.clear_base_defenses()
from .start_generator import BaseDefenseGenerator from .start_generator import BaseDefenseGenerator
BaseDefenseGenerator(game, self).generate() BaseDefenseGenerator(game, self).generate()
@abstractmethod @abstractmethod
@@ -514,16 +540,19 @@ class ControlPoint(MissionTarget, ABC):
if issubclass(unit_bought, FlyingType): if issubclass(unit_bought, FlyingType):
on_order += self.pending_unit_deliveries.units[unit_bought] on_order += self.pending_unit_deliveries.units[unit_bought]
return PendingOccupancy(self.base.total_aircraft, on_order, return PendingOccupancy(
self.aircraft_transferring(game)) self.base.total_aircraft, on_order, self.aircraft_transferring(game)
)
def unclaimed_parking(self, game: Game) -> int: def unclaimed_parking(self, game: Game) -> int:
return (self.total_aircraft_parking - return (
self.expected_aircraft_next_turn(game).total) self.total_aircraft_parking - self.expected_aircraft_next_turn(game).total
)
@abstractmethod @abstractmethod
def active_runway(self, conditions: Conditions, def active_runway(
dynamic_runways: Dict[str, RunwayData]) -> RunwayData: self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData]
) -> RunwayData:
... ...
@property @property
@@ -574,7 +603,13 @@ class ControlPoint(MissionTarget, ABC):
Get number of pending frontline aa units Get number of pending frontline aa units
""" """
if self.pending_unit_deliveries: if self.pending_unit_deliveries:
return sum([v for k,v in self.pending_unit_deliveries.units.items() if k in TYPE_SHORAD]) return sum(
[
v
for k, v in self.pending_unit_deliveries.units.items()
if k in TYPE_SHORAD
]
)
else: else:
return 0 return 0
@@ -598,22 +633,45 @@ class ControlPoint(MissionTarget, ABC):
continue continue
on_order += self.pending_unit_deliveries.units[unit_bought] on_order += self.pending_unit_deliveries.units[unit_bought]
return PendingOccupancy(self.base.total_armor, on_order, return PendingOccupancy(
# Ground unit transfers not yet implemented. self.base.total_armor,
transferring=0) on_order,
# Ground unit transfers not yet implemented.
transferring=0,
)
@property @property
def income_per_turn(self) -> int: def income_per_turn(self) -> int:
return 0 return 0
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType
if self.is_friendly(for_player):
yield from [
FlightType.AEWC,
]
yield from super().mission_types(for_player)
@property
def has_active_frontline(self) -> bool:
return any(not c.is_friendly(self.captured) for c in self.connected_points)
class Airfield(ControlPoint): class Airfield(ControlPoint):
def __init__(
def __init__(self, airport: Airport, size: int, self, airport: Airport, size: int, importance: float, has_frontline=True
importance: float, has_frontline=True): ):
super().__init__(airport.id, airport.name, airport.position, airport, super().__init__(
size, importance, has_frontline, airport.id,
cptype=ControlPointType.AIRBASE) airport.name,
airport.position,
airport,
size,
importance,
has_frontline,
cptype=ControlPointType.AIRBASE,
)
self.airport = airport self.airport = airport
self._runway_status = RunwayStatus() self._runway_status = RunwayStatus()
@@ -627,6 +685,7 @@ class Airfield(ControlPoint):
def mission_types(self, for_player: bool) -> Iterator[FlightType]: def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
if self.is_friendly(for_player): if self.is_friendly(for_player):
yield from [ yield from [
# TODO: FlightType.INTERCEPTION # TODO: FlightType.INTERCEPTION
@@ -657,8 +716,9 @@ class Airfield(ControlPoint):
def damage_runway(self) -> None: def damage_runway(self) -> None:
self.runway_status.damage() self.runway_status.damage()
def active_runway(self, conditions: Conditions, def active_runway(
dynamic_runways: Dict[str, RunwayData]) -> RunwayData: self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData]
) -> RunwayData:
assigner = RunwayAssigner(conditions) assigner = RunwayAssigner(conditions)
return assigner.get_preferred_runway(self.airport) return assigner.get_preferred_runway(self.airport)
@@ -676,13 +736,13 @@ class Airfield(ControlPoint):
class NavalControlPoint(ControlPoint, ABC): class NavalControlPoint(ControlPoint, ABC):
@property @property
def is_fleet(self) -> bool: def is_fleet(self) -> bool:
return True return True
def mission_types(self, for_player: bool) -> Iterator[FlightType]: def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
if self.is_friendly(for_player): if self.is_friendly(for_player):
yield from [ yield from [
# TODO: FlightType.INTERCEPTION # TODO: FlightType.INTERCEPTION
@@ -706,14 +766,17 @@ class NavalControlPoint(ControlPoint, ABC):
for group in g.groups: for group in g.groups:
for u in group.units: for u in group.units:
if db.unit_type_from_name(u.type) in [ if db.unit_type_from_name(u.type) in [
CVN_74_John_C__Stennis, LHA_1_Tarawa, CVN_74_John_C__Stennis,
CV_1143_5_Admiral_Kuznetsov, LHA_1_Tarawa,
Type_071_Amphibious_Transport_Dock]: CV_1143_5_Admiral_Kuznetsov,
Type_071_Amphibious_Transport_Dock,
]:
return True return True
return False return False
def active_runway(self, conditions: Conditions, def active_runway(
dynamic_runways: Dict[str, RunwayData]) -> RunwayData: self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData]
) -> RunwayData:
# TODO: Assign TACAN and ICLS earlier so we don't need this. # TODO: Assign TACAN and ICLS earlier so we don't need this.
fallback = RunwayData(self.full_name, runway_heading=0, runway_name="") fallback = RunwayData(self.full_name, runway_heading=0, runway_name="")
return dynamic_runways.get(self.name, fallback) return dynamic_runways.get(self.name, fallback)
@@ -736,12 +799,19 @@ class NavalControlPoint(ControlPoint, ABC):
class Carrier(NavalControlPoint): class Carrier(NavalControlPoint):
def __init__(self, name: str, at: Point, cp_id: int): def __init__(self, name: str, at: Point, cp_id: int):
import game.theater.conflicttheater import game.theater.conflicttheater
super().__init__(cp_id, name, at, at,
game.theater.conflicttheater.SIZE_SMALL, 1, super().__init__(
has_frontline=False, cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP) cp_id,
name,
at,
at,
game.theater.conflicttheater.SIZE_SMALL,
1,
has_frontline=False,
cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP,
)
def capture(self, game: Game, for_player: bool) -> None: def capture(self, game: Game, for_player: bool) -> None:
raise RuntimeError("Carriers cannot be captured") raise RuntimeError("Carriers cannot be captured")
@@ -759,12 +829,19 @@ class Carrier(NavalControlPoint):
class Lha(NavalControlPoint): class Lha(NavalControlPoint):
def __init__(self, name: str, at: Point, cp_id: int): def __init__(self, name: str, at: Point, cp_id: int):
import game.theater.conflicttheater import game.theater.conflicttheater
super().__init__(cp_id, name, at, at,
game.theater.conflicttheater.SIZE_SMALL, 1, super().__init__(
has_frontline=False, cptype=ControlPointType.LHA_GROUP) cp_id,
name,
at,
at,
game.theater.conflicttheater.SIZE_SMALL,
1,
has_frontline=False,
cptype=ControlPointType.LHA_GROUP,
)
def capture(self, game: Game, for_player: bool) -> None: def capture(self, game: Game, for_player: bool) -> None:
raise RuntimeError("LHAs cannot be captured") raise RuntimeError("LHAs cannot be captured")
@@ -782,15 +859,22 @@ class Lha(NavalControlPoint):
class OffMapSpawn(ControlPoint): class OffMapSpawn(ControlPoint):
def runway_is_operational(self) -> bool: def runway_is_operational(self) -> bool:
return True return True
def __init__(self, cp_id: int, name: str, position: Point): def __init__(self, cp_id: int, name: str, position: Point):
from . import IMPORTANCE_MEDIUM, SIZE_REGULAR from . import IMPORTANCE_MEDIUM, SIZE_REGULAR
super().__init__(cp_id, name, position, at=position,
size=SIZE_REGULAR, importance=IMPORTANCE_MEDIUM, super().__init__(
has_frontline=False, cptype=ControlPointType.OFF_MAP) cp_id,
name,
position,
at=position,
size=SIZE_REGULAR,
importance=IMPORTANCE_MEDIUM,
has_frontline=False,
cptype=ControlPointType.OFF_MAP,
)
def capture(self, game: Game, for_player: bool) -> None: def capture(self, game: Game, for_player: bool) -> None:
raise RuntimeError("Off map control points cannot be captured") raise RuntimeError("Off map control points cannot be captured")
@@ -809,8 +893,9 @@ class OffMapSpawn(ControlPoint):
def heading(self) -> int: def heading(self) -> int:
return 0 return 0
def active_runway(self, conditions: Conditions, def active_runway(
dynamic_runways: Dict[str, RunwayData]) -> RunwayData: self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData]
) -> RunwayData:
logging.warning("TODO: Off map spawns have no runways.") logging.warning("TODO: Off map spawns have no runways.")
return RunwayData(self.full_name, runway_heading=0, runway_name="") return RunwayData(self.full_name, runway_heading=0, runway_name="")
@@ -824,19 +909,27 @@ class OffMapSpawn(ControlPoint):
class Fob(ControlPoint): class Fob(ControlPoint):
def __init__(self, name: str, at: Point, cp_id: int): def __init__(self, name: str, at: Point, cp_id: int):
import game.theater.conflicttheater import game.theater.conflicttheater
super().__init__(cp_id, name, at, at,
game.theater.conflicttheater.SIZE_SMALL, 1, super().__init__(
has_frontline=True, cptype=ControlPointType.FOB) cp_id,
name,
at,
at,
game.theater.conflicttheater.SIZE_SMALL,
1,
has_frontline=True,
cptype=ControlPointType.FOB,
)
self.name = name self.name = name
def runway_is_operational(self) -> bool: def runway_is_operational(self) -> bool:
return False return False
def active_runway(self, conditions: Conditions, def active_runway(
dynamic_runways: Dict[str, RunwayData]) -> RunwayData: self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData]
) -> RunwayData:
logging.warning("TODO: FOBs have no runways.") logging.warning("TODO: FOBs have no runways.")
return RunwayData(self.full_name, runway_heading=0, runway_name="") return RunwayData(self.full_name, runway_heading=0, runway_name="")
@@ -846,6 +939,7 @@ class Fob(ControlPoint):
def mission_types(self, for_player: bool) -> Iterator[FlightType]: def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
if self.is_friendly(for_player): if self.is_friendly(for_player):
yield from [ yield from [
FlightType.BARCAP, FlightType.BARCAP,

View File

@@ -46,4 +46,3 @@ def poly_centroid(poly) -> Tuple[float, float]:
x = sum(x_list) / len(poly) x = sum(x_list) / len(poly)
y = sum(y_list) / len(poly) y = sum(y_list) / len(poly)
return (x, y) return (x, y)

View File

@@ -29,6 +29,7 @@ class MissionTarget:
def mission_types(self, for_player: bool) -> Iterator[FlightType]: def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
if self.is_friendly(for_player): if self.is_friendly(for_player):
yield FlightType.BARCAP yield FlightType.BARCAP
else: else:

View File

@@ -13,7 +13,7 @@ from dcs.vehicles import AirDefence
from game import Game, db from game import Game, db
from game.factions.faction import Faction from game.factions.faction import Faction
from game.theater import Carrier, Lha, LocationType from game.theater import Carrier, Lha, LocationType, PointWithHeading
from game.theater.theatergroundobject import ( from game.theater.theatergroundobject import (
BuildingGroundObject, BuildingGroundObject,
CarrierGroundObject, CarrierGroundObject,
@@ -23,9 +23,11 @@ from game.theater.theatergroundobject import (
SamGroundObject, SamGroundObject,
ShipGroundObject, ShipGroundObject,
VehicleGroupGroundObject, VehicleGroupGroundObject,
CoastalSiteGroundObject,
) )
from game.version import VERSION from game.version import VERSION
from gen import namegen from gen import namegen
from gen.coastal.coastal_group_generator import generate_coastal_group
from gen.defenses.armor_group_generator import generate_armor_group from gen.defenses.armor_group_generator import generate_armor_group
from gen.fleet.ship_group_generator import ( from gen.fleet.ship_group_generator import (
generate_carrier_group, generate_carrier_group,
@@ -35,10 +37,8 @@ from gen.fleet.ship_group_generator import (
from gen.locations.preset_location_finder import MizDataLocationFinder from gen.locations.preset_location_finder import MizDataLocationFinder
from gen.missiles.missiles_group_generator import generate_missile_group from gen.missiles.missiles_group_generator import generate_missile_group
from gen.sam.airdefensegroupgenerator import AirDefenseRange from gen.sam.airdefensegroupgenerator import AirDefenseRange
from gen.sam.sam_group_generator import ( from gen.sam.sam_group_generator import generate_anti_air_group
generate_anti_air_group, from gen.sam.ewr_group_generator import generate_ewr_group
generate_ewr_group,
)
from . import ( from . import (
ConflictTheater, ConflictTheater,
ControlPoint, ControlPoint,
@@ -76,9 +76,14 @@ class GeneratorSettings:
class GameGenerator: class GameGenerator:
def __init__(self, player: str, enemy: str, theater: ConflictTheater, def __init__(
settings: Settings, self,
generator_settings: GeneratorSettings) -> None: player: str,
enemy: str,
theater: ConflictTheater,
settings: Settings,
generator_settings: GeneratorSettings,
) -> None:
self.player = player self.player = player
self.enemy = enemy self.enemy = enemy
self.theater = theater self.theater = theater
@@ -96,7 +101,7 @@ class GameGenerator:
start_date=self.generator_settings.start_date, start_date=self.generator_settings.start_date,
settings=self.settings, settings=self.settings,
player_budget=self.generator_settings.player_budget, player_budget=self.generator_settings.player_budget,
enemy_budget=self.generator_settings.enemy_budget enemy_budget=self.generator_settings.enemy_budget,
) )
GroundObjectGenerator(game, self.generator_settings).generate() GroundObjectGenerator(game, self.generator_settings).generate()
@@ -108,7 +113,7 @@ class GameGenerator:
# Auto-capture half the bases if midgame. # Auto-capture half the bases if midgame.
if self.generator_settings.midgame: if self.generator_settings.midgame:
control_points = self.theater.controlpoints control_points = self.theater.controlpoints
for control_point in control_points[:len(control_points) // 2]: for control_point in control_points[: len(control_points) // 2]:
control_point.captured = True control_point.captured = True
# Remove carrier and lha, invert situation if needed # Remove carrier and lha, invert situation if needed
@@ -140,31 +145,40 @@ class LocationFinder:
self.game = game self.game = game
self.control_point = control_point self.control_point = control_point
self.miz_data = MizDataLocationFinder.compute_possible_locations( self.miz_data = MizDataLocationFinder.compute_possible_locations(
game.theater.terrain.name, control_point.full_name) game.theater.terrain.name, control_point.full_name
)
def location_for(self, location_type: LocationType) -> Optional[Point]: def location_for(self, location_type: LocationType) -> Optional[PointWithHeading]:
position = self.control_point.preset_locations.random_for(location_type) position = self.control_point.preset_locations.random_for(location_type)
if position is not None: if position is not None:
return position return position
logging.warning(f"No campaign location for %s at %s", logging.warning(
location_type.value, self.control_point) f"No campaign location for %s Mat %s",
location_type.value,
self.control_point,
)
position = self.random_from_miz_data( position = self.random_from_miz_data(
location_type == LocationType.OffshoreStrikeTarget) location_type == LocationType.OffshoreStrikeTarget
)
if position is not None: if position is not None:
return position return position
logging.debug(f"No mizdata location for %s at %s", location_type.value, logging.debug(
self.control_point) f"No mizdata location for %s at %s", location_type.value, self.control_point
)
position = self.random_position(location_type) position = self.random_position(location_type)
if position is not None: if position is not None:
return position return position
logging.error(f"Could not find position for %s at %s", logging.error(
location_type.value, self.control_point) f"Could not find position for %s at %s",
location_type.value,
self.control_point,
)
return None return None
def random_from_miz_data(self, offshore: bool) -> Optional[Point]: def random_from_miz_data(self, offshore: bool) -> Optional[PointWithHeading]:
if offshore: if offshore:
locations = self.miz_data.offshore_locations locations = self.miz_data.offshore_locations
else: else:
@@ -172,13 +186,23 @@ class LocationFinder:
if self.miz_data.offshore_locations: if self.miz_data.offshore_locations:
preset = random.choice(locations) preset = random.choice(locations)
locations.remove(preset) locations.remove(preset)
return preset.position return PointWithHeading.from_point(preset.position, preset.heading)
return None return None
def random_position(self, location_type: LocationType) -> Optional[Point]: def random_position(
self, location_type: LocationType
) -> Optional[PointWithHeading]:
# TODO: Flesh out preset locations so we never hit this case. # TODO: Flesh out preset locations so we never hit this case.
logging.warning("Falling back to random location for %s at %s",
location_type.value, self.control_point) if location_type == LocationType.Coastal:
# No coastal locations generated randomly
return None
logging.warning(
"Falling back to random location for %s at %s",
location_type.value,
self.control_point,
)
is_base_defense = location_type in { is_base_defense = location_type in {
LocationType.BaseAirDefense, LocationType.BaseAirDefense,
@@ -212,23 +236,28 @@ class LocationFinder:
min_range = 10000 min_range = 10000
max_range = 40000 max_range = 40000
position = self._find_random_position(min_range, max_range, position = self._find_random_position(
on_land, is_base_defense, min_range, max_range, on_land, is_base_defense, avoid_others
avoid_others) )
# Retry once, searching a bit further (On some big airbases, 3200 is too # Retry once, searching a bit further (On some big airbases, 3200 is too
# short (Ex : Incirlik)), but searching farther on every base would be # short (Ex : Incirlik)), but searching farther on every base would be
# problematic, as some base defense units would end up very far away # problematic, as some base defense units would end up very far away
# from small airfields. # from small airfields.
if position is None and is_base_defense: if position is None and is_base_defense:
position = self._find_random_position(3200, 4800, position = self._find_random_position(
on_land, is_base_defense, 3200, 4800, on_land, is_base_defense, avoid_others
avoid_others) )
return position return position
def _find_random_position(self, min_range: int, max_range: int, def _find_random_position(
on_ground: bool, is_base_defense: bool, self,
avoid_others: bool) -> Optional[Point]: min_range: int,
max_range: int,
on_ground: bool,
is_base_defense: bool,
avoid_others: bool,
) -> Optional[PointWithHeading]:
""" """
Find a valid ground object location Find a valid ground object location
:param on_ground: Whether it should be on ground or on sea (True = on :param on_ground: Whether it should be on ground or on sea (True = on
@@ -241,7 +270,7 @@ class LocationFinder:
near = self.control_point.position near = self.control_point.position
others = self.control_point.ground_objects others = self.control_point.ground_objects
def is_valid(point: Optional[Point]) -> bool: def is_valid(point: Optional[PointWithHeading]) -> bool:
if point is None: if point is None:
return False return False
@@ -272,15 +301,21 @@ class LocationFinder:
for _ in range(300): for _ in range(300):
# Check if on land or sea # Check if on land or sea
p = near.random_point_within(max_range, min_range) p = PointWithHeading.from_point(
near.random_point_within(max_range, min_range), random.randint(0, 360)
)
if is_valid(p): if is_valid(p):
return p return p
return None return None
class ControlPointGroundObjectGenerator: class ControlPointGroundObjectGenerator:
def __init__(self, game: Game, generator_settings: GeneratorSettings, def __init__(
control_point: ControlPoint) -> None: self,
game: Game,
generator_settings: GeneratorSettings,
control_point: ControlPoint,
) -> None:
self.game = game self.game = game
self.generator_settings = generator_settings self.generator_settings = generator_settings
self.control_point = control_point self.control_point = control_point
@@ -321,15 +356,15 @@ class ControlPointGroundObjectGenerator:
self.generate_ship() self.generate_ship()
def generate_ship(self) -> None: def generate_ship(self) -> None:
point = self.location_finder.location_for( point = self.location_finder.location_for(LocationType.OffshoreStrikeTarget)
LocationType.OffshoreStrikeTarget)
if point is None: if point is None:
return return
group_id = self.game.next_group_id() group_id = self.game.next_group_id()
g = ShipGroundObject(namegen.random_objective_name(), group_id, point, g = ShipGroundObject(
self.control_point) namegen.random_objective_name(), group_id, point, self.control_point
)
group = generate_ship_group(self.game, g, self.faction_name) group = generate_ship_group(self.game, g, self.faction_name)
g.groups = [] g.groups = []
@@ -352,13 +387,15 @@ class CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator):
if not carrier_names: if not carrier_names:
logging.info( logging.info(
f"Skipping generation of {self.control_point.name} because " f"Skipping generation of {self.control_point.name} because "
f"{self.faction_name} has no carriers") f"{self.faction_name} has no carriers"
)
return False return False
# Create ground object group # Create ground object group
group_id = self.game.next_group_id() group_id = self.game.next_group_id()
g = CarrierGroundObject(namegen.random_objective_name(), group_id, g = CarrierGroundObject(
self.control_point) namegen.random_objective_name(), group_id, self.control_point
)
group = generate_carrier_group(self.faction_name, self.game, g) group = generate_carrier_group(self.faction_name, self.game, g)
g.groups = [] g.groups = []
if group is not None: if group is not None:
@@ -377,13 +414,15 @@ class LhaGroundObjectGenerator(ControlPointGroundObjectGenerator):
if not lha_names: if not lha_names:
logging.info( logging.info(
f"Skipping generation of {self.control_point.name} because " f"Skipping generation of {self.control_point.name} because "
f"{self.faction_name} has no LHAs") f"{self.faction_name} has no LHAs"
)
return False return False
# Create ground object group # Create ground object group
group_id = self.game.next_group_id() group_id = self.game.next_group_id()
g = LhaGroundObject(namegen.random_objective_name(), group_id, g = LhaGroundObject(
self.control_point) namegen.random_objective_name(), group_id, self.control_point
)
group = generate_lha_group(self.faction_name, self.game, g) group = generate_lha_group(self.faction_name, self.game, g)
g.groups = [] g.groups = []
if group is not None: if group is not None:
@@ -416,14 +455,19 @@ class BaseDefenseGenerator:
self.generate_base_defenses() self.generate_base_defenses()
def generate_ewr(self) -> None: def generate_ewr(self) -> None:
position = self.location_finder.location_for(LocationType.Ewr) position = self.location_finder.location_for(LocationType.BaseEwr)
if position is None: if position is None:
return return
group_id = self.game.next_group_id() group_id = self.game.next_group_id()
g = EwrGroundObject(namegen.random_objective_name(), group_id, g = EwrGroundObject(
position, self.control_point) namegen.random_objective_name(),
group_id,
position,
self.control_point,
True,
)
group = generate_ewr_group(self.game, g, self.faction) group = generate_ewr_group(self.game, g, self.faction)
if group is None: if group is None:
@@ -454,28 +498,35 @@ class BaseDefenseGenerator:
group_id = self.game.next_group_id() group_id = self.game.next_group_id()
g = VehicleGroupGroundObject(namegen.random_objective_name(), group_id, g = VehicleGroupGroundObject(
position, self.control_point, namegen.random_objective_name(),
for_airbase=True) group_id,
position,
self.control_point,
for_airbase=True,
)
group = generate_armor_group(self.faction_name, self.game, g) group = generate_armor_group(self.faction_name, self.game, g)
if group is None: if group is None:
logging.error( logging.error(f"Could not generate garrison at {self.control_point}")
f"Could not generate garrison at {self.control_point}")
return return
g.groups.append(group) g.groups.append(group)
self.control_point.base_defenses.append(g) self.control_point.base_defenses.append(g)
def generate_sam(self) -> None: def generate_sam(self) -> None:
position = self.location_finder.location_for( position = self.location_finder.location_for(LocationType.BaseAirDefense)
LocationType.BaseAirDefense)
if position is None: if position is None:
return return
group_id = self.game.next_group_id() group_id = self.game.next_group_id()
g = SamGroundObject(namegen.random_objective_name(), group_id, g = SamGroundObject(
position, self.control_point, for_airbase=True) namegen.random_objective_name(),
group_id,
position,
self.control_point,
for_airbase=True,
)
groups = generate_anti_air_group(self.game, g, self.faction) groups = generate_anti_air_group(self.game, g, self.faction)
if not groups: if not groups:
@@ -485,21 +536,25 @@ class BaseDefenseGenerator:
self.control_point.base_defenses.append(g) self.control_point.base_defenses.append(g)
def generate_shorad(self) -> None: def generate_shorad(self) -> None:
position = self.location_finder.location_for( position = self.location_finder.location_for(LocationType.BaseAirDefense)
LocationType.BaseAirDefense)
if position is None: if position is None:
return return
group_id = self.game.next_group_id() group_id = self.game.next_group_id()
g = SamGroundObject(namegen.random_objective_name(), group_id, g = SamGroundObject(
position, self.control_point, for_airbase=True) namegen.random_objective_name(),
group_id,
position,
self.control_point,
for_airbase=True,
)
groups = generate_anti_air_group(self.game, g, self.faction, groups = generate_anti_air_group(
ranges=[{AirDefenseRange.Short}]) self.game, g, self.faction, ranges=[{AirDefenseRange.Short}]
)
if not groups: if not groups:
logging.error( logging.error(f"Could not generate SHORAD group at {self.control_point}")
f"Could not generate SHORAD group at {self.control_point}")
return return
g.groups = groups g.groups = groups
self.control_point.base_defenses.append(g) self.control_point.base_defenses.append(g)
@@ -509,7 +564,7 @@ class FobDefenseGenerator(BaseDefenseGenerator):
def generate(self) -> None: def generate(self) -> None:
self.generate_garrison() self.generate_garrison()
self.generate_fob_defenses() self.generate_fob_defenses()
def generate_fob_defenses(self): def generate_fob_defenses(self):
# First group has a 1/2 chance of being a SHORAD, # First group has a 1/2 chance of being a SHORAD,
# and a 1/2 chance of a garrison. # and a 1/2 chance of a garrison.
@@ -528,9 +583,13 @@ class FobDefenseGenerator(BaseDefenseGenerator):
class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
def __init__(self, game: Game, generator_settings: GeneratorSettings, def __init__(
control_point: ControlPoint, self,
templates: GroundObjectTemplates) -> None: game: Game,
generator_settings: GeneratorSettings,
control_point: ControlPoint,
templates: GroundObjectTemplates,
) -> None:
super().__init__(game, generator_settings, control_point) super().__init__(game, generator_settings, control_point)
self.templates = templates self.templates = templates
@@ -544,11 +603,15 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
if self.faction.missiles: if self.faction.missiles:
self.generate_missile_sites() self.generate_missile_sites()
if self.faction.coastal_defenses:
self.generate_coastal_sites()
return True return True
def generate_ground_points(self) -> None: def generate_ground_points(self) -> None:
"""Generate ground objects and AA sites for the control point.""" """Generate ground objects and AA sites for the control point."""
skip_sams = self.generate_required_aa() skip_sams = self.generate_required_aa()
skip_ewrs = self.generate_required_ewr()
if self.control_point.is_global: if self.control_point.is_global:
return return
@@ -565,6 +628,12 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
skip_sams -= 1 skip_sams -= 1
else: else:
self.generate_aa_site() self.generate_aa_site()
# 1 in 4 additional objectives are EWR.
elif random.randint(0, 3) == 0:
if skip_ewrs > 0:
skip_ewrs -= 1
else:
self.generate_ewr_site()
else: else:
self.generate_ground_point() self.generate_ground_point()
@@ -576,18 +645,36 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
""" """
presets = self.control_point.preset_locations presets = self.control_point.preset_locations
for position in presets.required_long_range_sams: for position in presets.required_long_range_sams:
self.generate_aa_at(position, ranges=[ self.generate_aa_at(
{AirDefenseRange.Long}, position,
{AirDefenseRange.Medium}, ranges=[
{AirDefenseRange.Short}, {AirDefenseRange.Long},
]) {AirDefenseRange.Medium},
{AirDefenseRange.Short},
],
)
for position in presets.required_medium_range_sams: for position in presets.required_medium_range_sams:
self.generate_aa_at(position, ranges=[ self.generate_aa_at(
{AirDefenseRange.Medium}, position,
{AirDefenseRange.Short}, ranges=[
]) {AirDefenseRange.Medium},
return (len(presets.required_long_range_sams) + {AirDefenseRange.Short},
len(presets.required_medium_range_sams)) ],
)
return len(presets.required_long_range_sams) + len(
presets.required_medium_range_sams
)
def generate_required_ewr(self) -> int:
"""Generates the EWR sites that are required by the campaign.
Returns:
The number of EWR sites that were generated.
"""
presets = self.control_point.preset_locations
for position in presets.required_ewrs:
self.generate_ewr_at(position)
return len(presets.required_ewrs)
def generate_ground_point(self) -> None: def generate_ground_point(self) -> None:
try: try:
@@ -618,8 +705,15 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
template_point = Point(unit["offset"].x, unit["offset"].y) template_point = Point(unit["offset"].x, unit["offset"].y)
g = BuildingGroundObject( g = BuildingGroundObject(
obj_name, category, group_id, object_id, point + template_point, obj_name,
unit["heading"], self.control_point, unit["type"]) category,
group_id,
object_id,
point + template_point,
unit["heading"],
self.control_point,
unit["type"],
)
self.control_point.connected_objectives.append(g) self.control_point.connected_objectives.append(g)
@@ -627,27 +721,65 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
position = self.location_finder.location_for(LocationType.Sam) position = self.location_finder.location_for(LocationType.Sam)
if position is None: if position is None:
return return
self.generate_aa_at(position, ranges=[ self.generate_aa_at(
# Prefer to use proper SAMs, but fall back to SHORADs if needed. position,
{AirDefenseRange.Long, AirDefenseRange.Medium}, ranges=[
{AirDefenseRange.Short}, # Prefer to use proper SAMs, but fall back to SHORADs if needed.
]) {AirDefenseRange.Long, AirDefenseRange.Medium},
{AirDefenseRange.Short},
],
)
def generate_aa_at( def generate_aa_at(
self, position: Point, self, position: Point, ranges: Iterable[Set[AirDefenseRange]]
ranges: Iterable[Set[AirDefenseRange]]) -> None: ) -> None:
group_id = self.game.next_group_id() group_id = self.game.next_group_id()
g = SamGroundObject(namegen.random_objective_name(), group_id, g = SamGroundObject(
position, self.control_point, for_airbase=False) namegen.random_objective_name(),
group_id,
position,
self.control_point,
for_airbase=False,
)
groups = generate_anti_air_group(self.game, g, self.faction, ranges) groups = generate_anti_air_group(self.game, g, self.faction, ranges)
if not groups: if not groups:
logging.error("Could not generate air defense group for %s at %s", logging.error(
g.name, self.control_point) "Could not generate air defense group for %s at %s",
g.name,
self.control_point,
)
return return
g.groups = groups g.groups = groups
self.control_point.connected_objectives.append(g) self.control_point.connected_objectives.append(g)
def generate_ewr_site(self) -> None:
position = self.location_finder.location_for(LocationType.Ewr)
if position is None:
return
self.generate_ewr_at(position)
def generate_ewr_at(self, position: Point) -> None:
group_id = self.game.next_group_id()
g = EwrGroundObject(
namegen.random_objective_name(),
group_id,
position,
self.control_point,
for_airbase=False,
)
group = generate_ewr_group(self.game, g, self.faction)
if group is None:
logging.error(
"Could not generate ewr group for %s at %s",
g.name,
self.control_point,
)
return
g.groups = [group]
self.control_point.connected_objectives.append(g)
def generate_missile_sites(self) -> None: def generate_missile_sites(self) -> None:
for i in range(self.faction.missiles_group_count): for i in range(self.faction.missiles_group_count):
self.generate_missile_site() self.generate_missile_site()
@@ -659,8 +791,9 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
group_id = self.game.next_group_id() group_id = self.game.next_group_id()
g = MissileSiteGroundObject(namegen.random_objective_name(), group_id, g = MissileSiteGroundObject(
position, self.control_point) namegen.random_objective_name(), group_id, position, self.control_point
)
group = generate_missile_group(self.game, g, self.faction_name) group = generate_missile_group(self.game, g, self.faction_name)
g.groups = [] g.groups = []
if group is not None: if group is not None:
@@ -668,6 +801,31 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
self.control_point.connected_objectives.append(g) self.control_point.connected_objectives.append(g)
return return
def generate_coastal_sites(self) -> None:
for i in range(self.faction.coastal_group_count):
self.generate_coastal_site()
def generate_coastal_site(self) -> None:
position = self.location_finder.location_for(LocationType.Coastal)
if position is None:
return
group_id = self.game.next_group_id()
g = CoastalSiteGroundObject(
namegen.random_objective_name(),
group_id,
position,
self.control_point,
position.heading,
)
group = generate_coastal_group(self.game, g, self.faction_name)
g.groups = []
if group is not None:
g.groups.append(group)
self.control_point.connected_objectives.append(g)
return
class FobGroundObjectGenerator(AirbaseGroundObjectGenerator): class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
def generate(self) -> bool: def generate(self) -> bool:
@@ -678,7 +836,7 @@ class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
def generate_fob(self) -> None: def generate_fob(self) -> None:
try: try:
category = self.faction.building_set[self.faction.building_set.index('fob')] category = self.faction.building_set[self.faction.building_set.index("fob")]
except IndexError: except IndexError:
logging.exception("Faction has no fob buildings defined") logging.exception("Faction has no fob buildings defined")
return return
@@ -696,14 +854,21 @@ class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
template_point = Point(unit["offset"].x, unit["offset"].y) template_point = Point(unit["offset"].x, unit["offset"].y)
g = BuildingGroundObject( g = BuildingGroundObject(
obj_name, category, group_id, object_id, point + template_point, obj_name,
unit["heading"], self.control_point, unit["type"], airbase_group=True) category,
group_id,
object_id,
point + template_point,
unit["heading"],
self.control_point,
unit["type"],
airbase_group=True,
)
self.control_point.connected_objectives.append(g) self.control_point.connected_objectives.append(g)
class GroundObjectGenerator: class GroundObjectGenerator:
def __init__(self, game: Game, def __init__(self, game: Game, generator_settings: GeneratorSettings) -> None:
generator_settings: GeneratorSettings) -> None:
self.game = game self.game = game
self.generator_settings = generator_settings self.generator_settings = generator_settings
with open("resources/groundobject_templates.p", "rb") as f: with open("resources/groundobject_templates.p", "rb") as f:
@@ -721,19 +886,22 @@ class GroundObjectGenerator:
generator: ControlPointGroundObjectGenerator generator: ControlPointGroundObjectGenerator
if control_point.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP: if control_point.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP:
generator = CarrierGroundObjectGenerator( generator = CarrierGroundObjectGenerator(
self.game, self.generator_settings, control_point) self.game, self.generator_settings, control_point
)
elif control_point.cptype == ControlPointType.LHA_GROUP: elif control_point.cptype == ControlPointType.LHA_GROUP:
generator = LhaGroundObjectGenerator( generator = LhaGroundObjectGenerator(
self.game, self.generator_settings, control_point) self.game, self.generator_settings, control_point
)
elif isinstance(control_point, OffMapSpawn): elif isinstance(control_point, OffMapSpawn):
generator = NoOpGroundObjectGenerator( generator = NoOpGroundObjectGenerator(
self.game, self.generator_settings, control_point) self.game, self.generator_settings, control_point
)
elif isinstance(control_point, Fob): elif isinstance(control_point, Fob):
generator = FobGroundObjectGenerator( generator = FobGroundObjectGenerator(
self.game, self.generator_settings, control_point, self.game, self.generator_settings, control_point, self.templates
self.templates) )
else: else:
generator = AirbaseGroundObjectGenerator( generator = AirbaseGroundObjectGenerator(
self.game, self.generator_settings, control_point, self.game, self.generator_settings, control_point, self.templates
self.templates) )
return generator.generate() return generator.generate()

View File

@@ -33,7 +33,7 @@ NAME_BY_CATEGORY = {
"ww2bunker": "Bunker", "ww2bunker": "Bunker",
"village": "Village", "village": "Village",
"allycamp": "Camp", "allycamp": "Camp",
"EWR":"EWR", "EWR": "EWR",
} }
ABBREV_NAME = { ABBREV_NAME = {
@@ -54,34 +54,59 @@ ABBREV_NAME = {
} }
CATEGORY_MAP = { CATEGORY_MAP = {
# Special cases # Special cases
"CARRIER": ["CARRIER"], "CARRIER": ["CARRIER"],
"LHA": ["LHA"], "LHA": ["LHA"],
"aa": ["AA"], "aa": ["AA"],
# Buildings # Buildings
"power": ["Workshop A", "Electric power box", "Garage small A", "Farm B", "Repair workshop", "Garage B"], "power": [
"Workshop A",
"Electric power box",
"Garage small A",
"Farm B",
"Repair workshop",
"Garage B",
],
"ware": ["Warehouse", "Hangar A"], "ware": ["Warehouse", "Hangar A"],
"fuel": ["Tank", "Tank 2", "Tank 3", "Fuel tank"], "fuel": ["Tank", "Tank 2", "Tank 3", "Fuel tank"],
"ammo": [".Ammunition depot", "Hangar B"], "ammo": [".Ammunition depot", "Hangar B"],
"farp": ["FARP Tent", "FARP Ammo Dump Coating", "FARP Fuel Depot", "FARP Command Post", "FARP CP Blindage"], "farp": [
"FARP Tent",
"FARP Ammo Dump Coating",
"FARP Fuel Depot",
"FARP Command Post",
"FARP CP Blindage",
],
"fob": ["Bunker 2", "Bunker 1", "Garage small B", ".Command Center", "Barracks 2"], "fob": ["Bunker 2", "Bunker 1", "Garage small B", ".Command Center", "Barracks 2"],
"factory": ["Tech combine", "Tech hangar A"], "factory": ["Tech combine", "Tech hangar A"],
"comms": ["TV tower", "Comms tower M"], "comms": ["TV tower", "Comms tower M"],
"oil": ["Oil platform"], "oil": ["Oil platform"],
"derrick": ["Oil derrick", "Pump station", "Subsidiary structure 2"], "derrick": ["Oil derrick", "Pump station", "Subsidiary structure 2"],
"ww2bunker": ["Siegfried Line", "Fire Control Bunker", "SK_C_28_naval_gun", "Concertina Wire", "Czech hedgehogs 1"], "ww2bunker": [
"Siegfried Line",
"Fire Control Bunker",
"SK_C_28_naval_gun",
"Concertina Wire",
"Czech hedgehogs 1",
],
"village": ["Small house 1B", "Small House 1A", "Small warehouse 1"], "village": ["Small house 1B", "Small House 1A", "Small warehouse 1"],
"allycamp": [], "allycamp": [],
} }
class TheaterGroundObject(MissionTarget): class TheaterGroundObject(MissionTarget):
def __init__(
def __init__(self, name: str, category: str, group_id: int, position: Point, self,
heading: int, control_point: ControlPoint, dcs_identifier: str, name: str,
airbase_group: bool, sea_object: bool) -> None: category: str,
group_id: int,
position: Point,
heading: int,
control_point: ControlPoint,
dcs_identifier: str,
airbase_group: bool,
sea_object: bool,
) -> None:
super().__init__(name, position) super().__init__(name, position)
self.category = category self.category = category
self.group_id = group_id self.group_id = group_id
@@ -131,6 +156,7 @@ class TheaterGroundObject(MissionTarget):
def mission_types(self, for_player: bool) -> Iterator[FlightType]: def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
if self.is_friendly(for_player): if self.is_friendly(for_player):
yield from [ yield from [
# TODO: FlightType.LOGISTICS # TODO: FlightType.LOGISTICS
@@ -193,9 +219,18 @@ class TheaterGroundObject(MissionTarget):
class BuildingGroundObject(TheaterGroundObject): class BuildingGroundObject(TheaterGroundObject):
def __init__(self, name: str, category: str, group_id: int, object_id: int, def __init__(
position: Point, heading: int, control_point: ControlPoint, self,
dcs_identifier: str, airbase_group=False) -> None: name: str,
category: str,
group_id: int,
object_id: int,
position: Point,
heading: int,
control_point: ControlPoint,
dcs_identifier: str,
airbase_group=False,
) -> None:
super().__init__( super().__init__(
name=name, name=name,
category=category, category=category,
@@ -205,7 +240,7 @@ class BuildingGroundObject(TheaterGroundObject):
control_point=control_point, control_point=control_point,
dcs_identifier=dcs_identifier, dcs_identifier=dcs_identifier,
airbase_group=airbase_group, airbase_group=airbase_group,
sea_object=False sea_object=False,
) )
self.object_id = object_id self.object_id = object_id
# Other TGOs track deadness based on the number of alive units, but # Other TGOs track deadness based on the number of alive units, but
@@ -234,6 +269,7 @@ class BuildingGroundObject(TheaterGroundObject):
class NavalGroundObject(TheaterGroundObject): class NavalGroundObject(TheaterGroundObject):
def mission_types(self, for_player: bool) -> Iterator[FlightType]: def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
if not self.is_friendly(for_player): if not self.is_friendly(for_player):
yield FlightType.ANTISHIP yield FlightType.ANTISHIP
yield from super().mission_types(for_player) yield from super().mission_types(for_player)
@@ -249,8 +285,7 @@ class GenericCarrierGroundObject(NavalGroundObject):
# TODO: Why is this both a CP and a TGO? # TODO: Why is this both a CP and a TGO?
class CarrierGroundObject(GenericCarrierGroundObject): class CarrierGroundObject(GenericCarrierGroundObject):
def __init__(self, name: str, group_id: int, def __init__(self, name: str, group_id: int, control_point: ControlPoint) -> None:
control_point: ControlPoint) -> None:
super().__init__( super().__init__(
name=name, name=name,
category="CARRIER", category="CARRIER",
@@ -260,7 +295,7 @@ class CarrierGroundObject(GenericCarrierGroundObject):
control_point=control_point, control_point=control_point,
dcs_identifier="CARRIER", dcs_identifier="CARRIER",
airbase_group=True, airbase_group=True,
sea_object=True sea_object=True,
) )
@property @property
@@ -272,8 +307,7 @@ class CarrierGroundObject(GenericCarrierGroundObject):
# TODO: Why is this both a CP and a TGO? # TODO: Why is this both a CP and a TGO?
class LhaGroundObject(GenericCarrierGroundObject): class LhaGroundObject(GenericCarrierGroundObject):
def __init__(self, name: str, group_id: int, def __init__(self, name: str, group_id: int, control_point: ControlPoint) -> None:
control_point: ControlPoint) -> None:
super().__init__( super().__init__(
name=name, name=name,
category="LHA", category="LHA",
@@ -283,7 +317,7 @@ class LhaGroundObject(GenericCarrierGroundObject):
control_point=control_point, control_point=control_point,
dcs_identifier="LHA", dcs_identifier="LHA",
airbase_group=True, airbase_group=True,
sea_object=True sea_object=True,
) )
@property @property
@@ -294,8 +328,9 @@ class LhaGroundObject(GenericCarrierGroundObject):
class MissileSiteGroundObject(TheaterGroundObject): class MissileSiteGroundObject(TheaterGroundObject):
def __init__(self, name: str, group_id: int, position: Point, def __init__(
control_point: ControlPoint) -> None: self, name: str, group_id: int, position: Point, control_point: ControlPoint
) -> None:
super().__init__( super().__init__(
name=name, name=name,
category="aa", category="aa",
@@ -305,7 +340,29 @@ class MissileSiteGroundObject(TheaterGroundObject):
control_point=control_point, control_point=control_point,
dcs_identifier="AA", dcs_identifier="AA",
airbase_group=False, airbase_group=False,
sea_object=False sea_object=False,
)
class CoastalSiteGroundObject(TheaterGroundObject):
def __init__(
self,
name: str,
group_id: int,
position: Point,
control_point: ControlPoint,
heading,
) -> None:
super().__init__(
name=name,
category="aa",
group_id=group_id,
position=position,
heading=heading,
control_point=control_point,
dcs_identifier="AA",
airbase_group=False,
sea_object=False,
) )
@@ -317,8 +374,14 @@ class BaseDefenseGroundObject(TheaterGroundObject):
# This type gets used both for AA sites (SAM, AAA, or SHORAD). These should each # This type gets used both for AA sites (SAM, AAA, or SHORAD). These should each
# be split into their own types. # be split into their own types.
class SamGroundObject(BaseDefenseGroundObject): class SamGroundObject(BaseDefenseGroundObject):
def __init__(self, name: str, group_id: int, position: Point, def __init__(
control_point: ControlPoint, for_airbase: bool) -> None: self,
name: str,
group_id: int,
position: Point,
control_point: ControlPoint,
for_airbase: bool,
) -> None:
super().__init__( super().__init__(
name=name, name=name,
category="aa", category="aa",
@@ -328,7 +391,7 @@ class SamGroundObject(BaseDefenseGroundObject):
control_point=control_point, control_point=control_point,
dcs_identifier="AA", dcs_identifier="AA",
airbase_group=for_airbase, airbase_group=for_airbase,
sea_object=False sea_object=False,
) )
# Set by the SAM unit generator if the generated group is compatible # Set by the SAM unit generator if the generated group is compatible
# with Skynet. # with Skynet.
@@ -345,6 +408,7 @@ class SamGroundObject(BaseDefenseGroundObject):
def mission_types(self, for_player: bool) -> Iterator[FlightType]: def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
if not self.is_friendly(for_player): if not self.is_friendly(for_player):
yield FlightType.DEAD yield FlightType.DEAD
yield from super().mission_types(for_player) yield from super().mission_types(for_player)
@@ -355,8 +419,14 @@ class SamGroundObject(BaseDefenseGroundObject):
class VehicleGroupGroundObject(BaseDefenseGroundObject): class VehicleGroupGroundObject(BaseDefenseGroundObject):
def __init__(self, name: str, group_id: int, position: Point, def __init__(
control_point: ControlPoint, for_airbase: bool) -> None: self,
name: str,
group_id: int,
position: Point,
control_point: ControlPoint,
for_airbase: bool,
) -> None:
super().__init__( super().__init__(
name=name, name=name,
category="aa", category="aa",
@@ -366,13 +436,19 @@ class VehicleGroupGroundObject(BaseDefenseGroundObject):
control_point=control_point, control_point=control_point,
dcs_identifier="AA", dcs_identifier="AA",
airbase_group=for_airbase, airbase_group=for_airbase,
sea_object=False sea_object=False,
) )
class EwrGroundObject(BaseDefenseGroundObject): class EwrGroundObject(BaseDefenseGroundObject):
def __init__(self, name: str, group_id: int, position: Point, def __init__(
control_point: ControlPoint) -> None: self,
name: str,
group_id: int,
position: Point,
control_point: ControlPoint,
for_airbase: bool,
) -> None:
super().__init__( super().__init__(
name=name, name=name,
category="EWR", category="EWR",
@@ -381,8 +457,8 @@ class EwrGroundObject(BaseDefenseGroundObject):
heading=0, heading=0,
control_point=control_point, control_point=control_point,
dcs_identifier="EWR", dcs_identifier="EWR",
airbase_group=True, airbase_group=for_airbase,
sea_object=False sea_object=False,
) )
@property @property
@@ -392,6 +468,7 @@ class EwrGroundObject(BaseDefenseGroundObject):
def mission_types(self, for_player: bool) -> Iterator[FlightType]: def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
if not self.is_friendly(for_player): if not self.is_friendly(for_player):
yield FlightType.DEAD yield FlightType.DEAD
yield from super().mission_types(for_player) yield from super().mission_types(for_player)
@@ -402,8 +479,9 @@ class EwrGroundObject(BaseDefenseGroundObject):
class ShipGroundObject(NavalGroundObject): class ShipGroundObject(NavalGroundObject):
def __init__(self, name: str, group_id: int, position: Point, def __init__(
control_point: ControlPoint) -> None: self, name: str, group_id: int, position: Point, control_point: ControlPoint
) -> None:
super().__init__( super().__init__(
name=name, name=name,
category="aa", category="aa",
@@ -413,7 +491,7 @@ class ShipGroundObject(NavalGroundObject):
control_point=control_point, control_point=control_point,
dcs_identifier="AA", dcs_identifier="AA",
airbase_group=False, airbase_group=False,
sea_object=True sea_object=True,
) )
@property @property

View File

@@ -15,6 +15,7 @@ from shapely.ops import nearest_points, unary_union
from game.theater import ControlPoint from game.theater import ControlPoint
from game.utils import Distance, meters, nautical_miles from game.utils import Distance, meters, nautical_miles
from gen import Conflict
from gen.flights.closestairfields import ObjectiveDistanceCache from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.flights.flight import Flight from gen.flights.flight import Flight
@@ -32,8 +33,9 @@ class ThreatZones:
self.all = unary_union([airbases, air_defenses]) self.all = unary_union([airbases, air_defenses])
def closest_boundary(self, point: DcsPoint) -> DcsPoint: def closest_boundary(self, point: DcsPoint) -> DcsPoint:
boundary, _ = nearest_points(self.all.boundary, boundary, _ = nearest_points(
self.dcs_to_shapely_point(point)) self.all.boundary, self.dcs_to_shapely_point(point)
)
return DcsPoint(boundary.x, boundary.y) return DcsPoint(boundary.x, boundary.y)
@singledispatchmethod @singledispatchmethod
@@ -49,8 +51,9 @@ class ThreatZones:
return self.all.intersects(self.dcs_to_shapely_point(position)) return self.all.intersects(self.dcs_to_shapely_point(position))
def path_threatened(self, a: DcsPoint, b: DcsPoint) -> bool: def path_threatened(self, a: DcsPoint, b: DcsPoint) -> bool:
return self.threatened(LineString( return self.threatened(
[self.dcs_to_shapely_point(a), self.dcs_to_shapely_point(b)])) LineString([self.dcs_to_shapely_point(a), self.dcs_to_shapely_point(b)])
)
@singledispatchmethod @singledispatchmethod
def threatened_by_aircraft(self, target) -> bool: def threatened_by_aircraft(self, target) -> bool:
@@ -62,9 +65,9 @@ class ThreatZones:
@threatened_by_aircraft.register @threatened_by_aircraft.register
def _threatened_by_aircraft_flight(self, flight: Flight) -> bool: def _threatened_by_aircraft_flight(self, flight: Flight) -> bool:
return self.threatened_by_aircraft(LineString(( return self.threatened_by_aircraft(
self.dcs_to_shapely_point(p.position) for p in flight.points LineString((self.dcs_to_shapely_point(p.position) for p in flight.points))
))) )
@singledispatchmethod @singledispatchmethod
def threatened_by_air_defense(self, target) -> bool: def threatened_by_air_defense(self, target) -> bool:
@@ -76,13 +79,14 @@ class ThreatZones:
@threatened_by_air_defense.register @threatened_by_air_defense.register
def _threatened_by_air_defense_flight(self, flight: Flight) -> bool: def _threatened_by_air_defense_flight(self, flight: Flight) -> bool:
return self.threatened_by_air_defense(LineString(( return self.threatened_by_air_defense(
self.dcs_to_shapely_point(p.position) for p in flight.points LineString((self.dcs_to_shapely_point(p.position) for p in flight.points))
))) )
@classmethod @classmethod
def closest_enemy_airbase(cls, location: ControlPoint, def closest_enemy_airbase(
max_distance: Distance) -> Optional[ControlPoint]: cls, location: ControlPoint, max_distance: Distance
) -> Optional[ControlPoint]:
airfields = ObjectiveDistanceCache.get_closest_airfields(location) airfields = ObjectiveDistanceCache.get_closest_airfields(location)
for airfield in airfields.airfields_within(max_distance): for airfield in airfields.airfields_within(max_distance):
if airfield.captured != location.captured: if airfield.captured != location.captured:
@@ -90,13 +94,14 @@ class ThreatZones:
return None return None
@classmethod @classmethod
def barcap_threat_range(cls, game: Game, def barcap_threat_range(cls, game: Game, control_point: ControlPoint) -> Distance:
control_point: ControlPoint) -> Distance:
doctrine = game.faction_for(control_point.captured).doctrine doctrine = game.faction_for(control_point.captured).doctrine
cap_threat_range = (doctrine.cap_max_distance_from_cp + cap_threat_range = (
doctrine.cap_engagement_range) doctrine.cap_max_distance_from_cp + doctrine.cap_engagement_range
opposing_airfield = cls.closest_enemy_airbase(control_point, )
cap_threat_range * 2) opposing_airfield = cls.closest_enemy_airbase(
control_point, cap_threat_range * 2
)
if opposing_airfield is None: if opposing_airfield is None:
return cap_threat_range return cap_threat_range
@@ -127,16 +132,15 @@ class ThreatZones:
zone belongs to the player, it is the zone that will be avoided by zone belongs to the player, it is the zone that will be avoided by
the enemy and vice versa. the enemy and vice versa.
""" """
airbases = [] air_threats = []
air_defenses = [] air_defenses = []
for control_point in game.theater.controlpoints: for control_point in game.theater.controlpoints:
if control_point.captured != player: if control_point.captured != player:
continue continue
if control_point.runway_is_operational(): if control_point.runway_is_operational():
point = ShapelyPoint(control_point.position.x, point = ShapelyPoint(control_point.position.x, control_point.position.y)
control_point.position.y)
cap_threat_range = cls.barcap_threat_range(game, control_point) cap_threat_range = cls.barcap_threat_range(game, control_point)
airbases.append(point.buffer(cap_threat_range.meters)) air_threats.append(point.buffer(cap_threat_range.meters))
for tgo in control_point.ground_objects: for tgo in control_point.ground_objects:
for group in tgo.groups: for group in tgo.groups:
@@ -149,10 +153,9 @@ class ThreatZones:
air_defenses.append(threat_zone) air_defenses.append(threat_zone)
return cls( return cls(
airbases=unary_union(airbases), airbases=unary_union(air_threats), air_defenses=unary_union(air_defenses)
air_defenses=unary_union(air_defenses)
) )
@staticmethod @staticmethod
def dcs_to_shapely_point(point: DcsPoint) -> ShapelyPoint: def dcs_to_shapely_point(point: DcsPoint) -> ShapelyPoint:
return ShapelyPoint(point.x, point.y) return ShapelyPoint(point.x, point.y)

View File

@@ -70,15 +70,19 @@ class UnitMap:
raise RuntimeError(f"Unknown unit type: {unit.type}") raise RuntimeError(f"Unknown unit type: {unit.type}")
if not issubclass(unit_type, VehicleType): if not issubclass(unit_type, VehicleType):
raise RuntimeError( raise RuntimeError(
f"{name} is a {unit_type.__name__}, expected a VehicleType") f"{name} is a {unit_type.__name__}, expected a VehicleType"
)
self.front_line_units[name] = FrontLineUnit(unit_type, origin) self.front_line_units[name] = FrontLineUnit(unit_type, origin)
def front_line_unit(self, name: str) -> Optional[FrontLineUnit]: def front_line_unit(self, name: str) -> Optional[FrontLineUnit]:
return self.front_line_units.get(name, None) return self.front_line_units.get(name, None)
def add_ground_object_units(self, ground_object: TheaterGroundObject, def add_ground_object_units(
persistence_group: Group, self,
miz_group: Group) -> None: ground_object: TheaterGroundObject,
persistence_group: Group,
miz_group: Group,
) -> None:
"""Adds a group associated with a TGO to the unit map. """Adds a group associated with a TGO to the unit map.
Args: Args:
@@ -103,13 +107,13 @@ class UnitMap:
if name in self.ground_object_units: if name in self.ground_object_units:
raise RuntimeError(f"Duplicate TGO unit: {name}") raise RuntimeError(f"Duplicate TGO unit: {name}")
self.ground_object_units[name] = GroundObjectUnit( self.ground_object_units[name] = GroundObjectUnit(
ground_object, persistence_group, persistent_unit) ground_object, persistence_group, persistent_unit
)
def ground_object_unit(self, name: str) -> Optional[GroundObjectUnit]: def ground_object_unit(self, name: str) -> Optional[GroundObjectUnit]:
return self.ground_object_units.get(name, None) return self.ground_object_units.get(name, None)
def add_building(self, ground_object: BuildingGroundObject, def add_building(self, ground_object: BuildingGroundObject, group: Group) -> None:
group: Group) -> None:
# The actual name is a String (the pydcs translatable string), which # The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__. # doesn't define __eq__.
# The name of the initiator in the DCS dead event will have " object" # The name of the initiator in the DCS dead event will have " object"
@@ -119,8 +123,9 @@ class UnitMap:
raise RuntimeError(f"Duplicate TGO unit: {name}") raise RuntimeError(f"Duplicate TGO unit: {name}")
self.buildings[name] = Building(ground_object) self.buildings[name] = Building(ground_object)
def add_fortification(self, ground_object: BuildingGroundObject, def add_fortification(
group: VehicleGroup) -> None: self, ground_object: BuildingGroundObject, group: VehicleGroup
) -> None:
if len(group.units) != 1: if len(group.units) != 1:
raise ValueError("Fortification groups must have exactly one unit.") raise ValueError("Fortification groups must have exactly one unit.")
unit = group.units[0] unit = group.units[0]

View File

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

View File

@@ -3,11 +3,12 @@ from __future__ import annotations
import datetime import datetime
import logging import logging
import random import random
from dataclasses import dataclass from dataclasses import dataclass, field
from enum import Enum from enum import Enum
from typing import Optional, TYPE_CHECKING from typing import Optional, TYPE_CHECKING
from dcs.weather import Weather as PydcsWeather, Wind from dcs.cloud_presets import Clouds as PydcsClouds
from dcs.weather import CloudPreset, Weather as PydcsWeather, Wind
from game.settings import Settings from game.settings import Settings
from game.utils import Distance, meters from game.utils import Distance, meters
@@ -36,6 +37,23 @@ class Clouds:
density: int density: int
thickness: int thickness: int
precipitation: PydcsWeather.Preceptions precipitation: PydcsWeather.Preceptions
preset: Optional[CloudPreset] = field(default=None)
@classmethod
def random_preset(cls, rain: bool) -> Clouds:
clouds = (p.value for p in PydcsClouds)
if rain:
presets = [p for p in clouds if "Rain" in p.name]
else:
presets = [p for p in clouds if "Rain" not in p.name]
preset = random.choice(presets)
return Clouds(
base=random.randint(preset.min_base, preset.max_base),
density=0,
thickness=0,
precipitation=PydcsWeather.Preceptions.None_,
preset=preset,
)
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -58,7 +76,7 @@ class Weather:
return None return None
return Fog( return Fog(
visibility=meters(random.randint(2500, 5000)), visibility=meters(random.randint(2500, 5000)),
thickness=random.randint(100, 500) thickness=random.randint(100, 500),
) )
def generate_wind(self) -> WindConditions: def generate_wind(self) -> WindConditions:
@@ -76,7 +94,7 @@ class Weather:
# Always some wind to make the smoke move a bit. # Always some wind to make the smoke move a bit.
at_0m=Wind(wind_direction, max(1, base_wind * at_0m_factor)), at_0m=Wind(wind_direction, max(1, base_wind * at_0m_factor)),
at_2000m=Wind(wind_direction, base_wind * at_2000m_factor), at_2000m=Wind(wind_direction, base_wind * at_2000m_factor),
at_8000m=Wind(wind_direction, base_wind * at_8000m_factor) at_8000m=Wind(wind_direction, base_wind * at_8000m_factor),
) )
@staticmethod @staticmethod
@@ -101,12 +119,11 @@ class ClearSkies(Weather):
class Cloudy(Weather): class Cloudy(Weather):
def generate_clouds(self) -> Optional[Clouds]: def generate_clouds(self) -> Optional[Clouds]:
return Clouds( return Clouds.random_preset(rain=False)
base=self.random_cloud_base(),
density=random.randint(1, 8), def generate_fog(self) -> Optional[Fog]:
thickness=self.random_cloud_thickness(), # DCS 2.7 says to not use fog with the cloud presets.
precipitation=PydcsWeather.Preceptions.None_ return None
)
def generate_wind(self) -> WindConditions: def generate_wind(self) -> WindConditions:
return self.random_wind(0, 4) return self.random_wind(0, 4)
@@ -114,12 +131,11 @@ class Cloudy(Weather):
class Raining(Weather): class Raining(Weather):
def generate_clouds(self) -> Optional[Clouds]: def generate_clouds(self) -> Optional[Clouds]:
return Clouds( return Clouds.random_preset(rain=True)
base=self.random_cloud_base(),
density=random.randint(5, 8), def generate_fog(self) -> Optional[Fog]:
thickness=self.random_cloud_thickness(), # DCS 2.7 says to not use fog with the cloud presets.
precipitation=PydcsWeather.Preceptions.Rain return None
)
def generate_wind(self) -> WindConditions: def generate_wind(self) -> WindConditions:
return self.random_wind(0, 6) return self.random_wind(0, 6)
@@ -131,7 +147,7 @@ class Thunderstorm(Weather):
base=self.random_cloud_base(), base=self.random_cloud_base(),
density=random.randint(9, 10), density=random.randint(9, 10),
thickness=self.random_cloud_thickness(), thickness=self.random_cloud_thickness(),
precipitation=PydcsWeather.Preceptions.Thunderstorm precipitation=PydcsWeather.Preceptions.Thunderstorm,
) )
def generate_wind(self) -> WindConditions: def generate_wind(self) -> WindConditions:
@@ -145,20 +161,29 @@ class Conditions:
weather: Weather weather: Weather
@classmethod @classmethod
def generate(cls, theater: ConflictTheater, day: datetime.date, def generate(
time_of_day: TimeOfDay, settings: Settings) -> Conditions: cls,
theater: ConflictTheater,
day: datetime.date,
time_of_day: TimeOfDay,
settings: Settings,
) -> Conditions:
return cls( return cls(
time_of_day=time_of_day, time_of_day=time_of_day,
start_time=cls.generate_start_time( start_time=cls.generate_start_time(
theater, day, time_of_day, settings.night_disabled theater, day, time_of_day, settings.night_disabled
), ),
weather=cls.generate_weather() weather=cls.generate_weather(),
) )
@classmethod @classmethod
def generate_start_time(cls, theater: ConflictTheater, day: datetime.date, def generate_start_time(
time_of_day: TimeOfDay, cls,
night_disabled: bool) -> datetime.datetime: theater: ConflictTheater,
day: datetime.date,
time_of_day: TimeOfDay,
night_disabled: bool,
) -> datetime.datetime:
if night_disabled: if night_disabled:
logging.info("Skip Night mission due to user settings") logging.info("Skip Night mission due to user settings")
time_range = { time_range = {
@@ -181,6 +206,7 @@ class Conditions:
Cloudy: 60, Cloudy: 60,
ClearSkies: 20, ClearSkies: 20,
} }
weather_type = random.choices(list(chances.keys()), weather_type = random.choices(
weights=list(chances.values()))[0] list(chances.keys()), weights=list(chances.values())
)[0]
return weather_type() return weather_type()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
import logging import logging
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import List, Type, Tuple from datetime import timedelta
from typing import List, Type, Tuple, Optional
from dcs.mission import Mission, StartType from dcs.mission import Mission, StartType
from dcs.planes import IL_78M, KC130, KC135MPRS, KC_135 from dcs.planes import IL_78M, KC130, KC135MPRS, KC_135
@@ -21,6 +22,7 @@ from .conflictgen import Conflict
from .radios import RadioFrequency, RadioRegistry from .radios import RadioFrequency, RadioRegistry
from .tacan import TacanBand, TacanChannel, TacanRegistry from .tacan import TacanBand, TacanChannel, TacanRegistry
TANKER_DISTANCE = 15000 TANKER_DISTANCE = 15000
TANKER_ALT = 4572 TANKER_ALT = 4572
TANKER_HEADING_OFFSET = 45 TANKER_HEADING_OFFSET = 45
@@ -32,14 +34,19 @@ AWACS_ALT = 13000
@dataclass @dataclass
class AwacsInfo: class AwacsInfo:
"""AWACS information for the kneeboard.""" """AWACS information for the kneeboard."""
dcsGroupName: str dcsGroupName: str
callsign: str callsign: str
freq: RadioFrequency freq: RadioFrequency
depature_location: Optional[str]
start_time: Optional[timedelta]
end_time: Optional[timedelta]
@dataclass @dataclass
class TankerInfo: class TankerInfo:
"""Tanker information for the kneeboard.""" """Tanker information for the kneeboard."""
dcsGroupName: str dcsGroupName: str
callsign: str callsign: str
variant: str variant: str
@@ -54,10 +61,14 @@ class AirSupport:
class AirSupportConflictGenerator: class AirSupportConflictGenerator:
def __init__(
def __init__(self, mission: Mission, conflict: Conflict, game, self,
radio_registry: RadioRegistry, mission: Mission,
tacan_registry: TacanRegistry) -> None: conflict: Conflict,
game,
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,
) -> None:
self.mission = mission self.mission = mission
self.conflict = conflict self.conflict = conflict
self.game = game self.game = game
@@ -78,22 +89,37 @@ class AirSupportConflictGenerator:
elif unit_type is KC135MPRS: elif unit_type is KC135MPRS:
return (TANKER_ALT + 500, 596) return (TANKER_ALT + 500, 596)
return (TANKER_ALT, 574) return (TANKER_ALT, 574)
def generate(self): def generate(self):
player_cp = self.conflict.from_cp if self.conflict.from_cp.captured else self.conflict.to_cp player_cp = (
self.conflict.from_cp
if self.conflict.from_cp.captured
else self.conflict.to_cp
)
fallback_tanker_number = 0 fallback_tanker_number = 0
for i, tanker_unit_type in enumerate(db.find_unittype(Refueling, self.conflict.attackers_side)): for i, tanker_unit_type in enumerate(
db.find_unittype(Refueling, self.conflict.attackers_side)
):
alt, airspeed = self._get_tanker_params(tanker_unit_type) alt, airspeed = self._get_tanker_params(tanker_unit_type)
variant = db.unit_type_name(tanker_unit_type) variant = db.unit_type_name(tanker_unit_type)
freq = self.radio_registry.alloc_uhf() freq = self.radio_registry.alloc_uhf()
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y) tacan = self.tacan_registry.alloc_for_band(TacanBand.Y)
tanker_heading = self.conflict.to_cp.position.heading_between_point(self.conflict.from_cp.position) + TANKER_HEADING_OFFSET * i tanker_heading = (
tanker_position = player_cp.position.point_from_heading(tanker_heading, TANKER_DISTANCE) self.conflict.to_cp.position.heading_between_point(
self.conflict.from_cp.position
)
+ TANKER_HEADING_OFFSET * i
)
tanker_position = player_cp.position.point_from_heading(
tanker_heading, TANKER_DISTANCE
)
tanker_group = self.mission.refuel_flight( tanker_group = self.mission.refuel_flight(
country=self.mission.country(self.game.player_country), country=self.mission.country(self.game.player_country),
name=namegen.next_tanker_name(self.mission.country(self.game.player_country), tanker_unit_type), name=namegen.next_tanker_name(
self.mission.country(self.game.player_country), tanker_unit_type
),
airport=None, airport=None,
plane_type=tanker_unit_type, plane_type=tanker_unit_type,
position=tanker_position, position=tanker_position,
@@ -124,37 +150,59 @@ class AirSupportConflictGenerator:
if tanker_unit_type != IL_78M: if tanker_unit_type != IL_78M:
# Override PyDCS tacan channel. # Override PyDCS tacan channel.
tanker_group.points[0].tasks.pop() tanker_group.points[0].tasks.pop()
tanker_group.points[0].tasks.append(ActivateBeaconCommand( tanker_group.points[0].tasks.append(
tacan.number, tacan.band.value, tacan_callsign, True, ActivateBeaconCommand(
tanker_group.units[0].id, True)) tacan.number,
tacan.band.value,
tacan_callsign,
True,
tanker_group.units[0].id,
True,
)
)
tanker_group.points[0].tasks.append(SetInvisibleCommand(True)) tanker_group.points[0].tasks.append(SetInvisibleCommand(True))
tanker_group.points[0].tasks.append(SetImmortalCommand(True)) tanker_group.points[0].tasks.append(SetImmortalCommand(True))
self.air_support.tankers.append(TankerInfo(str(tanker_group.name), callsign, variant, freq, tacan)) self.air_support.tankers.append(
TankerInfo(str(tanker_group.name), callsign, variant, freq, tacan)
possible_awacs = db.find_unittype(AWACS, self.conflict.attackers_side)
if len(possible_awacs) > 0:
awacs_unit = possible_awacs[0]
freq = self.radio_registry.alloc_uhf()
awacs_flight = self.mission.awacs_flight(
country=self.mission.country(self.game.player_country),
name=namegen.next_awacs_name(self.mission.country(self.game.player_country)),
plane_type=awacs_unit,
altitude=AWACS_ALT,
airport=None,
position=self.conflict.position.random_point_within(AWACS_DISTANCE, AWACS_DISTANCE),
frequency=freq.mhz,
start_type=StartType.Warm,
) )
awacs_flight.set_frequency(freq.mhz)
awacs_flight.points[0].tasks.append(SetInvisibleCommand(True)) if not self.game.settings.disable_legacy_aewc:
awacs_flight.points[0].tasks.append(SetImmortalCommand(True)) possible_awacs = db.find_unittype(AWACS, self.conflict.attackers_side)
self.air_support.awacs.append(AwacsInfo( if len(possible_awacs) > 0:
str(awacs_flight.name), callsign_for_support_unit(awacs_flight), freq)) awacs_unit = possible_awacs[0]
else: freq = self.radio_registry.alloc_uhf()
logging.warning("No AWACS for faction")
awacs_flight = self.mission.awacs_flight(
country=self.mission.country(self.game.player_country),
name=namegen.next_awacs_name(
self.mission.country(self.game.player_country)
),
plane_type=awacs_unit,
altitude=AWACS_ALT,
airport=None,
position=self.conflict.position.random_point_within(
AWACS_DISTANCE, AWACS_DISTANCE
),
frequency=freq.mhz,
start_type=StartType.Warm,
)
awacs_flight.set_frequency(freq.mhz)
awacs_flight.points[0].tasks.append(SetInvisibleCommand(True))
awacs_flight.points[0].tasks.append(SetImmortalCommand(True))
self.air_support.awacs.append(
AwacsInfo(
dcsGroupName=str(awacs_flight.name),
callsign=callsign_for_support_unit(awacs_flight),
freq=freq,
depature_location=None,
start_time=None,
end_time=None,
)
)
else:
logging.warning("No AWACS for faction")

View File

@@ -12,9 +12,17 @@ from dcs.country import Country
from dcs.mapping import Point from dcs.mapping import Point
from dcs.planes import MQ_9_Reaper from dcs.planes import MQ_9_Reaper
from dcs.point import PointAction from dcs.point import PointAction
from dcs.task import (EPLRS, AttackGroup, ControlledTask, FireAtPoint, from dcs.task import (
GoToWaypoint, Hold, OrbitAction, SetImmortalCommand, EPLRS,
SetInvisibleCommand) AttackGroup,
ControlledTask,
FireAtPoint,
GoToWaypoint,
Hold,
OrbitAction,
SetImmortalCommand,
SetInvisibleCommand,
)
from dcs.triggers import Event, TriggerOnce from dcs.triggers import Event, TriggerOnce
from dcs.unit import Vehicle from dcs.unit import Vehicle
from dcs.unitgroup import VehicleGroup from dcs.unitgroup import VehicleGroup
@@ -24,8 +32,11 @@ from game.unitmap import UnitMap
from game.utils import heading_sum, opposite_heading from game.utils import heading_sum, opposite_heading
from game.theater.controlpoint import ControlPoint from game.theater.controlpoint import ControlPoint
from gen.ground_forces.ai_ground_planner import (DISTANCE_FROM_FRONTLINE, from gen.ground_forces.ai_ground_planner import (
CombatGroup, CombatGroupRole) DISTANCE_FROM_FRONTLINE,
CombatGroup,
CombatGroupRole,
)
from .callsigns import callsign_for_support_unit from .callsigns import callsign_for_support_unit
from .conflictgen import Conflict from .conflictgen import Conflict
@@ -56,6 +67,7 @@ INFANTRY_GROUP_SIZE = 5
@dataclass(frozen=True) @dataclass(frozen=True)
class JtacInfo: class JtacInfo:
"""JTAC information.""" """JTAC information."""
dcsGroupName: str dcsGroupName: str
unit_name: str unit_name: str
callsign: str callsign: str
@@ -65,16 +77,16 @@ class JtacInfo:
class GroundConflictGenerator: class GroundConflictGenerator:
def __init__( def __init__(
self, self,
mission: Mission, mission: Mission,
conflict: Conflict, conflict: Conflict,
game: Game, game: Game,
player_planned_combat_groups: List[CombatGroup], player_planned_combat_groups: List[CombatGroup],
enemy_planned_combat_groups: List[CombatGroup], enemy_planned_combat_groups: List[CombatGroup],
player_stance: CombatStance, player_stance: CombatStance,
unit_map: UnitMap) -> None: unit_map: UnitMap,
) -> None:
self.mission = mission self.mission = mission
self.conflict = conflict self.conflict = conflict
self.enemy_planned_combat_groups = enemy_planned_combat_groups self.enemy_planned_combat_groups = enemy_planned_combat_groups
@@ -87,14 +99,16 @@ class GroundConflictGenerator:
def _enemy_stance(self): def _enemy_stance(self):
"""Picks the enemy stance according to the number of planned groups on the frontline for each side""" """Picks the enemy stance according to the number of planned groups on the frontline for each side"""
if len(self.enemy_planned_combat_groups) > len(self.player_planned_combat_groups): if len(self.enemy_planned_combat_groups) > len(
self.player_planned_combat_groups
):
return random.choice( return random.choice(
[ [
CombatStance.AGGRESSIVE, CombatStance.AGGRESSIVE,
CombatStance.AGGRESSIVE, CombatStance.AGGRESSIVE,
CombatStance.AGGRESSIVE, CombatStance.AGGRESSIVE,
CombatStance.ELIMINATION, CombatStance.ELIMINATION,
CombatStance.BREAKTHROUGH CombatStance.BREAKTHROUGH,
] ]
) )
else: else:
@@ -104,31 +118,37 @@ class GroundConflictGenerator:
CombatStance.DEFENSIVE, CombatStance.DEFENSIVE,
CombatStance.DEFENSIVE, CombatStance.DEFENSIVE,
CombatStance.AMBUSH, CombatStance.AMBUSH,
CombatStance.AGGRESSIVE CombatStance.AGGRESSIVE,
] ]
) )
@staticmethod @staticmethod
def _group_point(point: Point, base_distance) -> Point: def _group_point(point: Point, base_distance) -> Point:
distance = random.randint( distance = random.randint(
int(base_distance * SPREAD_DISTANCE_FACTOR[0]), int(base_distance * SPREAD_DISTANCE_FACTOR[0]),
int(base_distance * SPREAD_DISTANCE_FACTOR[1]), int(base_distance * SPREAD_DISTANCE_FACTOR[1]),
) )
return point.random_point_within(distance, base_distance * SPREAD_DISTANCE_SIZE_FACTOR) return point.random_point_within(
distance, base_distance * SPREAD_DISTANCE_SIZE_FACTOR
)
def generate(self): def generate(self):
position = Conflict.frontline_position(self.conflict.from_cp, self.conflict.to_cp, self.game.theater) position = Conflict.frontline_position(
self.conflict.from_cp, self.conflict.to_cp, self.game.theater
)
frontline_vector = Conflict.frontline_vector( frontline_vector = Conflict.frontline_vector(
self.conflict.from_cp, self.conflict.from_cp, self.conflict.to_cp, self.game.theater
self.conflict.to_cp, )
self.game.theater
)
# Create player groups at random position # Create player groups at random position
player_groups = self._generate_groups(self.player_planned_combat_groups, frontline_vector, True) player_groups = self._generate_groups(
self.player_planned_combat_groups, frontline_vector, True
)
# Create enemy groups at random position # Create enemy groups at random position
enemy_groups = self._generate_groups(self.enemy_planned_combat_groups, frontline_vector, False) enemy_groups = self._generate_groups(
self.enemy_planned_combat_groups, frontline_vector, False
)
# Plan combat actions for groups # Plan combat actions for groups
self.plan_action_for_groups( self.plan_action_for_groups(
@@ -137,7 +157,7 @@ class GroundConflictGenerator:
enemy_groups, enemy_groups,
self.conflict.heading + 90, self.conflict.heading + 90,
self.conflict.from_cp, self.conflict.from_cp,
self.conflict.to_cp self.conflict.to_cp,
) )
self.plan_action_for_groups( self.plan_action_for_groups(
self.enemy_stance, self.enemy_stance,
@@ -145,7 +165,7 @@ class GroundConflictGenerator:
player_groups, player_groups,
self.conflict.heading - 90, self.conflict.heading - 90,
self.conflict.to_cp, self.conflict.to_cp,
self.conflict.from_cp self.conflict.from_cp,
) )
# Add JTAC # Add JTAC
@@ -157,34 +177,38 @@ class GroundConflictGenerator:
if self.game.player_faction.jtac_unit is not None: if self.game.player_faction.jtac_unit is not None:
utype = self.game.player_faction.jtac_unit utype = self.game.player_faction.jtac_unit
jtac = self.mission.flight_group(country=self.mission.country(self.game.player_country), jtac = self.mission.flight_group(
name=n, country=self.mission.country(self.game.player_country),
aircraft_type=utype, name=n,
position=position[0], aircraft_type=utype,
airport=None, position=position[0],
altitude=5000) airport=None,
altitude=5000,
)
jtac.points[0].tasks.append(SetInvisibleCommand(True)) jtac.points[0].tasks.append(SetInvisibleCommand(True))
jtac.points[0].tasks.append(SetImmortalCommand(True)) jtac.points[0].tasks.append(SetImmortalCommand(True))
jtac.points[0].tasks.append(OrbitAction(5000, 300, OrbitAction.OrbitPattern.Circle)) jtac.points[0].tasks.append(
frontline = f"Frontline {self.conflict.from_cp.name}/{self.conflict.to_cp.name}" OrbitAction(5000, 300, OrbitAction.OrbitPattern.Circle)
)
frontline = (
f"Frontline {self.conflict.from_cp.name}/{self.conflict.to_cp.name}"
)
# Note: Will need to change if we ever add ground based JTAC. # Note: Will need to change if we ever add ground based JTAC.
callsign = callsign_for_support_unit(jtac) callsign = callsign_for_support_unit(jtac)
self.jtacs.append(JtacInfo(str(jtac.name), n, callsign, frontline, str(code))) self.jtacs.append(
JtacInfo(str(jtac.name), n, callsign, frontline, str(code))
)
def gen_infantry_group_for_group( def gen_infantry_group_for_group(
self, self, group: VehicleGroup, is_player: bool, side: Country, forward_heading: int
group: VehicleGroup,
is_player: bool,
side: Country,
forward_heading: int
) -> None: ) -> None:
infantry_position = self.conflict.find_ground_position( infantry_position = self.conflict.find_ground_position(
group.points[0].position.random_point_within(250, 50), group.points[0].position.random_point_within(250, 50),
500, 500,
forward_heading, forward_heading,
self.conflict.theater self.conflict.theater,
) )
if not infantry_position: if not infantry_position:
logging.warning("Could not find infantry position") logging.warning("Could not find infantry position")
return return
@@ -208,44 +232,50 @@ class GroundConflictGenerator:
u = random.choice(manpads) u = random.choice(manpads)
self.mission.vehicle_group( self.mission.vehicle_group(
side, side,
namegen.next_infantry_name(side, cp.id, u), u, namegen.next_infantry_name(side, cp.id, u),
u,
position=infantry_position, position=infantry_position,
group_size=1, group_size=1,
heading=forward_heading, heading=forward_heading,
move_formation=PointAction.OffRoad) move_formation=PointAction.OffRoad,
)
return return
possible_infantry_units = db.find_infantry(faction, allow_manpad=self.game.settings.manpads) possible_infantry_units = db.find_infantry(
faction, allow_manpad=self.game.settings.manpads
)
if len(possible_infantry_units) == 0: if len(possible_infantry_units) == 0:
return return
u = random.choice(possible_infantry_units) u = random.choice(possible_infantry_units)
self.mission.vehicle_group( self.mission.vehicle_group(
side, side,
namegen.next_infantry_name(side, cp.id, u), u, namegen.next_infantry_name(side, cp.id, u),
position=infantry_position, u,
group_size=1, position=infantry_position,
heading=forward_heading, group_size=1,
move_formation=PointAction.OffRoad) heading=forward_heading,
move_formation=PointAction.OffRoad,
)
for i in range(INFANTRY_GROUP_SIZE): for i in range(INFANTRY_GROUP_SIZE):
u = random.choice(possible_infantry_units) u = random.choice(possible_infantry_units)
position = infantry_position.random_point_within(55, 5) position = infantry_position.random_point_within(55, 5)
self.mission.vehicle_group( self.mission.vehicle_group(
side, side,
namegen.next_infantry_name(side, cp.id, u), u, namegen.next_infantry_name(side, cp.id, u),
u,
position=position, position=position,
group_size=1, group_size=1,
heading=forward_heading, heading=forward_heading,
move_formation=PointAction.OffRoad) move_formation=PointAction.OffRoad,
)
def _set_reform_waypoint( def _set_reform_waypoint(
self, self, dcs_group: VehicleGroup, forward_heading: int
dcs_group: VehicleGroup,
forward_heading: int
) -> None: ) -> None:
"""Setting a waypoint close to the spawn position allows the group to reform gracefully """Setting a waypoint close to the spawn position allows the group to reform gracefully
rather than spin rather than spin
""" """
reform_point = dcs_group.position.point_from_heading(forward_heading, 50) reform_point = dcs_group.position.point_from_heading(forward_heading, 50)
dcs_group.add_waypoint(reform_point) dcs_group.add_waypoint(reform_point)
@@ -256,7 +286,7 @@ class GroundConflictGenerator:
gen_group: CombatGroup, gen_group: CombatGroup,
dcs_group: VehicleGroup, dcs_group: VehicleGroup,
forward_heading: int, forward_heading: int,
target: Point target: Point,
) -> bool: ) -> bool:
""" """
Handles adding the DCS tasks for artillery groups for all combat stances. Handles adding the DCS tasks for artillery groups for all combat stances.
@@ -269,7 +299,9 @@ class GroundConflictGenerator:
dcs_group.add_trigger_action(hold_task) dcs_group.add_trigger_action(hold_task)
# Artillery strike random start # Artillery strike random start
artillery_trigger = TriggerOnce(Event.NoEvent, "ArtilleryFireTask #" + str(dcs_group.id)) artillery_trigger = TriggerOnce(
Event.NoEvent, "ArtilleryFireTask #" + str(dcs_group.id)
)
artillery_trigger.add_condition(TimeAfter(seconds=random.randint(1, 45) * 60)) artillery_trigger.add_condition(TimeAfter(seconds=random.randint(1, 45) * 60))
# TODO: Update to fire at group instead of point # TODO: Update to fire at group instead of point
fire_task = FireAtPoint(target, len(gen_group.units) * 10, 100) fire_task = FireAtPoint(target, len(gen_group.units) * 10, 100)
@@ -283,12 +315,19 @@ class GroundConflictGenerator:
# Hold position # Hold position
dcs_group.points[1].tasks.append(Hold()) dcs_group.points[1].tasks.append(Hold())
retreat = self.find_retreat_point(dcs_group, forward_heading, (int)(RETREAT_DISTANCE/3)) retreat = self.find_retreat_point(
dcs_group.add_waypoint(dcs_group.position.point_from_heading(forward_heading, 1), PointAction.OffRoad) 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.points[2].tasks.append(Hold())
dcs_group.add_waypoint(retreat, PointAction.OffRoad) dcs_group.add_waypoint(retreat, PointAction.OffRoad)
artillery_fallback = TriggerOnce(Event.NoEvent, "ArtilleryRetreat #" + str(dcs_group.id)) artillery_fallback = TriggerOnce(
Event.NoEvent, "ArtilleryRetreat #" + str(dcs_group.id)
)
for i, u in enumerate(dcs_group.units): for i, u in enumerate(dcs_group.units):
artillery_fallback.add_condition(UnitDamaged(u.id)) artillery_fallback.add_condition(UnitDamaged(u.id))
if i < len(dcs_group.units) - 1: if i < len(dcs_group.units) - 1:
@@ -302,7 +341,9 @@ class GroundConflictGenerator:
retreat_task.number = 4 retreat_task.number = 4
dcs_group.add_trigger_action(retreat_task) dcs_group.add_trigger_action(retreat_task)
artillery_fallback.add_action(AITaskPush(dcs_group.id, len(dcs_group.tasks))) artillery_fallback.add_action(
AITaskPush(dcs_group.id, len(dcs_group.tasks))
)
self.mission.triggerrules.triggers.append(artillery_fallback) self.mission.triggerrules.triggers.append(artillery_fallback)
for u in dcs_group.units: for u in dcs_group.units:
@@ -330,12 +371,8 @@ class GroundConflictGenerator:
target = self.find_nearest_enemy_group(dcs_group, enemy_groups) target = self.find_nearest_enemy_group(dcs_group, enemy_groups)
if target is not None: if target is not None:
rand_offset = Point( rand_offset = Point(
random.randint( random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK),
-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK),
),
random.randint(
-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK
)
) )
target_point = self.conflict.theater.nearest_land_pos( target_point = self.conflict.theater.nearest_land_pos(
target.points[0].position + rand_offset target.points[0].position + rand_offset
@@ -345,8 +382,7 @@ class GroundConflictGenerator:
if ( if (
to_cp.position.distance_to_point(dcs_group.points[0].position) to_cp.position.distance_to_point(dcs_group.points[0].position)
<= <= AGGRESIVE_MOVE_DISTANCE
AGGRESIVE_MOVE_DISTANCE
): ):
attack_point = self.conflict.theater.nearest_land_pos( attack_point = self.conflict.theater.nearest_land_pos(
to_cp.position.random_point_within(500, 0) to_cp.position.random_point_within(500, 0)
@@ -358,16 +394,16 @@ class GroundConflictGenerator:
if offset_heading < 0: if offset_heading < 0:
offset_heading = 358 offset_heading = 358
attack_point = self.find_offensive_point( attack_point = self.find_offensive_point(
dcs_group, dcs_group, offset_heading, AGGRESIVE_MOVE_DISTANCE
offset_heading,
AGGRESIVE_MOVE_DISTANCE
) )
dcs_group.add_waypoint(attack_point, PointAction.OffRoad) dcs_group.add_waypoint(attack_point, PointAction.OffRoad)
elif stance == CombatStance.BREAKTHROUGH: elif stance == CombatStance.BREAKTHROUGH:
# In breakthrough mode, the units will move forward # In breakthrough mode, the units will move forward
# If the enemy base is close enough, the units will attack the base # If the enemy base is close enough, the units will attack the base
if to_cp.position.distance_to_point( if (
dcs_group.points[0].position) <= BREAKTHROUGH_OFFENSIVE_DISTANCE: to_cp.position.distance_to_point(dcs_group.points[0].position)
<= BREAKTHROUGH_OFFENSIVE_DISTANCE
):
attack_point = self.conflict.theater.nearest_land_pos( attack_point = self.conflict.theater.nearest_land_pos(
to_cp.position.random_point_within(500, 0) to_cp.position.random_point_within(500, 0)
) )
@@ -377,27 +413,27 @@ class GroundConflictGenerator:
offset_heading = forward_heading - 1 offset_heading = forward_heading - 1
if offset_heading < 0: if offset_heading < 0:
offset_heading = 359 offset_heading = 359
attack_point = self.find_offensive_point(dcs_group, offset_heading, BREAKTHROUGH_OFFENSIVE_DISTANCE) attack_point = self.find_offensive_point(
dcs_group, offset_heading, BREAKTHROUGH_OFFENSIVE_DISTANCE
)
dcs_group.add_waypoint(attack_point, PointAction.OffRoad) dcs_group.add_waypoint(attack_point, PointAction.OffRoad)
elif stance == CombatStance.ELIMINATION: elif stance == CombatStance.ELIMINATION:
# In elimination mode, the units focus on destroying as much enemy groups as possible # In elimination mode, the units focus on destroying as much enemy groups as possible
targets = self.find_n_nearest_enemy_groups(dcs_group, enemy_groups, 3) targets = self.find_n_nearest_enemy_groups(dcs_group, enemy_groups, 3)
for i, target in enumerate(targets, start=1): for i, target in enumerate(targets, start=1):
rand_offset = Point( rand_offset = Point(
random.randint( random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK),
-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK),
),
random.randint(
-RANDOM_OFFSET_ATTACK,
RANDOM_OFFSET_ATTACK
)
) )
target_point = self.conflict.theater.nearest_land_pos( target_point = self.conflict.theater.nearest_land_pos(
target.points[0].position+rand_offset target.points[0].position + rand_offset
) )
dcs_group.add_waypoint(target_point, PointAction.OffRoad) dcs_group.add_waypoint(target_point, PointAction.OffRoad)
dcs_group.points[i + 1].tasks.append(AttackGroup(target.id)) dcs_group.points[i + 1].tasks.append(AttackGroup(target.id))
if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE: if (
to_cp.position.distance_to_point(dcs_group.points[0].position)
<= AGGRESIVE_MOVE_DISTANCE
):
attack_point = self.conflict.theater.nearest_land_pos( attack_point = self.conflict.theater.nearest_land_pos(
to_cp.position.random_point_within(500, 0) to_cp.position.random_point_within(500, 0)
) )
@@ -420,12 +456,23 @@ class GroundConflictGenerator:
Returns True if tasking was added, returns False if the stance was not a combat stance. Returns True if tasking was added, returns False if the stance was not a combat stance.
""" """
self._set_reform_waypoint(dcs_group, forward_heading) self._set_reform_waypoint(dcs_group, forward_heading)
if stance in [CombatStance.AGGRESSIVE, CombatStance.BREAKTHROUGH, CombatStance.ELIMINATION]: if stance in [
CombatStance.AGGRESSIVE,
CombatStance.BREAKTHROUGH,
CombatStance.ELIMINATION,
]:
# APC & ATGM will never move too much forward, but will follow along any offensive # APC & ATGM will never move too much forward, but will follow along any offensive
if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE: if (
attack_point = self.conflict.theater.nearest_land_pos(to_cp.position.random_point_within(500, 0)) to_cp.position.distance_to_point(dcs_group.points[0].position)
<= AGGRESIVE_MOVE_DISTANCE
):
attack_point = self.conflict.theater.nearest_land_pos(
to_cp.position.random_point_within(500, 0)
)
else: else:
attack_point = self.find_offensive_point(dcs_group, forward_heading, AGGRESIVE_MOVE_DISTANCE) attack_point = self.find_offensive_point(
dcs_group, forward_heading, AGGRESIVE_MOVE_DISTANCE
)
dcs_group.add_waypoint(attack_point, PointAction.OffRoad) dcs_group.add_waypoint(attack_point, PointAction.OffRoad)
if stance != CombatStance.RETREAT: if stance != CombatStance.RETREAT:
@@ -434,29 +481,36 @@ class GroundConflictGenerator:
return False return False
def plan_action_for_groups( def plan_action_for_groups(
self, stance: CombatStance, self,
stance: CombatStance,
ally_groups: List[Tuple[VehicleGroup, CombatGroup]], ally_groups: List[Tuple[VehicleGroup, CombatGroup]],
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]], enemy_groups: List[Tuple[VehicleGroup, CombatGroup]],
forward_heading: int, forward_heading: int,
from_cp: ControlPoint, from_cp: ControlPoint,
to_cp: ControlPoint to_cp: ControlPoint,
) -> None: ) -> None:
if not self.game.settings.perf_moving_units: if not self.game.settings.perf_moving_units:
return return
for dcs_group, group in ally_groups: for dcs_group, group in ally_groups:
if hasattr(group.units[0], 'eplrs') and group.units[0].eplrs: if hasattr(group.units[0], "eplrs") and group.units[0].eplrs:
dcs_group.points[0].tasks.append(EPLRS(dcs_group.id)) dcs_group.points[0].tasks.append(EPLRS(dcs_group.id))
if group.role == CombatGroupRole.ARTILLERY: if group.role == CombatGroupRole.ARTILLERY:
if self.game.settings.perf_artillery: if self.game.settings.perf_artillery:
target = self.get_artillery_target_in_range(dcs_group, group, enemy_groups) target = self.get_artillery_target_in_range(
dcs_group, group, enemy_groups
)
if target is not None: if target is not None:
self._plan_artillery_action(stance, group, dcs_group, forward_heading, target) self._plan_artillery_action(
stance, group, dcs_group, forward_heading, target
)
elif group.role in [CombatGroupRole.TANK, CombatGroupRole.IFV]: elif group.role in [CombatGroupRole.TANK, CombatGroupRole.IFV]:
self._plan_tank_ifv_action(stance, enemy_groups, dcs_group, forward_heading, to_cp) self._plan_tank_ifv_action(
stance, enemy_groups, dcs_group, forward_heading, to_cp
)
elif group.role in [CombatGroupRole.APC, CombatGroupRole.ATGM]: elif group.role in [CombatGroupRole.APC, CombatGroupRole.ATGM]:
self._plan_apc_atgm_action(stance, dcs_group, forward_heading, to_cp) self._plan_apc_atgm_action(stance, dcs_group, forward_heading, to_cp)
@@ -464,11 +518,16 @@ class GroundConflictGenerator:
if stance == CombatStance.RETREAT: if stance == CombatStance.RETREAT:
# In retreat mode, the units will fall back # In retreat mode, the units will fall back
# If the ally base is close enough, the units will even regroup there # If the ally base is close enough, the units will even regroup there
if from_cp.position.distance_to_point(dcs_group.points[0].position) <= RETREAT_DISTANCE: if (
from_cp.position.distance_to_point(dcs_group.points[0].position)
<= RETREAT_DISTANCE
):
retreat_point = from_cp.position.random_point_within(500, 250) retreat_point = from_cp.position.random_point_within(500, 250)
else: else:
retreat_point = self.find_retreat_point(dcs_group, forward_heading) retreat_point = self.find_retreat_point(dcs_group, forward_heading)
reposition_point = retreat_point.point_from_heading(forward_heading, 10) # Another point to make the unit face the enemy reposition_point = retreat_point.point_from_heading(
forward_heading, 10
) # Another point to make the unit face the enemy
dcs_group.add_waypoint(retreat_point, PointAction.OffRoad) dcs_group.add_waypoint(retreat_point, PointAction.OffRoad)
dcs_group.add_waypoint(reposition_point, PointAction.OffRoad) dcs_group.add_waypoint(reposition_point, PointAction.OffRoad)
@@ -490,8 +549,10 @@ class GroundConflictGenerator:
# We add a new retreat waypoint # We add a new retreat waypoint
dcs_group.add_waypoint( dcs_group.add_waypoint(
self.find_retreat_point(dcs_group, forward_heading, (int)(RETREAT_DISTANCE / 8)), self.find_retreat_point(
PointAction.OffRoad dcs_group, forward_heading, (int)(RETREAT_DISTANCE / 8)
),
PointAction.OffRoad,
) )
# Fallback task # Fallback task
@@ -515,7 +576,7 @@ class GroundConflictGenerator:
self, self,
dcs_group: VehicleGroup, dcs_group: VehicleGroup,
frontline_heading: int, frontline_heading: int,
distance: int = RETREAT_DISTANCE distance: int = RETREAT_DISTANCE,
) -> Point: ) -> Point:
""" """
Find a point to retreat to Find a point to retreat to
@@ -523,17 +584,15 @@ class GroundConflictGenerator:
:param frontline_heading: Heading of the frontline :param frontline_heading: Heading of the frontline
:return: dcs.mapping.Point object with the desired position :return: dcs.mapping.Point object with the desired position
""" """
desired_point = dcs_group.points[0].position.point_from_heading(heading_sum(frontline_heading, +180), distance) desired_point = dcs_group.points[0].position.point_from_heading(
heading_sum(frontline_heading, +180), distance
)
if self.conflict.theater.is_on_land(desired_point): if self.conflict.theater.is_on_land(desired_point):
return desired_point return desired_point
return self.conflict.theater.nearest_land_pos(desired_point) return self.conflict.theater.nearest_land_pos(desired_point)
def find_offensive_point( def find_offensive_point(
self, self, dcs_group: VehicleGroup, frontline_heading: int, distance: int
dcs_group: VehicleGroup,
frontline_heading: int,
distance: int
) -> Point: ) -> Point:
""" """
Find a point to attack Find a point to attack
@@ -542,7 +601,9 @@ class GroundConflictGenerator:
:param distance: Distance of the offensive (how far unit should move) :param distance: Distance of the offensive (how far unit should move)
:return: dcs.mapping.Point object with the desired position :return: dcs.mapping.Point object with the desired position
""" """
desired_point = dcs_group.points[0].position.point_from_heading(frontline_heading, distance) desired_point = dcs_group.points[0].position.point_from_heading(
frontline_heading, distance
)
if self.conflict.theater.is_on_land(desired_point): if self.conflict.theater.is_on_land(desired_point):
return desired_point return desired_point
return self.conflict.theater.nearest_land_pos(desired_point) return self.conflict.theater.nearest_land_pos(desired_point)
@@ -551,7 +612,7 @@ class GroundConflictGenerator:
def find_n_nearest_enemy_groups( def find_n_nearest_enemy_groups(
player_group: VehicleGroup, player_group: VehicleGroup,
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]], enemy_groups: List[Tuple[VehicleGroup, CombatGroup]],
n: int n: int,
) -> List[VehicleGroup]: ) -> List[VehicleGroup]:
""" """
Return the nearest enemy group for the player group Return the nearest enemy group for the player group
@@ -562,7 +623,9 @@ class GroundConflictGenerator:
targets = [] # type: List[Optional[VehicleGroup]] targets = [] # type: List[Optional[VehicleGroup]]
sorted_list = sorted( sorted_list = sorted(
enemy_groups, enemy_groups,
key=lambda group: player_group.points[0].position.distance_to_point(group[0].points[0].position) key=lambda group: player_group.points[0].position.distance_to_point(
group[0].points[0].position
),
) )
for i in range(n): for i in range(n):
# TODO: Is this supposed to return no groups if enemy_groups is less than n? # TODO: Is this supposed to return no groups if enemy_groups is less than n?
@@ -574,8 +637,7 @@ class GroundConflictGenerator:
@staticmethod @staticmethod
def find_nearest_enemy_group( def find_nearest_enemy_group(
player_group: VehicleGroup, player_group: VehicleGroup, enemy_groups: List[Tuple[VehicleGroup, CombatGroup]]
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]]
) -> Optional[VehicleGroup]: ) -> Optional[VehicleGroup]:
""" """
Search the enemy groups for a potential target suitable to armored assault Search the enemy groups for a potential target suitable to armored assault
@@ -585,7 +647,9 @@ class GroundConflictGenerator:
min_distance = 99999999 min_distance = 99999999
target = None target = None
for dcs_group, _ in enemy_groups: for dcs_group, _ in enemy_groups:
dist = player_group.points[0].position.distance_to_point(dcs_group.points[0].position) dist = player_group.points[0].position.distance_to_point(
dcs_group.points[0].position
)
if dist < min_distance: if dist < min_distance:
min_distance = dist min_distance = dist
target = dcs_group target = dcs_group
@@ -595,7 +659,7 @@ class GroundConflictGenerator:
def get_artillery_target_in_range( def get_artillery_target_in_range(
dcs_group: VehicleGroup, dcs_group: VehicleGroup,
group: CombatGroup, group: CombatGroup,
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]] enemy_groups: List[Tuple[VehicleGroup, CombatGroup]],
) -> Optional[Point]: ) -> Optional[Point]:
""" """
Search the enemy groups for a potential target suitable to an artillery unit Search the enemy groups for a potential target suitable to an artillery unit
@@ -606,7 +670,9 @@ class GroundConflictGenerator:
return None return None
for _ in range(10): for _ in range(10):
potential_target = random.choice(enemy_groups)[0] potential_target = random.choice(enemy_groups)[0]
distance_to_target = dcs_group.points[0].position.distance_to_point(potential_target.points[0].position) distance_to_target = dcs_group.points[0].position.distance_to_point(
potential_target.points[0].position
)
if distance_to_target < rng: if distance_to_target < rng:
return potential_target.points[0].position return potential_target.points[0].position
return None return None
@@ -620,12 +686,12 @@ class GroundConflictGenerator:
if rg > DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]: if rg > DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]:
rg = random.randint( rg = random.randint(
DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][0], DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][0],
DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1] DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1],
) )
elif rg < DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]: elif rg < DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]:
rg = random.randint( rg = random.randint(
DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK][0], DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK][0],
DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK][1] DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK][1],
) )
return rg return rg
@@ -635,42 +701,46 @@ class GroundConflictGenerator:
combat_width: int, combat_width: int,
distance_from_frontline: int, distance_from_frontline: int,
heading: int, heading: int,
spawn_heading: int spawn_heading: int,
): ):
shifted = conflict_position.point_from_heading(heading, random.randint(0, combat_width)) shifted = conflict_position.point_from_heading(
desired_point = shifted.point_from_heading( heading, random.randint(0, combat_width)
spawn_heading, )
distance_from_frontline desired_point = shifted.point_from_heading(
spawn_heading, distance_from_frontline
)
return Conflict.find_ground_position(
desired_point, combat_width, heading, self.conflict.theater
) )
return Conflict.find_ground_position(desired_point, combat_width, heading, self.conflict.theater)
def _generate_groups( def _generate_groups(
self, self,
groups: List[CombatGroup], groups: List[CombatGroup],
frontline_vector: Tuple[Point, int, int], frontline_vector: Tuple[Point, int, int],
is_player: bool is_player: bool,
) -> List[Tuple[VehicleGroup, CombatGroup]]: ) -> List[Tuple[VehicleGroup, CombatGroup]]:
"""Finds valid positions for planned groups and generates a pydcs group for them""" """Finds valid positions for planned groups and generates a pydcs group for them"""
positioned_groups = [] positioned_groups = []
position, heading, combat_width = frontline_vector position, heading, combat_width = frontline_vector
spawn_heading = int(heading_sum(heading, -90)) if is_player else int(heading_sum(heading, 90)) spawn_heading = (
int(heading_sum(heading, -90))
if is_player
else int(heading_sum(heading, 90))
)
country = self.game.player_country if is_player else self.game.enemy_country country = self.game.player_country if is_player else self.game.enemy_country
for group in groups: for group in groups:
if group.role == CombatGroupRole.ARTILLERY: if group.role == CombatGroupRole.ARTILLERY:
distance_from_frontline = self.get_artilery_group_distance_from_frontline(group) distance_from_frontline = (
self.get_artilery_group_distance_from_frontline(group)
)
else: else:
distance_from_frontline = random.randint( distance_from_frontline = random.randint(
DISTANCE_FROM_FRONTLINE[group.role][0], DISTANCE_FROM_FRONTLINE[group.role][0],
DISTANCE_FROM_FRONTLINE[group.role][1] DISTANCE_FROM_FRONTLINE[group.role][1],
) )
final_position = self.get_valid_position_for_group( final_position = self.get_valid_position_for_group(
position, position, combat_width, distance_from_frontline, heading, spawn_heading
combat_width,
distance_from_frontline,
heading,
spawn_heading
) )
if final_position is not None: if final_position is not None:
@@ -693,7 +763,7 @@ class GroundConflictGenerator:
g, g,
is_player, is_player,
self.mission.country(country), self.mission.country(country),
opposite_heading(spawn_heading) opposite_heading(spawn_heading),
) )
else: else:
logging.warning(f"Unable to get valid position for {group}") logging.warning(f"Unable to get valid position for {group}")
@@ -718,12 +788,14 @@ class GroundConflictGenerator:
logging.info("armorgen: {} for {}".format(unit, side.id)) logging.info("armorgen: {} for {}".format(unit, side.id))
group = self.mission.vehicle_group( group = self.mission.vehicle_group(
side, side,
namegen.next_unit_name(side, cp.id, unit), unit, namegen.next_unit_name(side, cp.id, unit),
position=at, unit,
group_size=count, position=at,
heading=heading, group_size=count,
move_formation=move_formation) heading=heading,
move_formation=move_formation,
)
self.unit_map.add_front_line_units(group, cp) self.unit_map.add_front_line_units(group, cp)

View File

@@ -96,7 +96,8 @@ class Package:
if tot is None: if tot is None:
logging.error( logging.error(
f"{flight} requested escort at {waypoint} but that " f"{flight} requested escort at {waypoint} but that "
"waypoint has no TOT. It may not be escorted.") "waypoint has no TOT. It may not be escorted."
)
continue continue
times.append(tot) times.append(tot)
if times: if times:
@@ -117,7 +118,8 @@ class Package:
logging.error( logging.error(
f"{flight} dismissed escort at {waypoint} but that " f"{flight} dismissed escort at {waypoint} but that "
"waypoint has no TOT or departure time. It may not be " "waypoint has no TOT or departure time. It may not be "
"escorted.") "escorted."
)
continue continue
times.append(tot) times.append(tot)
if times: if times:
@@ -173,6 +175,7 @@ class Package:
FlightType.SEAD, FlightType.SEAD,
FlightType.TARCAP, FlightType.TARCAP,
FlightType.BARCAP, FlightType.BARCAP,
FlightType.AEWC,
FlightType.SWEEP, FlightType.SWEEP,
FlightType.ESCORT, FlightType.ESCORT,
] ]

View File

@@ -20,12 +20,15 @@ from .ground_forces.combat_stance import CombatStance
from .radios import RadioFrequency from .radios import RadioFrequency
from .runways import RunwayData from .runways import RunwayData
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
@dataclass @dataclass
class CommInfo: class CommInfo:
"""Communications information for the kneeboard.""" """Communications information for the kneeboard."""
name: str name: str
freq: RadioFrequency freq: RadioFrequency
@@ -37,10 +40,13 @@ class FrontLineInfo:
self.enemy_base: ControlPoint = front_line.control_point_b self.enemy_base: ControlPoint = front_line.control_point_b
self.player_zero: bool = self.player_base.base.total_armor == 0 self.player_zero: bool = self.player_base.base.total_armor == 0
self.enemy_zero: bool = self.enemy_base.base.total_armor == 0 self.enemy_zero: bool = self.enemy_base.base.total_armor == 0
self.advantage: bool = self.player_base.base.total_armor > self.enemy_base.base.total_armor self.advantage: bool = (
self.player_base.base.total_armor > self.enemy_base.base.total_armor
)
self.stance: CombatStance = self.player_base.stances[self.enemy_base.id] self.stance: CombatStance = self.player_base.stances[self.enemy_base.id]
self.combat_stances = CombatStance self.combat_stances = CombatStance
class MissionInfoGenerator: class MissionInfoGenerator:
"""Base type for generators of mission information for the player. """Base type for generators of mission information for the player.
@@ -131,7 +137,6 @@ def format_waypoint_time(waypoint: FlightWaypoint, depart_prefix: str) -> str:
class BriefingGenerator(MissionInfoGenerator): class BriefingGenerator(MissionInfoGenerator):
def __init__(self, mission: Mission, game: Game): def __init__(self, mission: Mission, game: Game):
super().__init__(mission, game) super().__init__(mission, game)
self.allied_flights_by_departure: Dict[str, List[FlightData]] = {} self.allied_flights_by_departure: Dict[str, List[FlightData]] = {}
@@ -141,36 +146,36 @@ class BriefingGenerator(MissionInfoGenerator):
disabled_extensions=("",), disabled_extensions=("",),
default_for_string=True, default_for_string=True,
default=True, default=True,
), ),
trim_blocks=True, trim_blocks=True,
lstrip_blocks=True, lstrip_blocks=True,
) )
env.filters["waypoint_timing"] = format_waypoint_time env.filters["waypoint_timing"] = format_waypoint_time
self.template = env.get_template("briefingtemplate_EN.j2") self.template = env.get_template("briefingtemplate_EN.j2")
def generate(self) -> None: def generate(self) -> None:
"""Generate the mission briefing """Generate the mission briefing"""
"""
self._generate_frontline_info() self._generate_frontline_info()
self.generate_allied_flights_by_departure() self.generate_allied_flights_by_departure()
self.mission.set_description_text(self.template.render(vars(self))) self.mission.set_description_text(self.template.render(vars(self)))
self.mission.add_picture_blue(os.path.abspath( self.mission.add_picture_blue(
"./resources/ui/splash_screen.png")) os.path.abspath("./resources/ui/splash_screen.png")
)
def _generate_frontline_info(self) -> None: def _generate_frontline_info(self) -> None:
"""Build FrontLineInfo objects from FrontLine type and append to briefing. """Build FrontLineInfo objects from FrontLine type and append to briefing."""
"""
for front_line in self.game.theater.conflicts(from_player=True): for front_line in self.game.theater.conflicts(from_player=True):
self.add_frontline(FrontLineInfo(front_line)) self.add_frontline(FrontLineInfo(front_line))
# TODO: This should determine if runway is friendly through a method more robust than the existing string match # TODO: This should determine if runway is friendly through a method more robust than the existing string match
def generate_allied_flights_by_departure(self) -> None: def generate_allied_flights_by_departure(self) -> None:
"""Create iterable to display allied flights grouped by departure airfield. """Create iterable to display allied flights grouped by departure airfield."""
"""
for flight in self.flights: for flight in self.flights:
if not flight.client_units and flight.friendly: if not flight.client_units and flight.friendly:
name = flight.departure.airfield_name name = flight.departure.airfield_name
if name in self.allied_flights_by_departure: # where else can we get this? if (
name in self.allied_flights_by_departure
): # where else can we get this?
self.allied_flights_by_departure[name].append(flight) self.allied_flights_by_departure[name].append(flight)
else: else:
self.allied_flights_by_departure[name] = [flight] self.allied_flights_by_departure[name] = [flight]

View File

@@ -0,0 +1,31 @@
import logging
import random
from game import db
from gen.coastal.silkworm import SilkwormGenerator
COASTAL_MAP = {
"SilkwormGenerator": SilkwormGenerator,
}
def generate_coastal_group(game, ground_object, faction_name: str):
"""
This generate a coastal defenses group
:return: Nothing, but put the group reference inside the ground object
"""
faction = db.FACTIONS[faction_name]
if len(faction.coastal_defenses) > 0:
generators = faction.coastal_defenses
if len(generators) > 0:
gen = random.choice(generators)
if gen in COASTAL_MAP.keys():
generator = COASTAL_MAP[gen](game, ground_object, faction)
generator.generate()
return generator.get_generated_group()
else:
logging.info(
"Unable to generate missile group, generator : "
+ str(gen)
+ "does not exists"
)
return None

58
gen/coastal/silkworm.py Normal file
View File

@@ -0,0 +1,58 @@
from dcs.vehicles import MissilesSS, Unarmed, AirDefence
from gen.sam.group_generator import GroupGenerator
class SilkwormGenerator(GroupGenerator):
def __init__(self, game, ground_object, faction):
super(SilkwormGenerator, self).__init__(game, ground_object)
self.faction = faction
def generate(self):
positions = self.get_circular_position(5, launcher_distance=120, coverage=180)
self.add_unit(
MissilesSS.AShM_Silkworm_SR,
"SR#0",
self.position.x,
self.position.y,
self.heading,
)
# Launchers
for i, p in enumerate(positions):
self.add_unit(
MissilesSS.AShM_SS_N_2_Silkworm,
"Missile#" + str(i),
p[0],
p[1],
self.heading,
)
# Commander
self.add_unit(
Unarmed.Truck_KAMAZ_43101,
"KAMAZ#0",
self.position.x - 35,
self.position.y - 20,
self.heading,
)
# Shorad
self.add_unit(
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
"SHILKA#0",
self.position.x - 55,
self.position.y - 38,
self.heading,
)
# Shorad 2
self.add_unit(
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL,
"STRELA#0",
self.position.x + 200,
self.position.y + 15,
90,
)

View File

@@ -12,19 +12,21 @@ from game.utils import heading_sum, opposite_heading
FRONTLINE_LENGTH = 80000 FRONTLINE_LENGTH = 80000
class Conflict: class Conflict:
def __init__(self, def __init__(
theater: ConflictTheater, self,
from_cp: ControlPoint, theater: ConflictTheater,
to_cp: ControlPoint, from_cp: ControlPoint,
attackers_side: str, to_cp: ControlPoint,
defenders_side: str, attackers_side: str,
attackers_country: Country, defenders_side: str,
defenders_country: Country, attackers_country: Country,
position: Point, defenders_country: Country,
heading: Optional[int] = None, position: Point,
size: Optional[int] = None heading: Optional[int] = None,
): size: Optional[int] = None,
):
self.attackers_side = attackers_side self.attackers_side = attackers_side
self.defenders_side = defenders_side self.defenders_side = defenders_side
@@ -43,27 +45,49 @@ class Conflict:
return from_cp.has_frontline and to_cp.has_frontline return from_cp.has_frontline and to_cp.has_frontline
@classmethod @classmethod
def frontline_position(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int]: def frontline_position(
cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater
) -> Tuple[Point, int]:
frontline = FrontLine(from_cp, to_cp, theater) frontline = FrontLine(from_cp, to_cp, theater)
attack_heading = frontline.attack_heading attack_heading = frontline.attack_heading
position = cls.find_ground_position(frontline.position, FRONTLINE_LENGTH, heading_sum(attack_heading, 90), theater) position = cls.find_ground_position(
frontline.position,
FRONTLINE_LENGTH,
heading_sum(attack_heading, 90),
theater,
)
return position, opposite_heading(attack_heading) return position, opposite_heading(attack_heading)
@classmethod @classmethod
def frontline_vector(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int, int]: def frontline_vector(
cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater
) -> Tuple[Point, int, int]:
""" """
Returns a vector for a valid frontline location avoiding exclusion zones. Returns a vector for a valid frontline location avoiding exclusion zones.
""" """
center_position, heading = cls.frontline_position(from_cp, to_cp, theater) center_position, heading = cls.frontline_position(from_cp, to_cp, theater)
left_heading = heading_sum(heading, -90) 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) left_position = cls.extend_ground_position(
right_position = cls.extend_ground_position(center_position, int(FRONTLINE_LENGTH / 2), right_heading, theater) 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)) distance = int(left_position.distance_to_point(right_position))
return left_position, right_heading, distance return left_position, right_heading, distance
@classmethod @classmethod
def frontline_cas_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater): def frontline_cas_conflict(
cls,
attacker_name: str,
defender_name: str,
attacker: Country,
defender: Country,
from_cp: ControlPoint,
to_cp: ControlPoint,
theater: ConflictTheater,
):
assert cls.has_frontline_between(from_cp, to_cp) assert cls.has_frontline_between(from_cp, to_cp)
position, heading, distance = cls.frontline_vector(from_cp, to_cp, theater) position, heading, distance = cls.frontline_vector(from_cp, to_cp, theater)
conflict = cls( conflict = cls(
@@ -76,12 +100,14 @@ class Conflict:
defenders_side=defender_name, defenders_side=defender_name,
attackers_country=attacker, attackers_country=attacker,
defenders_country=defender, defenders_country=defender,
size=distance size=distance,
) )
return conflict return conflict
@classmethod @classmethod
def extend_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point: 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""" """Finds the first intersection with an exclusion zone in one heading from an initial point up to max_distance"""
extended = initial.point_from_heading(heading, max_distance) extended = initial.point_from_heading(heading, max_distance)
if theater.landmap is None: if theater.landmap is None:
@@ -92,8 +118,7 @@ class Conflict:
p1 = ShapelyPoint(extended.x, extended.y) p1 = ShapelyPoint(extended.x, extended.y)
line = LineString([p0, p1]) line = LineString([p0, p1])
intersection = line.intersection( intersection = line.intersection(theater.landmap.inclusion_zone_only.boundary)
theater.landmap.inclusion_zone_only.boundary)
if intersection.is_empty: if intersection.is_empty:
# Max extent does not intersect with the boundary of the inclusion # Max extent does not intersect with the boundary of the inclusion
# zone, so the full front line is usable. This does assume that the # zone, so the full front line is usable. This does assume that the
@@ -104,7 +129,14 @@ class Conflict:
return initial.point_from_heading(heading, p0.distance(intersection)) return initial.point_from_heading(heading, p0.distance(intersection))
@classmethod @classmethod
def find_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater, coerce=True) -> Optional[Point]: def find_ground_position(
cls,
initial: Point,
max_distance: int,
heading: int,
theater: ConflictTheater,
coerce=True,
) -> Optional[Point]:
""" """
Finds the nearest valid ground position along a provided heading and it's inverse up to max_distance. Finds the nearest valid ground position along a provided heading and it's inverse up to max_distance.
`coerce=True` will return the closest land position to `initial` regardless of heading or distance `coerce=True` will return the closest land position to `initial` regardless of heading or distance
@@ -123,4 +155,3 @@ class Conflict:
return pos return pos
logging.error("Didn't find ground position ({})!".format(initial)) logging.error("Didn't find ground position ({})!".format(initial))
return None return None

View File

@@ -3,15 +3,20 @@ import random
from dcs.vehicles import Armor from dcs.vehicles import Armor
from game import db from game import db
from gen.defenses.armored_group_generator import ArmoredGroupGenerator, FixedSizeArmorGroupGenerator from gen.defenses.armored_group_generator import (
ArmoredGroupGenerator,
FixedSizeArmorGroupGenerator,
)
def generate_armor_group(faction:str, game, ground_object): def generate_armor_group(faction: str, game, ground_object):
""" """
This generate a group of ground units This generate a group of ground units
:return: Generated group :return: Generated group
""" """
possible_unit = [u for u in db.FACTIONS[faction].frontline_units if u in Armor.__dict__.values()] possible_unit = [
u for u in db.FACTIONS[faction].frontline_units if u in Armor.__dict__.values()
]
if len(possible_unit) > 0: if len(possible_unit) > 0:
unit_type = random.choice(possible_unit) unit_type = random.choice(possible_unit)
return generate_armor_group_of_type(game, ground_object, unit_type) return generate_armor_group_of_type(game, ground_object, unit_type)
@@ -36,4 +41,3 @@ def generate_armor_group_of_type_and_size(game, ground_object, unit_type, size:
generator = FixedSizeArmorGroupGenerator(game, ground_object, unit_type, size) generator = FixedSizeArmorGroupGenerator(game, ground_object, unit_type, size)
generator.generate() generator.generate()
return generator.get_generated_group() return generator.get_generated_group()

View File

@@ -4,7 +4,6 @@ from gen.sam.group_generator import GroupGenerator
class ArmoredGroupGenerator(GroupGenerator): class ArmoredGroupGenerator(GroupGenerator):
def __init__(self, game, ground_object, unit_type): def __init__(self, game, ground_object, unit_type):
super(ArmoredGroupGenerator, self).__init__(game, ground_object) super(ArmoredGroupGenerator, self).__init__(game, ground_object)
self.unit_type = unit_type self.unit_type = unit_type
@@ -20,13 +19,16 @@ class ArmoredGroupGenerator(GroupGenerator):
for i in range(grid_x): for i in range(grid_x):
for j in range(grid_y): for j in range(grid_y):
index = index + 1 index = index + 1
self.add_unit(self.unit_type, "Armor#" + str(index), self.add_unit(
self.position.x + spacing * i, self.unit_type,
self.position.y + spacing * j, self.heading) "Armor#" + str(index),
self.position.x + spacing * i,
self.position.y + spacing * j,
self.heading,
)
class FixedSizeArmorGroupGenerator(GroupGenerator): class FixedSizeArmorGroupGenerator(GroupGenerator):
def __init__(self, game, ground_object, unit_type, size): def __init__(self, game, ground_object, unit_type, size):
super(FixedSizeArmorGroupGenerator, self).__init__(game, ground_object) super(FixedSizeArmorGroupGenerator, self).__init__(game, ground_object)
self.unit_type = unit_type self.unit_type = unit_type
@@ -38,7 +40,10 @@ class FixedSizeArmorGroupGenerator(GroupGenerator):
index = 0 index = 0
for i in range(self.size): for i in range(self.size):
index = index + 1 index = index + 1
self.add_unit(self.unit_type, "Armor#" + str(index), self.add_unit(
self.position.x + spacing * i, self.unit_type,
self.position.y, self.heading) "Armor#" + str(index),
self.position.x + spacing * i,
self.position.y,
self.heading,
)

View File

@@ -17,6 +17,7 @@ class EnvironmentGenerator:
self.mission.weather.clouds_thickness = clouds.thickness self.mission.weather.clouds_thickness = clouds.thickness
self.mission.weather.clouds_density = clouds.density self.mission.weather.clouds_density = clouds.density
self.mission.weather.clouds_iprecptns = clouds.precipitation self.mission.weather.clouds_iprecptns = clouds.precipitation
self.mission.weather.clouds_preset = clouds.preset
def set_fog(self, fog: Optional[Fog]) -> None: def set_fog(self, fog: Optional[Fog]) -> None:
if fog is None: if fog is None:

View File

@@ -2,25 +2,122 @@ import random
from gen.sam.group_generator import ShipGroupGenerator from gen.sam.group_generator import ShipGroupGenerator
from dcs.ships import DDG_Arleigh_Burke_IIa, CG_Ticonderoga
class CarrierGroupGenerator(ShipGroupGenerator): class CarrierGroupGenerator(ShipGroupGenerator):
def generate(self): def generate(self):
# Add carrier # Carrier Strike Group 8
if len(self.faction.aircraft_carrier) > 0: if self.faction.carrier_names[0] == "Carrier Strike Group 8":
carrier_type = random.choice(self.faction.aircraft_carrier) carrier_type = random.choice(self.faction.aircraft_carrier)
self.add_unit(carrier_type, "Carrier", self.position.x, self.position.y, self.heading)
self.add_unit(
carrier_type,
"CVN-75 Harry S. Truman",
self.position.x,
self.position.y,
self.heading,
)
# Add Arleigh Burke escort
self.add_unit(
DDG_Arleigh_Burke_IIa,
"USS Ramage",
self.position.x + 6482,
self.position.y + 6667,
self.heading,
)
self.add_unit(
DDG_Arleigh_Burke_IIa,
"USS Mitscher",
self.position.x - 7963,
self.position.y + 7037,
self.heading,
)
self.add_unit(
DDG_Arleigh_Burke_IIa,
"USS Forrest Sherman",
self.position.x - 7408,
self.position.y - 7408,
self.heading,
)
self.add_unit(
DDG_Arleigh_Burke_IIa,
"USS Lassen",
self.position.x + 8704,
self.position.y - 6296,
self.heading,
)
# Add Ticonderoga escort
if self.heading >= 180:
self.add_unit(
CG_Ticonderoga,
"USS Hué City",
self.position.x + 2222,
self.position.y - 3333,
self.heading,
)
else:
self.add_unit(
CG_Ticonderoga,
"USS Hué City",
self.position.x - 3333,
self.position.y + 2222,
self.heading,
)
self.get_generated_group().points[0].speed = 20
##################################################################################################
# Add carrier for normal generation
else: else:
return if len(self.faction.aircraft_carrier) > 0:
carrier_type = random.choice(self.faction.aircraft_carrier)
self.add_unit(
carrier_type,
"Carrier",
self.position.x,
self.position.y,
self.heading,
)
else:
return
# Add destroyers escort # Add destroyers escort
if len(self.faction.destroyers) > 0: if len(self.faction.destroyers) > 0:
dd_type = random.choice(self.faction.destroyers) dd_type = random.choice(self.faction.destroyers)
self.add_unit(dd_type, "DD1", self.position.x + 2500, self.position.y + 4500, self.heading) self.add_unit(
self.add_unit(dd_type, "DD2", self.position.x + 2500, self.position.y - 4500, self.heading) dd_type,
"DD1",
self.position.x + 2500,
self.position.y + 4500,
self.heading,
)
self.add_unit(
dd_type,
"DD2",
self.position.x + 2500,
self.position.y - 4500,
self.heading,
)
self.add_unit(dd_type, "DD3", self.position.x + 4500, self.position.y + 8500, self.heading) self.add_unit(
self.add_unit(dd_type, "DD4", self.position.x + 4500, self.position.y - 8500, self.heading) dd_type,
"DD3",
self.position.x + 4500,
self.position.y + 8500,
self.heading,
)
self.add_unit(
dd_type,
"DD4",
self.position.x + 4500,
self.position.y - 8500,
self.heading,
)
self.get_generated_group().points[0].speed = 20 self.get_generated_group().points[0].speed = 20

View File

@@ -20,7 +20,6 @@ if TYPE_CHECKING:
class ChineseNavyGroupGenerator(ShipGroupGenerator): class ChineseNavyGroupGenerator(ShipGroupGenerator):
def generate(self): def generate(self):
include_frigate = random.choice([True, True, False]) include_frigate = random.choice([True, True, False])
@@ -30,17 +29,45 @@ class ChineseNavyGroupGenerator(ShipGroupGenerator):
include_frigate = True include_frigate = True
if include_frigate: if include_frigate:
self.add_unit(Type_054A_Frigate, "FF1", self.position.x + 1200, self.position.y + 900, self.heading) self.add_unit(
self.add_unit(Type_054A_Frigate, "FF2", self.position.x + 1200, self.position.y - 900, self.heading) Type_054A_Frigate,
"FF1",
self.position.x + 1200,
self.position.y + 900,
self.heading,
)
self.add_unit(
Type_054A_Frigate,
"FF2",
self.position.x + 1200,
self.position.y - 900,
self.heading,
)
if include_dd: if include_dd:
dd_type = random.choice([Type_052C_Destroyer, Type_052B_Destroyer]) dd_type = random.choice([Type_052C_Destroyer, Type_052B_Destroyer])
self.add_unit(dd_type, "DD1", self.position.x + 2400, self.position.y + 900, self.heading) self.add_unit(
self.add_unit(dd_type, "DD2", self.position.x + 2400, self.position.y - 900, self.heading) 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,
)
self.get_generated_group().points[0].speed = 20 self.get_generated_group().points[0].speed = 20
class Type54GroupGenerator(DDGroupGenerator): class Type54GroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction): def __init__(
super(Type54GroupGenerator, self).__init__(game, ground_object, faction, Type_054A_Frigate) self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
super(Type54GroupGenerator, self).__init__(
game, ground_object, faction, Type_054A_Frigate
)

View File

@@ -6,29 +6,54 @@ from game.theater.theatergroundobject import TheaterGroundObject
from gen.sam.group_generator import ShipGroupGenerator from gen.sam.group_generator import ShipGroupGenerator
from dcs.unittype import ShipType from dcs.unittype import ShipType
from dcs.ships import Oliver_Hazzard_Perry_class, USS_Arleigh_Burke_IIa from dcs.ships import FFG_Oliver_Hazzard_Perry, DDG_Arleigh_Burke_IIa
if TYPE_CHECKING: if TYPE_CHECKING:
from game.game import Game from game.game import Game
class DDGroupGenerator(ShipGroupGenerator): class DDGroupGenerator(ShipGroupGenerator):
def __init__(
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction, ddtype: ShipType): self,
game: Game,
ground_object: TheaterGroundObject,
faction: Faction,
ddtype: ShipType,
):
super(DDGroupGenerator, self).__init__(game, ground_object, faction) super(DDGroupGenerator, self).__init__(game, ground_object, faction)
self.ddtype = ddtype self.ddtype = ddtype
def generate(self): def generate(self):
self.add_unit(self.ddtype, "DD1", self.position.x + 500, self.position.y + 900, self.heading) self.add_unit(
self.add_unit(self.ddtype, "DD2", self.position.x + 500, self.position.y - 900, self.heading) self.ddtype,
"DD1",
self.position.x + 500,
self.position.y + 900,
self.heading,
)
self.add_unit(
self.ddtype,
"DD2",
self.position.x + 500,
self.position.y - 900,
self.heading,
)
self.get_generated_group().points[0].speed = 20 self.get_generated_group().points[0].speed = 20
class OliverHazardPerryGroupGenerator(DDGroupGenerator): class OliverHazardPerryGroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction): def __init__(
super(OliverHazardPerryGroupGenerator, self).__init__(game, ground_object, faction, Oliver_Hazzard_Perry_class) self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
super(OliverHazardPerryGroupGenerator, self).__init__(
game, ground_object, faction, FFG_Oliver_Hazzard_Perry
)
class ArleighBurkeGroupGenerator(DDGroupGenerator): class ArleighBurkeGroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction): def __init__(
super(ArleighBurkeGroupGenerator, self).__init__(game, ground_object, faction, USS_Arleigh_Burke_IIa) self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
super(ArleighBurkeGroupGenerator, self).__init__(
game, ground_object, faction, DDG_Arleigh_Burke_IIa
)

View File

@@ -4,18 +4,31 @@ from gen.sam.group_generator import ShipGroupGenerator
class LHAGroupGenerator(ShipGroupGenerator): class LHAGroupGenerator(ShipGroupGenerator):
def generate(self): def generate(self):
# Add carrier # Add carrier
if len(self.faction.helicopter_carrier) > 0: if len(self.faction.helicopter_carrier) > 0:
carrier_type = random.choice(self.faction.helicopter_carrier) carrier_type = random.choice(self.faction.helicopter_carrier)
self.add_unit(carrier_type, "LHA", self.position.x, self.position.y, self.heading) self.add_unit(
carrier_type, "LHA", self.position.x, self.position.y, self.heading
)
# Add destroyers escort # Add destroyers escort
if len(self.faction.destroyers) > 0: if len(self.faction.destroyers) > 0:
dd_type = random.choice(self.faction.destroyers) dd_type = random.choice(self.faction.destroyers)
self.add_unit(dd_type, "DD1", self.position.x + 1250, self.position.y + 1450, self.heading) self.add_unit(
self.add_unit(dd_type, "DD2", self.position.x + 1250, self.position.y - 1450, self.heading) dd_type,
"DD1",
self.position.x + 1250,
self.position.y + 1450,
self.heading,
)
self.add_unit(
dd_type,
"DD2",
self.position.x + 1250,
self.position.y - 1450,
self.heading,
)
self.get_generated_group().points[0].speed = 20 self.get_generated_group().points[0].speed = 20

View File

@@ -3,13 +3,13 @@ import random
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from dcs.ships import ( from dcs.ships import (
FFL_1124_4_Grisha, Corvette_1124_4_Grisha,
FSG_1241_1MP_Molniya, Corvette_1241_1_Molniya,
FFG_11540_Neustrashimy, Frigate_11540_Neustrashimy,
FF_1135M_Rezky, Frigate_1135M_Rezky,
CG_1164_Moskva, Cruiser_1164_Moskva,
SSK_877, SSK_877V_Kilo,
SSK_641B SSK_641B_Tango,
) )
from gen.fleet.dd_group import DDGroupGenerator from gen.fleet.dd_group import DDGroupGenerator
@@ -23,7 +23,6 @@ if TYPE_CHECKING:
class RussianNavyGroupGenerator(ShipGroupGenerator): class RussianNavyGroupGenerator(ShipGroupGenerator):
def generate(self): def generate(self):
include_frigate = random.choice([True, True, False]) include_frigate = random.choice([True, True, False])
@@ -38,38 +37,86 @@ class RussianNavyGroupGenerator(ShipGroupGenerator):
include_frigate = True include_frigate = True
if include_frigate: if include_frigate:
frigate_type = random.choice([FFL_1124_4_Grisha, FSG_1241_1MP_Molniya]) frigate_type = random.choice(
self.add_unit(frigate_type, "FF1", self.position.x + 1200, self.position.y + 900, self.heading) [Corvette_1124_4_Grisha, Corvette_1241_1_Molniya]
self.add_unit(frigate_type, "FF2", self.position.x + 1200, self.position.y - 900, self.heading) )
self.add_unit(
frigate_type,
"FF1",
self.position.x + 1200,
self.position.y + 900,
self.heading,
)
self.add_unit(
frigate_type,
"FF2",
self.position.x + 1200,
self.position.y - 900,
self.heading,
)
if include_dd: if include_dd:
dd_type = random.choice([FFG_11540_Neustrashimy, FF_1135M_Rezky]) dd_type = random.choice([Frigate_11540_Neustrashimy, Frigate_1135M_Rezky])
self.add_unit(dd_type, "DD1", self.position.x + 2400, self.position.y + 900, self.heading) self.add_unit(
self.add_unit(dd_type, "DD2", self.position.x + 2400, self.position.y - 900, self.heading) 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: if include_cc:
# Only include the Moskva for now, the Pyotry Velikiy is an unkillable monster. # Only include the Moskva for now, the Pyotry Velikiy is an unkillable monster.
# See https://github.com/Khopa/dcs_liberation/issues/567 # 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.add_unit(
Cruiser_1164_Moskva,
"CC1",
self.position.x,
self.position.y,
self.heading,
)
self.get_generated_group().points[0].speed = 20 self.get_generated_group().points[0].speed = 20
class GrishaGroupGenerator(DDGroupGenerator): class GrishaGroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction): def __init__(
super(GrishaGroupGenerator, self).__init__(game, ground_object, faction, FFL_1124_4_Grisha) self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
super(GrishaGroupGenerator, self).__init__(
game, ground_object, faction, Corvette_1124_4_Grisha
)
class MolniyaGroupGenerator(DDGroupGenerator): class MolniyaGroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction): def __init__(
super(MolniyaGroupGenerator, self).__init__(game, ground_object, faction, FSG_1241_1MP_Molniya) self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
super(MolniyaGroupGenerator, self).__init__(
game, ground_object, faction, Corvette_1241_1_Molniya
)
class KiloSubGroupGenerator(DDGroupGenerator): class KiloSubGroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction): def __init__(
super(KiloSubGroupGenerator, self).__init__(game, ground_object, faction, SSK_877) self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
super(KiloSubGroupGenerator, self).__init__(
game, ground_object, faction, SSK_877V_Kilo
)
class TangoSubGroupGenerator(DDGroupGenerator): class TangoSubGroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction): def __init__(
super(TangoSubGroupGenerator, self).__init__(game, ground_object, faction, SSK_641B) self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
super(TangoSubGroupGenerator, self).__init__(
game, ground_object, faction, SSK_641B_Tango
)

View File

@@ -1,15 +1,20 @@
import random import random
from dcs.ships import Schnellboot_type_S130 from dcs.ships import Boat_Schnellboot_type_S130
from gen.sam.group_generator import ShipGroupGenerator from gen.sam.group_generator import ShipGroupGenerator
class SchnellbootGroupGenerator(ShipGroupGenerator): class SchnellbootGroupGenerator(ShipGroupGenerator):
def generate(self): def generate(self):
for i in range(random.randint(2, 4)): for i in range(random.randint(2, 4)):
self.add_unit(Schnellboot_type_S130, "Schnellboot" + str(i), self.position.x + i * random.randint(100, 250), self.position.y + (random.randint(100, 200)-100), self.heading) self.add_unit(
Boat_Schnellboot_type_S130,
"Schnellboot" + str(i),
self.position.x + i * random.randint(100, 250),
self.position.y + (random.randint(100, 200) - 100),
self.heading,
)
self.get_generated_group().points[0].speed = 20 self.get_generated_group().points[0].speed = 20

View File

@@ -4,10 +4,18 @@ import random
from game import db from game import db
from gen.fleet.carrier_group import CarrierGroupGenerator from gen.fleet.carrier_group import CarrierGroupGenerator
from gen.fleet.cn_dd_group import ChineseNavyGroupGenerator, Type54GroupGenerator from gen.fleet.cn_dd_group import ChineseNavyGroupGenerator, Type54GroupGenerator
from gen.fleet.dd_group import ArleighBurkeGroupGenerator, OliverHazardPerryGroupGenerator from gen.fleet.dd_group import (
ArleighBurkeGroupGenerator,
OliverHazardPerryGroupGenerator,
)
from gen.fleet.lha_group import LHAGroupGenerator from gen.fleet.lha_group import LHAGroupGenerator
from gen.fleet.ru_dd_group import RussianNavyGroupGenerator, GrishaGroupGenerator, MolniyaGroupGenerator, \ from gen.fleet.ru_dd_group import (
KiloSubGroupGenerator, TangoSubGroupGenerator RussianNavyGroupGenerator,
GrishaGroupGenerator,
MolniyaGroupGenerator,
KiloSubGroupGenerator,
TangoSubGroupGenerator,
)
from gen.fleet.schnellboot import SchnellbootGroupGenerator from gen.fleet.schnellboot import SchnellbootGroupGenerator
from gen.fleet.uboat import UBoatGroupGenerator from gen.fleet.uboat import UBoatGroupGenerator
from gen.fleet.ww2lst import WW2LSTGroupGenerator from gen.fleet.ww2lst import WW2LSTGroupGenerator
@@ -25,7 +33,7 @@ SHIP_MAP = {
"MolniyaGroupGenerator": MolniyaGroupGenerator, "MolniyaGroupGenerator": MolniyaGroupGenerator,
"KiloSubGroupGenerator": KiloSubGroupGenerator, "KiloSubGroupGenerator": KiloSubGroupGenerator,
"TangoSubGroupGenerator": TangoSubGroupGenerator, "TangoSubGroupGenerator": TangoSubGroupGenerator,
"Type54GroupGenerator": Type54GroupGenerator "Type54GroupGenerator": Type54GroupGenerator,
} }
@@ -39,10 +47,15 @@ def generate_ship_group(game, ground_object, faction_name: str):
gen = random.choice(faction.navy_generators) gen = random.choice(faction.navy_generators)
if gen in SHIP_MAP.keys(): if gen in SHIP_MAP.keys():
generator = SHIP_MAP[gen](game, ground_object, faction) generator = SHIP_MAP[gen](game, ground_object, faction)
print(generator.position)
generator.generate() generator.generate()
return generator.get_generated_group() return generator.get_generated_group()
else: else:
logging.info("Unable to generate ship group, generator : " + str(gen) + "does not exists") logging.info(
"Unable to generate ship group, generator : "
+ str(gen)
+ "does not exists"
)
return None return None

View File

@@ -1,15 +1,20 @@
import random import random
from dcs.ships import Uboat_VIIC_U_flak from dcs.ships import U_boat_VIIC_U_flak
from gen.sam.group_generator import ShipGroupGenerator from gen.sam.group_generator import ShipGroupGenerator
class UBoatGroupGenerator(ShipGroupGenerator): class UBoatGroupGenerator(ShipGroupGenerator):
def generate(self): def generate(self):
for i in range(random.randint(1, 4)): for i in range(random.randint(1, 4)):
self.add_unit(Uboat_VIIC_U_flak, "Uboat" + str(i), self.position.x + i * random.randint(100, 250), self.position.y + (random.randint(100, 200)-100), self.heading) self.add_unit(
U_boat_VIIC_U_flak,
"Uboat" + str(i),
self.position.x + i * random.randint(100, 250),
self.position.y + (random.randint(100, 200) - 100),
self.heading,
)
self.get_generated_group().points[0].speed = 20 self.get_generated_group().points[0].speed = 20

View File

@@ -6,13 +6,24 @@ from gen.sam.group_generator import ShipGroupGenerator
class WW2LSTGroupGenerator(ShipGroupGenerator): class WW2LSTGroupGenerator(ShipGroupGenerator):
def generate(self): def generate(self):
# Add LS Samuel Chase # Add LS Samuel Chase
self.add_unit(LS_Samuel_Chase, "SamuelChase", self.position.x, self.position.y, self.heading) self.add_unit(
LS_Samuel_Chase,
"SamuelChase",
self.position.x,
self.position.y,
self.heading,
)
for i in range(1, random.randint(3, 4)): for i in range(1, random.randint(3, 4)):
self.add_unit(LST_Mk_II, "LST" + str(i), self.position.x + i * random.randint(800, 1200), self.position.y, self.heading) self.add_unit(
LST_Mk_II,
"LST" + str(i),
self.position.x + i * random.randint(800, 1200),
self.position.y,
self.heading,
)
self.get_generated_group().points[0].speed = 20 self.get_generated_group().points[0].speed = 20

View File

@@ -109,22 +109,25 @@ class ProposedMission:
flights: List[ProposedFlight] flights: List[ProposedFlight]
def __str__(self) -> str: def __str__(self) -> str:
flights = ', '.join([str(f) for f in self.flights]) flights = ", ".join([str(f) for f in self.flights])
return f"{self.location.name}: {flights}" return f"{self.location.name}: {flights}"
class AircraftAllocator: class AircraftAllocator:
"""Finds suitable aircraft for proposed missions.""" """Finds suitable aircraft for proposed missions."""
def __init__(self, closest_airfields: ClosestAirfields, def __init__(
global_inventory: GlobalAircraftInventory, self,
is_player: bool) -> None: closest_airfields: ClosestAirfields,
global_inventory: GlobalAircraftInventory,
is_player: bool,
) -> None:
self.closest_airfields = closest_airfields self.closest_airfields = closest_airfields
self.global_inventory = global_inventory self.global_inventory = global_inventory
self.is_player = is_player self.is_player = is_player
def find_aircraft_for_flight( def find_aircraft_for_flight(
self, flight: ProposedFlight self, flight: ProposedFlight
) -> Optional[Tuple[ControlPoint, Type[FlyingType]]]: ) -> Optional[Tuple[ControlPoint, Type[FlyingType]]]:
"""Finds aircraft suitable for the given mission. """Finds aircraft suitable for the given mission.
@@ -144,12 +147,12 @@ class AircraftAllocator:
on subsequent calls. If the found aircraft are not used, the caller is on subsequent calls. If the found aircraft are not used, the caller is
responsible for returning them to the inventory. responsible for returning them to the inventory.
""" """
return self.find_aircraft_of_type( return self.find_aircraft_of_type(flight, aircraft_for_task(flight.task))
flight, aircraft_for_task(flight.task)
)
def find_aircraft_of_type( def find_aircraft_of_type(
self, flight: ProposedFlight, types: List[Type[FlyingType]], self,
flight: ProposedFlight,
types: List[Type[FlyingType]],
) -> Optional[Tuple[ControlPoint, Type[FlyingType]]]: ) -> Optional[Tuple[ControlPoint, Type[FlyingType]]]:
airfields_in_range = self.closest_airfields.airfields_within( airfields_in_range = self.closest_airfields.airfields_within(
flight.max_distance flight.max_distance
@@ -171,18 +174,22 @@ class AircraftAllocator:
class PackageBuilder: class PackageBuilder:
"""Builds a Package for the flights it receives.""" """Builds a Package for the flights it receives."""
def __init__(self, location: MissionTarget, def __init__(
closest_airfields: ClosestAirfields, self,
global_inventory: GlobalAircraftInventory, location: MissionTarget,
is_player: bool, closest_airfields: ClosestAirfields,
package_country: str, global_inventory: GlobalAircraftInventory,
start_type: str) -> None: is_player: bool,
package_country: str,
start_type: str,
) -> None:
self.closest_airfields = closest_airfields self.closest_airfields = closest_airfields
self.is_player = is_player self.is_player = is_player
self.package_country = package_country self.package_country = package_country
self.package = Package(location) self.package = Package(location)
self.allocator = AircraftAllocator(closest_airfields, global_inventory, self.allocator = AircraftAllocator(
is_player) closest_airfields, global_inventory, is_player
)
self.global_inventory = global_inventory self.global_inventory = global_inventory
self.start_type = start_type self.start_type = start_type
@@ -203,14 +210,23 @@ class PackageBuilder:
else: else:
start_type = self.start_type start_type = self.start_type
flight = Flight(self.package, self.package_country, aircraft, plan.num_aircraft, plan.task, flight = Flight(
start_type, departure=airfield, arrival=airfield, self.package,
divert=self.find_divert_field(aircraft, airfield)) 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) self.package.add_flight(flight)
return True return True
def find_divert_field(self, aircraft: Type[FlyingType], def find_divert_field(
arrival: ControlPoint) -> Optional[ControlPoint]: self, aircraft: Type[FlyingType], arrival: ControlPoint
) -> Optional[ControlPoint]:
divert_limit = nautical_miles(150) divert_limit = nautical_miles(150)
for airfield in self.closest_airfields.airfields_within(divert_limit): for airfield in self.closest_airfields.airfields_within(divert_limit):
if airfield.captured != self.is_player: if airfield.captured != self.is_player:
@@ -323,8 +339,8 @@ class ObjectiveFinder:
return self._targets_by_range(self.enemy_ships()) return self._targets_by_range(self.enemy_ships())
def _targets_by_range( def _targets_by_range(
self, self, targets: Iterable[MissionTarget]
targets: Iterable[MissionTarget]) -> Iterator[MissionTarget]: ) -> Iterator[MissionTarget]:
target_ranges: List[Tuple[MissionTarget, int]] = [] target_ranges: List[Tuple[MissionTarget, int]] = []
for target in targets: for target in targets:
ranges: List[int] = [] ranges: List[int] = []
@@ -430,13 +446,43 @@ class ObjectiveFinder:
def friendly_control_points(self) -> Iterator[ControlPoint]: def friendly_control_points(self) -> Iterator[ControlPoint]:
"""Iterates over all friendly control points.""" """Iterates over all friendly control points."""
return (c for c in self.game.theater.controlpoints if return (
c.is_friendly(self.is_player)) c for c in self.game.theater.controlpoints if c.is_friendly(self.is_player)
)
def farthest_friendly_control_point(self) -> Optional[ControlPoint]:
"""
Iterates over all friendly control points and find the one farthest away from the frontline
BUT! prefer Cvs. Everybody likes CVs!
"""
from_frontline = 0
cp = None
first_friendly_cp = None
for c in self.game.theater.controlpoints:
if c.is_friendly(self.is_player):
if first_friendly_cp is None:
first_friendly_cp = c
if c.is_carrier:
return c
if c.has_active_frontline:
if c.distance_to(self.front_lines().__next__()) > from_frontline:
from_frontline = c.distance_to(self.front_lines().__next__())
cp = c
# If no frontlines on the map, return the first friendly cp
if cp is None:
return first_friendly_cp
else:
return cp
def enemy_control_points(self) -> Iterator[ControlPoint]: def enemy_control_points(self) -> Iterator[ControlPoint]:
"""Iterates over all enemy control points.""" """Iterates over all enemy control points."""
return (c for c in self.game.theater.controlpoints if return (
not c.is_friendly(self.is_player)) c
for c in self.game.theater.controlpoints
if not c.is_friendly(self.is_player)
)
def all_possible_targets(self) -> Iterator[MissionTarget]: def all_possible_targets(self) -> Iterator[MissionTarget]:
"""Iterates over all possible mission targets in the theater. """Iterates over all possible mission targets in the theater.
@@ -487,6 +533,7 @@ class CoalitionMissionPlanner:
MAX_OCA_RANGE = nautical_miles(150) MAX_OCA_RANGE = nautical_miles(150)
MAX_SEAD_RANGE = nautical_miles(150) MAX_SEAD_RANGE = nautical_miles(150)
MAX_STRIKE_RANGE = nautical_miles(150) MAX_STRIKE_RANGE = nautical_miles(150)
MAX_AWEC_RANGE = nautical_miles(200)
def __init__(self, game: Game, is_player: bool) -> None: def __init__(self, game: Game, is_player: bool) -> None:
self.game = game self.game = game
@@ -506,27 +553,62 @@ class CoalitionMissionPlanner:
ensure that they can be planned again next turn even if all aircraft are ensure that they can be planned again next turn even if all aircraft are
eliminated this turn. eliminated this turn.
""" """
# Find farthest, friendly CP for AEWC
cp = self.objective_finder.farthest_friendly_control_point()
if cp is not None:
yield ProposedMission(
cp, [ProposedFlight(FlightType.AEWC, 1, self.MAX_AWEC_RANGE)]
)
# Find friendly CPs within 100 nmi from an enemy airfield, plan CAP. # Find friendly CPs within 100 nmi from an enemy airfield, plan CAP.
for cp in self.objective_finder.vulnerable_control_points(): for cp in self.objective_finder.vulnerable_control_points():
# Plan three rounds of CAP to give ~90 minutes coverage. Spacing # Plan three rounds of CAP to give ~90 minutes coverage. Spacing
# these out appropriately is done in stagger_missions. # these out appropriately is done in stagger_missions.
yield ProposedMission(cp, [ yield ProposedMission(
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE), cp,
]) [
yield ProposedMission(cp, [ ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE), ],
]) )
yield ProposedMission(cp, [ yield ProposedMission(
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE), cp,
]) [
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
],
)
yield ProposedMission(
cp,
[
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
],
)
# Find front lines, plan CAS. # Find front lines, plan CAS.
for front_line in self.objective_finder.front_lines(): for front_line in self.objective_finder.front_lines():
yield ProposedMission(front_line, [ yield ProposedMission(
ProposedFlight(FlightType.CAS, 2, self.MAX_CAS_RANGE), front_line,
ProposedFlight(FlightType.TARCAP, 2, self.MAX_CAP_RANGE, [
EscortType.AirToAir), ProposedFlight(FlightType.CAS, 2, self.MAX_CAS_RANGE),
]) # This is *not* an escort because front lines don't create a threat
# zone. Generating threat zones from front lines causes the front
# line to push back BARCAPs as it gets closer to the base. While
# front lines do have the same problem of potentially pulling
# BARCAPs off bases to engage a front line TARCAP, that's probably
# the one time where we do want that.
#
# TODO: Use intercepts and extra TARCAPs to cover bases near fronts.
# We don't have intercept missions yet so this isn't something we
# can do today, but we should probably return to having the front
# line project a threat zone (so that strike missions will route
# around it) and instead *not plan* a BARCAP at bases near the
# front, since there isn't a place to put a barrier. Instead, the
# aircraft that would have been a BARCAP could be used as additional
# interceptors and TARCAPs which will defend the base but won't be
# trying to avoid front line contacts.
ProposedFlight(FlightType.TARCAP, 2, self.MAX_CAP_RANGE),
],
)
def propose_missions(self) -> Iterator[ProposedMission]: def propose_missions(self) -> Iterator[ProposedMission]:
"""Identifies and iterates over potential mission in priority order.""" """Identifies and iterates over potential mission in priority order."""
@@ -537,30 +619,46 @@ class CoalitionMissionPlanner:
# Find enemy SAM sites with ranges that extend to within 50 nmi of # Find enemy SAM sites with ranges that extend to within 50 nmi of
# friendly CPs, front, lines, or objects, plan DEAD. # friendly CPs, front, lines, or objects, plan DEAD.
for sam in self.objective_finder.threatening_sams(): for sam in self.objective_finder.threatening_sams():
yield ProposedMission(sam, [ yield ProposedMission(
ProposedFlight(FlightType.DEAD, 2, self.MAX_SEAD_RANGE), sam,
# TODO: Max escort range. [
ProposedFlight(FlightType.ESCORT, 2, self.MAX_SEAD_RANGE, ProposedFlight(FlightType.DEAD, 2, self.MAX_SEAD_RANGE),
EscortType.AirToAir), # TODO: Max escort range.
]) ProposedFlight(
FlightType.ESCORT, 2, self.MAX_SEAD_RANGE, EscortType.AirToAir
),
],
)
for group in self.objective_finder.threatening_ships(): for group in self.objective_finder.threatening_ships():
yield ProposedMission(group, [ yield ProposedMission(
ProposedFlight(FlightType.ANTISHIP, 2, self.MAX_ANTISHIP_RANGE), group,
# TODO: Max escort range. [
ProposedFlight(FlightType.ESCORT, 2, self.MAX_ANTISHIP_RANGE, ProposedFlight(FlightType.ANTISHIP, 2, self.MAX_ANTISHIP_RANGE),
EscortType.AirToAir), # TODO: Max escort range.
]) ProposedFlight(
FlightType.ESCORT,
2,
self.MAX_ANTISHIP_RANGE,
EscortType.AirToAir,
),
],
)
for group in self.objective_finder.threatening_vehicle_groups(): for group in self.objective_finder.threatening_vehicle_groups():
yield ProposedMission(group, [ yield ProposedMission(
ProposedFlight(FlightType.BAI, 2, self.MAX_BAI_RANGE), group,
# TODO: Max escort range. [
ProposedFlight(FlightType.ESCORT, 2, self.MAX_BAI_RANGE, ProposedFlight(FlightType.BAI, 2, self.MAX_BAI_RANGE),
EscortType.AirToAir), # TODO: Max escort range.
ProposedFlight(FlightType.SEAD, 2, self.MAX_OCA_RANGE, ProposedFlight(
EscortType.Sead), 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): for target in self.objective_finder.oca_targets(min_aircraft=20):
flights = [ flights = [
@@ -569,27 +667,37 @@ class CoalitionMissionPlanner:
if self.game.settings.default_start_type == "Cold": if self.game.settings.default_start_type == "Cold":
# Only schedule if the default start type is Cold. If the player # Only schedule if the default start type is Cold. If the player
# has set anything else there are no targets to hit. # has set anything else there are no targets to hit.
flights.append(ProposedFlight(FlightType.OCA_AIRCRAFT, 2, flights.append(
self.MAX_OCA_RANGE)) ProposedFlight(FlightType.OCA_AIRCRAFT, 2, self.MAX_OCA_RANGE)
flights.extend([ )
# TODO: Max escort range. flights.extend(
ProposedFlight(FlightType.ESCORT, 2, self.MAX_OCA_RANGE, [
EscortType.AirToAir), # TODO: Max escort range.
ProposedFlight(FlightType.SEAD, 2, self.MAX_OCA_RANGE, ProposedFlight(
EscortType.Sead), FlightType.ESCORT, 2, self.MAX_OCA_RANGE, EscortType.AirToAir
]) ),
ProposedFlight(
FlightType.SEAD, 2, self.MAX_OCA_RANGE, EscortType.Sead
),
]
)
yield ProposedMission(target, flights) yield ProposedMission(target, flights)
# Plan strike missions. # Plan strike missions.
for target in self.objective_finder.strike_targets(): for target in self.objective_finder.strike_targets():
yield ProposedMission(target, [ yield ProposedMission(
ProposedFlight(FlightType.STRIKE, 2, self.MAX_STRIKE_RANGE), target,
# TODO: Max escort range. [
ProposedFlight(FlightType.ESCORT, 2, self.MAX_STRIKE_RANGE, ProposedFlight(FlightType.STRIKE, 2, self.MAX_STRIKE_RANGE),
EscortType.AirToAir), # TODO: Max escort range.
ProposedFlight(FlightType.SEAD, 2, self.MAX_STRIKE_RANGE, ProposedFlight(
EscortType.Sead), FlightType.ESCORT, 2, self.MAX_STRIKE_RANGE, EscortType.AirToAir
]) ),
ProposedFlight(
FlightType.SEAD, 2, self.MAX_STRIKE_RANGE, EscortType.Sead
),
],
)
def plan_missions(self) -> None: def plan_missions(self) -> None:
"""Identifies and plans mission for the turn.""" """Identifies and plans mission for the turn."""
@@ -604,19 +712,23 @@ class CoalitionMissionPlanner:
for cp in self.objective_finder.friendly_control_points(): for cp in self.objective_finder.friendly_control_points():
inventory = self.game.aircraft_inventory.for_control_point(cp) inventory = self.game.aircraft_inventory.for_control_point(cp)
for aircraft, available in inventory.all_aircraft: for aircraft, available in inventory.all_aircraft:
self.message("Unused aircraft", self.message("Unused aircraft", f"{available} {aircraft.id} from {cp}")
f"{available} {aircraft.id} from {cp}")
def plan_flight(self, mission: ProposedMission, flight: ProposedFlight, def plan_flight(
builder: PackageBuilder, missing_types: Set[FlightType], self,
for_reserves: bool) -> None: mission: ProposedMission,
flight: ProposedFlight,
builder: PackageBuilder,
missing_types: Set[FlightType],
for_reserves: bool,
) -> None:
if not builder.plan_flight(flight): if not builder.plan_flight(flight):
missing_types.add(flight.task) missing_types.add(flight.task)
purchase_order = AircraftProcurementRequest( purchase_order = AircraftProcurementRequest(
near=mission.location, near=mission.location,
range=flight.max_distance, range=flight.max_distance,
task_capability=flight.task, task_capability=flight.task,
number=flight.num_aircraft number=flight.num_aircraft,
) )
if for_reserves: if for_reserves:
# Reserves are planned for critical missions, so prioritize # Reserves are planned for critical missions, so prioritize
@@ -626,26 +738,28 @@ class CoalitionMissionPlanner:
self.procurement_requests.append(purchase_order) self.procurement_requests.append(purchase_order)
def scrub_mission_missing_aircraft( def scrub_mission_missing_aircraft(
self, mission: ProposedMission, builder: PackageBuilder, self,
missing_types: Set[FlightType], mission: ProposedMission,
not_attempted: Iterable[ProposedFlight], builder: PackageBuilder,
reserves: bool) -> None: 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 # Try to plan the rest of the mission just so we can count the missing
# types to buy. # types to buy.
for flight in not_attempted: for flight in not_attempted:
self.plan_flight(mission, flight, builder, missing_types, reserves) self.plan_flight(mission, flight, builder, missing_types, reserves)
missing_types_str = ", ".join( missing_types_str = ", ".join(sorted([t.name for t in missing_types]))
sorted([t.name for t in missing_types]))
builder.release_planned_aircraft() builder.release_planned_aircraft()
desc = "reserve aircraft" if reserves else "aircraft" desc = "reserve aircraft" if reserves else "aircraft"
self.message( self.message(
"Insufficient aircraft", "Insufficient aircraft",
f"Not enough {desc} in range for {mission.location.name} " f"Not enough {desc} in range for {mission.location.name} "
f"capable of: {missing_types_str}") f"capable of: {missing_types_str}",
)
def check_needed_escorts( def check_needed_escorts(self, builder: PackageBuilder) -> Dict[EscortType, bool]:
self, builder: PackageBuilder) -> Dict[EscortType, bool]:
threats = defaultdict(bool) threats = defaultdict(bool)
for flight in builder.package.flights: for flight in builder.package.flights:
if self.threat_zones.threatened_by_aircraft(flight): if self.threat_zones.threatened_by_aircraft(flight):
@@ -654,8 +768,7 @@ class CoalitionMissionPlanner:
threats[EscortType.Sead] = True threats[EscortType.Sead] = True
return threats return threats
def plan_mission(self, mission: ProposedMission, def plan_mission(self, mission: ProposedMission, reserves: bool = False) -> None:
reserves: bool = False) -> None:
"""Allocates aircraft for a proposed mission and adds it to the ATO.""" """Allocates aircraft for a proposed mission and adds it to the ATO."""
if self.is_player: if self.is_player:
@@ -669,7 +782,7 @@ class CoalitionMissionPlanner:
self.game.aircraft_inventory, self.game.aircraft_inventory,
self.is_player, self.is_player,
package_country, package_country,
self.game.settings.default_start_type self.game.settings.default_start_type,
) )
# Attempt to plan all the main elements of the mission first. Escorts # Attempt to plan all the main elements of the mission first. Escorts
@@ -683,12 +796,12 @@ class CoalitionMissionPlanner:
# If the package does not need escorts they may be pruned. # If the package does not need escorts they may be pruned.
escorts.append(proposed_flight) escorts.append(proposed_flight)
continue continue
self.plan_flight(mission, proposed_flight, builder, missing_types, self.plan_flight(mission, proposed_flight, builder, missing_types, reserves)
reserves)
if missing_types: if missing_types:
self.scrub_mission_missing_aircraft(mission, builder, missing_types, self.scrub_mission_missing_aircraft(
escorts, reserves) mission, builder, missing_types, escorts, reserves
)
return return
# Create flight plans for the main flights of the package so we can # Create flight plans for the main flights of the package so we can
@@ -697,8 +810,9 @@ class CoalitionMissionPlanner:
# flights that will rendezvous with their package will be affected by # flights that will rendezvous with their package will be affected by
# the other flights in the package. Escorts will not be able to # the other flights in the package. Escorts will not be able to
# contribute to this. # contribute to this.
flight_plan_builder = FlightPlanBuilder(self.game, builder.package, flight_plan_builder = FlightPlanBuilder(
self.is_player) self.game, builder.package, self.is_player
)
for flight in builder.package.flights: for flight in builder.package.flights:
flight_plan_builder.populate_flight_plan(flight) flight_plan_builder.populate_flight_plan(flight)
@@ -708,14 +822,14 @@ class CoalitionMissionPlanner:
# impossible. # impossible.
assert escort.escort_type is not None assert escort.escort_type is not None
if needed_escorts[escort.escort_type]: if needed_escorts[escort.escort_type]:
self.plan_flight(mission, escort, builder, missing_types, self.plan_flight(mission, escort, builder, missing_types, reserves)
reserves)
# Check again for unavailable aircraft. If the escort was required and # Check again for unavailable aircraft. If the escort was required and
# none were found, scrub the mission. # none were found, scrub the mission.
if missing_types: if missing_types:
self.scrub_mission_missing_aircraft(mission, builder, missing_types, self.scrub_mission_missing_aircraft(
escorts, reserves) mission, builder, missing_types, escorts, reserves
)
return return
if reserves: if reserves:
@@ -732,8 +846,9 @@ class CoalitionMissionPlanner:
self.ato.add_package(package) self.ato.add_package(package)
def stagger_missions(self) -> None: def stagger_missions(self) -> None:
def start_time_generator(count: int, earliest: int, latest: int, def start_time_generator(
margin: int) -> Iterator[timedelta]: count: int, earliest: int, latest: int, margin: int
) -> Iterator[timedelta]:
interval = (latest - earliest) // count interval = (latest - earliest) // count
for time in range(earliest, latest, interval): for time in range(earliest, latest, interval):
error = random.randint(-margin, margin) error = random.randint(-margin, margin)
@@ -744,17 +859,13 @@ class CoalitionMissionPlanner:
FlightType.TARCAP, FlightType.TARCAP,
} }
previous_cap_end_time: Dict[MissionTarget, timedelta] = defaultdict( previous_cap_end_time: Dict[MissionTarget, timedelta] = defaultdict(timedelta)
timedelta non_dca_packages = [
) p for p in self.ato.packages if p.primary_task not in dca_types
non_dca_packages = [p for p in self.ato.packages if ]
p.primary_task not in dca_types]
start_time = start_time_generator( start_time = start_time_generator(
count=len(non_dca_packages), count=len(non_dca_packages), earliest=5, latest=90, margin=5
earliest=5,
latest=90,
margin=5
) )
for package in self.ato.packages: for package in self.ato.packages:
tot = TotEstimator(package).earliest_tot() tot = TotEstimator(package).earliest_tot()
@@ -771,8 +882,7 @@ class CoalitionMissionPlanner:
departure_time = package.mission_departure_time departure_time = package.mission_departure_time
# Should be impossible for CAPs # Should be impossible for CAPs
if departure_time is None: if departure_time is None:
logging.error( logging.error(f"Could not determine mission end time for {package}")
f"Could not determine mission end time for {package}")
continue continue
previous_cap_end_time[package.target] = departure_time previous_cap_end_time[package.target] = departure_time
else: else:
@@ -791,8 +901,6 @@ class CoalitionMissionPlanner:
message to the info panel. message to the info panel.
""" """
if self.is_player: if self.is_player:
self.game.informations.append( self.game.informations.append(Information(title, text, self.game.turn))
Information(title, text, self.game.turn)
)
else: else:
logging.info(f"{title}: {text}") logging.info(f"{title}: {text}")

View File

@@ -12,8 +12,8 @@ from dcs.helicopters import (
OH_58D, OH_58D,
SA342L, SA342L,
SA342M, SA342M,
SH_60B,
UH_1H, UH_1H,
SH_60B
) )
from dcs.planes import ( from dcs.planes import (
AJS37, AJS37,
@@ -22,11 +22,14 @@ from dcs.planes import (
A_10C, A_10C,
A_10C_2, A_10C_2,
A_20G, A_20G,
A_50,
B_17G, B_17G,
B_1B, B_1B,
B_52H, B_52H,
Bf_109K_4, Bf_109K_4,
C_101CC, C_101CC,
E_2C,
E_3A,
FA_18C_hornet, FA_18C_hornet,
FW_190A8, FW_190A8,
FW_190D9, FW_190D9,
@@ -40,9 +43,11 @@ from dcs.planes import (
F_4E, F_4E,
F_5E_3, F_5E_3,
F_86F_Sabre, F_86F_Sabre,
I_16,
JF_17, JF_17,
J_11A, J_11A,
Ju_88A4, Ju_88A4,
KJ_2000,
L_39ZA, L_39ZA,
MQ_9_Reaper, MQ_9_Reaper,
M_2000C, M_2000C,
@@ -54,7 +59,6 @@ from dcs.planes import (
MiG_27K, MiG_27K,
MiG_29A, MiG_29A,
MiG_29G, MiG_29G,
MiG_29K,
MiG_29S, MiG_29S,
MiG_31, MiG_31,
Mirage_2000_5, Mirage_2000_5,
@@ -83,18 +87,16 @@ from dcs.planes import (
Tu_22M3, Tu_22M3,
Tu_95MS, Tu_95MS,
WingLoong_I, WingLoong_I,
I_16 I_16,
) )
from dcs.unittype import FlyingType from dcs.unittype import FlyingType
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
from pydcs_extensions.a4ec.a4ec import A_4E_C from pydcs_extensions.a4ec.a4ec import A_4E_C
from pydcs_extensions.f22a.f22a import F_22A 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 from pydcs_extensions.hercules.hercules import Hercules
from pydcs_extensions.mb339.mb339 import MB_339PAN
from pydcs_extensions.su57.su57 import Su_57
# All aircraft lists are in priority order. Aircraft higher in the list will be # All aircraft lists are in priority order. Aircraft higher in the list will be
# preferred over those lower in the list. # preferred over those lower in the list.
@@ -111,14 +113,12 @@ CAP_CAPABLE = [
F_14B, F_14B,
F_14A_135_GR, F_14A_135_GR,
MiG_25PD, MiG_25PD,
Rafale_M,
Su_33, Su_33,
Su_30, Su_30,
Su_27, Su_27,
J_11A, J_11A,
F_15C, F_15C,
MiG_29S, MiG_29S,
MiG_29K,
MiG_29G, MiG_29G,
MiG_29A, MiG_29A,
F_16C_50, F_16C_50,
@@ -155,8 +155,8 @@ CAP_CAPABLE = [
# Used for CAS (Close air support) and BAI (Battlefield Interdiction) # Used for CAS (Close air support) and BAI (Battlefield Interdiction)
CAS_CAPABLE = [ CAS_CAPABLE = [
A_10C_2, A_10C_2,
A_10C,
B_1B, B_1B,
A_10C,
F_14B, F_14B,
F_14A_135_GR, F_14A_135_GR,
Su_25TM, Su_25TM,
@@ -165,8 +165,6 @@ CAS_CAPABLE = [
F_15E, F_15E,
F_16C_50, F_16C_50,
FA_18C_hornet, FA_18C_hornet,
Rafale_A_S,
Rafale_B,
Tornado_GR4, Tornado_GR4,
Tornado_IDS, Tornado_IDS,
JF_17, JF_17,
@@ -180,6 +178,7 @@ CAS_CAPABLE = [
S_3B, S_3B,
Su_34, Su_34,
Su_30, Su_30,
MiG_19P,
MiG_29S, MiG_29S,
MiG_27K, MiG_27K,
MiG_29A, MiG_29A,
@@ -227,8 +226,6 @@ SEAD_CAPABLE = [
Tornado_IDS, Tornado_IDS,
Su_25T, Su_25T,
Su_25TM, Su_25TM,
Rafale_A_S,
Rafale_B,
F_4E, F_4E,
A_4E_C, A_4E_C,
AV8BNA, AV8BNA,
@@ -276,8 +273,6 @@ STRIKE_CAPABLE = [
Tu_22M3, Tu_22M3,
F_15E, F_15E,
AJS37, AJS37,
Rafale_A_S,
Rafale_B,
Tornado_GR4, Tornado_GR4,
F_16C_50, F_16C_50,
FA_18C_hornet, FA_18C_hornet,
@@ -296,7 +291,6 @@ STRIKE_CAPABLE = [
Su_30, Su_30,
Su_27, Su_27,
MiG_29S, MiG_29S,
MiG_29K,
MiG_29G, MiG_29G,
MiG_29A, MiG_29A,
JF_17, JF_17,
@@ -333,8 +327,6 @@ ANTISHIP_CAPABLE = [
AJS37, AJS37,
Tu_22M3, Tu_22M3,
FA_18C_hornet, FA_18C_hornet,
Rafale_A_S,
Rafale_B,
Su_24M, Su_24M,
Su_17M4, Su_17M4,
JF_17, JF_17,
@@ -367,10 +359,13 @@ TRANSPORT_CAPABLE = [
UH_1H, UH_1H,
] ]
DRONES = [ DRONES = [MQ_9_Reaper, RQ_1A_Predator, WingLoong_I]
MQ_9_Reaper,
RQ_1A_Predator, AEWC_CAPABLE = [
WingLoong_I E_3A,
E_2C,
A_50,
KJ_2000,
] ]
@@ -396,6 +391,8 @@ def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
return STRIKE_CAPABLE return STRIKE_CAPABLE
elif task == FlightType.ESCORT: elif task == FlightType.ESCORT:
return CAP_CAPABLE return CAP_CAPABLE
elif task == FlightType.AEWC:
return AEWC_CAPABLE
else: else:
logging.error(f"Unplannable flight type: {task}") logging.error(f"Unplannable flight type: {task}")
return [] return []

View File

@@ -12,8 +12,9 @@ if TYPE_CHECKING:
class ClosestAirfields: class ClosestAirfields:
"""Precalculates which control points are closes to the given target.""" """Precalculates which control points are closes to the given target."""
def __init__(self, target: MissionTarget, def __init__(
all_control_points: List[ControlPoint]) -> None: self, target: MissionTarget, all_control_points: List[ControlPoint]
) -> None:
self.target = target self.target = target
# This cache is configured once on load, so it's important that it is # This cache is configured once on load, so it's important that it is
# complete and deterministic to avoid different behaviors across loads. # complete and deterministic to avoid different behaviors across loads.
@@ -52,9 +53,7 @@ class ObjectiveDistanceCache:
@classmethod @classmethod
def get_closest_airfields(cls, location: MissionTarget) -> ClosestAirfields: def get_closest_airfields(cls, location: MissionTarget) -> ClosestAirfields:
if cls.theater is None: if cls.theater is None:
raise RuntimeError( raise RuntimeError("Call ObjectiveDistanceCache.set_theater before using")
"Call ObjectiveDistanceCache.set_theater before using"
)
if location.name not in cls.closest_airfields: if location.name not in cls.closest_airfields:
cls.closest_airfields[location.name] = ClosestAirfields( cls.closest_airfields[location.name] = ClosestAirfields(

View File

@@ -20,6 +20,15 @@ if TYPE_CHECKING:
class FlightType(Enum): class FlightType(Enum):
"""Enumeration of mission types.
The value of each enumeration is the name that will be shown in the UI.
These values are persisted to the save game as well since they are a part of
each flight and thus a part of the ATO, so changing these values will break
save compat.
"""
TARCAP = "TARCAP" TARCAP = "TARCAP"
BARCAP = "BARCAP" BARCAP = "BARCAP"
CAS = "CAS" CAS = "CAS"
@@ -33,28 +42,29 @@ class FlightType(Enum):
SWEEP = "Fighter sweep" SWEEP = "Fighter sweep"
OCA_RUNWAY = "OCA/Runway" OCA_RUNWAY = "OCA/Runway"
OCA_AIRCRAFT = "OCA/Aircraft" OCA_AIRCRAFT = "OCA/Aircraft"
AEWC = "AEW&C"
def __str__(self) -> str: def __str__(self) -> str:
return self.value return self.value
class FlightWaypointType(Enum): class FlightWaypointType(Enum):
TAKEOFF = 0 # Take off point TAKEOFF = 0 # Take off point
ASCEND_POINT = 1 # Ascension point after take off ASCEND_POINT = 1 # Ascension point after take off
PATROL = 2 # Patrol point PATROL = 2 # Patrol point
PATROL_TRACK = 3 # Patrol race track PATROL_TRACK = 3 # Patrol race track
NAV = 4 # Nav point NAV = 4 # Nav point
INGRESS_STRIKE = 5 # Ingress strike (For generator, means that this should have bombing on next TARGET_POINT points) INGRESS_STRIKE = 5 # Ingress strike (For generator, means that this should have bombing on next TARGET_POINT points)
INGRESS_SEAD = 6 # Ingress sead (For generator, means that this should attack groups on TARGET_GROUP_LOC points) INGRESS_SEAD = 6 # Ingress sead (For generator, means that this should attack groups on TARGET_GROUP_LOC points)
INGRESS_CAS = 7 # Ingress cas (should start CAS task) INGRESS_CAS = 7 # Ingress cas (should start CAS task)
CAS = 8 # Should do CAS there CAS = 8 # Should do CAS there
EGRESS = 9 # Should stop attack EGRESS = 9 # Should stop attack
DESCENT_POINT = 10 # Should start descending to pattern alt DESCENT_POINT = 10 # Should start descending to pattern alt
LANDING_POINT = 11 # Should land there LANDING_POINT = 11 # Should land there
TARGET_POINT = 12 # A target building or static object, position TARGET_POINT = 12 # A target building or static object, position
TARGET_GROUP_LOC = 13 # A target group approximate location TARGET_GROUP_LOC = 13 # A target group approximate location
TARGET_SHIP = 14 # A target ship known location TARGET_SHIP = 14 # A target ship known location
CUSTOM = 15 # User waypoint (no specific behaviour) CUSTOM = 15 # User waypoint (no specific behaviour)
JOIN = 16 JOIN = 16
SPLIT = 17 SPLIT = 17
LOITER = 18 LOITER = 18
@@ -68,9 +78,13 @@ class FlightWaypointType(Enum):
class FlightWaypoint: class FlightWaypoint:
def __init__(
def __init__(self, waypoint_type: FlightWaypointType, x: float, y: float, self,
alt: Distance = meters(0)) -> None: waypoint_type: FlightWaypointType,
x: float,
y: float,
alt: Distance = meters(0),
) -> None:
"""Creates a flight waypoint. """Creates a flight waypoint.
Args: Args:
@@ -108,10 +122,13 @@ class FlightWaypoint:
return Point(self.x, self.y) return Point(self.x, self.y)
@classmethod @classmethod
def from_pydcs(cls, point: MovingPoint, def from_pydcs(cls, point: MovingPoint, from_cp: ControlPoint) -> "FlightWaypoint":
from_cp: ControlPoint) -> "FlightWaypoint": waypoint = FlightWaypoint(
waypoint = FlightWaypoint(FlightWaypointType.NAV, point.position.x, FlightWaypointType.NAV,
point.position.y, meters(point.alt)) point.position.x,
point.position.y,
meters(point.alt),
)
waypoint.alt_type = point.alt_type waypoint.alt_type = point.alt_type
# Other actions exist... but none of them *should* be the first # Other actions exist... but none of them *should* be the first
# waypoint for a flight. # waypoint for a flight.
@@ -135,12 +152,19 @@ class FlightWaypoint:
class Flight: class Flight:
def __init__(
def __init__(self, package: Package, country: str, unit_type: Type[FlyingType], self,
count: int, flight_type: FlightType, start_type: str, package: Package,
departure: ControlPoint, arrival: ControlPoint, country: str,
divert: Optional[ControlPoint], unit_type: Type[FlyingType],
custom_name: Optional[str] = None) -> None: count: int,
flight_type: FlightType,
start_type: str,
departure: ControlPoint,
arrival: ControlPoint,
divert: Optional[ControlPoint],
custom_name: Optional[str] = None,
) -> None:
self.package = package self.package = package
self.country = country self.country = country
self.unit_type = unit_type self.unit_type = unit_type
@@ -161,10 +185,9 @@ class Flight:
# FlightPlanBuilder, but an empty flight plan the flight begins with an # FlightPlanBuilder, but an empty flight plan the flight begins with an
# empty flight plan. # empty flight plan.
from gen.flights.flightplan import CustomFlightPlan from gen.flights.flightplan import CustomFlightPlan
self.flight_plan: FlightPlan = CustomFlightPlan( self.flight_plan: FlightPlan = CustomFlightPlan(
package=package, package=package, flight=self, custom_waypoints=[]
flight=self,
custom_waypoints=[]
) )
@property @property
@@ -182,7 +205,7 @@ class Flight:
return f"[{self.flight_type}] {self.count} x {name}" return f"[{self.flight_type}] {self.count} x {name}"
def __str__(self): def __str__(self):
name = db.unit_get_expanded_info(self.country, self.unit_type, 'name') name = db.unit_get_expanded_info(self.country, self.unit_type, "name")
if self.custom_name: if self.custom_name:
return f"{self.custom_name} {self.count} x {name}" return f"{self.custom_name} {self.count} x {name}"
return f"[{self.flight_type}] {self.count} x {name}" return f"[{self.flight_type}] {self.count} x {name}"

File diff suppressed because it is too large Load Diff

View File

@@ -23,7 +23,6 @@ if TYPE_CHECKING:
class GroundSpeed: class GroundSpeed:
@classmethod @classmethod
def for_flight(cls, flight: Flight, altitude: Distance) -> Speed: def for_flight(cls, flight: Flight, altitude: Distance) -> Speed:
if not issubclass(flight.unit_type, FlyingType): if not issubclass(flight.unit_type, FlyingType):
@@ -55,13 +54,11 @@ class TravelTime:
def between_points(a: Point, b: Point, speed: Speed) -> timedelta: def between_points(a: Point, b: Point, speed: Speed) -> timedelta:
error_factor = 1.1 error_factor = 1.1
distance = meters(a.distance_to_point(b)) distance = meters(a.distance_to_point(b))
return timedelta( return timedelta(hours=distance.nautical_miles / speed.knots * error_factor)
hours=distance.nautical_miles / speed.knots * error_factor)
# TODO: Most if not all of this should move into FlightPlan. # TODO: Most if not all of this should move into FlightPlan.
class TotEstimator: class TotEstimator:
def __init__(self, package: Package) -> None: def __init__(self, package: Package) -> None:
self.package = package self.package = package
@@ -75,9 +72,9 @@ class TotEstimator:
return startup_time return startup_time
def earliest_tot(self) -> timedelta: def earliest_tot(self) -> timedelta:
earliest_tot = max(( earliest_tot = max(
self.earliest_tot_for_flight(f) for f in self.package.flights (self.earliest_tot_for_flight(f) for f in self.package.flights)
)) )
# Trim microseconds. DCS doesn't handle sub-second resolution for tasks, # Trim microseconds. DCS doesn't handle sub-second resolution for tasks,
# and they're not interesting from a mission planning perspective so we # and they're not interesting from a mission planning perspective so we

View File

@@ -14,7 +14,7 @@ from typing import (
from dcs.mapping import Point from dcs.mapping import Point
from dcs.unit import Unit from dcs.unit import Unit
from dcs.unitgroup import VehicleGroup from dcs.unitgroup import Group, VehicleGroup
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
@@ -32,12 +32,17 @@ from .flight import Flight, FlightWaypoint, FlightWaypointType
@dataclass(frozen=True) @dataclass(frozen=True)
class StrikeTarget: class StrikeTarget:
name: str name: str
target: Union[VehicleGroup, TheaterGroundObject, Unit] target: Union[VehicleGroup, TheaterGroundObject, Unit, Group]
class WaypointBuilder: class WaypointBuilder:
def __init__(self, flight: Flight, game: Game, player: bool, def __init__(
targets: Optional[List[StrikeTarget]] = None) -> None: self,
flight: Flight,
game: Game,
player: bool,
targets: Optional[List[StrikeTarget]] = None,
) -> None:
self.flight = flight self.flight = flight
self.conditions = game.conditions self.conditions = game.conditions
self.doctrine = game.faction_for(player).doctrine self.doctrine = game.faction_for(player).doctrine
@@ -65,9 +70,7 @@ class WaypointBuilder:
FlightWaypointType.NAV, FlightWaypointType.NAV,
position.x, position.x,
position.y, position.y,
meters( meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
500
) if self.is_helo else self.doctrine.rendezvous_altitude
) )
waypoint.name = "NAV" waypoint.name = "NAV"
waypoint.alt_type = "BARO" waypoint.alt_type = "BARO"
@@ -75,10 +78,7 @@ class WaypointBuilder:
waypoint.pretty_name = "Enter theater" waypoint.pretty_name = "Enter theater"
else: else:
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
FlightWaypointType.TAKEOFF, FlightWaypointType.TAKEOFF, position.x, position.y, meters(0)
position.x,
position.y,
meters(0)
) )
waypoint.name = "TAKEOFF" waypoint.name = "TAKEOFF"
waypoint.alt_type = "RADIO" waypoint.alt_type = "RADIO"
@@ -98,9 +98,7 @@ class WaypointBuilder:
FlightWaypointType.NAV, FlightWaypointType.NAV,
position.x, position.x,
position.y, position.y,
meters( meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
500
) if self.is_helo else self.doctrine.rendezvous_altitude
) )
waypoint.name = "NAV" waypoint.name = "NAV"
waypoint.alt_type = "BARO" waypoint.alt_type = "BARO"
@@ -108,10 +106,7 @@ class WaypointBuilder:
waypoint.pretty_name = "Exit theater" waypoint.pretty_name = "Exit theater"
else: else:
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
FlightWaypointType.LANDING_POINT, FlightWaypointType.LANDING_POINT, position.x, position.y, meters(0)
position.x,
position.y,
meters(0)
) )
waypoint.name = "LANDING" waypoint.name = "LANDING"
waypoint.alt_type = "RADIO" waypoint.alt_type = "RADIO"
@@ -119,8 +114,7 @@ class WaypointBuilder:
waypoint.pretty_name = "Land" waypoint.pretty_name = "Land"
return waypoint return waypoint
def divert(self, def divert(self, divert: Optional[ControlPoint]) -> Optional[FlightWaypoint]:
divert: Optional[ControlPoint]) -> Optional[FlightWaypoint]:
"""Create divert waypoint for the given arrival airfield or carrier. """Create divert waypoint for the given arrival airfield or carrier.
Args: Args:
@@ -141,10 +135,7 @@ class WaypointBuilder:
altitude_type = "RADIO" altitude_type = "RADIO"
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
FlightWaypointType.DIVERT, FlightWaypointType.DIVERT, position.x, position.y, altitude
position.x,
position.y,
altitude
) )
waypoint.alt_type = altitude_type waypoint.alt_type = altitude_type
waypoint.name = "DIVERT" waypoint.name = "DIVERT"
@@ -158,9 +149,7 @@ class WaypointBuilder:
FlightWaypointType.LOITER, FlightWaypointType.LOITER,
position.x, position.x,
position.y, position.y,
meters( meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
500
) if self.is_helo else self.doctrine.rendezvous_altitude
) )
waypoint.pretty_name = "Hold" waypoint.pretty_name = "Hold"
waypoint.description = "Wait until push time" waypoint.description = "Wait until push time"
@@ -172,10 +161,10 @@ class WaypointBuilder:
FlightWaypointType.JOIN, FlightWaypointType.JOIN,
position.x, position.x,
position.y, position.y,
meters( meters(80) if self.is_helo else self.doctrine.ingress_altitude,
500
) if self.is_helo else self.doctrine.ingress_altitude
) )
if self.is_helo:
waypoint.alt_type = "RADIO"
waypoint.pretty_name = "Join" waypoint.pretty_name = "Join"
waypoint.description = "Rendezvous with package" waypoint.description = "Rendezvous with package"
waypoint.name = "JOIN" waypoint.name = "JOIN"
@@ -186,25 +175,29 @@ class WaypointBuilder:
FlightWaypointType.SPLIT, FlightWaypointType.SPLIT,
position.x, position.x,
position.y, position.y,
meters( meters(80) if self.is_helo else self.doctrine.ingress_altitude,
500
) if self.is_helo else self.doctrine.ingress_altitude
) )
if self.is_helo:
waypoint.alt_type = "RADIO"
waypoint.pretty_name = "Split" waypoint.pretty_name = "Split"
waypoint.description = "Depart from package" waypoint.description = "Depart from package"
waypoint.name = "SPLIT" waypoint.name = "SPLIT"
return waypoint return waypoint
def ingress(self, ingress_type: FlightWaypointType, position: Point, def ingress(
objective: MissionTarget) -> FlightWaypoint: self,
ingress_type: FlightWaypointType,
position: Point,
objective: MissionTarget,
) -> FlightWaypoint:
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
ingress_type, ingress_type,
position.x, position.x,
position.y, position.y,
meters( meters(50) if self.is_helo else self.doctrine.ingress_altitude,
500
) if self.is_helo else self.doctrine.ingress_altitude
) )
if self.is_helo:
waypoint.alt_type = "RADIO"
waypoint.pretty_name = "INGRESS on " + objective.name waypoint.pretty_name = "INGRESS on " + objective.name
waypoint.description = "INGRESS on " + objective.name waypoint.description = "INGRESS on " + objective.name
waypoint.name = "INGRESS" waypoint.name = "INGRESS"
@@ -217,10 +210,10 @@ class WaypointBuilder:
FlightWaypointType.EGRESS, FlightWaypointType.EGRESS,
position.x, position.x,
position.y, position.y,
meters( meters(50) if self.is_helo else self.doctrine.ingress_altitude,
500
) if self.is_helo else self.doctrine.ingress_altitude
) )
if self.is_helo:
waypoint.alt_type = "RADIO"
waypoint.pretty_name = "EGRESS from " + target.name waypoint.pretty_name = "EGRESS from " + target.name
waypoint.description = "EGRESS from " + target.name waypoint.description = "EGRESS from " + target.name
waypoint.name = "EGRESS" waypoint.name = "EGRESS"
@@ -244,7 +237,7 @@ class WaypointBuilder:
FlightWaypointType.TARGET_POINT, FlightWaypointType.TARGET_POINT,
target.target.position.x, target.target.position.x,
target.target.position.y, target.target.position.y,
meters(0) meters(0),
) )
waypoint.description = description waypoint.description = description
waypoint.pretty_name = description waypoint.pretty_name = description
@@ -269,13 +262,14 @@ class WaypointBuilder:
return self._target_area(f"ATTACK {target.name}", target, flyover=True) return self._target_area(f"ATTACK {target.name}", target, flyover=True)
@staticmethod @staticmethod
def _target_area(name: str, location: MissionTarget, def _target_area(
flyover: bool = False) -> FlightWaypoint: name: str, location: MissionTarget, flyover: bool = False
) -> FlightWaypoint:
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
FlightWaypointType.TARGET_GROUP_LOC, FlightWaypointType.TARGET_GROUP_LOC,
location.position.x, location.position.x,
location.position.y, location.position.y,
meters(0) meters(0),
) )
waypoint.description = name waypoint.description = name
waypoint.pretty_name = name waypoint.pretty_name = name
@@ -300,7 +294,7 @@ class WaypointBuilder:
FlightWaypointType.CAS, FlightWaypointType.CAS,
position.x, position.x,
position.y, position.y,
meters(500) if self.is_helo else meters(1000) meters(50) if self.is_helo else meters(1000),
) )
waypoint.alt_type = "RADIO" waypoint.alt_type = "RADIO"
waypoint.description = "Provide CAS" waypoint.description = "Provide CAS"
@@ -317,10 +311,7 @@ class WaypointBuilder:
altitude: Altitude of the racetrack. altitude: Altitude of the racetrack.
""" """
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
FlightWaypointType.PATROL_TRACK, FlightWaypointType.PATROL_TRACK, position.x, position.y, altitude
position.x,
position.y,
altitude
) )
waypoint.name = "RACETRACK START" waypoint.name = "RACETRACK START"
waypoint.description = "Orbit between this point and the next point" waypoint.description = "Orbit between this point and the next point"
@@ -336,18 +327,16 @@ class WaypointBuilder:
altitude: Altitude of the racetrack. altitude: Altitude of the racetrack.
""" """
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
FlightWaypointType.PATROL, FlightWaypointType.PATROL, position.x, position.y, altitude
position.x,
position.y,
altitude
) )
waypoint.name = "RACETRACK END" waypoint.name = "RACETRACK END"
waypoint.description = "Orbit between this point and the previous point" waypoint.description = "Orbit between this point and the previous point"
waypoint.pretty_name = "Race-track end" waypoint.pretty_name = "Race-track end"
return waypoint return waypoint
def race_track(self, start: Point, end: Point, def race_track(
altitude: Distance) -> Tuple[FlightWaypoint, FlightWaypoint]: self, start: Point, end: Point, altitude: Distance
) -> Tuple[FlightWaypoint, FlightWaypoint]:
"""Creates two waypoint for a racetrack orbit. """Creates two waypoint for a racetrack orbit.
Args: Args:
@@ -355,8 +344,25 @@ class WaypointBuilder:
end: The ending racetrack waypoint. end: The ending racetrack waypoint.
altitude: The racetrack altitude. altitude: The racetrack altitude.
""" """
return (self.race_track_start(start, altitude), return (
self.race_track_end(end, altitude)) self.race_track_start(start, altitude),
self.race_track_end(end, altitude),
)
@staticmethod
def orbit(start: Point, altitude: Distance) -> FlightWaypoint:
"""Creates an circular orbit point.
Args:
start: Position of the waypoint.
altitude: Altitude of the racetrack.
"""
waypoint = FlightWaypoint(FlightWaypointType.LOITER, start.x, start.y, altitude)
waypoint.name = "ORBIT"
waypoint.description = "Anchor and hold at this point"
waypoint.pretty_name = "Orbit"
return waypoint
@staticmethod @staticmethod
def sweep_start(position: Point, altitude: Distance) -> FlightWaypoint: def sweep_start(position: Point, altitude: Distance) -> FlightWaypoint:
@@ -367,10 +373,7 @@ class WaypointBuilder:
altitude: Altitude of the sweep in meters. altitude: Altitude of the sweep in meters.
""" """
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
FlightWaypointType.INGRESS_SWEEP, FlightWaypointType.INGRESS_SWEEP, position.x, position.y, altitude
position.x,
position.y,
altitude
) )
waypoint.name = "SWEEP START" waypoint.name = "SWEEP START"
waypoint.description = "Proceed to the target and engage enemy aircraft" waypoint.description = "Proceed to the target and engage enemy aircraft"
@@ -386,18 +389,16 @@ class WaypointBuilder:
altitude: Altitude of the sweep in meters. altitude: Altitude of the sweep in meters.
""" """
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
FlightWaypointType.EGRESS, FlightWaypointType.EGRESS, position.x, position.y, altitude
position.x,
position.y,
altitude
) )
waypoint.name = "SWEEP END" waypoint.name = "SWEEP END"
waypoint.description = "End of sweep" waypoint.description = "End of sweep"
waypoint.pretty_name = "Sweep end" waypoint.pretty_name = "Sweep end"
return waypoint return waypoint
def sweep(self, start: Point, end: Point, def sweep(
altitude: Distance) -> Tuple[FlightWaypoint, FlightWaypoint]: self, start: Point, end: Point, altitude: Distance
) -> Tuple[FlightWaypoint, FlightWaypoint]:
"""Creates two waypoint for a racetrack orbit. """Creates two waypoint for a racetrack orbit.
Args: Args:
@@ -405,11 +406,11 @@ class WaypointBuilder:
end: The end of the sweep. end: The end of the sweep.
altitude: The sweep altitude. altitude: The sweep altitude.
""" """
return (self.sweep_start(start, altitude), return (self.sweep_start(start, altitude), self.sweep_end(end, altitude))
self.sweep_end(end, altitude))
def escort(self, ingress: Point, target: MissionTarget, egress: Point) -> \ def escort(
Tuple[FlightWaypoint, FlightWaypoint, FlightWaypoint]: self, ingress: Point, target: MissionTarget, egress: Point
) -> Tuple[FlightWaypoint, FlightWaypoint, FlightWaypoint]:
"""Creates the waypoints needed to escort the package. """Creates the waypoints needed to escort the package.
Args: Args:
@@ -423,17 +424,16 @@ class WaypointBuilder:
# description in gen.aircraft.JoinPointBuilder), so instead we give # description in gen.aircraft.JoinPointBuilder), so instead we give
# the escort flights a flight plan including the ingress point, target # the escort flights a flight plan including the ingress point, target
# area, and egress point. # area, and egress point.
ingress = self.ingress(FlightWaypointType.INGRESS_ESCORT, ingress, ingress = self.ingress(FlightWaypointType.INGRESS_ESCORT, ingress, target)
target)
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
FlightWaypointType.TARGET_GROUP_LOC, FlightWaypointType.TARGET_GROUP_LOC,
target.position.x, target.position.x,
target.position.y, target.position.y,
meters( meters(50) if self.is_helo else self.doctrine.ingress_altitude,
500
) if self.is_helo else self.doctrine.ingress_altitude
) )
if self.is_helo:
waypoint.alt_type = "RADIO"
waypoint.name = "TARGET" waypoint.name = "TARGET"
waypoint.description = "Escort the package" waypoint.description = "Escort the package"
waypoint.pretty_name = "Target area" waypoint.pretty_name = "Target area"
@@ -450,18 +450,14 @@ class WaypointBuilder:
altitude: Altitude of the waypoint. altitude: Altitude of the waypoint.
""" """
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
FlightWaypointType.NAV, FlightWaypointType.NAV, position.x, position.y, altitude
position.x,
position.y,
altitude
) )
waypoint.name = "NAV" waypoint.name = "NAV"
waypoint.description = "NAV" waypoint.description = "NAV"
waypoint.pretty_name = "Nav" waypoint.pretty_name = "Nav"
return waypoint return waypoint
def nav_path(self, a: Point, b: Point, def nav_path(self, a: Point, b: Point, altitude: Distance) -> List[FlightWaypoint]:
altitude: Distance) -> List[FlightWaypoint]:
path = self.clean_nav_points(self.navmesh.shortest_path(a, b)) path = self.clean_nav_points(self.navmesh.shortest_path(a, b))
return [self.nav(self.perturb(p), altitude) for p in path] return [self.nav(self.perturb(p), altitude) for p in path]
@@ -488,10 +484,8 @@ class WaypointBuilder:
previous = current previous = current
current = nxt current = nxt
def nav_point_prunable(self, previous: Point, current: Point, def nav_point_prunable(self, previous: Point, current: Point, nxt: Point) -> bool:
nxt: Point) -> bool: previous_threatened = self.threat_zones.path_threatened(previous, current)
previous_threatened = self.threat_zones.path_threatened(previous,
current)
next_threatened = self.threat_zones.path_threatened(current, nxt) next_threatened = self.threat_zones.path_threatened(current, nxt)
pruned_threatened = self.threat_zones.path_threatened(previous, nxt) pruned_threatened = self.threat_zones.path_threatened(previous, nxt)
previous_distance = meters(previous.distance_to_point(current)) previous_distance = meters(previous.distance_to_point(current))

View File

@@ -15,11 +15,15 @@ class ForcedOptionsGenerator:
self.game = game self.game = game
def _set_options_view(self) -> None: def _set_options_view(self) -> None:
self.mission.forced_options.options_view = self.game.settings.map_coalition_visibility self.mission.forced_options.options_view = (
self.game.settings.map_coalition_visibility
)
def _set_external_views(self) -> None: def _set_external_views(self) -> None:
if not self.game.settings.external_views_allowed: if not self.game.settings.external_views_allowed:
self.mission.forced_options.external_views = self.game.settings.external_views_allowed self.mission.forced_options.external_views = (
self.game.settings.external_views_allowed
)
def _set_labels(self) -> None: def _set_labels(self) -> None:
# TODO: Fix settings to use the real type. # TODO: Fix settings to use the real type.

View File

@@ -39,12 +39,11 @@ GROUP_SIZES_BY_COMBAT_STANCE = {
CombatStance.RETREAT: [2, 4, 6, 8], CombatStance.RETREAT: [2, 4, 6, 8],
CombatStance.BREAKTHROUGH: [4, 6, 6, 8], CombatStance.BREAKTHROUGH: [4, 6, 6, 8],
CombatStance.ELIMINATION: [2, 4, 4, 4, 6], CombatStance.ELIMINATION: [2, 4, 4, 4, 6],
CombatStance.AMBUSH: [1, 1, 2, 2, 2, 2, 4] CombatStance.AMBUSH: [1, 1, 2, 2, 2, 2, 4],
} }
class CombatGroup: class CombatGroup:
def __init__(self, role: CombatGroupRole): def __init__(self, role: CombatGroupRole):
self.units: List[VehicleType] = [] self.units: List[VehicleType] = []
self.role = role self.role = role
@@ -60,11 +59,12 @@ class CombatGroup:
class GroundPlanner: class GroundPlanner:
def __init__(self, cp: ControlPoint, game):
def __init__(self, cp:ControlPoint, game):
self.cp = cp self.cp = cp
self.game = game self.game = game
self.connected_enemy_cp = [cp for cp in self.cp.connected_points if cp.captured != self.cp.captured] self.connected_enemy_cp = [
cp for cp in self.cp.connected_points if cp.captured != self.cp.captured
]
self.tank_groups: List[CombatGroup] = [] self.tank_groups: List[CombatGroup] = []
self.apc_group: List[CombatGroup] = [] self.apc_group: List[CombatGroup] = []
self.ifv_group: List[CombatGroup] = [] self.ifv_group: List[CombatGroup] = []
@@ -80,7 +80,7 @@ class GroundPlanner:
def plan_groundwar(self): def plan_groundwar(self):
if hasattr(self.cp, 'stance'): if hasattr(self.cp, "stance"):
group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[self.cp.stance] group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[self.cp.stance]
else: else:
self.cp.stance = CombatStance.DEFENSIVE self.cp.stance = CombatStance.DEFENSIVE
@@ -152,12 +152,3 @@ class GroundPlanner:
print("For : #" + str(key)) print("For : #" + str(key))
for group in self.units_per_cp[key]: for group in self.units_per_cp[key]:
print(str(group)) print(str(group))

View File

@@ -14,51 +14,44 @@ TYPE_TANKS = [
Armor.MBT_Challenger_II, Armor.MBT_Challenger_II,
Armor.MBT_M1A2_Abrams, Armor.MBT_M1A2_Abrams,
Armor.MBT_M60A3_Patton, Armor.MBT_M60A3_Patton,
Armor.MBT_Merkava_Mk__4, Armor.MBT_Merkava_IV,
Armor.ZTZ_96B, Armor.ZTZ_96B,
# WW2 # WW2
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G, Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
Armor.MT_Pz_Kpfw_IV_Ausf_H, Armor.MT_PzIV_H,
Armor.HT_Pz_Kpfw_VI_Tiger_I, Armor.HT_Pz_Kpfw_VI_Tiger_I,
Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II, Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II,
Armor.MT_M4_Sherman, Armor.MT_M4_Sherman,
Armor.MT_M4A4_Sherman_Firefly, Armor.MT_M4A4_Sherman_Firefly,
Armor.StuG_IV, Armor.SPG_StuG_IV,
Armor.CT_Centaur_IV, Armor.CT_Centaur_IV,
Armor.CT_Cromwell_IV, Armor.CT_Cromwell_IV,
Armor.HIT_Churchill_VII, Armor.HIT_Churchill_VII,
Armor.LT_Mk_VII_Tetrarch, Armor.LT_Mk_VII_Tetrarch,
# Mods # Mods
frenchpack.DIM__TOYOTA_BLUE, frenchpack.DIM__TOYOTA_BLUE,
frenchpack.DIM__TOYOTA_GREEN, frenchpack.DIM__TOYOTA_GREEN,
frenchpack.DIM__TOYOTA_DESERT, frenchpack.DIM__TOYOTA_DESERT,
frenchpack.DIM__KAMIKAZE, frenchpack.DIM__KAMIKAZE,
frenchpack.AMX_10RCR, frenchpack.AMX_10RCR,
frenchpack.AMX_10RCR_SEPAR, frenchpack.AMX_10RCR_SEPAR,
frenchpack.AMX_30B2, frenchpack.AMX_30B2,
frenchpack.Leclerc_Serie_XXI, frenchpack.Leclerc_Serie_XXI,
] ]
TYPE_ATGM = [ TYPE_ATGM = [
Armor.ATGM_M1045_HMMWV_TOW, Armor.ATGM_HMMWV,
Armor.ATGM_M1134_Stryker, Armor.ATGM_Stryker,
Armor.IFV_BMP_2, Armor.IFV_BMP_2,
# WW2 (Tank Destroyers) # WW2 (Tank Destroyers)
Armor.M30_Cargo_Carrier, Unarmed.Carrier_M30_Cargo,
Armor.TD_Jagdpanzer_IV, Armor.SPG_Jagdpanzer_IV,
Armor.TD_Jagdpanther_G1, Armor.SPG_Jagdpanther_G1,
Armor.TD_M10_GMC, Armor.SPG_M10_GMC,
# Mods # Mods
frenchpack.VBAE_CRAB_MMP, frenchpack.VBAE_CRAB_MMP,
frenchpack.VAB_MEPHISTO, frenchpack.VAB_MEPHISTO,
frenchpack.TRM_2000_PAMELA, frenchpack.TRM_2000_PAMELA,
] ]
TYPE_IFV = [ TYPE_IFV = [
@@ -66,134 +59,124 @@ TYPE_IFV = [
Armor.IFV_BMP_2, Armor.IFV_BMP_2,
Armor.IFV_BMP_1, Armor.IFV_BMP_1,
Armor.IFV_Marder, Armor.IFV_Marder,
Armor.IFV_MCV_80, Armor.IFV_Warrior,
Armor.IFV_LAV_25, Armor.IFV_LAV_25,
Armor.SPG_M1128_Stryker_MGS, Armor.SPG_Stryker_MGS,
Armor.AC_Sd_Kfz_234_2_Puma, Armor.IFV_Sd_Kfz_234_2_Puma,
Armor.IFV_M2A2_Bradley, Armor.IFV_M2A2_Bradley,
Armor.IFV_BMD_1, Armor.IFV_BMD_1,
Armor.ZBD_04A, Armor.ZBD_04A,
# WW2 # WW2
Armor.AC_Sd_Kfz_234_2_Puma, Armor.IFV_Sd_Kfz_234_2_Puma,
Armor.LAC_M8_Greyhound, Armor.Car_M8_Greyhound_Armored,
Armor.Daimler_Armoured_Car, Armor.Car_Daimler_Armored,
# Mods # Mods
frenchpack.ERC_90, frenchpack.ERC_90,
frenchpack.VBAE_CRAB, frenchpack.VBAE_CRAB,
frenchpack.VAB_T20_13 frenchpack.VAB_T20_13,
] ]
TYPE_APC = [ TYPE_APC = [
Armor.APC_M1043_HMMWV_Armament, Armor.APC_HMMWV__Scout,
Armor.APC_M1126_Stryker_ICV, Armor.IFV_M1126_Stryker_ICV,
Armor.APC_M113, Armor.APC_M113,
Armor.APC_BTR_80, Armor.APC_BTR_80,
Armor.APC_BTR_82A, Armor.APC_BTR_82A,
Armor.APC_MTLB, Armor.APC_MTLB,
Armor.APC_M2A1, Armor.APC_M2A1_Halftrack,
Armor.APC_Cobra, Armor.APC_Cobra__Scout,
Armor.APC_Sd_Kfz_251, Armor.APC_Sd_Kfz_251_Halftrack,
Armor.APC_AAV_7, Armor.APC_AAV_7_Amphibious,
Armor.TPz_Fuchs, Armor.APC_TPz_Fuchs,
Armor.ARV_BRDM_2, Armor.IFV_BRDM_2,
Armor.ARV_BTR_RD, Armor.APC_BTR_RD,
Armor.FDDM_Grad, Artillery.Grad_MRL_FDDM__FC,
# WW2 # WW2
Armor.APC_M2A1, Armor.APC_M2A1_Halftrack,
Armor.APC_Sd_Kfz_251, Armor.APC_Sd_Kfz_251_Halftrack,
# Mods # Mods
frenchpack.VAB__50, frenchpack.VAB__50,
frenchpack.VBL__50, frenchpack.VBL__50,
frenchpack.VBL_AANF1, frenchpack.VBL_AANF1,
] ]
TYPE_ARTILLERY = [ TYPE_ARTILLERY = [
Artillery.MLRS_9A52_Smerch, Artillery.MLRS_9A52_Smerch_HE_300mm,
Artillery.SPH_2S1_Gvozdika, Artillery.SPH_2S1_Gvozdika_122mm,
Artillery.SPH_2S3_Akatsia, Artillery.SPH_2S3_Akatsia_152mm,
Artillery.MLRS_BM_21_Grad, Artillery.MLRS_BM_21_Grad_122mm,
Artillery.MLRS_9K57_Uragan_BM_27, Artillery.MLRS_BM_27_Uragan_220mm,
Artillery.SPH_M109_Paladin, Artillery.SPH_M109_Paladin_155mm,
Artillery.MLRS_M270, Artillery.MLRS_M270_227mm,
Artillery.SPH_2S9_Nona, Artillery.SPH_2S9_Nona_120mm_M,
Artillery.SpGH_Dana, Artillery.SPH_Dana_vz77_152mm,
Artillery.SPH_2S19_Msta, Artillery.PLZ_05,
Artillery.MLRS_FDDM, Artillery.SPH_2S19_Msta_152mm,
Artillery.MLRS_9A52_Smerch_CM_300mm,
# WW2 # WW2
Artillery.Sturmpanzer_IV_Brummbär, Artillery.SPG_Sturmpanzer_IV_Brummbar,
Artillery.M12_GMC Artillery.SPG_M12_GMC_155mm,
] ]
TYPE_LOGI = [ TYPE_LOGI = [
Unarmed.Transport_M818, Unarmed.Truck_M818_6x6,
Unarmed.Transport_KAMAZ_43101, Unarmed.Truck_KAMAZ_43101,
Unarmed.Transport_Ural_375, Unarmed.Truck_Ural_375,
Unarmed.Transport_GAZ_66, Unarmed.Truck_GAZ_66,
Unarmed.Transport_GAZ_3307, Unarmed.Truck_GAZ_3307,
Unarmed.Transport_GAZ_3308, Unarmed.Truck_GAZ_3308,
Unarmed.Transport_Ural_4320_31_Armored, Unarmed.Truck_Ural_4320_31_Arm_d,
Unarmed.Transport_Ural_4320T, Unarmed.Truck_Ural_4320T,
Unarmed.Blitz_3_6_6700A, Unarmed.Truck_Opel_Blitz,
Unarmed.belwagen_82, Unarmed.LUV_Kubelwagen_82,
Unarmed.Sd_Kfz_7, Unarmed.Carrier_Sd_Kfz_7_Tractor,
Unarmed.Sd_Kfz_2, Unarmed.LUV_Kettenrad,
Unarmed.Willys_MB, Unarmed.Car_Willys_Jeep,
Unarmed.Land_Rover_109_S3, Unarmed.LUV_Land_Rover_109,
Unarmed.Land_Rover_101_FC, Unarmed.Truck_Land_Rover_101_FC,
# Mods # Mods
frenchpack.VBL, frenchpack.VBL,
frenchpack.VAB, frenchpack.VAB,
] ]
TYPE_INFANTRY = [ TYPE_INFANTRY = [
Infantry.Infantry_Soldier_Insurgents, Infantry.Insurgent_AK_74,
Infantry.Soldier_AK, Infantry.Infantry_AK_74,
Infantry.Infantry_M1_Garand, Infantry.Infantry_M1_Garand,
Infantry.Infantry_Mauser_98, Infantry.Infantry_Mauser_98,
Infantry.Infantry_SMLE_No_4_Mk_1, Infantry.Infantry_SMLE_No_4_Mk_1,
Infantry.Georgian_soldier_with_M4, Infantry.Infantry_M4_Georgia,
Infantry.Infantry_Soldier_Rus, Infantry.Infantry_AK_74_Rus,
Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS,
Infantry.Paratrooper_RPG_16, Infantry.Paratrooper_RPG_16,
Infantry.Soldier_M249, Infantry.Infantry_M249,
Infantry.Infantry_M4, Infantry.Infantry_M4,
Infantry.Soldier_RPG, Infantry.Infantry_RPG,
] ]
TYPE_SHORAD = [ TYPE_SHORAD = [
AirDefence.AAA_ZU_23_on_Ural_375, AirDefence.SPAAA_ZU_23_2_Mounted_Ural_375,
AirDefence.AAA_ZU_23_Insurgent_on_Ural_375, AirDefence.SPAAA_ZU_23_2_Insurgent_Mounted_Ural_375,
AirDefence.AAA_ZSU_57_2, AirDefence.SPAAA_ZSU_57_2,
AirDefence.SPAAA_ZSU_23_4_Shilka, AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
AirDefence.SAM_SA_8_Osa_9A33, AirDefence.SAM_SA_8_Osa_Gecko_TEL,
AirDefence.SAM_SA_9_Strela_1_9P31, AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL,
AirDefence.SAM_SA_13_Strela_10M3_9A35M3, AirDefence.SAM_SA_13_Strela_10M3_Gopher_TEL,
AirDefence.SAM_SA_15_Tor_9A331, AirDefence.SAM_SA_15_Tor_Gauntlet,
AirDefence.SAM_SA_19_Tunguska_2S6, AirDefence.SAM_SA_19_Tunguska_Grison,
AirDefence.SPAAA_Gepard, AirDefence.SPAAA_Gepard,
AirDefence.AAA_Vulcan_M163, AirDefence.SPAAA_Vulcan_M163,
AirDefence.SAM_Linebacker_M6, AirDefence.SAM_Linebacker___Bradley_M6,
AirDefence.SAM_Chaparral_M48, AirDefence.SAM_Chaparral_M48,
AirDefence.SAM_Avenger_M1097, AirDefence.SAM_Avenger__Stinger,
AirDefence.SAM_Roland_ADS, AirDefence.SAM_Roland_ADS,
AirDefence.HQ_7_Self_Propelled_LN, AirDefence.HQ_7_Self_Propelled_LN,
AirDefence.AAA_8_8cm_Flak_18, AirDefence.AAA_8_8cm_Flak_18,
AirDefence.AAA_8_8cm_Flak_36, AirDefence.AAA_8_8cm_Flak_36,
AirDefence.AAA_8_8cm_Flak_37, AirDefence.AAA_8_8cm_Flak_37,
AirDefence.AAA_8_8cm_Flak_41, AirDefence.AAA_8_8cm_Flak_41,
AirDefence.AAA_Bofors_40mm, AirDefence.AAA_40mm_Bofors,
AirDefence.AAA_S_60_57mm,
AirDefence.AAA_M1_37mm, AirDefence.AAA_M1_37mm,
AirDefence.AA_gun_QF_3_7, AirDefence.AAA_QF_3_7,
] ]

View File

@@ -2,10 +2,11 @@ from enum import Enum
class CombatStance(Enum): class CombatStance(Enum):
DEFENSIVE = 0 # Unit will adopt defensive stance with medium group of units DEFENSIVE = 0 # Unit will adopt defensive stance with medium group of units
AGGRESSIVE = 1 # Unit will attempt to make progress with medium sized group of units AGGRESSIVE = (
RETREAT = 2 # Unit will retreat 1 # Unit will attempt to make progress with medium sized group of units
BREAKTHROUGH = 3 # Unit will attempt a breakthrough, rushing forward very aggresively with big group of armored units, and even less armored units will move aggresively )
RETREAT = 2 # Unit will retreat
BREAKTHROUGH = 3 # Unit will attempt a breakthrough, rushing forward very aggresively with big group of armored units, and even less armored units will move aggresively
ELIMINATION = 4 # Unit will progress aggresively toward anemy units, attempting to eliminate the ennemy force ELIMINATION = 4 # Unit will progress aggresively toward anemy units, attempting to eliminate the ennemy force
AMBUSH = 5 # Units will adopt a defensive stance a bit different from 'DEFENSIVE', ATGM & INFANTRY with RPG will be located on frontline with the armored units. (The groups of units will be smaller) AMBUSH = 5 # Units will adopt a defensive stance a bit different from 'DEFENSIVE', ATGM & INFANTRY with RPG will be located on frontline with the armored units. (The groups of units will be smaller)

View File

@@ -11,16 +11,18 @@ import logging
import random import random
from typing import Dict, Iterator, Optional, TYPE_CHECKING, Type, List from typing import Dict, Iterator, Optional, TYPE_CHECKING, Type, List
from dcs import Mission, Point from dcs import Mission, Point, unitgroup
from dcs.country import Country from dcs.country import Country
from dcs.statics import fortification_map, warehouse_map from dcs.point import StaticPoint
from dcs.statics import fortification_map, warehouse_map, Warehouse
from dcs.task import ( from dcs.task import (
ActivateBeaconCommand, ActivateBeaconCommand,
ActivateICLSCommand, ActivateICLSCommand,
EPLRS, EPLRS,
OptAlarmState, FireAtPoint, OptAlarmState,
FireAtPoint,
) )
from dcs.unit import Ship, Unit, Vehicle from dcs.unit import Ship, Unit, Vehicle, SingleHeliPad, Static
from dcs.unitgroup import Group, ShipGroup, StaticGroup, VehicleGroup from dcs.unitgroup import Group, ShipGroup, StaticGroup, VehicleGroup
from dcs.unittype import StaticType, UnitType from dcs.unittype import StaticType, UnitType
from dcs.vehicles import vehicle_map from dcs.vehicles import vehicle_map
@@ -30,9 +32,12 @@ from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID
from game.db import unit_type_from_name from game.db import unit_type_from_name
from game.theater import ControlPoint, TheaterGroundObject from game.theater import ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import ( from game.theater.theatergroundobject import (
BuildingGroundObject, CarrierGroundObject, BuildingGroundObject,
CarrierGroundObject,
GenericCarrierGroundObject, GenericCarrierGroundObject,
LhaGroundObject, ShipGroundObject, MissileSiteGroundObject, LhaGroundObject,
ShipGroundObject,
MissileSiteGroundObject,
) )
from game.unitmap import UnitMap from game.unitmap import UnitMap
from game.utils import knots, mps from game.utils import knots, mps
@@ -43,7 +48,6 @@ from .tacan import TacanBand, TacanChannel, TacanRegistry
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
FARP_FRONTLINE_DISTANCE = 10000 FARP_FRONTLINE_DISTANCE = 10000
AA_CP_MIN_DISTANCE = 40000 AA_CP_MIN_DISTANCE = 40000
@@ -53,8 +57,15 @@ class GenericGroundObjectGenerator:
Currently used only for SAM Currently used only for SAM
""" """
def __init__(self, ground_object: TheaterGroundObject, country: Country,
game: Game, mission: Mission, unit_map: UnitMap) -> None: def __init__(
self,
ground_object: TheaterGroundObject,
country: Country,
game: Game,
mission: Mission,
unit_map: UnitMap,
) -> None:
self.ground_object = ground_object self.ground_object = ground_object
self.country = country self.country = country
self.game = game self.game = game
@@ -72,18 +83,22 @@ class GenericGroundObjectGenerator:
unit_type = unit_type_from_name(group.units[0].type) unit_type = unit_type_from_name(group.units[0].type)
if unit_type is None: if unit_type is None:
raise RuntimeError( raise RuntimeError(f"Unrecognized unit type: {group.units[0].type}")
f"Unrecognized unit type: {group.units[0].type}")
vg = self.m.vehicle_group(self.country, group.name, unit_type, vg = self.m.vehicle_group(
position=group.position, self.country,
heading=group.units[0].heading) group.name,
unit_type,
position=group.position,
heading=group.units[0].heading,
)
vg.units[0].name = self.m.string(group.units[0].name) vg.units[0].name = self.m.string(group.units[0].name)
vg.units[0].player_can_drive = True vg.units[0].player_can_drive = True
for i, u in enumerate(group.units): for i, u in enumerate(group.units):
if i > 0: if i > 0:
vehicle = Vehicle(self.m.next_unit_id(), vehicle = Vehicle(
self.m.string(u.name), u.type) self.m.next_unit_id(), self.m.string(u.name), u.type
)
vehicle.position.x = u.position.x vehicle.position.x = u.position.x
vehicle.position.y = u.position.y vehicle.position.y = u.position.y
vehicle.heading = u.heading vehicle.heading = u.heading
@@ -96,7 +111,7 @@ class GenericGroundObjectGenerator:
@staticmethod @staticmethod
def enable_eplrs(group: Group, unit_type: Type[UnitType]) -> None: def enable_eplrs(group: Group, unit_type: Type[UnitType]) -> None:
if hasattr(unit_type, 'eplrs'): if hasattr(unit_type, "eplrs"):
if unit_type.eplrs: if unit_type.eplrs:
group.points[0].tasks.append(EPLRS(group.id)) group.points[0].tasks.append(EPLRS(group.id))
@@ -106,14 +121,13 @@ class GenericGroundObjectGenerator:
else: else:
group.points[0].tasks.append(OptAlarmState(1)) group.points[0].tasks.append(OptAlarmState(1))
def _register_unit_group(self, persistence_group: Group, def _register_unit_group(self, persistence_group: Group, miz_group: Group) -> None:
miz_group: Group) -> None: self.unit_map.add_ground_object_units(
self.unit_map.add_ground_object_units(self.ground_object, self.ground_object, persistence_group, miz_group
persistence_group, miz_group) )
class MissileSiteGenerator(GenericGroundObjectGenerator): class MissileSiteGenerator(GenericGroundObjectGenerator):
def generate(self) -> None: def generate(self) -> None:
super(MissileSiteGenerator, self).generate() super(MissileSiteGenerator, self).generate()
# Note : Only the SCUD missiles group can fire (V1 site cannot fire in game right now) # Note : Only the SCUD missiles group can fire (V1 site cannot fire in game right now)
@@ -125,13 +139,19 @@ class MissileSiteGenerator(GenericGroundObjectGenerator):
targets = self.possible_missile_targets(vg) targets = self.possible_missile_targets(vg)
if targets: if targets:
target = random.choice(targets) target = random.choice(targets)
real_target = target.point_from_heading(random.randint(0, 360), random.randint(0, 2500)) real_target = target.point_from_heading(
random.randint(0, 360), random.randint(0, 2500)
)
vg.points[0].add_task(FireAtPoint(real_target)) vg.points[0].add_task(FireAtPoint(real_target))
logging.info("Set up fire task for missile group.") logging.info("Set up fire task for missile group.")
else: else:
logging.info("Couldn't setup missile site to fire, no valid target in range.") logging.info(
"Couldn't setup missile site to fire, no valid target in range."
)
else: else:
logging.info("Couldn't setup missile site to fire, group was not generated.") logging.info(
"Couldn't setup missile site to fire, group was not generated."
)
def possible_missile_targets(self, vg: Group) -> List[Point]: def possible_missile_targets(self, vg: Group) -> List[Point]:
""" """
@@ -190,7 +210,8 @@ class BuildingSiteGenerator(GenericGroundObjectGenerator):
break break
else: else:
logging.error( logging.error(
f"{self.ground_object.dcs_identifier} not found in static maps") f"{self.ground_object.dcs_identifier} not found in static maps"
)
def generate_vehicle_group(self, unit_type: UnitType) -> None: def generate_vehicle_group(self, unit_type: UnitType) -> None:
if not self.ground_object.is_dead: if not self.ground_object.is_dead:
@@ -228,11 +249,20 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
Used by both CV(N) groups and LHA groups. Used by both CV(N) groups and LHA groups.
""" """
def __init__(self, ground_object: GenericCarrierGroundObject,
control_point: ControlPoint, country: Country, game: Game, def __init__(
mission: Mission, radio_registry: RadioRegistry, self,
tacan_registry: TacanRegistry, icls_alloc: Iterator[int], ground_object: GenericCarrierGroundObject,
runways: Dict[str, RunwayData], unit_map: UnitMap) -> None: control_point: ControlPoint,
country: Country,
game: Game,
mission: Mission,
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,
icls_alloc: Iterator[int],
runways: Dict[str, RunwayData],
unit_map: UnitMap,
) -> None:
super().__init__(ground_object, country, game, mission, unit_map) super().__init__(ground_object, country, game, mission, unit_map)
self.ground_object = ground_object self.ground_object = ground_object
self.control_point = control_point self.control_point = control_point
@@ -245,8 +275,7 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
# TODO: Require single group? # TODO: Require single group?
for group in self.ground_object.groups: for group in self.ground_object.groups:
if not group.units: if not group.units:
logging.warning( logging.warning(f"Found empty carrier group in {self.control_point}")
f"Found empty carrier group in {self.control_point}")
continue continue
atc = self.radio_registry.alloc_uhf() atc = self.radio_registry.alloc_uhf()
@@ -270,25 +299,29 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
def get_carrier_type(self, group: Group) -> Type[UnitType]: def get_carrier_type(self, group: Group) -> Type[UnitType]:
unit_type = unit_type_from_name(group.units[0].type) unit_type = unit_type_from_name(group.units[0].type)
if unit_type is None: if unit_type is None:
raise RuntimeError( raise RuntimeError(f"Unrecognized carrier name: {group.units[0].type}")
f"Unrecognized carrier name: {group.units[0].type}")
return unit_type return unit_type
def configure_carrier(self, group: Group, def configure_carrier(self, group: Group, atc_channel: RadioFrequency) -> ShipGroup:
atc_channel: RadioFrequency) -> ShipGroup:
unit_type = self.get_carrier_type(group) unit_type = self.get_carrier_type(group)
ship_group = self.m.ship_group(self.country, group.name, unit_type, ship_group = self.m.ship_group(
position=group.position, self.country,
heading=group.units[0].heading) group.name,
unit_type,
position=group.position,
heading=group.units[0].heading,
)
ship_group.set_frequency(atc_channel.hertz) ship_group.set_frequency(atc_channel.hertz)
ship_group.units[0].name = self.m.string(group.units[0].name) ship_group.units[0].name = self.m.string(group.units[0].name)
return ship_group return ship_group
def create_ship(self, unit: Unit, atc_channel: RadioFrequency) -> Ship: def create_ship(self, unit: Unit, atc_channel: RadioFrequency) -> Ship:
ship = Ship(self.m.next_unit_id(), ship = Ship(
self.m.string(unit.name), self.m.next_unit_id(),
unit_type_from_name(unit.type)) self.m.string(unit.name),
unit_type_from_name(unit.type),
)
ship.position.x = unit.position.x ship.position.x = unit.position.x
ship.position.y = unit.position.y ship.position.y = unit.position.y
ship.heading = unit.heading ship.heading = unit.heading
@@ -303,7 +336,8 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
carrier_speed = knots(25) - mps(wind.speed) carrier_speed = knots(25) - mps(wind.speed)
for attempt in range(5): for attempt in range(5):
point = group.points[0].position.point_from_heading( point = group.points[0].position.point_from_heading(
brc, 100000 - attempt * 20000) brc, 100000 - attempt * 20000
)
if self.game.theater.is_in_sea(point): if self.game.theater.is_in_sea(point):
group.points[0].speed = carrier_speed.meters_per_second group.points[0].speed = carrier_speed.meters_per_second
group.add_waypoint(point, carrier_speed.kph) group.add_waypoint(point, carrier_speed.kph)
@@ -314,21 +348,30 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
raise NotImplementedError raise NotImplementedError
@staticmethod @staticmethod
def activate_beacons(group: ShipGroup, tacan: TacanChannel, def activate_beacons(
callsign: str, icls: int) -> None: group: ShipGroup, tacan: TacanChannel, callsign: str, icls: int
group.points[0].tasks.append(ActivateBeaconCommand( ) -> None:
channel=tacan.number, group.points[0].tasks.append(
modechannel=tacan.band.value, ActivateBeaconCommand(
callsign=callsign, channel=tacan.number,
unit_id=group.units[0].id, modechannel=tacan.band.value,
aa=False callsign=callsign,
)) unit_id=group.units[0].id,
group.points[0].tasks.append(ActivateICLSCommand( aa=False,
icls, unit_id=group.units[0].id )
)) )
group.points[0].tasks.append(
ActivateICLSCommand(icls, unit_id=group.units[0].id)
)
def add_runway_data(self, brc: int, atc: RadioFrequency, def add_runway_data(
tacan: TacanChannel, callsign: str, icls: int) -> None: self,
brc: int,
atc: RadioFrequency,
tacan: TacanChannel,
callsign: str,
icls: int,
) -> None:
# TODO: Make unit name usable. # TODO: Make unit name usable.
# This relies on one control point mapping exactly # This relies on one control point mapping exactly
# to one LHA, carrier, or other usable "runway". # to one LHA, carrier, or other usable "runway".
@@ -354,24 +397,28 @@ class CarrierGenerator(GenericCarrierGenerator):
def get_carrier_type(self, group: Group) -> UnitType: def get_carrier_type(self, group: Group) -> UnitType:
unit_type = super().get_carrier_type(group) unit_type = super().get_carrier_type(group)
if self.game.settings.supercarrier: if self.game.settings.supercarrier:
unit_type = db.upgrade_to_supercarrier(unit_type, unit_type = db.upgrade_to_supercarrier(unit_type, self.control_point.name)
self.control_point.name)
return unit_type return unit_type
def tacan_callsign(self) -> str: def tacan_callsign(self) -> str:
# TODO: Assign these properly. # TODO: Assign these properly.
return random.choice([ if self.control_point.name == "Carrier Strike Group 8":
"STE", return "TRU"
"CVN", else:
"CVH", return random.choice(
"CCV", [
"ACC", "STE",
"ARC", "CVN",
"GER", "CVH",
"ABR", "CCV",
"LIN", "ACC",
"TRU", "ARC",
]) "GER",
"ABR",
"LIN",
"TRU",
]
)
class LhaGenerator(GenericCarrierGenerator): class LhaGenerator(GenericCarrierGenerator):
@@ -379,14 +426,16 @@ class LhaGenerator(GenericCarrierGenerator):
def tacan_callsign(self) -> str: def tacan_callsign(self) -> str:
# TODO: Assign these properly. # TODO: Assign these properly.
return random.choice([ return random.choice(
"LHD", [
"LHA", "LHD",
"LHB", "LHA",
"LHC", "LHB",
"LHD", "LHC",
"LDS", "LHD",
]) "LDS",
]
)
class ShipObjectGenerator(GenericGroundObjectGenerator): class ShipObjectGenerator(GenericGroundObjectGenerator):
@@ -403,22 +452,23 @@ class ShipObjectGenerator(GenericGroundObjectGenerator):
unit_type = unit_type_from_name(group.units[0].type) unit_type = unit_type_from_name(group.units[0].type)
if unit_type is None: if unit_type is None:
raise RuntimeError( raise RuntimeError(f"Unrecognized unit type: {group.units[0].type}")
f"Unrecognized unit type: {group.units[0].type}")
self.generate_group(group, unit_type) self.generate_group(group, unit_type)
def generate_group(self, group_def: Group, def generate_group(self, group_def: Group, first_unit_type: Type[UnitType]) -> None:
first_unit_type: Type[UnitType]) -> None: group = self.m.ship_group(
group = self.m.ship_group(self.country, group_def.name, first_unit_type, self.country,
position=group_def.position, group_def.name,
heading=group_def.units[0].heading) first_unit_type,
position=group_def.position,
heading=group_def.units[0].heading,
)
group.units[0].name = self.m.string(group_def.units[0].name) group.units[0].name = self.m.string(group_def.units[0].name)
# TODO: Skipping the first unit looks like copy pasta from the carrier. # TODO: Skipping the first unit looks like copy pasta from the carrier.
for unit in group_def.units[1:]: for unit in group_def.units[1:]:
unit_type = unit_type_from_name(unit.type) unit_type = unit_type_from_name(unit.type)
ship = Ship(self.m.next_unit_id(), ship = Ship(self.m.next_unit_id(), self.m.string(unit.name), unit_type)
self.m.string(unit.name), unit_type)
ship.position.x = unit.position.x ship.position.x = unit.position.x
ship.position.y = unit.position.y ship.position.y = unit.position.y
ship.heading = unit.heading ship.heading = unit.heading
@@ -427,6 +477,48 @@ class ShipObjectGenerator(GenericGroundObjectGenerator):
self._register_unit_group(group_def, group) self._register_unit_group(group_def, group)
class HelipadGenerator:
"""
Generates helipads for given control point
"""
def __init__(
self,
mission: Mission,
cp: ControlPoint,
game: Game,
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,
):
self.m = mission
self.cp = cp
self.game = game
self.radio_registry = radio_registry
self.tacan_registry = tacan_registry
def generate(self) -> None:
if self.cp.captured:
country_name = self.game.player_country
else:
country_name = self.game.enemy_country
country = self.m.country(country_name)
for i, helipad in enumerate(self.cp.helipads):
name = self.cp.name + "_helipad_" + str(i)
logging.info("Generating helipad : " + name)
pad = SingleHeliPad(name=self.m.string(name + "_unit"))
pad.position = Point(helipad.x, helipad.y)
pad.heading = helipad.heading
# pad.heliport_frequency = self.radio_registry.alloc_uhf() TODO : alloc radio & callsign
sg = unitgroup.StaticGroup(self.m.next_group_id(), self.m.string(name))
sg.add_unit(pad)
sp = StaticPoint()
sp.position = pad.position
sg.add_point(sp)
country.add_static_group(sg)
class GroundObjectsGenerator: class GroundObjectsGenerator:
"""Creates DCS groups and statics for the theater during mission generation. """Creates DCS groups and statics for the theater during mission generation.
@@ -436,9 +528,14 @@ class GroundObjectsGenerator:
the appropriate generators. the appropriate generators.
""" """
def __init__(self, mission: Mission, game: Game, def __init__(
radio_registry: RadioRegistry, tacan_registry: TacanRegistry, self,
unit_map: UnitMap) -> None: mission: Mission,
game: Game,
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,
unit_map: UnitMap,
) -> None:
self.m = mission self.m = mission
self.game = game self.game = game
self.radio_registry = radio_registry self.radio_registry = radio_registry
@@ -455,31 +552,51 @@ class GroundObjectsGenerator:
country_name = self.game.enemy_country country_name = self.game.enemy_country
country = self.m.country(country_name) country = self.m.country(country_name)
HelipadGenerator(
self.m, cp, self.game, self.radio_registry, self.tacan_registry
).generate()
for ground_object in cp.ground_objects: for ground_object in cp.ground_objects:
if isinstance(ground_object, BuildingGroundObject): if isinstance(ground_object, BuildingGroundObject):
generator = BuildingSiteGenerator( generator = BuildingSiteGenerator(
ground_object, country, self.game, self.m, ground_object, country, self.game, self.m, self.unit_map
self.unit_map) )
elif isinstance(ground_object, CarrierGroundObject): elif isinstance(ground_object, CarrierGroundObject):
generator = CarrierGenerator( generator = CarrierGenerator(
ground_object, cp, country, self.game, self.m, ground_object,
self.radio_registry, self.tacan_registry, cp,
self.icls_alloc, self.runways, self.unit_map) country,
self.game,
self.m,
self.radio_registry,
self.tacan_registry,
self.icls_alloc,
self.runways,
self.unit_map,
)
elif isinstance(ground_object, LhaGroundObject): elif isinstance(ground_object, LhaGroundObject):
generator = CarrierGenerator( generator = CarrierGenerator(
ground_object, cp, country, self.game, self.m, ground_object,
self.radio_registry, self.tacan_registry, cp,
self.icls_alloc, self.runways, self.unit_map) country,
self.game,
self.m,
self.radio_registry,
self.tacan_registry,
self.icls_alloc,
self.runways,
self.unit_map,
)
elif isinstance(ground_object, ShipGroundObject): elif isinstance(ground_object, ShipGroundObject):
generator = ShipObjectGenerator( generator = ShipObjectGenerator(
ground_object, country, self.game, self.m, ground_object, country, self.game, self.m, self.unit_map
self.unit_map) )
elif isinstance(ground_object, MissileSiteGroundObject): elif isinstance(ground_object, MissileSiteGroundObject):
generator = MissileSiteGenerator( generator = MissileSiteGenerator(
ground_object, country, self.game, self.m, ground_object, country, self.game, self.m, self.unit_map
self.unit_map) )
else: else:
generator = GenericGroundObjectGenerator( generator = GenericGroundObjectGenerator(
ground_object, country, self.game, self.m, ground_object, country, self.game, self.m, self.unit_map
self.unit_map) )
generator.generate() generator.generate()

View File

@@ -41,6 +41,7 @@ from .flights.flight import FlightWaypoint, FlightWaypointType
from .radios import RadioFrequency from .radios import RadioFrequency
from .runways import RunwayData from .runways import RunwayData
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
@@ -48,18 +49,33 @@ if TYPE_CHECKING:
class KneeboardPageWriter: class KneeboardPageWriter:
"""Creates kneeboard images.""" """Creates kneeboard images."""
def __init__(self, page_margin: int = 24, line_spacing: int = 12) -> None: def __init__(
self.image = Image.new('RGB', (768, 1024), (0xff, 0xff, 0xff)) self, page_margin: int = 24, line_spacing: int = 12, dark_theme: bool = False
) -> None:
if dark_theme:
self.foreground_fill = (215, 200, 200)
self.background_fill = (10, 5, 5)
else:
self.foreground_fill = (15, 15, 15)
self.background_fill = (255, 252, 252)
self.image = Image.new("RGB", (768, 1024), self.background_fill)
# These font sizes create a relatively full page for current sorties. If # These font sizes create a relatively full page for current sorties. If
# we start generating more complicated flight plans, or start including # we start generating more complicated flight plans, or start including
# more information in the comm ladder (the latter of which we should # more information in the comm ladder (the latter of which we should
# probably do), we'll need to split some of this information off into a # probably do), we'll need to split some of this information off into a
# second page. # second page.
self.title_font = ImageFont.truetype("arial.ttf", 32) self.title_font = ImageFont.truetype(
self.heading_font = ImageFont.truetype("arial.ttf", 24) "arial.ttf", 32, layout_engine=ImageFont.LAYOUT_BASIC
self.content_font = ImageFont.truetype("arial.ttf", 20) )
self.heading_font = ImageFont.truetype(
"arial.ttf", 24, layout_engine=ImageFont.LAYOUT_BASIC
)
self.content_font = ImageFont.truetype(
"arial.ttf", 20, layout_engine=ImageFont.LAYOUT_BASIC
)
self.table_font = ImageFont.truetype( self.table_font = ImageFont.truetype(
"resources/fonts/Inconsolata.otf", 20) "resources/fonts/Inconsolata.otf", 20, layout_engine=ImageFont.LAYOUT_BASIC
)
self.draw = ImageDraw.Draw(self.image) self.draw = ImageDraw.Draw(self.image)
self.x = page_margin self.x = page_margin
self.y = page_margin self.y = page_margin
@@ -69,8 +85,9 @@ class KneeboardPageWriter:
def position(self) -> Tuple[int, int]: def position(self) -> Tuple[int, int]:
return self.x, self.y return self.x, self.y
def text(self, text: str, font=None, def text(
fill: Tuple[int, int, int] = (0, 0, 0)) -> None: self, text: str, font=None, fill: Tuple[int, int, int] = (0, 0, 0)
) -> None:
if font is None: if font is None:
font = self.content_font font = self.content_font
@@ -79,17 +96,18 @@ class KneeboardPageWriter:
self.y += height + self.line_spacing self.y += height + self.line_spacing
def title(self, title: str) -> None: def title(self, title: str) -> None:
self.text(title, font=self.title_font) self.text(title, font=self.title_font, fill=self.foreground_fill)
def heading(self, text: str) -> None: def heading(self, text: str) -> None:
self.text(text, font=self.heading_font) self.text(text, font=self.heading_font, fill=self.foreground_fill)
def table(self, cells: List[List[str]], def table(
headers: Optional[List[str]] = None) -> None: self, cells: List[List[str]], headers: Optional[List[str]] = None
) -> None:
if headers is None: if headers is None:
headers = [] headers = []
table = tabulate(cells, headers=headers, numalign="right") table = tabulate(cells, headers=headers, numalign="right")
self.text(table, font=self.table_font) self.text(table, font=self.table_font, fill=self.foreground_fill)
def write(self, path: Path) -> None: def write(self, path: Path) -> None:
self.image.save(path) self.image.save(path)
@@ -157,29 +175,34 @@ class FlightPlanBuilder:
first_waypoint_num = self.target_points[0].number first_waypoint_num = self.target_points[0].number
last_waypoint_num = self.target_points[-1].number last_waypoint_num = self.target_points[-1].number
self.rows.append([ self.rows.append(
f"{first_waypoint_num}-{last_waypoint_num}", [
"Target points", f"{first_waypoint_num}-{last_waypoint_num}",
"0", "Target points",
self._waypoint_distance(self.target_points[0].waypoint), "0",
self._ground_speed(self.target_points[0].waypoint), self._waypoint_distance(self.target_points[0].waypoint),
self._format_time(self.target_points[0].waypoint.tot), self._ground_speed(self.target_points[0].waypoint),
self._format_time(self.target_points[0].waypoint.departure_time), self._format_time(self.target_points[0].waypoint.tot),
]) self._format_time(self.target_points[0].waypoint.departure_time),
]
)
self.last_waypoint = self.target_points[-1].waypoint self.last_waypoint = self.target_points[-1].waypoint
def add_waypoint_row(self, waypoint: NumberedWaypoint) -> None: def add_waypoint_row(self, waypoint: NumberedWaypoint) -> None:
self.rows.append([ self.rows.append(
str(waypoint.number), [
KneeboardPageWriter.wrap_line( str(waypoint.number),
waypoint.waypoint.pretty_name, KneeboardPageWriter.wrap_line(
FlightPlanBuilder.WAYPOINT_DESC_MAX_LEN), waypoint.waypoint.pretty_name,
str(int(waypoint.waypoint.alt.feet)), FlightPlanBuilder.WAYPOINT_DESC_MAX_LEN,
self._waypoint_distance(waypoint.waypoint), ),
self._ground_speed(waypoint.waypoint), str(int(waypoint.waypoint.alt.feet)),
self._format_time(waypoint.waypoint.tot), self._waypoint_distance(waypoint.waypoint),
self._format_time(waypoint.waypoint.departure_time), self._ground_speed(waypoint.waypoint),
]) self._format_time(waypoint.waypoint.tot),
self._format_time(waypoint.waypoint.departure_time),
]
)
def _format_time(self, time: Optional[datetime.timedelta]) -> str: def _format_time(self, time: Optional[datetime.timedelta]) -> str:
if time is None: if time is None:
@@ -191,9 +214,9 @@ class FlightPlanBuilder:
if self.last_waypoint is None: if self.last_waypoint is None:
return "-" return "-"
distance = meters(self.last_waypoint.position.distance_to_point( distance = meters(
waypoint.position self.last_waypoint.position.distance_to_point(waypoint.position)
)) )
return f"{distance.nautical_miles:.1f} NM" return f"{distance.nautical_miles:.1f} NM"
def _ground_speed(self, waypoint: FlightWaypoint) -> str: def _ground_speed(self, waypoint: FlightWaypoint) -> str:
@@ -210,9 +233,9 @@ class FlightPlanBuilder:
else: else:
return "-" return "-"
distance = meters(self.last_waypoint.position.distance_to_point( distance = meters(
waypoint.position self.last_waypoint.position.distance_to_point(waypoint.position)
)) )
duration = (waypoint.tot - last_time).total_seconds() / 3600 duration = (waypoint.tot - last_time).total_seconds() / 3600
return f"{int(distance.nautical_miles / duration)} kt" return f"{int(distance.nautical_miles / duration)} kt"
@@ -222,66 +245,113 @@ class FlightPlanBuilder:
class BriefingPage(KneeboardPage): class BriefingPage(KneeboardPage):
"""A kneeboard page containing briefing information.""" """A kneeboard page containing briefing information."""
def __init__(self, flight: FlightData, comms: List[CommInfo],
awacs: List[AwacsInfo], tankers: List[TankerInfo], def __init__(
jtacs: List[JtacInfo], start_time: datetime.datetime) -> None: self,
flight: FlightData,
comms: List[CommInfo],
awacs: List[AwacsInfo],
tankers: List[TankerInfo],
jtacs: List[JtacInfo],
start_time: datetime.datetime,
dark_kneeboard: bool,
) -> None:
self.flight = flight self.flight = flight
self.comms = list(comms) self.comms = list(comms)
self.awacs = awacs self.awacs = awacs
self.tankers = tankers self.tankers = tankers
self.jtacs = jtacs self.jtacs = jtacs
self.start_time = start_time self.start_time = start_time
self.dark_kneeboard = dark_kneeboard
self.comms.append(CommInfo("Flight", self.flight.intra_flight_channel)) self.comms.append(CommInfo("Flight", self.flight.intra_flight_channel))
def write(self, path: Path) -> None: def write(self, path: Path) -> None:
writer = KneeboardPageWriter() writer = KneeboardPageWriter(dark_theme=self.dark_kneeboard)
writer.title(f"{self.flight.callsign} Mission Info") if self.flight.custom_name is not None:
custom_name_title = ' ("{}")'.format(self.flight.custom_name)
else:
custom_name_title = ""
writer.title(f"{self.flight.callsign} Mission Info{custom_name_title}")
# TODO: Handle carriers. # TODO: Handle carriers.
writer.heading("Airfield Info") writer.heading("Airfield Info")
writer.table([ writer.table(
self.airfield_info_row("Departure", self.flight.departure), [
self.airfield_info_row("Arrival", self.flight.arrival), self.airfield_info_row("Departure", self.flight.departure),
self.airfield_info_row("Divert", self.flight.divert), self.airfield_info_row("Arrival", self.flight.arrival),
], headers=["", "Airbase", "ATC", "TCN", "I(C)LS", "RWY"]) self.airfield_info_row("Divert", self.flight.divert),
],
headers=["", "Airbase", "ATC", "TCN", "I(C)LS", "RWY"],
)
writer.heading("Flight Plan") writer.heading("Flight Plan")
flight_plan_builder = FlightPlanBuilder(self.start_time) flight_plan_builder = FlightPlanBuilder(self.start_time)
for num, waypoint in enumerate(self.flight.waypoints): for num, waypoint in enumerate(self.flight.waypoints):
flight_plan_builder.add_waypoint(num, waypoint) flight_plan_builder.add_waypoint(num, waypoint)
writer.table(flight_plan_builder.build(), headers=[ writer.table(
"#", "Action", "Alt", "Dist", "GSPD", "Time", "Departure" flight_plan_builder.build(),
]) headers=["#", "Action", "Alt", "Dist", "GSPD", "Time", "Departure"],
)
flight_plan_builder flight_plan_builder
writer.table([ writer.table(
["{}lbs".format(self.flight.bingo_fuel), "{}lbs".format(self.flight.joker_fuel)] [
], ['Bingo', 'Joker']) [
"{}lbs".format(self.flight.bingo_fuel),
"{}lbs".format(self.flight.joker_fuel),
]
],
["Bingo", "Joker"],
)
# AEW&C
writer.heading("AEW&C")
aewc_ladder = []
for single_aewc in self.awacs:
if single_aewc.depature_location is None:
dep = "-"
arr = "-"
else:
dep = self._format_time(single_aewc.start_time)
arr = self._format_time(single_aewc.end_time)
aewc_ladder.append(
[
str(single_aewc.callsign),
str(single_aewc.freq),
str(single_aewc.depature_location),
str(dep),
str(arr),
]
)
writer.table(
aewc_ladder,
headers=["Callsign", "FREQ", "Depature", "ETD", "ETA"],
)
# Package Section # Package Section
writer.heading("Comm ladder") writer.heading("Comm ladder")
comm_ladder = [] comm_ladder = []
for comm in self.comms: for comm in self.comms:
comm_ladder.append([comm.name, '', '', '', self.format_frequency(comm.freq)]) comm_ladder.append(
[comm.name, "", "", "", self.format_frequency(comm.freq)]
)
for a in self.awacs:
comm_ladder.append([
a.callsign,
'AWACS',
'',
'',
self.format_frequency(a.freq)
])
for tanker in self.tankers: for tanker in self.tankers:
comm_ladder.append([ comm_ladder.append(
tanker.callsign, [
"Tanker", tanker.callsign,
tanker.variant, "Tanker",
str(tanker.tacan), tanker.variant,
self.format_frequency(tanker.freq), str(tanker.tacan),
]) self.format_frequency(tanker.freq),
]
writer.table(comm_ladder, headers=["Callsign","Task", "Type", "TACAN", "FREQ"]) )
writer.table(comm_ladder, headers=["Callsign", "Task", "Type", "TACAN", "FREQ"])
writer.heading("JTAC") writer.heading("JTAC")
jtacs = [] jtacs = []
@@ -291,8 +361,9 @@ class BriefingPage(KneeboardPage):
writer.write(path) writer.write(path)
def airfield_info_row(self, row_title: str, def airfield_info_row(
runway: Optional[RunwayData]) -> List[str]: self, row_title: str, runway: Optional[RunwayData]
) -> List[str]:
"""Creates a table row for a given airfield. """Creates a table row for a given airfield.
Args: Args:
@@ -337,12 +408,21 @@ class BriefingPage(KneeboardPage):
channel_name = namer.channel_name(channel.radio_id, channel.channel) channel_name = namer.channel_name(channel.radio_id, channel.channel)
return f"{channel_name} {frequency}" return f"{channel_name} {frequency}"
def _format_time(self, time: Optional[datetime.timedelta]) -> str:
if time is None:
return ""
local_time = self.start_time + time
return local_time.strftime(f"%H:%M:%S")
class KneeboardGenerator(MissionInfoGenerator): class KneeboardGenerator(MissionInfoGenerator):
"""Creates kneeboard pages for each client flight in the mission.""" """Creates kneeboard pages for each client flight in the mission."""
def __init__(self, mission: Mission, game: "Game") -> None: def __init__(self, mission: Mission, game: "Game") -> None:
super().__init__(mission, game) super().__init__(mission, game)
self.dark_kneeboard = self.game.settings.generate_dark_kneeboard and (
self.mission.start_time.hour > 19 or self.mission.start_time.hour < 7
)
def generate(self) -> None: def generate(self) -> None:
"""Generates a kneeboard per client flight.""" """Generates a kneeboard per client flight."""
@@ -372,7 +452,8 @@ class KneeboardGenerator(MissionInfoGenerator):
if not flight.client_units: if not flight.client_units:
continue continue
all_flights[flight.aircraft_type].extend( all_flights[flight.aircraft_type].extend(
self.generate_flight_kneeboard(flight)) self.generate_flight_kneeboard(flight)
)
return all_flights return all_flights
def generate_flight_kneeboard(self, flight: FlightData) -> List[KneeboardPage]: def generate_flight_kneeboard(self, flight: FlightData) -> List[KneeboardPage]:
@@ -384,6 +465,7 @@ class KneeboardGenerator(MissionInfoGenerator):
self.awacs, self.awacs,
self.tankers, self.tankers,
self.jtacs, self.jtacs,
self.mission.start_time self.mission.start_time,
self.dark_kneeboard,
), ),
] ]

View File

@@ -9,7 +9,7 @@ from gen.locations.preset_locations import PresetLocation
class PresetControlPointLocations: class PresetControlPointLocations:
"""A repository of preset locations for a given control point""" """A repository of preset locations for a given control point"""
# List of possible ashore locations to generate objects (Represented in miz file by an APC_AAV_7) # List of possible ashore locations to generate objects (Represented in miz file by an APC_AAV_7_Amphibious)
ashore_locations: List[PresetLocation] = field(default_factory=list) ashore_locations: List[PresetLocation] = field(default_factory=list)
# List of possible offshore locations to generate ship groups (Represented in miz file by an Oliver Hazard Perry) # List of possible offshore locations to generate ship groups (Represented in miz file by an Oliver Hazard Perry)

View File

@@ -9,9 +9,10 @@ from gen.locations.preset_locations import PresetLocation
class MizDataLocationFinder: class MizDataLocationFinder:
@staticmethod @staticmethod
def compute_possible_locations(terrain_name: str, cp_name: str) -> PresetControlPointLocations: def compute_possible_locations(
terrain_name: str, cp_name: str
) -> PresetControlPointLocations:
""" """
Extract the list of preset locations from miz data Extract the list of preset locations from miz data
:param terrain_name: Terrain/Map name :param terrain_name: Terrain/Map name
@@ -32,28 +33,55 @@ class MizDataLocationFinder:
for vehicle_group in m.country("USA").vehicle_group: for vehicle_group in m.country("USA").vehicle_group:
if len(vehicle_group.units) > 0: if len(vehicle_group.units) > 0:
ashore_locations.append(PresetLocation(vehicle_group.position, ashore_locations.append(
vehicle_group.units[0].heading, PresetLocation(
vehicle_group.name)) vehicle_group.position,
vehicle_group.units[0].heading,
vehicle_group.name,
)
)
for ship_group in m.country("USA").ship_group: for ship_group in m.country("USA").ship_group:
if len(ship_group.units) > 0 and ship_group.units[0].type == ships.Oliver_Hazzard_Perry_class.id: if (
offshore_locations.append(PresetLocation(ship_group.position, len(ship_group.units) > 0
ship_group.units[0].heading, and ship_group.units[0].type == ships.FFG_Oliver_Hazzard_Perry.id
ship_group.name)) ):
offshore_locations.append(
PresetLocation(
ship_group.position,
ship_group.units[0].heading,
ship_group.name,
)
)
for static_group in m.country("USA").static_group: for static_group in m.country("USA").static_group:
if len(static_group.units) > 0: if len(static_group.units) > 0:
powerplants_locations.append(PresetLocation(static_group.position, powerplants_locations.append(
static_group.units[0].heading, PresetLocation(
static_group.name)) static_group.position,
static_group.units[0].heading,
static_group.name,
)
)
if m.country("Iran") is not None: if m.country("Iran") is not None:
for vehicle_group in m.country("Iran").vehicle_group: for vehicle_group in m.country("Iran").vehicle_group:
if len(vehicle_group.units) > 0 and vehicle_group.units[0].type == MissilesSS.SS_N_2_Silkworm.id: if (
antiship_locations.append(PresetLocation(vehicle_group.position, len(vehicle_group.units) > 0
vehicle_group.units[0].heading, and vehicle_group.units[0].type
vehicle_group.name)) == MissilesSS.AShM_SS_N_2_Silkworm.id
):
antiship_locations.append(
PresetLocation(
vehicle_group.position,
vehicle_group.units[0].heading,
vehicle_group.name,
)
)
return PresetControlPointLocations(ashore_locations, offshore_locations, return PresetControlPointLocations(
antiship_locations, powerplants_locations) ashore_locations,
offshore_locations,
antiship_locations,
powerplants_locations,
)

View File

@@ -6,10 +6,16 @@ from dcs import Point
@dataclass @dataclass
class PresetLocation: class PresetLocation:
"""A preset location""" """A preset location"""
position: Point position: Point
heading: int heading: int
id: str id: str
def __str__(self): def __str__(self):
return "-" * 10 + "X: {}\n Y: {}\nHdg: {}°\nId: {}".format(self.position.x, self.position.y, self.heading, return (
self.id) + "-" * 10 "-" * 10
+ "X: {}\n Y: {}\nHdg: {}°\nId: {}".format(
self.position.x, self.position.y, self.heading, self.id
)
+ "-" * 10
)

View File

@@ -4,15 +4,12 @@ from game import db
from gen.missiles.scud_site import ScudGenerator from gen.missiles.scud_site import ScudGenerator
from gen.missiles.v1_group import V1GroupGenerator from gen.missiles.v1_group import V1GroupGenerator
MISSILES_MAP = { MISSILES_MAP = {"V1GroupGenerator": V1GroupGenerator, "ScudGenerator": ScudGenerator}
"V1GroupGenerator": V1GroupGenerator,
"ScudGenerator": ScudGenerator
}
def generate_missile_group(game, ground_object, faction_name: str): def generate_missile_group(game, ground_object, faction_name: str):
""" """
This generate a ship group This generate a missiles group
:return: Nothing, but put the group reference inside the ground object :return: Nothing, but put the group reference inside the ground object
""" """
faction = db.FACTIONS[faction_name] faction = db.FACTIONS[faction_name]
@@ -25,5 +22,9 @@ def generate_missile_group(game, ground_object, faction_name: str):
generator.generate() generator.generate()
return generator.get_generated_group() return generator.get_generated_group()
else: else:
logging.info("Unable to generate missile group, generator : " + str(gen) + "does not exists") logging.info(
return None "Unable to generate missile group, generator : "
+ str(gen)
+ "does not exists"
)
return None

View File

@@ -6,7 +6,6 @@ from gen.sam.group_generator import GroupGenerator
class ScudGenerator(GroupGenerator): class ScudGenerator(GroupGenerator):
def __init__(self, game, ground_object, faction): def __init__(self, game, ground_object, faction):
super(ScudGenerator, self).__init__(game, ground_object) super(ScudGenerator, self).__init__(game, ground_object)
self.faction = faction self.faction = faction
@@ -14,17 +13,50 @@ class ScudGenerator(GroupGenerator):
def generate(self): def generate(self):
# Scuds # Scuds
self.add_unit(MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M, "V1#0", self.position.x, self.position.y + random.randint(1, 8), self.heading) self.add_unit(
self.add_unit(MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M, "V1#1", self.position.x + 50, self.position.y + random.randint(1, 8), self.heading) MissilesSS.SSM_SS_1C_Scud_B,
self.add_unit(MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M, "V1#2", self.position.x + 100, self.position.y + random.randint(1, 8), self.heading) "V1#0",
self.position.x,
self.position.y + random.randint(1, 8),
self.heading,
)
self.add_unit(
MissilesSS.SSM_SS_1C_Scud_B,
"V1#1",
self.position.x + 50,
self.position.y + random.randint(1, 8),
self.heading,
)
self.add_unit(
MissilesSS.SSM_SS_1C_Scud_B,
"V1#2",
self.position.x + 100,
self.position.y + random.randint(1, 8),
self.heading,
)
# Commander # Commander
self.add_unit(Unarmed.Transport_UAZ_469, "Kubel#0", self.position.x - 35, self.position.y - 20, self.add_unit(
self.heading) Unarmed.LUV_UAZ_469_Jeep,
"Kubel#0",
self.position.x - 35,
self.position.y - 20,
self.heading,
)
# Shorad # Shorad
self.add_unit(AirDefence.SPAAA_ZSU_23_4_Shilka, "SHILKA#0", self.position.x - 55, self.position.y - 38, self.add_unit(
self.heading) AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
"SHILKA#0",
self.position.x - 55,
self.position.y - 38,
self.heading,
)
self.add_unit(AirDefence.SAM_SA_9_Strela_1_9P31, "STRELA#0", self.add_unit(
self.position.x + 200, self.position.y + 15, 90) AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL,
"STRELA#0",
self.position.x + 200,
self.position.y + 15,
90,
)

View File

@@ -6,7 +6,6 @@ from gen.sam.group_generator import GroupGenerator
class V1GroupGenerator(GroupGenerator): class V1GroupGenerator(GroupGenerator):
def __init__(self, game, ground_object, faction): def __init__(self, game, ground_object, faction):
super(V1GroupGenerator, self).__init__(game, ground_object) super(V1GroupGenerator, self).__init__(game, ground_object)
self.faction = faction self.faction = faction
@@ -14,19 +13,54 @@ class V1GroupGenerator(GroupGenerator):
def generate(self): def generate(self):
# Ramps # Ramps
self.add_unit(MissilesSS.V_1_ramp, "V1#0", self.position.x, self.position.y + random.randint(1, 8), self.heading) self.add_unit(
self.add_unit(MissilesSS.V_1_ramp, "V1#1", self.position.x + 50, self.position.y + random.randint(1, 8), self.heading) MissilesSS.SSM_V_1_Launcher,
self.add_unit(MissilesSS.V_1_ramp, "V1#2", self.position.x + 100, self.position.y + random.randint(1, 8), self.heading) "V1#0",
self.position.x,
self.position.y + random.randint(1, 8),
self.heading,
)
self.add_unit(
MissilesSS.SSM_V_1_Launcher,
"V1#1",
self.position.x + 50,
self.position.y + random.randint(1, 8),
self.heading,
)
self.add_unit(
MissilesSS.SSM_V_1_Launcher,
"V1#2",
self.position.x + 100,
self.position.y + random.randint(1, 8),
self.heading,
)
# Commander # Commander
self.add_unit(Unarmed.Kübelwagen_82, "Kubel#0", self.position.x - 35, self.position.y - 20, self.add_unit(
self.heading) Unarmed.LUV_Kubelwagen_82,
"Kubel#0",
self.position.x - 35,
self.position.y - 20,
self.heading,
)
# Self defense flak # Self defense flak
flak_unit = random.choice([AirDefence.AAA_Flak_Vierling_38, AirDefence.AAA_Flak_38]) flak_unit = random.choice(
[AirDefence.AAA_Flak_Vierling_38_Quad_20mm, AirDefence.AAA_Flak_38_20mm]
)
self.add_unit(flak_unit, "FLAK#0", self.position.x - 55, self.position.y - 38, self.add_unit(
self.heading) flak_unit,
"FLAK#0",
self.position.x - 55,
self.position.y - 38,
self.heading,
)
self.add_unit(Unarmed.Blitz_3_6_6700A, "Blitz#0", self.add_unit(
self.position.x + 200, self.position.y + 15, 90) Unarmed.Truck_Opel_Blitz,
"Blitz#0",
self.position.x + 200,
self.position.y + 15,
90,
)

View File

@@ -9,39 +9,242 @@ from game import db
from gen.flights.flight import Flight from gen.flights.flight import Flight
ALPHA_MILITARY = ["Alpha", "Bravo", "Charlie", "Delta", "Echo", "Foxtrot", ALPHA_MILITARY = [
"Golf", "Hotel", "India", "Juliet", "Kilo", "Lima", "Mike", "Alpha",
"November", "Oscar", "Papa", "Quebec", "Romeo", "Sierra", "Bravo",
"Tango", "Uniform", "Victor", "Whisky", "XRay", "Yankee", "Charlie",
"Zulu", "Zero"] "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 = [ ANIMALS = [
"SHARK", "TORTOISE", "BAT", "PANGOLIN", "AARDWOLF", "SHARK",
"MONKEY", "BUFFALO", "DOG", "BOBCAT", "LYNX", "PANTHER", "TIGER", "TORTOISE",
"LION", "OWL", "BUTTERFLY", "BISON", "DUCK", "COBRA", "MAMBA", "BAT",
"DOLPHIN", "PHEASANT", "ARMADILLLO", "RACOON", "ZEBRA", "COW", "COYOTE", "FOX", "PANGOLIN",
"LIGHTFOOT", "COTTONMOUTH", "TAURUS", "VIPER", "CASTOR", "GIRAFFE", "SNAKE", "AARDWOLF",
"MONSTER", "ALBATROSS", "HAWK", "DOVE", "MOCKINGBIRD", "GECKO", "ORYX", "GORILLA", "MONKEY",
"HARAMBE", "GOOSE", "MAVERICK", "HARE", "JACKAL", "LEOPARD", "CAT", "MUSK", "ORCA", "BUFFALO",
"OCELOT", "BEAR", "PANDA", "GULL", "PENGUIN", "PYTHON", "RAVEN", "DEER", "MOOSE", "DOG",
"REINDEER", "SHEEP", "GAZELLE", "INSECT", "VULTURE", "WALLABY", "KANGAROO", "KOALA", "BOBCAT",
"KIWI", "WHALE", "FISH", "RHINO", "HIPPO", "RAT", "WOODPECKER", "WORM", "BABOON", "LYNX",
"YAK", "SCORPIO", "HORSE", "POODLE", "CENTIPEDE", "CHICKEN", "CHEETAH", "CHAMELEON", "PANTHER",
"CATFISH", "CATERPILLAR", "CARACAL", "CAMEL", "CAIMAN", "BARRACUDA", "BANDICOOT", "TIGER",
"ALLIGATOR", "BONGO", "CORAL", "ELEPHANT", "ANTELOPE", "CRAB", "DACHSHUND", "DODO", "LION",
"FLAMINGO", "FERRET", "FALCON", "BULLDOG", "DONKEY", "IGUANA", "TAMARIN", "HARRIER", "OWL",
"GRIZZLY", "GREYHOUND", "GRASSHOPPER", "JAGUAR", "LADYBUG", "KOMODO", "DRAGON", "LIZARD", "BUTTERFLY",
"LLAMA", "LOBSTER", "OCTOPUS", "MANATEE", "MAGPIE", "MACAW", "OSTRICH", "OYSTER", "BISON",
"MOLE", "MULE", "MOTH", "MONGOOSE", "MOLLY", "MEERKAT", "MOUSE", "PEACOCK", "PIKE", "ROBIN", "DUCK",
"RAGDOLL", "PLATYPUS", "PELICAN", "PARROT", "PORCUPINE", "PIRANHA", "PUMA", "PUG", "TAPIR", "COBRA",
"TERMITE", "URCHIN", "SHRIMP", "TURKEY", "TOUCAN", "TETRA", "HUSKY", "STARFISH", "SWAN", "MAMBA",
"FROG", "SQUIRREL", "WALRUS", "WARTHOG", "CORGI", "WEASEL", "WOMBAT", "WOLVERINE", "MAMMOTH", "DOLPHIN",
"TOAD", "WOLF", "ZEBU", "SEAL", "SKATE", "JELLYFISH", "MOSQUITO", "LOCUST", "SLUG", "SNAIL", "PHEASANT",
"HEDGEHOG", "PIGLET", "FENNEC", "BADGER", "ALPACA", "DINGO", "COLT", "SKUNK", "BUNNY", "IMPALA", "ARMADILLO",
"GUANACO", "CAPYBARA", "ELK", "MINK", "PRONGHORN", "CROW", "BUMBLEBEE", "FAWN", "OTTER", "WATERBUCK", "RACOON",
"JERBOA", "KITTEN", "ARGALI", "OX", "MARE", "FINCH", "BASILISK", "GOPHER", "HAMSTER", "CANARY", "WOODCHUCK", "ZEBRA",
"ANACONDA" "COW",
] "COYOTE",
"FOX",
"LIGHTFOOT",
"COTTONMOUTH",
"TAURUS",
"VIPER",
"CASTOR",
"GIRAFFE",
"SNAKE",
"MONSTER",
"ALBATROSS",
"HAWK",
"DOVE",
"MOCKINGBIRD",
"GECKO",
"ORYX",
"GORILLA",
"HARAMBE",
"GOOSE",
"MAVERICK",
"HARE",
"JACKAL",
"LEOPARD",
"CAT",
"MUSK",
"ORCA",
"OCELOT",
"BEAR",
"PANDA",
"GULL",
"PENGUIN",
"PYTHON",
"RAVEN",
"DEER",
"MOOSE",
"REINDEER",
"SHEEP",
"GAZELLE",
"INSECT",
"VULTURE",
"WALLABY",
"KANGAROO",
"KOALA",
"KIWI",
"WHALE",
"FISH",
"RHINO",
"HIPPO",
"RAT",
"WOODPECKER",
"WORM",
"BABOON",
"YAK",
"SCORPIO",
"HORSE",
"POODLE",
"CENTIPEDE",
"CHICKEN",
"CHEETAH",
"CHAMELEON",
"CATFISH",
"CATERPILLAR",
"CARACAL",
"CAMEL",
"CAIMAN",
"BARRACUDA",
"BANDICOOT",
"ALLIGATOR",
"BONGO",
"CORAL",
"ELEPHANT",
"ANTELOPE",
"CRAB",
"DACHSHUND",
"DODO",
"FLAMINGO",
"FERRET",
"FALCON",
"BULLDOG",
"DONKEY",
"IGUANA",
"TAMARIN",
"HARRIER",
"GRIZZLY",
"GREYHOUND",
"GRASSHOPPER",
"JAGUAR",
"LADYBUG",
"KOMODO",
"DRAGON",
"LIZARD",
"LLAMA",
"LOBSTER",
"OCTOPUS",
"MANATEE",
"MAGPIE",
"MACAW",
"OSTRICH",
"OYSTER",
"MOLE",
"MULE",
"MOTH",
"MONGOOSE",
"MOLLY",
"MEERKAT",
"MOUSE",
"PEACOCK",
"PIKE",
"ROBIN",
"RAGDOLL",
"PLATYPUS",
"PELICAN",
"PARROT",
"PORCUPINE",
"PIRANHA",
"PUMA",
"PUG",
"TAPIR",
"TERMITE",
"URCHIN",
"SHRIMP",
"TURKEY",
"TOUCAN",
"TETRA",
"HUSKY",
"STARFISH",
"SWAN",
"FROG",
"SQUIRREL",
"WALRUS",
"WARTHOG",
"CORGI",
"WEASEL",
"WOMBAT",
"WOLVERINE",
"MAMMOTH",
"TOAD",
"WOLF",
"ZEBU",
"SEAL",
"SKATE",
"JELLYFISH",
"MOSQUITO",
"LOCUST",
"SLUG",
"SNAIL",
"HEDGEHOG",
"PIGLET",
"FENNEC",
"BADGER",
"ALPACA",
"DINGO",
"COLT",
"SKUNK",
"BUNNY",
"IMPALA",
"GUANACO",
"CAPYBARA",
"ELK",
"MINK",
"PRONGHORN",
"CROW",
"BUMBLEBEE",
"FAWN",
"OTTER",
"WATERBUCK",
"JERBOA",
"KITTEN",
"ARGALI",
"OX",
"MARE",
"FINCH",
"BASILISK",
"GOPHER",
"HAMSTER",
"CANARY",
"WOODCHUCK",
"ANACONDA",
]
class NameGenerator: class NameGenerator:
number = 0 number = 0
@@ -72,21 +275,36 @@ class NameGenerator:
name_str = flight.custom_name name_str = flight.custom_name
else: else:
name_str = "{} {}".format( name_str = "{} {}".format(
flight.package.target.name, flight.flight_type) flight.package.target.name, flight.flight_type
)
except AttributeError: # Here to maintain save compatibility with 2.3 except AttributeError: # Here to maintain save compatibility with 2.3
name_str = "{} {}".format( name_str = "{} {}".format(flight.package.target.name, flight.flight_type)
flight.package.target.name, flight.flight_type) return "{}|{}|{}|{}|{}|".format(
return "{}|{}|{}|{}|{}|".format(name_str, country.id, cls.aircraft_number, parent_base_id, db.unit_type_name(flight.unit_type)) name_str,
country.id,
cls.aircraft_number,
parent_base_id,
db.unit_type_name(flight.unit_type),
)
@classmethod @classmethod
def next_unit_name(cls, country: Country, parent_base_id: int, unit_type: UnitType): def next_unit_name(cls, country: Country, parent_base_id: int, unit_type: UnitType):
cls.number += 1 cls.number += 1
return "unit|{}|{}|{}|{}|".format(country.id, cls.number, parent_base_id, db.unit_type_name(unit_type)) return "unit|{}|{}|{}|{}|".format(
country.id, cls.number, parent_base_id, db.unit_type_name(unit_type)
)
@classmethod @classmethod
def next_infantry_name(cls, country: Country, parent_base_id: int, unit_type: UnitType): def next_infantry_name(
cls, country: Country, parent_base_id: int, unit_type: UnitType
):
cls.infantry_number += 1 cls.infantry_number += 1
return "infantry|{}|{}|{}|{}|".format(country.id, cls.infantry_number, parent_base_id, db.unit_type_name(unit_type)) return "infantry|{}|{}|{}|{}|".format(
country.id,
cls.infantry_number,
parent_base_id,
db.unit_type_name(unit_type),
)
@staticmethod @staticmethod
def next_basedefense_name(): def next_basedefense_name():
@@ -100,7 +318,9 @@ class NameGenerator:
@classmethod @classmethod
def next_tanker_name(cls, country: Country, unit_type: UnitType): def next_tanker_name(cls, country: Country, unit_type: UnitType):
cls.number += 1 cls.number += 1
return "tanker|{}|{}|0|{}".format(country.id, cls.number, db.unit_type_name(unit_type)) return "tanker|{}|{}|0|{}".format(
country.id, cls.number, db.unit_type_name(unit_type)
)
@classmethod @classmethod
def next_carrier_name(cls, country: Country): def next_carrier_name(cls, country: Country):
@@ -112,7 +332,11 @@ class NameGenerator:
if len(cls.ANIMALS) == 0: if len(cls.ANIMALS) == 0:
for i in range(10): for i in range(10):
new_name_generated = True new_name_generated = True
alpha_mil_name = random.choice(ALPHA_MILITARY).upper() + "#" + str(random.randint(0, 100)) alpha_mil_name = (
random.choice(ALPHA_MILITARY).upper()
+ "#"
+ str(random.randint(0, 100))
)
for existing_name in cls.existing_alphas: for existing_name in cls.existing_alphas:
if existing_name == alpha_mil_name: if existing_name == alpha_mil_name:
new_name_generated = False new_name_generated = False

View File

@@ -68,9 +68,10 @@ class Radio:
def range(self) -> Iterator[RadioFrequency]: def range(self) -> Iterator[RadioFrequency]:
"""Returns an iterator over the usable frequencies of this radio.""" """Returns an iterator over the usable frequencies of this radio."""
return (RadioFrequency(x) for x in range( return (
self.minimum.hertz, self.maximum.hertz, self.step.hertz RadioFrequency(x)
)) for x in range(self.minimum.hertz, self.maximum.hertz, self.step.hertz)
)
@property @property
def last_channel(self) -> RadioFrequency: def last_channel(self) -> RadioFrequency:
@@ -99,14 +100,12 @@ RADIOS: List[Radio] = [
Radio("SCR-522", MHz(100), MHz(156), step=MHz(1)), Radio("SCR-522", MHz(100), MHz(156), step=MHz(1)),
Radio("A.R.I. 1063", MHz(100), MHz(156), step=MHz(1)), Radio("A.R.I. 1063", MHz(100), MHz(156), step=MHz(1)),
Radio("BC-1206", kHz(200), kHz(400), step=kHz(10)), Radio("BC-1206", kHz(200), kHz(400), step=kHz(10)),
# Note: The M2000C V/UHF can operate in both ranges, but has a gap between # Note: The M2000C V/UHF can operate in both ranges, but has a gap between
# 150 MHz and 225 MHz. We can't allocate in that gap, and the current # 150 MHz and 225 MHz. We can't allocate in that gap, and the current
# system doesn't model gaps, so just pretend it ends at 150 MHz for now. We # system doesn't model gaps, so just pretend it ends at 150 MHz for now. We
# can model gaps later if needed. # can model gaps later if needed.
Radio("TRT ERA 7000 V/UHF", MHz(118), MHz(150), step=MHz(1)), Radio("TRT ERA 7000 V/UHF", MHz(118), MHz(150), step=MHz(1)),
Radio("TRT ERA 7200 UHF", MHz(225), MHz(400), step=MHz(1)), Radio("TRT ERA 7200 UHF", MHz(225), MHz(400), step=MHz(1)),
# Tomcat radios # Tomcat radios
# # https://www.heatblur.se/F-14Manual/general.html#an-arc-159-uhf-1-radio # # https://www.heatblur.se/F-14Manual/general.html#an-arc-159-uhf-1-radio
Radio("AN/ARC-159", MHz(225), MHz(400), step=MHz(1)), Radio("AN/ARC-159", MHz(225), MHz(400), step=MHz(1)),
@@ -114,31 +113,23 @@ RADIOS: List[Radio] = [
# to 400 MHz range, but we can't model gaps with the current implementation. # to 400 MHz range, but we can't model gaps with the current implementation.
# https://www.heatblur.se/F-14Manual/general.html#an-arc-182-v-uhf-2-radio # https://www.heatblur.se/F-14Manual/general.html#an-arc-182-v-uhf-2-radio
Radio("AN/ARC-182", MHz(108), MHz(174), step=MHz(1)), Radio("AN/ARC-182", MHz(108), MHz(174), step=MHz(1)),
# Also capable of [103, 156) at 25 kHz intervals, but we can't do gaps. # Also capable of [103, 156) at 25 kHz intervals, but we can't do gaps.
Radio("FR 22", MHz(225), MHz(400), step=kHz(50)), Radio("FR 22", MHz(225), MHz(400), step=kHz(50)),
# P-51 / P-47 Radio # P-51 / P-47 Radio
# 4 preset channels (A/B/C/D) # 4 preset channels (A/B/C/D)
Radio("SCR522", MHz(100), MHz(156), step=kHz(25)), Radio("SCR522", MHz(100), MHz(156), step=kHz(25)),
Radio("R&S M3AR VHF", MHz(120), MHz(174), step=MHz(1)), Radio("R&S M3AR VHF", MHz(120), MHz(174), step=MHz(1)),
Radio("R&S M3AR UHF", MHz(225), MHz(400), step=MHz(1)), Radio("R&S M3AR UHF", MHz(225), MHz(400), step=MHz(1)),
# MiG-15bis # MiG-15bis
Radio("RSI-6K HF", MHz(3, 750), MHz(5), step=kHz(25)), Radio("RSI-6K HF", MHz(3, 750), MHz(5), step=kHz(25)),
# MiG-19P # MiG-19P
Radio("RSIU-4V", MHz(100), MHz(150), step=MHz(1)), Radio("RSIU-4V", MHz(100), MHz(150), step=MHz(1)),
# MiG-21bis # MiG-21bis
Radio("RSIU-5V", MHz(118), MHz(140), step=MHz(1)), Radio("RSIU-5V", MHz(118), MHz(140), step=MHz(1)),
# Ka-50 # Ka-50
# Note: Also capable of 100MHz-150MHz, but we can't model gaps. # Note: Also capable of 100MHz-150MHz, but we can't model gaps.
Radio("R-800L1", MHz(220), MHz(400), step=kHz(25)), Radio("R-800L1", MHz(220), MHz(400), step=kHz(25)),
Radio("R-828", MHz(20), MHz(60), step=kHz(25)), Radio("R-828", MHz(20), MHz(60), step=kHz(25)),
# UH-1H # UH-1H
Radio("AN/ARC-51BX", MHz(225), MHz(400), step=kHz(50)), Radio("AN/ARC-51BX", MHz(225), MHz(400), step=kHz(50)),
Radio("AN/ARC-131", MHz(30), MHz(76), step=kHz(50)), Radio("AN/ARC-131", MHz(30), MHz(76), step=kHz(50)),
@@ -218,7 +209,8 @@ class RadioRegistry:
# https://github.com/Khopa/dcs_liberation/issues/598 # https://github.com/Khopa/dcs_liberation/issues/598
channel = radio.last_channel channel = radio.last_channel
logging.warning( logging.warning(
f"No more free channels for {radio.name}. Reusing {channel}.") f"No more free channels for {radio.name}. Reusing {channel}."
)
return channel return channel
def alloc_uhf(self) -> RadioFrequency: def alloc_uhf(self) -> RadioFrequency:

View File

@@ -25,8 +25,9 @@ class RunwayData:
icls: Optional[int] = None icls: Optional[int] = None
@classmethod @classmethod
def for_airfield(cls, airport: Airport, runway_heading: int, def for_airfield(
runway_name: str) -> RunwayData: cls, airport: Airport, runway_heading: int, runway_name: str
) -> RunwayData:
"""Creates RunwayData for the given runway of an airfield. """Creates RunwayData for the given runway of an airfield.
Args: Args:
@@ -56,7 +57,7 @@ class RunwayData:
atc=atc, atc=atc,
tacan=tacan, tacan=tacan,
tacan_callsign=tacan_callsign, tacan_callsign=tacan_callsign,
ils=ils ils=ils,
) )
@classmethod @classmethod

View File

@@ -20,15 +20,19 @@ class BoforsGenerator(AirDefenseGroupGenerator):
grid_x = random.randint(2, 3) grid_x = random.randint(2, 3)
grid_y = random.randint(2, 3) grid_y = random.randint(2, 3)
spacing = random.randint(10,40) spacing = random.randint(10, 40)
index = 0 index = 0
for i in range(grid_x): for i in range(grid_x):
for j in range(grid_y): for j in range(grid_y):
index = index+1 index = index + 1
self.add_unit(AirDefence.AAA_Bofors_40mm, "AAA#" + str(index), self.add_unit(
self.position.x + spacing*i, AirDefence.AAA_40mm_Bofors,
self.position.y + spacing*j, self.heading) "AAA#" + str(index),
self.position.x + spacing * i,
self.position.y + spacing * j,
self.heading,
)
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:

View File

@@ -8,12 +8,12 @@ from gen.sam.airdefensegroupgenerator import (
) )
GFLAK = [ GFLAK = [
AirDefence.AAA_Flak_Vierling_38, AirDefence.AAA_Flak_Vierling_38_Quad_20mm,
AirDefence.AAA_8_8cm_Flak_18, AirDefence.AAA_8_8cm_Flak_18,
AirDefence.AAA_8_8cm_Flak_36, AirDefence.AAA_8_8cm_Flak_36,
AirDefence.AAA_8_8cm_Flak_37, AirDefence.AAA_8_8cm_Flak_37,
AirDefence.AAA_8_8cm_Flak_41, AirDefence.AAA_8_8cm_Flak_41,
AirDefence.AAA_Flak_38, AirDefence.AAA_Flak_38_20mm,
] ]
@@ -37,34 +37,64 @@ class FlakGenerator(AirDefenseGroupGenerator):
for i in range(grid_x): for i in range(grid_x):
for j in range(grid_y): for j in range(grid_y):
index = index+1 index = index + 1
self.add_unit(unit_type, "AAA#" + str(index), self.add_unit(
self.position.x + spacing*i + random.randint(1,5), unit_type,
self.position.y + spacing*j + random.randint(1,5), self.heading) "AAA#" + str(index),
self.position.x + spacing * i + random.randint(1, 5),
self.position.y + spacing * j + random.randint(1, 5),
self.heading,
)
if(mixed): if mixed:
unit_type = random.choice(GFLAK) unit_type = random.choice(GFLAK)
# Search lights # Search lights
search_pos = self.get_circular_position(random.randint(2,3), 80) search_pos = self.get_circular_position(random.randint(2, 3), 80)
for index, pos in enumerate(search_pos): for index, pos in enumerate(search_pos):
self.add_unit(AirDefence.Flak_Searchlight_37, "SearchLight#" + str(index), pos[0], pos[1], self.heading) self.add_unit(
AirDefence.SL_Flakscheinwerfer_37,
"SearchLight#" + str(index),
pos[0],
pos[1],
self.heading,
)
# Support # Support
self.add_unit(AirDefence.Maschinensatz_33, "MC33#", self.position.x-20, self.position.y-20, self.heading) self.add_unit(
self.add_unit(AirDefence.AAA_Kdo_G_40, "KDO#", self.position.x - 25, self.position.y - 20, AirDefence.PU_Maschinensatz_33,
self.heading) "MC33#",
self.position.x - 20,
self.position.y - 20,
self.heading,
)
self.add_unit(
AirDefence.AAA_SP_Kdo_G_40,
"KDO#",
self.position.x - 25,
self.position.y - 20,
self.heading,
)
# Commander # Commander
self.add_unit(Unarmed.Kübelwagen_82, "Kubel#", self.position.x - 35, self.position.y - 20, self.add_unit(
self.heading) Unarmed.LUV_Kubelwagen_82,
"Kubel#",
self.position.x - 35,
self.position.y - 20,
self.heading,
)
# Some Opel Blitz trucks # Some Opel Blitz trucks
for i in range(int(max(1,grid_x/2))): for i in range(int(max(1, grid_x / 2))):
for j in range(int(max(1,grid_x/2))): for j in range(int(max(1, grid_x / 2))):
self.add_unit(Unarmed.Blitz_3_6_6700A, "BLITZ#" + str(index), self.add_unit(
self.position.x + 125 + 15*i + random.randint(1,5), Unarmed.Truck_Opel_Blitz,
self.position.y + 15*j + random.randint(1,5), 75) "BLITZ#" + str(index),
self.position.x + 125 + 15 * i + random.randint(1, 5),
self.position.y + 15 * j + random.randint(1, 5),
75,
)
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:

View File

@@ -24,12 +24,22 @@ class Flak18Generator(AirDefenseGroupGenerator):
for i in range(3): for i in range(3):
for j in range(2): for j in range(2):
index = index + 1 index = index + 1
self.add_unit(AirDefence.AAA_8_8cm_Flak_18, "AAA#" + str(index), self.add_unit(
self.position.x + spacing * i + random.randint(1, 5), AirDefence.AAA_8_8cm_Flak_18,
self.position.y + spacing * j + random.randint(1, 5), self.heading) "AAA#" + str(index),
self.position.x + spacing * i + random.randint(1, 5),
self.position.y + spacing * j + random.randint(1, 5),
self.heading,
)
# Add a commander truck # Add a commander truck
self.add_unit(Unarmed.Blitz_3_6_6700A, "Blitz#", self.position.x - 35, self.position.y - 20, self.heading) self.add_unit(
Unarmed.Truck_Opel_Blitz,
"Blitz#",
self.position.x - 35,
self.position.y - 20,
self.heading,
)
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:

View File

@@ -19,15 +19,25 @@ class KS19Generator(AirDefenseGroupGenerator):
spacing = random.randint(10, 40) 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) self.add_unit(
highdigitsams.AAA_SON_9_Fire_Can,
"TR",
self.position.x - 20,
self.position.y - 20,
self.heading,
)
index = 0 index = 0
for i in range(3): for i in range(3):
for j in range(3): for j in range(3):
index = index + 1 index = index + 1
self.add_unit(highdigitsams.AAA_100mm_KS_19, "AAA#" + str(index), self.add_unit(
self.position.x + spacing * i, highdigitsams.AAA_100mm_KS_19,
self.position.y + spacing * j, self.heading) "AAA#" + str(index),
self.position.x + spacing * i,
self.position.y + spacing * j,
self.heading,
)
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:

View File

@@ -20,21 +20,63 @@ class AllyWW2FlakGenerator(AirDefenseGroupGenerator):
positions = self.get_circular_position(4, launcher_distance=30, coverage=360) positions = self.get_circular_position(4, launcher_distance=30, coverage=360)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.AA_gun_QF_3_7, "AA#" + str(i), position[0], position[1], position[2]) self.add_unit(
AirDefence.AAA_QF_3_7,
"AA#" + str(i),
position[0],
position[1],
position[2],
)
positions = self.get_circular_position(8, launcher_distance=60, coverage=360) positions = self.get_circular_position(8, launcher_distance=60, coverage=360)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.AAA_M1_37mm, "AA#" + str(4 + i), position[0], position[1], position[2]) self.add_unit(
AirDefence.AAA_M1_37mm,
"AA#" + str(4 + i),
position[0],
position[1],
position[2],
)
positions = self.get_circular_position(8, launcher_distance=90, coverage=360) positions = self.get_circular_position(8, launcher_distance=90, coverage=360)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.AAA_M45_Quadmount, "AA#" + str(12 + i), position[0], position[1], position[2]) self.add_unit(
AirDefence.AAA_M45_Quadmount_HB_12_7mm,
"AA#" + str(12 + i),
position[0],
position[1],
position[2],
)
# Add a commander truck # Add a commander truck
self.add_unit(Unarmed.Willys_MB, "CMD#1", self.position.x, self.position.y - 20, random.randint(0, 360)) self.add_unit(
self.add_unit(Armor.M30_Cargo_Carrier, "LOG#1", self.position.x, self.position.y + 20, random.randint(0, 360)) Unarmed.Car_Willys_Jeep,
self.add_unit(Armor.M4_Tractor, "LOG#2", self.position.x + 20, self.position.y, random.randint(0, 360)) "CMD#1",
self.add_unit(Unarmed.Bedford_MWD, "LOG#3", self.position.x - 20, self.position.y, random.randint(0, 360)) self.position.x,
self.position.y - 20,
random.randint(0, 360),
)
self.add_unit(
Unarmed.Carrier_M30_Cargo,
"LOG#1",
self.position.x,
self.position.y + 20,
random.randint(0, 360),
)
self.add_unit(
Unarmed.Tractor_M4_Hi_Speed,
"LOG#2",
self.position.x + 20,
self.position.y,
random.randint(0, 360),
)
self.add_unit(
Unarmed.Truck_Bedford,
"LOG#3",
self.position.x - 20,
self.position.y,
random.randint(0, 360),
)
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:

View File

@@ -16,9 +16,17 @@ class ZSU57Generator(AirDefenseGroupGenerator):
def generate(self): def generate(self):
num_launchers = 5 num_launchers = 5
positions = self.get_circular_position(num_launchers, launcher_distance=110, coverage=360) positions = self.get_circular_position(
num_launchers, launcher_distance=110, coverage=360
)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.AAA_ZSU_57_2, "SPAA#" + str(i), position[0], position[1], position[2]) self.add_unit(
AirDefence.SPAAA_ZSU_57_2,
"SPAA#" + str(i),
position[0],
position[1],
position[2],
)
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:

View File

@@ -20,15 +20,19 @@ class ZU23InsurgentGenerator(AirDefenseGroupGenerator):
grid_x = random.randint(2, 3) grid_x = random.randint(2, 3)
grid_y = random.randint(2, 3) grid_y = random.randint(2, 3)
spacing = random.randint(10,40) spacing = random.randint(10, 40)
index = 0 index = 0
for i in range(grid_x): for i in range(grid_x):
for j in range(grid_y): for j in range(grid_y):
index = index+1 index = index + 1
self.add_unit(AirDefence.AAA_ZU_23_Insurgent_Closed, "AAA#" + str(index), self.add_unit(
self.position.x + spacing*i, AirDefence.AAA_ZU_23_Closed_Emplacement_Insurgent,
self.position.y + spacing*j, self.heading) "AAA#" + str(index),
self.position.x + spacing * i,
self.position.y + spacing * j,
self.heading,
)
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:

View File

@@ -28,8 +28,9 @@ class AirDefenseGroupGenerator(GroupGenerator, ABC):
self.auxiliary_groups: List[VehicleGroup] = [] self.auxiliary_groups: List[VehicleGroup] = []
def add_auxiliary_group(self, name_suffix: str) -> VehicleGroup: def add_auxiliary_group(self, name_suffix: str) -> VehicleGroup:
group = VehicleGroup(self.game.next_group_id(), group = VehicleGroup(
"|".join([self.go.group_name, name_suffix])) self.game.next_group_id(), "|".join([self.go.group_name, name_suffix])
)
self.auxiliary_groups.append(group) self.auxiliary_groups.append(group)
return group return group
@@ -37,7 +38,8 @@ class AirDefenseGroupGenerator(GroupGenerator, ABC):
raise RuntimeError( raise RuntimeError(
"Deprecated call to AirDefenseGroupGenerator.get_generated_group " "Deprecated call to AirDefenseGroupGenerator.get_generated_group "
"misses auxiliary groups. Use AirDefenseGroupGenerator.groups " "misses auxiliary groups. Use AirDefenseGroupGenerator.groups "
"instead.") "instead."
)
@property @property
def groups(self) -> Iterator[VehicleGroup]: def groups(self) -> Iterator[VehicleGroup]:

View File

@@ -12,13 +12,13 @@ from gen.sam.group_generator import GroupGenerator
class EarlyColdWarFlakGenerator(AirDefenseGroupGenerator): class EarlyColdWarFlakGenerator(AirDefenseGroupGenerator):
""" """
This generator attempt to mimic an early cold-war era flak AAA site. This generator attempt to mimic an early cold-war era flak AAA site.
The Flak 18 88mm is used as the main long range gun and 2 Bofors 40mm guns provide short range protection. The Flak 18 88mm is used as the main long range gun, S-60 is used as a mid range gun and 2 Bofors 40mm guns provide short range protection.
This does not include search lights and telemeter computer (Kdo.G 40) because these are paid units only available in WW2 asset pack This does not include search lights and telemeter computer (Kdo.G 40) because these are paid units only available in WW2 asset pack
""" """
name = "Early Cold War Flak Site" name = "Early Cold War Flak Site"
price = 58 price = 74
def generate(self): def generate(self):
@@ -29,18 +29,54 @@ class EarlyColdWarFlakGenerator(AirDefenseGroupGenerator):
for i in range(3): for i in range(3):
for j in range(2): for j in range(2):
index = index + 1 index = index + 1
self.add_unit(AirDefence.AAA_8_8cm_Flak_18, "AAA#" + str(index), self.add_unit(
self.position.x + spacing * i + random.randint(1, 5), AirDefence.AAA_8_8cm_Flak_18,
self.position.y + spacing * j + random.randint(1, 5), self.heading) "AAA#" + str(index),
self.position.x + spacing * i + random.randint(1, 5),
self.position.y + spacing * j + random.randint(1, 5),
self.heading,
)
# Medium range guns
self.add_unit(
AirDefence.AAA_S_60_57mm,
"SHO#1",
self.position.x - 40,
self.position.y - 40,
self.heading + 180,
),
self.add_unit(
AirDefence.AAA_S_60_57mm,
"SHO#2",
self.position.x + spacing * 2 + 40,
self.position.y + spacing + 40,
self.heading,
),
# Short range guns # Short range guns
self.add_unit(AirDefence.AAA_Bofors_40mm, "SHO#1", self.add_unit(
self.position.x - 40, self.position.y - 40, self.heading + 180), AirDefence.AAA_ZU_23_Closed_Emplacement,
self.add_unit(AirDefence.AAA_Bofors_40mm, "SHO#2", "SHO#3",
self.position.x + spacing * 2 + 40, self.position.y + spacing + 40, self.heading), self.position.x - 80,
self.position.y - 40,
self.heading + 180,
),
self.add_unit(
AirDefence.AAA_ZU_23_Closed_Emplacement,
"SHO#4",
self.position.x + spacing * 2 + 80,
self.position.y + spacing + 40,
self.heading,
),
# Add a truck # Add a truck
self.add_unit(Unarmed.Transport_KAMAZ_43101, "Truck#", self.position.x - 60, self.position.y - 20, self.heading) self.add_unit(
Unarmed.Truck_KAMAZ_43101,
"Truck#",
self.position.x - 60,
self.position.y - 20,
self.heading,
)
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:
@@ -50,7 +86,7 @@ class EarlyColdWarFlakGenerator(AirDefenseGroupGenerator):
class ColdWarFlakGenerator(AirDefenseGroupGenerator): class ColdWarFlakGenerator(AirDefenseGroupGenerator):
""" """
This generator attempt to mimic a cold-war era flak AAA site. This generator attempt to mimic a cold-war era flak AAA site.
The Flak 18 88mm is used as the main long range gun while 2 Zu-23 guns provide short range protection. The Flak 18 88mm is used as the main long range gun, 2 S-60 57mm gun improve mid range firepower, while 2 Zu-23 guns even provide short range protection.
The site is also fitted with a P-19 radar for early detection. The site is also fitted with a P-19 radar for early detection.
""" """
@@ -66,18 +102,54 @@ class ColdWarFlakGenerator(AirDefenseGroupGenerator):
for i in range(3): for i in range(3):
for j in range(2): for j in range(2):
index = index + 1 index = index + 1
self.add_unit(AirDefence.AAA_8_8cm_Flak_18, "AAA#" + str(index), self.add_unit(
self.position.x + spacing * i + random.randint(1, 5), AirDefence.AAA_8_8cm_Flak_18,
self.position.y + spacing * j + random.randint(1, 5), self.heading) "AAA#" + str(index),
self.position.x + spacing * i + random.randint(1, 5),
self.position.y + spacing * j + random.randint(1, 5),
self.heading,
)
# Medium range guns
self.add_unit(
AirDefence.AAA_S_60_57mm,
"SHO#1",
self.position.x - 40,
self.position.y - 40,
self.heading + 180,
),
self.add_unit(
AirDefence.AAA_S_60_57mm,
"SHO#2",
self.position.x + spacing * 2 + 40,
self.position.y + spacing + 40,
self.heading,
),
# Short range guns # Short range guns
self.add_unit(AirDefence.AAA_ZU_23_Closed, "SHO#1", self.add_unit(
self.position.x - 40, self.position.y - 40, self.heading + 180), AirDefence.AAA_ZU_23_Closed_Emplacement,
self.add_unit(AirDefence.AAA_ZU_23_Closed, "SHO#2", "SHO#3",
self.position.x + spacing * 2 + 40, self.position.y + spacing + 40, self.heading), self.position.x - 80,
self.position.y - 40,
self.heading + 180,
),
self.add_unit(
AirDefence.AAA_ZU_23_Closed_Emplacement,
"SHO#4",
self.position.x + spacing * 2 + 80,
self.position.y + spacing + 40,
self.heading,
),
# Add a P19 Radar for EWR # Add a P19 Radar for EWR
self.add_unit(AirDefence.SAM_SR_P_19, "SR#0", self.position.x - 60, self.position.y - 20, self.heading) self.add_unit(
AirDefence.SAM_P19_Flat_Face_SR__SA_2_3,
"SR#0",
self.position.x - 60,
self.position.y - 20,
self.heading,
)
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:

View File

@@ -0,0 +1,63 @@
import random
from typing import List, Optional, Type
from dcs.unitgroup import VehicleGroup
from game import Game
from game.factions.faction import Faction
from game.theater.theatergroundobject import EwrGroundObject
from gen.sam.ewrs import (
BigBirdGenerator,
BoxSpringGenerator,
DogEarGenerator,
FlatFaceGenerator,
HawkEwrGenerator,
PatriotEwrGenerator,
RolandEwrGenerator,
SnowDriftGenerator,
StraightFlushGenerator,
TallRackGenerator,
)
from gen.sam.group_generator import GroupGenerator
EWR_MAP = {
"BoxSpringGenerator": BoxSpringGenerator,
"TallRackGenerator": TallRackGenerator,
"DogEarGenerator": DogEarGenerator,
"RolandEwrGenerator": RolandEwrGenerator,
"FlatFaceGenerator": FlatFaceGenerator,
"PatriotEwrGenerator": PatriotEwrGenerator,
"BigBirdGenerator": BigBirdGenerator,
"SnowDriftGenerator": SnowDriftGenerator,
"StraightFlushGenerator": StraightFlushGenerator,
"HawkEwrGenerator": HawkEwrGenerator,
}
def get_faction_possible_ewrs_generator(
faction: Faction,
) -> List[Type[GroupGenerator]]:
"""
Return the list of possible EWR generators for the given faction
:param faction: Faction name to search units for
"""
return [EWR_MAP[s] for s in faction.ewrs]
def generate_ewr_group(
game: Game, ground_object: EwrGroundObject, faction: Faction
) -> Optional[VehicleGroup]:
"""Generates an early warning radar group.
:param game: The Game.
:param ground_object: The ground object which will own the EWR group.
:param faction: Owner faction.
:return: The generated group, or None if one could not be generated.
"""
generators = get_faction_possible_ewrs_generator(faction)
if len(generators) > 0:
generator_class = random.choice(generators)
generator = generator_class(game, ground_object)
generator.generate()
return generator.get_generated_group()
return None

View File

@@ -10,8 +10,9 @@ class EwrGenerator(GroupGenerator):
raise NotImplementedError raise NotImplementedError
def generate(self) -> None: def generate(self) -> None:
self.add_unit(self.unit_type, "EWR", self.position.x, self.position.y, self.add_unit(
self.heading) self.unit_type, "EWR", self.position.x, self.position.y, self.heading
)
class BoxSpringGenerator(EwrGenerator): class BoxSpringGenerator(EwrGenerator):
@@ -32,7 +33,7 @@ class DogEarGenerator(EwrGenerator):
This is the SA-8 search radar, but used as an early warning radar. This is the SA-8 search radar, but used as an early warning radar.
""" """
unit_type = AirDefence.CP_9S80M1_Sborka unit_type = AirDefence.MCC_SR_Sborka_Dog_Ear_SR
class RolandEwrGenerator(EwrGenerator): class RolandEwrGenerator(EwrGenerator):
@@ -50,7 +51,7 @@ class FlatFaceGenerator(EwrGenerator):
This is the SA-3 search radar, but used as an early warning radar. This is the SA-3 search radar, but used as an early warning radar.
""" """
unit_type = AirDefence.SAM_SR_P_19 unit_type = AirDefence.SAM_P19_Flat_Face_SR__SA_2_3
class PatriotEwrGenerator(EwrGenerator): class PatriotEwrGenerator(EwrGenerator):
@@ -59,7 +60,7 @@ class PatriotEwrGenerator(EwrGenerator):
This is the Patriot search/track radar, but used as an early warning radar. This is the Patriot search/track radar, but used as an early warning radar.
""" """
unit_type = AirDefence.SAM_Patriot_STR_AN_MPQ_53 unit_type = AirDefence.SAM_Patriot_STR
class BigBirdGenerator(EwrGenerator): class BigBirdGenerator(EwrGenerator):
@@ -68,7 +69,7 @@ class BigBirdGenerator(EwrGenerator):
This is the SA-10 track radar, but used as an early warning radar. This is the SA-10 track radar, but used as an early warning radar.
""" """
unit_type = AirDefence.SAM_SA_10_S_300PS_SR_64H6E unit_type = AirDefence.SAM_SA_10_S_300_Grumble_Big_Bird_SR
class SnowDriftGenerator(EwrGenerator): class SnowDriftGenerator(EwrGenerator):
@@ -77,7 +78,7 @@ class SnowDriftGenerator(EwrGenerator):
This is the SA-11 search radar, but used as an early warning radar. This is the SA-11 search radar, but used as an early warning radar.
""" """
unit_type = AirDefence.SAM_SA_11_Buk_SR_9S18M1 unit_type = AirDefence.SAM_SA_11_Buk_Gadfly_Snow_Drift_SR
class StraightFlushGenerator(EwrGenerator): class StraightFlushGenerator(EwrGenerator):
@@ -86,7 +87,7 @@ class StraightFlushGenerator(EwrGenerator):
This is the SA-6 search/track radar, but used as an early warning radar. This is the SA-6 search/track radar, but used as an early warning radar.
""" """
unit_type = AirDefence.SAM_SA_6_Kub_STR_9S91 unit_type = AirDefence.SAM_SA_6_Kub_Long_Track_STR
class HawkEwrGenerator(EwrGenerator): class HawkEwrGenerator(EwrGenerator):
@@ -95,4 +96,4 @@ class HawkEwrGenerator(EwrGenerator):
This is the Hawk search radar, but used as an early warning radar. This is the Hawk search radar, but used as an early warning radar.
""" """
unit_type = AirDefence.SAM_Hawk_SR_AN_MPQ_50 unit_type = AirDefence.SAM_Hawk_SR__AN_MPQ_50

View File

@@ -17,27 +17,93 @@ class FreyaGenerator(AirDefenseGroupGenerator):
def generate(self): def generate(self):
# TODO : would be better with the Concrete structure that is supposed to protect it # TODO : would be better with the Concrete structure that is supposed to protect it
self.add_unit(AirDefence.EWR_FuMG_401_Freya_LZ, "EWR#1", self.position.x, self.position.y, self.heading) self.add_unit(
AirDefence.EWR_FuMG_401_Freya_LZ,
"EWR#1",
self.position.x,
self.position.y,
self.heading,
)
positions = self.get_circular_position(4, launcher_distance=50, coverage=360) positions = self.get_circular_position(4, launcher_distance=50, coverage=360)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.AAA_Flak_Vierling_38, "AA#" + str(i), position[0], position[1], position[2]) self.add_unit(
AirDefence.AAA_Flak_Vierling_38_Quad_20mm,
"AA#" + str(i),
position[0],
position[1],
position[2],
)
positions = self.get_circular_position(4, launcher_distance=100, coverage=360) positions = self.get_circular_position(4, launcher_distance=100, coverage=360)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.AAA_8_8cm_Flak_18, "AA#" + str(4+i), position[0], position[1], position[2]) self.add_unit(
AirDefence.AAA_8_8cm_Flak_18,
"AA#" + str(4 + i),
position[0],
position[1],
position[2],
)
# Command/Logi # Command/Logi
self.add_unit(Unarmed.Kübelwagen_82, "Kubel#1", self.position.x - 20, self.position.y - 20, self.heading) self.add_unit(
self.add_unit(Unarmed.Sd_Kfz_7, "Sdkfz#1", self.position.x + 20, self.position.y + 22, self.heading) Unarmed.LUV_Kubelwagen_82,
self.add_unit(Unarmed.Sd_Kfz_2, "Sdkfz#2", self.position.x - 22, self.position.y + 20, self.heading) "Kubel#1",
self.position.x - 20,
self.position.y - 20,
self.heading,
)
self.add_unit(
Unarmed.Carrier_Sd_Kfz_7_Tractor,
"Sdkfz#1",
self.position.x + 20,
self.position.y + 22,
self.heading,
)
self.add_unit(
Unarmed.LUV_Kettenrad,
"Sdkfz#2",
self.position.x - 22,
self.position.y + 20,
self.heading,
)
# Maschinensatz_33 and Kdo.g 40 Telemeter # PU_Maschinensatz_33 and Kdo.g 40 Telemeter
self.add_unit(AirDefence.Maschinensatz_33, "Energy#1", self.position.x + 20, self.position.y - 20, self.heading) self.add_unit(
self.add_unit(AirDefence.AAA_Kdo_G_40, "Telemeter#1", self.position.x + 20, self.position.y - 10, self.heading) AirDefence.PU_Maschinensatz_33,
self.add_unit(Infantry.Infantry_Mauser_98, "Inf#1", self.position.x + 20, self.position.y - 14, self.heading) "Energy#1",
self.add_unit(Infantry.Infantry_Mauser_98, "Inf#2", self.position.x + 20, self.position.y - 22, self.heading) self.position.x + 20,
self.add_unit(Infantry.Infantry_Mauser_98, "Inf#3", self.position.x + 20, self.position.y - 24, self.heading + 45) self.position.y - 20,
self.heading,
)
self.add_unit(
AirDefence.AAA_SP_Kdo_G_40,
"Telemeter#1",
self.position.x + 20,
self.position.y - 10,
self.heading,
)
self.add_unit(
Infantry.Infantry_Mauser_98,
"Inf#1",
self.position.x + 20,
self.position.y - 14,
self.heading,
)
self.add_unit(
Infantry.Infantry_Mauser_98,
"Inf#2",
self.position.x + 20,
self.position.y - 22,
self.heading,
)
self.add_unit(
Infantry.Infantry_Mauser_98,
"Inf#3",
self.position.x + 20,
self.position.y - 24,
self.heading + 45,
)
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:

View File

@@ -23,14 +23,12 @@ if TYPE_CHECKING:
# care about in the format we want if we just generate our own group description # care about in the format we want if we just generate our own group description
# types rather than pydcs groups. # types rather than pydcs groups.
class GroupGenerator: class GroupGenerator:
def __init__(self, game: Game, ground_object: TheaterGroundObject) -> None: def __init__(self, game: Game, ground_object: TheaterGroundObject) -> None:
self.game = game self.game = game
self.go = ground_object self.go = ground_object
self.position = ground_object.position self.position = ground_object.position
self.heading = random.randint(0, 359) self.heading = random.randint(0, 359)
self.vg = unitgroup.VehicleGroup(self.game.next_group_id(), self.vg = unitgroup.VehicleGroup(self.game.next_group_id(), self.go.group_name)
self.go.group_name)
wp = self.vg.add_waypoint(self.position, PointAction.OffRoad, 0) wp = self.vg.add_waypoint(self.position, PointAction.OffRoad, 0)
wp.ETA_locked = True wp.ETA_locked = True
@@ -40,16 +38,27 @@ class GroupGenerator:
def get_generated_group(self) -> unitgroup.VehicleGroup: def get_generated_group(self) -> unitgroup.VehicleGroup:
return self.vg return self.vg
def add_unit(self, unit_type: Type[VehicleType], name: str, pos_x: float, def add_unit(
pos_y: float, heading: int) -> Vehicle: self,
return self.add_unit_to_group(self.vg, unit_type, name, unit_type: Type[VehicleType],
Point(pos_x, pos_y), heading) 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, def add_unit_to_group(
unit_type: Type[VehicleType], name: str, self,
position: Point, heading: int) -> Vehicle: group: unitgroup.VehicleGroup,
unit = Vehicle(self.game.next_unit_id(), unit_type: Type[VehicleType],
f"{group.name}|{name}", unit_type.id) name: str,
position: Point,
heading: int,
) -> Vehicle:
unit = Vehicle(self.game.next_unit_id(), f"{group.name}|{name}", unit_type.id)
unit.position = position unit.position = position
unit.heading = heading unit.heading = heading
group.add_unit(unit) group.add_unit(unit)
@@ -82,31 +91,36 @@ class GroupGenerator:
current_offset = self.heading current_offset = self.heading
current_offset -= outer_offset * (math.ceil(num_units / 2) - 1) current_offset -= outer_offset * (math.ceil(num_units / 2) - 1)
for x in range(1, num_units + 1): for x in range(1, num_units + 1):
positions.append(( positions.append(
self.position.x + launcher_distance * math.cos(math.radians(current_offset)), (
self.position.y + launcher_distance * math.sin(math.radians(current_offset)), self.position.x
current_offset, + launcher_distance * math.cos(math.radians(current_offset)),
)) self.position.y
+ launcher_distance * math.sin(math.radians(current_offset)),
current_offset,
)
)
current_offset += outer_offset current_offset += outer_offset
return positions return positions
class ShipGroupGenerator(GroupGenerator): class ShipGroupGenerator(GroupGenerator):
"""Abstract class for other ship generator classes""" """Abstract class for other ship generator classes"""
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
def __init__(
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
self.game = game self.game = game
self.go = ground_object self.go = ground_object
self.position = ground_object.position self.position = ground_object.position
self.heading = random.randint(0, 359) self.heading = random.randint(0, 359)
self.faction = faction self.faction = faction
self.vg = unitgroup.ShipGroup(self.game.next_group_id(), self.vg = unitgroup.ShipGroup(self.game.next_group_id(), self.go.group_name)
self.go.group_name)
wp = self.vg.add_waypoint(self.position, 0) wp = self.vg.add_waypoint(self.position, 0)
wp.ETA_locked = True wp.ETA_locked = True
def add_unit(self, unit_type, name, pos_x, pos_y, heading) -> Ship: def add_unit(self, unit_type, name, pos_x, pos_y, heading) -> Ship:
unit = Ship(self.game.next_unit_id(), unit = Ship(self.game.next_unit_id(), f"{self.go.group_name}|{name}", unit_type)
f"{self.go.group_name}|{name}", unit_type)
unit.position.x = pos_x unit.position.x = pos_x
unit.position.y = pos_y unit.position.y = pos_y
unit.heading = heading unit.heading = heading

View File

@@ -19,10 +19,24 @@ class AvengerGenerator(AirDefenseGroupGenerator):
def generate(self): def generate(self):
num_launchers = random.randint(2, 3) num_launchers = random.randint(2, 3)
self.add_unit(Unarmed.Transport_M818, "TRUCK", self.position.x, self.position.y, self.heading) self.add_unit(
positions = self.get_circular_position(num_launchers, launcher_distance=110, coverage=180) Unarmed.Truck_M818_6x6,
"TRUCK",
self.position.x,
self.position.y,
self.heading,
)
positions = self.get_circular_position(
num_launchers, launcher_distance=110, coverage=180
)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.SAM_Avenger_M1097, "SPAA#" + str(i), position[0], position[1], position[2]) self.add_unit(
AirDefence.SAM_Avenger__Stinger,
"SPAA#" + str(i),
position[0],
position[1],
position[2],
)
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:

View File

@@ -19,10 +19,24 @@ class ChaparralGenerator(AirDefenseGroupGenerator):
def generate(self): def generate(self):
num_launchers = random.randint(2, 4) num_launchers = random.randint(2, 4)
self.add_unit(Unarmed.Transport_M818, "TRUCK", self.position.x, self.position.y, self.heading) self.add_unit(
positions = self.get_circular_position(num_launchers, launcher_distance=110, coverage=180) Unarmed.Truck_M818_6x6,
"TRUCK",
self.position.x,
self.position.y,
self.heading,
)
positions = self.get_circular_position(
num_launchers, launcher_distance=110, coverage=180
)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.SAM_Chaparral_M48, "SPAA#" + str(i), position[0], position[1], position[2]) self.add_unit(
AirDefence.SAM_Chaparral_M48,
"SPAA#" + str(i),
position[0],
position[1],
position[2],
)
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:

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