Compare commits

..

235 Commits
3.0.0 ... 4.0.0

Author SHA1 Message Date
Dan Albert
8e6e1469d7 Merge branch 'develop-4.x' into master. 2021-06-26 12:59:07 -07:00
Dan Albert
6cc967742a Add the most important feature to the changelog.
(cherry picked from commit aa86a6e53b)
2021-06-26 12:34:51 -07:00
Brock Greman
17f2bcc9c9 Clarify the impact of non-cold flight starts.
(cherry picked from commit 34470336e4)
2021-06-26 12:29:23 -07:00
Mustang-25
9d499a1430 Update Op Mole Cricket 2010 Campaign.
Moved SAM generator at Rosh Pina so it does not spawn units on the runway.

(cherry picked from commit 5a2a89f19e)
2021-06-26 12:17:33 -07:00
Dan Albert
3b55dfad40 Revert accidental change to default pilot limit.
(cherry picked from commit 7eb4df770e)
2021-06-26 12:06:26 -07:00
Simon Clark
9d3c7a86b6 Bump campaign versions. 2021-06-26 19:29:04 +01:00
Chris Seagraves
7f68846023 Include control point name in ground object info.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/498

(cherry picked from commit ffcae66f59)
2021-06-26 11:24:20 -07:00
Dan Albert
c1534cba9e Ack campaign version update.
No scenery targets in this campaign so no work needed.

https://github.com/dcs-liberation/dcs_liberation/issues/1359
(cherry picked from commit d2df795ba7)
2021-06-26 11:19:43 -07:00
Dan Albert
eea31168c1 Remove dead campaign.
https://github.com/dcs-liberation/dcs_liberation/issues/1359
(cherry picked from commit b930e13964)
2021-06-26 11:19:42 -07:00
Dan Albert
f2de1fdac6 Fix save path for new games.
(cherry picked from commit e6bf318cdf)
2021-06-26 11:01:22 -07:00
Dan Albert
8dd29d2319 Disband unfilled incompletable transfers.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1317

(cherry picked from commit 4cfed08247)
2021-06-26 10:55:19 -07:00
Khopa
b402dad801 Mod support : Updated frenchpach to version 4.6 (Added new units VBCI and AMX-13 support) + some frenchpack units yaml tweaks 2021-06-26 19:23:27 +02:00
Khopa
7199fead00 Fixed duplicates in france 2005 faction 2021-06-26 15:23:55 +02:00
Khopa
4ff0f29fe0 Fixed yaml issue causing an issue with Leclerc MBT 2021-06-26 15:18:47 +02:00
Khopa
1b8992eb04 Updated campaign : Operation Dynamo for The Channel map 2021-06-26 14:42:42 +02:00
Khopa
ccbcf4f69a Updated campaign : Russia Small, renamed it to "From Mozdok to Maykop" 2021-06-26 13:48:55 +02:00
Khopa
0747007f58 Updated campaign : Battle for Golan Heights 2021-06-26 13:20:13 +02:00
Dan Albert
723588666f Fix save path cleanup.
(cherry picked from commit 959a13a514)
2021-06-25 23:21:49 -07:00
Chris Seagraves
94861ca477 Use directory of current save for open/save-as.
(cherry picked from commit b601d713d2)
2021-06-25 23:02:09 -07:00
Dan Albert
e8992c5bed Add "Nevada Limited Air" campaign.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1358

(cherry picked from commit dc96d8699a)
2021-06-25 21:34:55 -07:00
Dan Albert
e841358f74 Add "Scenic Route" campaign.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1334

(cherry picked from commit f38cdd8432)
2021-06-25 21:28:59 -07:00
Dan Albert
3c135720a0 Fix lint.
(cherry picked from commit 91655a3d5a)
2021-06-25 19:34:10 -07:00
Dan Albert
d7db290892 Move the default save game directory.
The top level DCS directory gets messy fast if we fill it with save
games.

(cherry picked from commit 7774a9b2ab)
2021-06-25 17:48:45 -07:00
Dan Albert
b7626c10da Fix targeting of carrier groups with TGOs.
The assumption that the first group is the carrier group is wrong.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1346

(cherry picked from commit 80cf8f484d)
2021-06-25 16:47:39 -07:00
Dan Albert
d79e8f46f3 State carrier requirement for Blackball.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1355

(cherry picked from commit cb7c075a61)
2021-06-25 16:35:43 -07:00
Dan Albert
278b9730cd Fix crash when buying or selling TGO units.
Updating the game destroys this window so we cannot continue with the
calls. It worked in my initial testing, so presumably it's partly
dependent on when the finalizers run.

Since the windows will be destroyed there's nothing for us to actually
update, so just remove that signal and the explicit close calls.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1344

(cherry picked from commit 4d0fb67c53)
2021-06-25 16:30:51 -07:00
Dan Albert
d187c571ea Ack campaign version bump.
Campaigns don't use scenery targets.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1341

(cherry picked from commit 380d1d4f18)
2021-06-24 18:18:07 -07:00
Dan Albert
b3705531d4 Update pydcs to use latest master.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/993

(cherry picked from commit 71832859a5)
2021-06-24 18:14:27 -07:00
docofmur
666b389821 Fixes #1337 by making ground location search look in both directions (#1338)
(cherry picked from commit a31432ad9e)
2021-06-24 13:25:41 -04:00
bgreman
ddc076b141 Implements #1331 by changing the Pass Turn button text on Turn 0. (#1333)
(cherry picked from commit 26743154d8)
2021-06-24 11:00:05 -04:00
bgreman
eee1791a79 Adds a ruler to the map (#1332)
* Adds a ruler to the map

* Updating changelog

* Updating changelog

(cherry picked from commit a50a6fa917)
2021-06-24 02:59:16 -04:00
bgreman
fb5a6d3243 Fix #1329 player loses frontline progress when skipping turn 0 (#1330)
(cherry picked from commit b43e5bac0b)
2021-06-24 02:06:26 -04:00
Dan Albert
113c00ac05 Retry reading state.json on failure.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1313

(cherry picked from commit ddaef1fb64)
2021-06-23 20:18:36 -07:00
Dan Albert
85ca85ac6d Signal game update when buying/selling TGO units.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1312

(cherry picked from commit 6f264ff5de)
2021-06-23 20:08:31 -07:00
Dan Albert
da917a7dde Fix another unit type mismatch.
(cherry picked from commit a06fc6d80f)
2021-06-23 20:02:02 -07:00
Dan Albert
b03d1599e1 Add a feature flag for pilot limits.
This doesn't currently interact very well with the auto purchase since
the procurer might by aircraft that don't have pilots available. That
should be fixed, but for the short term we should just default to not
enabling this new feature.

(cherry picked from commit 3ddfc47d3a)
2021-06-23 18:47:47 -07:00
docofmur
2b3c56ad38 Campaign version update (#1326)
Caucasus Multi part campaign version update. No map strike objects so just the version change

(cherry picked from commit 905bd05ba8)
2021-06-23 20:01:18 -04:00
bgreman
8dc35bec5a Fix empty convoys (#1327)
* Hopefully getting rid of empty convoys for good

* changing Dict to dict for type checks

(cherry picked from commit 3274f3ec35)
2021-06-23 19:51:37 -04:00
bgreman
3f4f27612b Fixes #1310 (#1325)
* Fixes #1310 by only refunding GUs if no faction CP has an attached factory.  Previously it would refund all units at the CP, including aircraft.

Also changes the CP CAPTURE cheat to work at any CP regardless of adjacency to frontline or BLUEFOR/OPFOR state.

* Fixing typing issues, changint all Dict[] types to dict[]

* Updating changelog

(cherry picked from commit c3b8c48ca2)
2021-06-23 17:19:58 -04:00
Dan Albert
17f9487fe0 Update From Caen to Evreux.
Add support for inversion and ack the version change (Normandy is
unaffected by ID updates).

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1232

(cherry picked from commit d365094616)
2021-06-23 14:00:08 -07:00
Dan Albert
e15b10ae7e Ack version update for PG campaigns.
PG is unaffected by building ID changes.

(cherry picked from commit 7c76684076)
2021-06-23 13:50:01 -07:00
Dan Albert
17d56beeaa Update Vectron's Claw and Peace Spring.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1323

(cherry picked from commit 0ef27b038a)
2021-06-23 12:59:22 -07:00
Dan Albert
53c7912592 Copy initialization fix to AircraftType.
(cherry picked from commit 610a27c0e4)
2021-06-23 12:50:55 -07:00
RndName
1f318aff3c set window title empty on new game
also fixed small exception when aborting the open file dialog which lead to " as filename

fixes #1305

(cherry picked from commit 752c91a721)
2021-06-23 12:44:49 -07:00
Dan Albert
2bb1c0b3f2 Fixed missed initialization of unit data on load.
We'd only load unit data if a name lookup was done and missed it on a
type lookup. Ideally we wouldn't need to do a type lookup here until the
ground unit templates are reworked we still do.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1299

(cherry picked from commit d3d655da07)
2021-06-22 23:42:25 -07:00
Dan Albert
b057f027d5 Return pilots when canceling flight creation.
(cherry picked from commit db36cf248e)
2021-06-22 23:36:49 -07:00
Dan Albert
cc079ad44e Add the Around the Mountain campaign.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1280

(cherry picked from commit 153d8e106e)
2021-06-22 23:29:04 -07:00
Dan Albert
974c0069e6 Add Operation Blackball campaign.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1320

(cherry picked from commit df8829b477)
2021-06-22 23:23:04 -07:00
Dan Albert
9028109fe3 Update Syria Full campaign.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1319

(cherry picked from commit 569bc297a8)
2021-06-22 23:20:17 -07:00
Dan Albert
db27f3b0d9 Update Northern Russia campaign.
I bumped the submitted 6.1 to 7.0 (which didn't exist when the files
were uploaded) because this campaign uses no scenery targets.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1318

(cherry picked from commit 099cbbdb64)
2021-06-22 23:17:12 -07:00
Dan Albert
cb542b6af4 Update Allied Sword.
Only change from the uploaded files is that I increased the campaign
version to 7.0 since this doesn't use any scenery targets so has no work
to do for that.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1249

(cherry picked from commit ca7469b92e)
2021-06-22 23:14:12 -07:00
Dan Albert
fcea37c340 Correct mistakenly updated campaign.
(cherry picked from commit 6db4145927)
2021-06-22 23:08:30 -07:00
Dan Albert
cf3d13f9d3 Bump campaign version to account for DCS changes.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1308

(cherry picked from commit ca93f2baff)
2021-06-22 23:04:41 -07:00
Dan Albert
6789beb4b5 Fix unit type mismatch.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1314

(cherry picked from commit 84a0a3caeb)
2021-06-22 22:55:26 -07:00
Dan Albert
8f1ec4a519 Update Operation Peace Spring.
https://github.com/dcs-liberation/dcs_liberation/issues/1303
(cherry picked from commit 7b327693e2)
2021-06-22 15:18:12 -07:00
docofmur
b8bc9d87ec Faction Audit.
Transports and mod aircraft added where needed cleaned up various
duplicates in factions.

(cherry picked from commit dba70dc6d5)
2021-06-22 15:02:04 -07:00
Mike Jones
52aff8bc30 Use pydcs has_tacan attribute to check if tankers support TACAN.
(cherry picked from commit bd1618e41d)
2021-06-22 14:35:47 -07:00
Mike Jones
5c81ac06ac Add gunfighter flag to aircraft data files.
(cherry picked from commit 08b7aff0d8)
2021-06-22 14:35:46 -07:00
Mike Jones
8364148305 Add patrol configuration to unit data files.
This allows altitude/speed of AEW&C and tankers to be configured.

(cherry picked from commit a75688f89c)
2021-06-22 14:35:45 -07:00
Mike Jones
2bcff5a5c2 Fix unit type comparisons.
When comparing UnitType against a pydcs type, use .dcs_unit_type.

(cherry picked from commit 30763b5401)
2021-06-22 14:35:43 -07:00
Chris Seagraves
c227923bdf Fix bug with file name in title with invalid save games.
(cherry picked from commit 814519248c)
2021-06-22 14:20:16 -07:00
Simon Clark
4569b1b45a Campaign clarity. 2021-06-22 17:20:35 +01:00
Simon Clark
3a193d1dd4 Add clarity for mod selection page. 2021-06-21 20:04:34 +01:00
Simon Clark
9334cba564 Add Operation Atilla campaign.
It's a Cyprus invasion campaign - what's not to like!
2021-06-21 19:45:19 +01:00
Dan Albert
4dc1daa100 Fix command line campaign generator.
(cherry picked from commit 47e038c9fa)
2021-06-20 23:46:32 -07:00
Dan Albert
0d99fc3d36 Don't order transports for incapable factions.
If these orders can't be fulfilled for the faction it will prevent the
faction from ordering any non-reserve aircraft since transports are
given priority after reserve missions, and they'll never be fulfillable.
As such, no non-reserve aircraft will ever be purchased for factions
without transport aircraft.

Factions without transport aircraft are screwed in other ways, but this
will fix their air planning for campaigns that aren't dependent on
airlift.

(cherry picked from commit e96210f48c)
2021-06-20 23:44:16 -07:00
Simon Clark
eee78288c9 Updated factions to reflect mod select changes. 2021-06-21 01:33:13 +01:00
Simon Clark
c2f112e3a6 Refactor the mod select changes, re-add accidentally deleted factions. 2021-06-21 01:14:07 +01:00
Simon Clark
ef3f7125b3 Make mod selection nicer and deprecate MB-339.
Mod selection is now done via checkbox in the new game wizard.

The MB-339 is being turned into a paid module, and the free mod no longer works, so it's been removed.
2021-06-21 00:03:22 +01:00
Dan Albert
4558088412 Revert "Don't propose missions the air wing can't plan."
This is redundant because plan_mission already checks this.

This reverts commit 3338df9836.

(cherry picked from commit d074500109)
2021-06-20 15:59:09 -07:00
Dan Albert
d2cc3f673e Switch pydcs to a pip requirement. 2021-06-20 15:16:22 -07:00
Dan Albert
dc85644d71 Exclude weapon names and weights from comparisons.
Only the class ID matters, and the names sometimes change with new pydcs
updates.
2021-06-20 15:11:35 -07:00
SnappyComebacks
0b5bdf8151 Move max_group_size from S-3B to S-3B Tanker. 2021-06-20 14:13:23 -07:00
jsjlewis96
b27238a69a Moved Hind-F to separate faction, not operational at time, but only gunship pilotable 2021-06-20 14:13:03 -07:00
Mike
bb2bf78e8a Fix current_airlift_capacity always returning 0.
Squadron.aircraft is of type AircraftType, while TRANSPORT_CAPABLE is
a list of pydcs DcsUnitTypes. As a result, the intersection was always
empty causing the function to always return 0.
2021-06-20 13:56:00 -07:00
Chris Seagraves
7e17533cc6 Air/ground intel prettification. (#1285)
* Sort rows.
* Add totals to group headers.
* Indent content.
* Add space between sections.
2021-06-20 13:43:54 -07:00
Chris Seagraves
7808da118a Include the save name in the window title. 2021-06-20 13:33:27 -07:00
Mustang-25
4259cf8764 Add IAF Eagle Sqn 2021-06-20 13:33:01 -07:00
Mustang-25
994c55945e Add IAF Strike Eagle Sqn 2021-06-20 13:33:01 -07:00
Mustang-25
f20c145ece Add IAF Viper Sqns 2021-06-20 13:33:01 -07:00
SnappyComebacks
5b31026e1c Fix UI to obey max group sizes.
This also adds max group sizes for aircraft that need it but don't
according to DCS. Only the first tanker or AEW&C unit in a group can be
contacted by radio.
2021-06-20 13:32:00 -07:00
Simon Clark
39fe5951f7 Add factories to Russian Intervention Campaign. 2021-06-20 19:23:07 +01:00
Simon Clark
9d767c3dd8 Fix destination opacity bug. 2021-06-20 19:06:24 +01:00
Simon Clark
2a3f9bf81c More Russian Intervention campaign work. 2021-06-20 18:48:46 +01:00
Simon Clark
3fd4359cb1 Adds Russian Intervention 2015 campaign. 2021-06-20 17:08:44 +01:00
Khopa
ca1be580df Squadrons : Added french Sa-342 and Mirage 2000-5 squadrons 2021-06-20 17:43:35 +02:00
Khopa
28820f2e64 Squadrons : Allow unicode characters in squadrons names 2021-06-20 17:43:07 +02:00
Simon Clark
6c3987ec86 Updates the intel box text for turn 0.
It was a bit misleading beforehand, as there were no forces on either side.
2021-06-20 15:56:53 +01:00
Simon Clark
089eb9e86b Changelog. 2021-06-20 15:36:40 +01:00
Simon Clark
0793e9afc5 Addresses #1184.
CV and LHA icons are now the same colour as airfields.

Destination markers now have transparency.
2021-06-20 15:33:01 +01:00
Dan Albert
1e2522375b Increase squadron size and replenishment rate.
Given the current lack of control over the number of squadrons this
needs be be raised to make it have less of an impact.
2021-06-19 23:24:23 -07:00
Dan Albert
e09f53da8f Fix exceptions when no aircraft are selected.
This commonly happens during reset of the UI, but also happens when the
player is out of aircraft.
2021-06-19 20:26:33 -07:00
Dan Albert
29b4b62a44 Revert "Add Around the Mountain campaign."
Doesn't include basic requirements like factories.

This reverts commit 30cab8e3a7.
2021-06-19 15:11:41 -07:00
Dan Albert
b1a63db1fc Correct some ILS/VOR frequencies in Syria.
`MHz(110, 30)` is 110.03 MHz, not 110.30 MHz.
2021-06-19 15:07:45 -07:00
Dan Albert
9940dc8451 Fix type annotations for some UI code. 2021-06-19 15:03:39 -07:00
Dan Albert
703c68eb66 Add Akrotiri to Inherent Resolve. 2021-06-19 12:17:47 -07:00
Dan Albert
3338df9836 Don't propose missions the air wing can't plan.
We were doignt his for escorts, but now that we quit planning as soon as
we find an unplannable mission (to save money for higher priority
missions), if we hit an early unplannable mission like BARCAP no other
missions wil be planned.

Maybe fixes https://github.com/dcs-liberation/dcs_liberation/issues/1228
2021-06-19 11:47:43 -07:00
SnappyComebacks
dc4794b246 Add kneeboard data for new Syria Cyprus airfields. (#1277)
* Add kneeboard data for new Syria Cyprus airfields.
2021-06-19 12:29:21 -06:00
Dan Albert
b130c9882a Remove max distance for AEW&C auto planning.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1259
2021-06-19 11:27:52 -07:00
Dan Albert
5f8b838652 Add new campaign minor version for Cyprus. 2021-06-19 11:18:31 -07:00
Dan Albert
4efd1b5d3e Note EWR waypoint selector fix. 2021-06-19 11:16:20 -07:00
RndName
ad6ed21b6b Add EWR objects to predefined waypoints 2021-06-19 11:01:35 -07:00
Dan Albert
2ffaa71bb5 Note some new features in the changelog. 2021-06-19 10:47:59 -07:00
RndName
1763f59320 Allow deletion of multiple waypoints by selection
#1221
2021-06-19 10:40:40 -07:00
RndName
08d32ffc77 Allow shift/ctrl click to buy/sell multiple units.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1221
2021-06-19 10:36:33 -07:00
RndName
7e3cebb96d Fix purchase groups.
The new class PurchaseGroup coming in with commit 9bb986c was not
initiallized correctly.

This causes the bug that the update function is not working when you
for example open the AircraftRecruitmentMenu press "+" or "-", close
the dialog and then open ArmorRecruitmentMenu. If you then want to buy
or sell the update function will raise an error "Internal C++ Object
Already Deleted".
2021-06-19 10:33:35 -07:00
Jake Lewis
930fb404af Updated Hind-F price for rebalance 2021-06-19 03:43:55 -07:00
jsjlewis96
6cd711a1e2 Added option to disable AI pilot levelling 2021-06-19 03:03:50 -07:00
docofmur
1bcc332885 Syrian Terrain update 2021-06-19 02:28:11 -07:00
Dan Albert
9bb986cff9 Update *all* buy/sell buttons, not just the row.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1207
2021-06-18 20:14:44 -07:00
Dan Albert
1247942bf1 Note fix for convoy naming bug. 2021-06-18 19:41:03 -07:00
Dan Albert
95d3ff4cbe Don't show ground unit menu at carriers.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1220
2021-06-18 19:39:42 -07:00
Marcel
0a874a28ef Fix group name for EWRs.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1246
2021-06-18 19:26:42 -07:00
Dan Albert
2dee702060 Persist name generator state to the save game.
This is a bit of an ugly hack but it's effectively what we would need
anyway. We could clean up the global replacement by making the name
generator _only_ a property of Game and plumbing it through to a large
number of places. Could maybe also use `__getstate__` and `__setstate__`
tricks to save `naming.namegen` to the file even without making it truly
a part of Game.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1247
2021-06-18 19:24:23 -07:00
Dan Albert
4ea66477fe Add options for changing pilot limits and rates. 2021-06-18 18:33:15 -07:00
Dan Albert
d3be732566 Fix tooltips for scuds/silkworms.
Same problem as the ground object menu, same cleanup required at some
point.
2021-06-18 18:14:20 -07:00
Dan Albert
933517055e Fix ground object menus for scuds and silkworms.
These really need to be added to the unit data, but this will do as a
stop gap since the group generators need an overhaul anyway.
2021-06-18 18:06:05 -07:00
Dan Albert
040a3d9b36 Add coastal defenses and missiles to Abu Dhabi. 2021-06-18 18:02:33 -07:00
Florian
2c859bf280 Rebalance aircraft prices.
Main goal here is to make sure that warbirds don't cost more than early
jets, but this includes rebalancing of all aircraft.
2021-06-18 17:53:19 -07:00
jsjlewis96
fe227e02b8 Shows total at top for economic intel 2021-06-18 17:35:18 -07:00
Dan Albert
c68e583c20 Note the Skynet EWR fix. 2021-06-18 17:02:22 -07:00
Khopa
6620d56859 Factions : Added hind-P to Russia 75. 2021-06-19 01:58:11 +02:00
Dan Albert
1ec72d3e94 Fix mission generation when infantry is disabled.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1256
2021-06-18 16:52:03 -07:00
Dan Albert
5c3bb75786 Fix EPLRS for aircraft.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1237
2021-06-18 16:48:59 -07:00
Dan Albert
a90cb0dad9 Fix carrier names in factions.
The rest of the ships were covered but it looks like carriers were
missed.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1258
2021-06-18 16:42:34 -07:00
Khopa
8854a491ab Custom banner for Mi-24P (different from Mi-24V) 2021-06-19 01:32:43 +02:00
UKayeF
74e8073328 add Mi-24V Hind icons as placeholders for Mi-24P icons yet to come 2021-06-19 01:32:43 +02:00
Dan Albert
a6d62a7596 Add some missing changes to the changelog. 2021-06-18 16:23:44 -07:00
Dan Albert
980a224d02 Move fixes to the correct changelog section.
What was to be 3.1.0 is now 4.0.0 to accomodate that DCS update.
2021-06-18 16:11:02 -07:00
Khopa
0c6b83fc35 Added default Mi-24P payloads 2021-06-19 01:05:14 +02:00
UKayeF
6d2310f59d add Mi-24P as CAS capable aircraft - unsure which other tasks it could be useful for? 2021-06-19 00:54:20 +02:00
jsjlewis96
05107fab1c Added Mi-24P to factions post '80 that use Hind-E 2021-06-19 00:51:49 +02:00
jsjlewis96
285bed65c6 Added Mi-24P to Russia 1990 & 2010 factions 2021-06-19 00:51:47 +02:00
Khopa
b523c23e7a Added french Mirage-2000C squadrons 2021-06-18 18:38:05 +02:00
Khopa
4c9a028a4e Fixed pydcs error in test_factions.py 2021-06-18 18:19:48 +02:00
C. Perreau
cea970f065 Merge pull request #1248 from jsjlewis96/dot-neutral
#987 Neutral dot labels in options
2021-06-18 18:13:09 +02:00
Khopa
d8511fab1d Fixed pydcs error with missiles units 2021-06-18 18:08:03 +02:00
Khopa
b2d10e92e9 Fixed requirements.txt changes not required 2021-06-18 18:01:44 +02:00
Khopa
0582d5e2b6 Merge branch 'develop' of https://github.com/khopa/dcs_liberation into develop 2021-06-18 18:00:58 +02:00
C. Perreau
da2b56b5b1 Merge pull request #1252 from docofmur/terrain_update_cyprus
Syria Terrian map update
2021-06-18 18:00:09 +02:00
docofmur
46c15f37c5 exclusion zone update 1 test change on caucases, massive update on syria 2021-06-18 17:58:08 +02:00
Dan Albert
4ddc02d7fe Update pydcs. 2021-06-17 23:01:20 -07:00
Dan Albert
4c3ac0af91 Adapt to DCS update. 2021-06-17 22:58:46 -07:00
Dan Albert
edd0b90576 Update pydcs. 2021-06-17 22:22:47 -07:00
Dan Albert
11dca41945 Remove unused file. 2021-06-17 22:11:00 -07:00
Dan Albert
75c4724200 Delete the obsolete converter. 2021-06-17 22:10:15 -07:00
Dan Albert
09704b6f37 Add a wrapper type for ground unit info. 2021-06-17 22:09:17 -07:00
Dan Albert
8a0824880e Add unit classes and weights to infantry. 2021-06-17 22:09:16 -07:00
Dan Albert
d84abf021e Fix some bad unit data. 2021-06-17 22:09:16 -07:00
Dan Albert
e7223da19f Convert remaining unit data. 2021-06-17 22:09:16 -07:00
Dan Albert
499d143199 Add a converter for groun unit info. 2021-06-17 22:09:06 -07:00
Dan Albert
fefeb3c006 Fix broken factions. 2021-06-17 18:49:55 -07:00
Dan Albert
6aeee933d2 Fix broken ground unit data. 2021-06-17 18:39:45 -07:00
Dan Albert
34c0698c48 Remove unused aircraft from the old unit data. 2021-06-17 18:39:45 -07:00
jsjlewis96
1cc1a00820 Updated changelog 2021-06-17 23:12:57 +01:00
jsjlewis96
62f6b57948 Added neutral dot option 2021-06-17 23:12:30 +01:00
docofmur
5387acf533 exclusion zone update 1 test change on caucases, massive update on syria 2021-06-17 14:51:48 -07:00
C. Perreau
077b3ef04d Merge pull request #1243 from DanAlbert/f-16-strike-gbu-31
Update F-16 strike loadout to use GBU-31.
2021-06-17 22:39:58 +02:00
Dan Albert
9c654254d3 Update F-16 strike loadout to use GBU-31. 2021-06-16 21:43:09 -07:00
Mustang-25
4bb8bbbad8 Add remaining HAF Viper squadrons. 2021-06-16 21:05:50 -07:00
Dan Albert
39adafb1be Fix Greek F-16 livery ID. 2021-06-16 20:52:15 -07:00
Dan Albert
e19bfcdd04 Add a Greek F-16 squadron. 2021-06-16 20:37:07 -07:00
Dan Albert
6fde92f5ac Add the 191. Filo Turkish F-16 squadron. 2021-06-16 20:13:07 -07:00
Dan Albert
7170a7b302 Fix spawning unused aircraft.
These are assigned a squadron even though they're unused as a hack. We
need to tolerate these aircraft having no pilot assigned since that's
the desired case for unused aircraft (though only happens when the
squadron runs out of pilots, which should be fixed).
2021-06-16 20:11:36 -07:00
Dan Albert
24884e4a77 Import latest beacon data from DCS. 2021-06-16 19:46:19 -07:00
Dan Albert
384be8ceae Update Turkish faction.
Add a bunch of missing helicopters, some ground units, and remove the
KC-130 which they don't seem to use.
2021-06-16 19:10:23 -07:00
Dan Albert
ee9a5e8482 Update the Greek faction.
Greece has C-130s and Patriots. They don't have the E-3 but they do have
AEW&C via the ER-99, which isn't in DCS so just use an E-3 to pretend.
Also remove the Hawk radar as an EWR since we have the P-19 which is
better.
2021-06-16 18:54:12 -07:00
Dan Albert
34453fa3be Fix incorrect conditional. 2021-06-16 17:23:57 -07:00
Dan Albert
f727712bfa Make non-interactive map elements unobstructive.
This makes most of the lines and polygons on the map non-interactive so
they don't capture mouse events, and also makes the culling exclusion
zones unfilled so they don't obscure real map objects in dense areas.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1217
2021-06-16 17:22:07 -07:00
Dan Albert
3bb974b9e0 Note procurement fix in the changelog. 2021-06-16 17:10:21 -07:00
Dan Albert
021445216e Escape the JTAC zone name in the plugin data.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1218
2021-06-16 17:06:45 -07:00
Schneefl0cke
c13bf3ccd1 Fix procurement for factions that lack some units.
Fixes procurement for factions with no aircraft, no ground units, or no
tanks.
2021-06-16 09:34:58 -07:00
Mustang-25
8d53f42421 Update KC135MPRS.yaml
Fixed minor spelling error
2021-06-13 22:56:35 -07:00
Dan Albert
ace42019fb Cap squadron size, limit replenishment rate.
This caps squadrons to 12 pilots and limits their replenishment rate to
1 pilot per turn. Should probably make those values configurable, but
they aren't currently.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1136
2021-06-13 14:40:15 -07:00
Dan Albert
54aa161da0 Fix new game wizard faction template.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1227
2021-06-12 21:48:22 -07:00
Dan Albert
25c289deaa Make squadron nicknames optional. 2021-06-12 21:43:26 -07:00
Dan Albert
3c802e7d55 Fix unit info window. 2021-06-12 21:35:01 -07:00
Dan Albert
ba3cf4d2bd Put back all the radio data. 2021-06-12 20:56:24 -07:00
Dan Albert
4aa905716b Remove old file.
This was from testing and shouldn't have been kept.
2021-06-12 20:28:14 -07:00
Dan Albert
0fc1e8ec10 Remove unused file, begin fixing radios.
My radio data converter broke at some point while testing, so adding
this all back manually.
2021-06-12 20:27:18 -07:00
Dan Albert
0875d35129 Fix some squadrons.
I accidentally reverted my changes here while testing something earlier.
2021-06-12 20:24:04 -07:00
Dan Albert
8c62a081fe Remove unused code from AircraftType conversion. 2021-06-12 20:13:45 -07:00
Dan Albert
f811ae6c61 Convert factions and unit data. 2021-06-12 20:13:45 -07:00
Dan Albert
4a3ef42e67 Wrap the pydcs FlyingType in our own AircraftType.
This is an attempt to remove a lot of our supposedly unnecessary error
handling. Every aircraft should have a price, a description, a name,
etc; and none of those should require carrying around the faction's
country as context.

This moves all the data for aircraft into yaml files (only one converted
here as an example). Most of the "extended unit info" isn't actually
being read yet.

To replace the renaming of units based on the county, we instead
generate multiple types of each unit when necessary. The CF-18 is just
as much a first-class type as the F/A-18 is.

This doesn't work in its current state because it does break all the
existing names for aircraft that are used in the faction and squadron
files, and we no longer let those errors go as a warning. It will be an
annoying one time switch, but it allows us to define the names that get
used in these files instead of being sensitive to changes as they happen
in pydcs, and allows faction designers to specifically choose, for
example, the Su-22 instead of the Su-17.

One thing not handled by this is aircraft task capability. This is
because the lists in ai_flight_planner_db.py are a priority list, and to
move it out to a yaml file we'd need to assign a weight to it that would
be used to stack rank each aircraft. That's doable, but it makes it much
more difficult to see the ordering of aircraft at a glance, and much
more annoying to move aircraft around in the priority list. I don't
think this is worth doing, and the priority lists will remain in their
own separate lists.

This includes the converted I used to convert all the old unit info and
factions to the new format. This doesn't need to live long, but we may
want to reuse it in the future so we want it in the version history.
2021-06-12 20:13:45 -07:00
Dan Albert
88abaef7f9 Fix inconsistencies in prices and unit data. 2021-06-12 20:13:45 -07:00
Schneefl0cke
21fe746f2f Use ID for unit info lookups instead of name. 2021-06-12 02:24:21 -07:00
Dan Albert
c3c6915fa0 Include the micro version in the version string. 2021-06-11 16:52:23 -07:00
Dan Albert
b2705c1a13 Update Northern Russia campaign.
https://github.com/dcs-liberation/dcs_liberation/issues/1206
2021-06-11 16:42:12 -07:00
Mustang-25
75e3b4cc84 Add the First Lebanon War Historical Campaign. 2021-06-10 17:38:40 -07:00
Khopa
78f5235eca Removed helipad from golan heights campaign to avoid capture trigger error 2021-06-10 23:27:45 +02:00
Florian
78cd17e279 added missing units 2021-06-10 11:18:52 -07:00
Dan Albert
c51c8aae5c Clarify the docs/name of the task type list. 2021-06-09 21:53:45 -07:00
Dan Albert
40aa7734e1 Fix CAS commit range display.
CAS commits around the target, not its flight plan.
2021-06-09 21:51:26 -07:00
Dan Albert
0594e1148e Update Operation Mole Cricket.
https://github.com/dcs-liberation/dcs_liberation/issues/1203
2021-06-09 21:26:47 -07:00
Dan Albert
9eacd1563f Add Northern Russia campaign.
https://github.com/dcs-liberation/dcs_liberation/issues/1202
2021-06-09 21:25:59 -07:00
SnappyComebacks
a53a648a63 Add plannable tankers.
This Pull Request lets users plan Tanker flights.

Features:

- Introduction of `Refueling` flight type.
- Tankers can be purchased at airbases and carriers.
- Tankers get planned by AI.
- Tankers are planned from airbases and at aircraft carriers.
- Tankers aim to be at high, fast, and 70 miles from the nearest threat.
  (A10s won't be able to tank)
- Tankers racetrack orbit for one hour.
- Optional Tickbox to enable legacy tankers.
- S-3B Tanker added to factions.
- KC-130 MPRS added to factions.
- Kneeboard shows planned tankers, their tacans, and radios.

Limitations:

- AI doesn't know whether to plan probe and drogue or boom refueling
  tankers.
- User can't choose tanker speed.  Heavily loaded aircraft may have
  trouble.
- User can't choose tanker altitude.  A-10s will not make it to high
  altitude.

Problems:

- Tanker callsigns do not increment, see attached image.  (Investigated:
  Need to use `FlyingType.callsign_dict`, instead of just
  `FlyingType.callsign`.  This seems like it might be significant work
  to do.).
- Having a flight of two or more tankers only spawns one tanker.
- Let me know if you have a solution, or feel free to commit one.

https://user-images.githubusercontent.com/74509817/120909602-d7bc3680-c633-11eb-80d7-eccd4e095770.png
2021-06-09 21:14:10 -07:00
Dan Albert
a9dacf4a29 Fix engagement distance display. 2021-06-09 21:01:14 -07:00
Dan Albert
66f82b6ff9 Update mission start guidance. 2021-06-09 19:20:56 -07:00
Dan Albert
0e68884493 Remove incompatible campaigns.
We have quite a few campaigns now, so removing the broken ones.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1178
2021-06-09 19:08:36 -07:00
Dan Albert
f8d885fc9a Fix broken import. 2021-06-09 19:06:02 -07:00
Florian
366190ee99 added missing units to price table 2021-06-09 19:03:25 -07:00
Dan Albert
42d56a324f Make campaign names consistent. 2021-06-09 18:58:35 -07:00
docofmur
7d1f1ea2f7 Campaigns for 3.0
4 campaigns updated for 3.0 1 small PG 3 for Caucasus 1 full and 2 parts based on the full
2021-06-09 18:55:22 -07:00
Dan Albert
30cab8e3a7 Add Around the Mountain campaign.
https://github.com/dcs-liberation/dcs_liberation/issues/1199
2021-06-09 18:51:34 -07:00
Dan Albert
e0e2162c6d Add Operation Allied Sword campaign and factions.
https://github.com/dcs-liberation/dcs_liberation/issues/1196
2021-06-09 18:44:09 -07:00
Dan Albert
f1582fcc10 Add Humble Helper campaign and factions.
https://github.com/dcs-liberation/dcs_liberation/issues/1197
2021-06-09 18:39:26 -07:00
Florian
eb6206ea57 added texts for all units 2021-06-09 12:42:56 -07:00
Brock Greman
3ad51cafa8 Fixing display of "sunny" during clear conditions at night. 2021-06-09 02:16:05 -07:00
Dan Albert
b8c14d69c3 Make enable_and_reset not half lie.
https://github.com/dcs-liberation/dcs_liberation/issues/1185
2021-06-08 21:19:48 -07:00
Dan Albert
725b5083c7 Fix typo in Incirlik runway data.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1188
2021-06-08 21:14:36 -07:00
Dan Albert
87dd6b19bf Fix repeated JTACs after multiple generations.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1191
2021-06-08 20:56:00 -07:00
bgreman
3188994261 Gripen mod support.
(cherry picked from commit 0220fa4ff6)
2021-06-08 20:49:24 -07:00
Schneefl0cke
e4c9d8799e Add Recon combat role. 2021-06-08 19:48:56 -07:00
dependabot[bot]
bc938db7f9 Bump pillow from 8.1.1 to 8.2.0
Bumps [pillow](https://github.com/python-pillow/Pillow) from 8.1.1 to 8.2.0.
- [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/8.1.1...8.2.0)

---
updated-dependencies:
- dependency-name: pillow
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-08 16:15:25 -07:00
Dan Albert
0a9dc49e7f Remove UNIT_BY_TASK. 2021-06-07 19:13:49 -07:00
Dan Albert
07cdfc16d0 Move support tanker off find_unittype. 2021-06-07 19:12:30 -07:00
Dan Albert
622a171ac4 Move armor TGO purchase off find_unittype. 2021-06-07 19:01:16 -07:00
Dan Albert
fd85efbf55 Remove more dead code in game.db. 2021-06-07 18:59:02 -07:00
Dan Albert
ae2a818d8c Move the base intel menu off find_unittype. 2021-06-07 18:54:51 -07:00
Dan Albert
6966c16dd2 Remove dead code. 2021-06-07 18:54:39 -07:00
Dan Albert
27b5f24a0f Move unit purchase off find_unittype. 2021-06-07 18:54:21 -07:00
Dan Albert
ea15421308 Migrate support AEW&C away from find_unittype. 2021-06-07 18:52:54 -07:00
Dan Albert
ef35ad90b8 Remove one user of UNIT_BY_TASK. 2021-06-07 18:01:31 -07:00
Dan Albert
914691eaa7 Remove more unused code from Base. 2021-06-07 17:51:25 -07:00
Dan Albert
37bb83dfa6 Delete a bunch of unused code from Base. 2021-06-07 17:48:03 -07:00
Dan Albert
d8881e2734 Fix hangar status display. 2021-06-06 17:12:44 -07:00
Dan Albert
45869c428e Label the player checkbox in the roster editor. 2021-06-06 13:43:34 -07:00
Dan Albert
40832bd3a1 Update screenshot on the front page. 2021-06-06 13:37:01 -07:00
Khopa
126a8e8efb Added a small WW2 campaign on Normandy map (Replacing the former Normandy Small campaign). 2021-06-06 18:50:16 +02:00
Dan Albert
1796c21f48 Update the Syria full campaign.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1182
2021-06-05 21:39:45 -07:00
Dan Albert
363d4af639 Add Jordan 2010 faction. 2021-06-05 19:08:52 -07:00
Dan Albert
f1c881378c Add/updates campaigns from Starfire.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1181
2021-06-05 19:07:13 -07:00
Dan Albert
d316e13fa6 Suppress events fired while rebuilding model. 2021-06-05 15:21:23 -07:00
Dan Albert
1ea98a6ed1 Hide incompatible campaigns by default.
https://github.com/dcs-liberation/dcs_liberation/issues/1178
2021-06-05 15:15:30 -07:00
Dan Albert
3d4415d5d2 Move develop to 4.0. 2021-06-05 14:35:07 -07:00
713 changed files with 11161 additions and 10338 deletions

1
.gitignore vendored
View File

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

4
.gitmodules vendored
View File

@@ -1,4 +0,0 @@
[submodule "pydcs"]
path = pydcs
url = https://github.com/pydcs/dcs
branch = master

View File

@@ -14,7 +14,7 @@
DCS Liberation is a [DCS World](https://www.digitalcombatsimulator.com/en/products/world/) turn based single-player or co-op dynamic campaign. DCS Liberation is a [DCS World](https://www.digitalcombatsimulator.com/en/products/world/) turn based single-player or co-op dynamic campaign.
It is an external program that generates full and complex DCS missions and manage a persistent combat environment. It is an external program that generates full and complex DCS missions and manage a persistent combat environment.
![Logo](https://i.imgur.com/4hq0rLq.png) ![Screenshot](https://user-images.githubusercontent.com/315852/120939254-0b4a9f80-c6cc-11eb-82f5-ce3f8d714bfe.png)
## Downloads ## Downloads

View File

@@ -1,3 +1,51 @@
# 4.0.0
Saves from 3.x are not compatible with 4.0.
## Features/Improvements
* **[Engine]** Support for DCS 2.7.2.7910.1 and newer, including Cyprus, F-16 JDAMs, and the Hind.
* **[Campaign]** Squadrons now (optionally, off by default) have a maximum size and killed pilots replenish at a limited rate.
* **[Campaign]** Added an option to disable levelling up of AI pilots.
* **[Campaign]** Added Russian Intervention 2015 campaign on Syria, for a small and somewhat realistic Russian COIN scenario.
* **[Campaign]** Added Operation Atilla campaign on Syria, for a reasonably large invasion of Cyprus scenario.
* **[Campaign AI]** AI will plan Tanker flights.
* **[Campaign AI]** Removed max distance for AEW&C auto planning.
* **[Economy]** Adjusted prices for aircraft to balance out some price inconsistencies.
* **[Factions]** Added more tankers to factions.
* **[Flight Planner]** Added ability to plan Tankers.
* **[Modding]** Campaign format version is now 7.0 to account for DCS map changes that made scenery strike targets incompatible with existing campaigns.
* **[Mods]** Added support for the Gripen mod.
* **[Mods]** Removes MB-339PAN support, as the mod is now deprecated and no longer works with DCS 2.7+.
* **[Mission Generation]** Added support for "Neutral Dot" label options.
* **[New Game Wizard]** Mods are now selected via checkboxes in the new game wizard, not as separate factions.
* **[UI]** Ctrl click and shift click now buy or sell 5 or 10 units respectively.
* **[UI]** Multiple waypoints can now be deleted simultaneously if multiple waypoints are selected.
* **[UI]** Carriers and LHAs now match the colour of airfields, and their destination icons are translucent.
* **[UI]** Updated intel box text for first turn.
* **[UI]** Base Capture Cheat is now usable at all bases and can also be used to transfer player-owned bases to OPFOR.
* **[UI]** Pass Turn button is relabled as "Begin Campaign" on Turn 0.
* **[UI]** Added a ruler to the map.
* **[UI]** Liberation now saves games to `<DCS user directory>/Liberation/Saves` by default to declutter the main directory.
## Fixes
* **[Campaign AI]** Fix procurement for factions that lack some unit types.
* **[Campaign AI]** Fix auto purchase of aircraft for factions that have no transport aircraft.
* **[Campaign AI]** Fix refunding of pending aircraft purchases when a side has no factory available.
* **[Mission Generation]** Fixed problem with mission load when control point name contained an apostrophe.
* **[Mission Generation]** Fixed EWR group names so they contribute to Skynet again.
* **[Mission Generation]** Fixed duplicate name error when generating convoys and cargo ships when creating manual transfers after loading a game.
* **[Mission Generation]** Fixed empty convoys not being disbanded when all units are killed/removed.
* **[Mission Generation]** Fixed player losing frontline progress when skipping from turn 0 to turn 1.
* **[Mission Generation]** Fixed issue where frontline would only search to the right for valid locations.
* **[UI]** Made non-interactive map elements less obstructive.
* **[UI]** Added support for Neutral Dot difficulty label
* **[UI]** Clear skies at night no longer described as "Sunny" by the weather widget.
* **[UI]** Removed ability to buy (useless) ground units at carriers and LHAs.
* **[UI]** Fixed enable/disable of buy/sell buttons.
* **[UI]** EWRs now appear in the custom waypoint list.
# 3.0.0 # 3.0.0
Saves from 2.5 are not compatible with 3.0. Saves from 2.5 are not compatible with 3.0.

View File

@@ -1,22 +0,0 @@
from dcs.vehicles import AirDefence
AAA_UNITS = [
AirDefence.SPAAA_Gepard,
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
AirDefence.SPAAA_Vulcan_M163,
AirDefence.AAA_ZU_23_Closed_Emplacement,
AirDefence.AAA_ZU_23_Emplacement,
AirDefence.SPAAA_ZU_23_2_Mounted_Ural_375,
AirDefence.AAA_ZU_23_Insurgent_Closed_Emplacement,
AirDefence.SPAAA_ZU_23_2_Insurgent_Mounted_Ural_375,
AirDefence.AAA_ZU_23_Insurgent_Emplacement,
AirDefence.AAA_8_8cm_Flak_18,
AirDefence.AAA_Flak_38_20mm,
AirDefence.AAA_8_8cm_Flak_36,
AirDefence.AAA_8_8cm_Flak_37,
AirDefence.AAA_Flak_Vierling_38_Quad_20mm,
AirDefence.AAA_SP_Kdo_G_40,
AirDefence.AAA_8_8cm_Flak_41,
AirDefence.AAA_Bofors_40mm,
AirDefence.AAA_S_60_57mm,
]

View File

@@ -4,35 +4,35 @@ from dcs.vehicles import AirDefence
class AlicCodes: class AlicCodes:
CODES = { CODES = {
AirDefence.EWR_1L13.id: 101, AirDefence._1L13_EWR.id: 101,
AirDefence.EWR_55G6.id: 102, AirDefence._55G6_EWR.id: 102,
AirDefence.SAM_SA_10_S_300_Grumble_Clam_Shell_SR.id: 103, AirDefence.S_300PS_40B6MD_sr.id: 103,
AirDefence.SAM_SA_10_S_300_Grumble_Big_Bird_SR.id: 104, AirDefence.S_300PS_64H6E_sr.id: 104,
AirDefence.SAM_SA_11_Buk_Gadfly_Snow_Drift_SR.id: 107, AirDefence.SA_11_Buk_SR_9S18M1.id: 107,
AirDefence.SAM_SA_6_Kub_Straight_Flush_STR.id: 108, AirDefence.Kub_1S91_str.id: 108,
AirDefence.MCC_SR_Sborka_Dog_Ear_SR.id: 109, AirDefence.Dog_Ear_radar.id: 109,
AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR.id: 110, AirDefence.S_300PS_40B6M_tr.id: 110,
AirDefence.SAM_SA_11_Buk_Gadfly_Fire_Dome_TEL.id: 115, AirDefence.SA_11_Buk_LN_9A310M1.id: 115,
AirDefence.SAM_SA_8_Osa_Gecko_TEL.id: 117, AirDefence.Osa_9A33_ln.id: 117,
AirDefence.SAM_SA_13_Strela_10M3_Gopher_TEL.id: 118, AirDefence.Strela_10M3.id: 118,
AirDefence.SAM_SA_15_Tor_Gauntlet.id: 119, AirDefence.Tor_9A331.id: 119,
AirDefence.SAM_SA_19_Tunguska_Grison.id: 120, AirDefence._2S6_Tunguska.id: 120,
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish.id: 121, AirDefence.ZSU_23_4_Shilka.id: 121,
AirDefence.SAM_P19_Flat_Face_SR__SA_2_3.id: 122, AirDefence.P_19_s_125_sr.id: 122,
AirDefence.SAM_SA_3_S_125_Low_Blow_TR.id: 123, AirDefence.Snr_s_125_tr.id: 123,
AirDefence.SAM_Rapier_Blindfire_TR.id: 124, AirDefence.Rapier_fsa_blindfire_radar.id: 124,
AirDefence.SAM_Rapier_LN.id: 125, AirDefence.Rapier_fsa_launcher.id: 125,
AirDefence.SAM_SA_2_S_75_Fan_Song_TR.id: 126, AirDefence.SNR_75V.id: 126,
AirDefence.HQ_7_Self_Propelled_LN.id: 127, AirDefence.HQ_7_LN_SP.id: 127,
AirDefence.HQ_7_Self_Propelled_STR.id: 128, AirDefence.HQ_7_STR_SP.id: 128,
AirDefence.SAM_Roland_ADS.id: 201, AirDefence.Roland_ADS.id: 201,
AirDefence.SAM_Patriot_STR.id: 202, AirDefence.Patriot_str.id: 202,
AirDefence.SAM_Hawk_SR__AN_MPQ_50.id: 203, AirDefence.Hawk_sr.id: 203,
AirDefence.SAM_Hawk_TR__AN_MPQ_46.id: 204, AirDefence.Hawk_tr.id: 204,
AirDefence.SAM_Roland_EWR.id: 205, AirDefence.Roland_Radar.id: 205,
AirDefence.SAM_Hawk_CWAR_AN_MPQ_55.id: 206, AirDefence.Hawk_cwar.id: 206,
AirDefence.SPAAA_Gepard.id: 207, AirDefence.Gepard.id: 207,
AirDefence.SPAAA_Vulcan_M163.id: 208, AirDefence.Vulcan.id: 208,
} }
@classmethod @classmethod

View File

@@ -1,51 +0,0 @@
from dcs.planes import (
Bf_109K_4,
C_101CC,
FW_190A8,
FW_190D9,
F_5E_3,
F_86F_Sabre,
I_16,
L_39ZA,
MiG_15bis,
MiG_19P,
MiG_21Bis,
P_47D_30,
P_47D_30bl1,
P_47D_40,
P_51D,
P_51D_30_NA,
SpitfireLFMkIX,
SpitfireLFMkIXCW,
)
from pydcs_extensions.a4ec.a4ec import A_4E_C
"""
This list contains the aircraft that do not use the guns as the last resort weapons, but as a main weapon
They'll RTB when they don't have gun ammo left
"""
GUNFIGHTERS = [
# Cold War
MiG_15bis,
MiG_19P,
MiG_21Bis,
F_86F_Sabre,
A_4E_C,
F_5E_3,
# Trainers
C_101CC,
L_39ZA,
# WW2
P_51D_30_NA,
P_51D,
P_47D_30,
P_47D_30bl1,
P_47D_40,
SpitfireLFMkIXCW,
SpitfireLFMkIX,
Bf_109K_4,
FW_190D9,
FW_190A8,
I_16,
]

View File

@@ -1,239 +1,17 @@
from __future__ import annotations
from enum import unique, Enum from enum import unique, Enum
from typing import Type
from dcs.vehicles import AirDefence, Infantry, Unarmed, Artillery, Armor
from dcs.unittype import VehicleType
from pydcs_extensions.frenchpack import frenchpack
@unique @unique
class GroundUnitClass(Enum): class GroundUnitClass(Enum):
Tank = ( Tank = "Tank"
"Tank", Atgm = "ATGM"
( Ifv = "IFV"
Armor.MBT_T_55, Apc = "APC"
Armor.MBT_T_72B, Artillery = "Artillery"
Armor.MBT_T_72B3, Logistics = "Logistics"
Armor.MBT_T_80U, Recon = "Recon"
Armor.MBT_T_90, Infantry = "Infantry"
Armor.MBT_Leopard_2A4, Shorads = "SHORADS"
Armor.MBT_Leopard_2A4_Trs, Manpads = "MANPADS"
Armor.MBT_Leopard_2A5,
Armor.MBT_Leopard_2A6M,
Armor.MBT_Leopard_1A3,
Armor.MBT_Leclerc,
Armor.MBT_Challenger_II,
Armor.MBT_Chieftain_Mk_3,
Armor.MBT_M1A2_Abrams,
Armor.MBT_M60A3_Patton,
Armor.MBT_Merkava_IV,
Armor.ZTZ_96B,
# WW2
# Axis
Armor.Tk_PzIV_H,
Armor.SPG_Sturmpanzer_IV_Brummbar,
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
Armor.HT_Pz_Kpfw_VI_Tiger_I,
Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II,
# Allies
Armor.Tk_M4_Sherman,
Armor.CT_Centaur_IV,
Armor.CT_Cromwell_IV,
Armor.HIT_Churchill_VII,
# Mods
frenchpack.DIM__TOYOTA_BLUE,
frenchpack.DIM__TOYOTA_GREEN,
frenchpack.DIM__TOYOTA_DESERT,
frenchpack.DIM__KAMIKAZE,
frenchpack.AMX_30B2,
frenchpack.Leclerc_Serie_XXI,
),
)
Atgm = (
"ATGM",
(
Armor.ATGM_HMMWV,
Armor.ATGM_VAB_Mephisto,
Armor.ATGM_Stryker,
Armor.IFV_BMP_2,
# WW2 (Tank Destroyers)
# Axxis
Armor.SPG_StuG_III_Ausf__G,
Armor.SPG_StuG_IV,
Armor.SPG_Jagdpanzer_IV,
Armor.SPG_Jagdpanther_G1,
Armor.SPG_Sd_Kfz_184_Elefant,
# Allies
Armor.SPG_M10_GMC,
Armor.MT_M4A4_Sherman_Firefly,
# Mods
frenchpack.VBAE_CRAB_MMP,
frenchpack.VAB_MEPHISTO,
frenchpack.TRM_2000_PAMELA,
),
)
Ifv = (
"IFV",
(
Armor.IFV_BMP_3,
Armor.IFV_BMP_2,
Armor.IFV_BMP_1,
Armor.IFV_Marder,
Armor.IFV_Warrior,
Armor.SPG_Stryker_MGS,
Armor.IFV_M2A2_Bradley,
Armor.IFV_BMD_1,
Armor.ZBD_04A,
# Mods
frenchpack.VBAE_CRAB,
frenchpack.VAB_T20_13,
),
)
Apc = (
"APC",
(
Armor.IFV_M1126_Stryker_ICV,
Armor.APC_M113,
Armor.APC_BTR_80,
Armor.IFV_BTR_82A,
Armor.APC_MTLB,
Armor.APC_AAV_7_Amphibious,
Armor.APC_TPz_Fuchs,
Armor.APC_BTR_RD,
# WW2
Armor.APC_M2A1_Halftrack,
Armor.APC_Sd_Kfz_251_Halftrack,
# Mods
frenchpack.VAB__50,
frenchpack.VBL__50,
frenchpack.VBL_AANF1,
),
)
Artillery = (
"Artillery",
(
Artillery.Grad_MRL_FDDM__FC,
Artillery.MLRS_9A52_Smerch_HE_300mm,
Artillery.SPH_2S1_Gvozdika_122mm,
Artillery.SPH_2S3_Akatsia_152mm,
Artillery.MLRS_BM_21_Grad_122mm,
Artillery.MLRS_9K57_Uragan_BM_27_220mm,
Artillery.SPH_M109_Paladin_155mm,
Artillery.MLRS_M270_227mm,
Artillery.SPM_2S9_Nona_120mm_M,
Artillery.SPH_Dana_vz77_152mm,
Artillery.SPH_T155_Firtina_155mm,
Artillery.PLZ_05,
Artillery.SPH_2S19_Msta_152mm,
Artillery.MLRS_9A52_Smerch_CM_300mm,
# WW2
Artillery.SPG_M12_GMC_155mm,
),
)
Logistics = (
"Logistics",
(
Unarmed.Carrier_M30_Cargo,
Unarmed.Truck_M818_6x6,
Unarmed.Truck_KAMAZ_43101,
Unarmed.Truck_Ural_375,
Unarmed.Truck_GAZ_66,
Unarmed.Truck_GAZ_3307,
Unarmed.Truck_GAZ_3308,
Unarmed.Truck_Ural_4320_31_Arm_d,
Unarmed.Truck_Ural_4320T,
Unarmed.Truck_Opel_Blitz,
Unarmed.LUV_Kubelwagen_82,
Unarmed.Carrier_Sd_Kfz_7_Tractor,
Unarmed.LUV_Kettenrad,
Unarmed.Car_Willys_Jeep,
Unarmed.LUV_Land_Rover_109,
Unarmed.Truck_Land_Rover_101_FC,
# Mods
frenchpack.VBL,
frenchpack.VAB,
),
)
Recon = (
"Recon",
(
Armor.Scout_HMMWV,
Armor.Scout_Cobra,
Armor.LT_PT_76,
Armor.IFV_LAV_25,
Armor.Scout_BRDM_2,
# WW2
Armor.LT_Mk_VII_Tetrarch,
Armor.IFV_Sd_Kfz_234_2_Puma,
Armor.Car_M8_Greyhound_Armored,
Armor.Car_Daimler_Armored,
# Mods
frenchpack.ERC_90,
frenchpack.AMX_10RCR,
frenchpack.AMX_10RCR_SEPAR,
),
)
Infantry = (
"Infantry",
(
Infantry.Insurgent_AK_74,
Infantry.Infantry_AK_74,
Infantry.Infantry_M1_Garand,
Infantry.Infantry_Mauser_98,
Infantry.Infantry_SMLE_No_4_Mk_1,
Infantry.Infantry_M4_Georgia,
Infantry.Infantry_AK_74_Rus,
Infantry.Paratrooper_AKS,
Infantry.Paratrooper_RPG_16,
Infantry.Infantry_M249,
Infantry.Infantry_M4,
Infantry.Infantry_RPG,
),
)
Shorads = (
"SHORADS",
(
AirDefence.SPAAA_ZU_23_2_Mounted_Ural_375,
AirDefence.SPAAA_ZU_23_2_Insurgent_Mounted_Ural_375,
AirDefence.SPAAA_ZSU_57_2,
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
AirDefence.SAM_SA_8_Osa_Gecko_TEL,
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL,
AirDefence.SAM_SA_13_Strela_10M3_Gopher_TEL,
AirDefence.SAM_SA_15_Tor_Gauntlet,
AirDefence.SAM_SA_19_Tunguska_Grison,
AirDefence.SPAAA_Gepard,
AirDefence.SPAAA_Vulcan_M163,
AirDefence.SAM_Linebacker___Bradley_M6,
AirDefence.SAM_Chaparral_M48,
AirDefence.SAM_Avenger__Stinger,
AirDefence.SAM_Roland_ADS,
AirDefence.HQ_7_Self_Propelled_LN,
AirDefence.AAA_8_8cm_Flak_18,
AirDefence.AAA_8_8cm_Flak_36,
AirDefence.AAA_8_8cm_Flak_37,
AirDefence.AAA_8_8cm_Flak_41,
AirDefence.AAA_Bofors_40mm,
AirDefence.AAA_S_60_57mm,
AirDefence.AAA_M1_37mm,
AirDefence.AAA_QF_3_7,
),
)
def __init__(
self, class_name: str, unit_list: tuple[Type[VehicleType], ...]
) -> None:
self.class_name = class_name
self.unit_list = unit_list
def __contains__(self, unit_type: Type[VehicleType]) -> bool:
return unit_type in self.unit_list

View File

@@ -1,108 +1,108 @@
from dcs.ships import ( from dcs.ships import (
Battlecruiser_1144_2_Pyotr_Velikiy, PIOTR,
Cruiser_1164_Moskva, MOSCOW,
CVN_70_Carl_Vinson, VINSON,
CVN_71_Theodore_Roosevelt, CVN_71,
CVN_72_Abraham_Lincoln, CVN_72,
CVN_73_George_Washington, CVN_73,
CVN_74_John_C__Stennis, Stennis,
CV_1143_5_Admiral_Kuznetsov, KUZNECOW,
CV_1143_5_Admiral_Kuznetsov_2017, CV_1143_5,
Frigate_11540_Neustrashimy, NEUSTRASH,
Corvette_1124_4_Grisha, ALBATROS,
Frigate_1135M_Rezky, REZKY,
Corvette_1241_1_Molniya, MOLNIYA,
LHA_1_Tarawa, LHA_Tarawa,
FFG_Oliver_Hazzard_Perry, PERRY,
CG_Ticonderoga, TICONDEROG,
Type_052B_Destroyer, Type_052B,
Type_052C_Destroyer, Type_052C,
Type_054A_Frigate, Type_054A,
DDG_Arleigh_Burke_IIa, USS_Arleigh_Burke_IIa,
) )
from dcs.vehicles import AirDefence from dcs.vehicles import AirDefence
TELARS = { TELARS = {
AirDefence.SAM_SA_19_Tunguska_Grison, AirDefence._2S6_Tunguska,
AirDefence.SAM_SA_11_Buk_Gadfly_Fire_Dome_TEL, AirDefence.SA_11_Buk_SR_9S18M1,
AirDefence.SAM_SA_8_Osa_Gecko_TEL, AirDefence.Osa_9A33_ln,
AirDefence.SAM_SA_15_Tor_Gauntlet, AirDefence.Tor_9A331,
AirDefence.SAM_Roland_ADS, AirDefence.Roland_ADS,
} }
TRACK_RADARS = { TRACK_RADARS = {
AirDefence.SAM_SA_6_Kub_Straight_Flush_STR, AirDefence.Kub_1S91_str,
AirDefence.SAM_SA_3_S_125_Low_Blow_TR, AirDefence.Snr_s_125_tr,
AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR, AirDefence.S_300PS_40B6M_tr,
AirDefence.SAM_Hawk_TR__AN_MPQ_46, AirDefence.Hawk_tr,
AirDefence.SAM_Patriot_STR, AirDefence.Patriot_str,
AirDefence.SAM_SA_2_S_75_Fan_Song_TR, AirDefence.SNR_75V,
AirDefence.SAM_Rapier_Blindfire_TR, AirDefence.Rapier_fsa_blindfire_radar,
AirDefence.HQ_7_Self_Propelled_STR, AirDefence.HQ_7_STR_SP,
} }
LAUNCHER_TRACKER_PAIRS = { LAUNCHER_TRACKER_PAIRS = {
AirDefence.SAM_SA_6_Kub_Gainful_TEL: AirDefence.SAM_SA_6_Kub_Straight_Flush_STR, AirDefence.Kub_2P25_ln: AirDefence.Kub_1S91_str,
AirDefence.SAM_SA_3_S_125_Goa_LN: AirDefence.SAM_SA_3_S_125_Low_Blow_TR, AirDefence._5p73_s_125_ln: AirDefence.Snr_s_125_tr,
AirDefence.SAM_SA_10_S_300_Grumble_TEL_D: AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR, AirDefence.S_300PS_5P85C_ln: AirDefence.S_300PS_40B6M_tr,
AirDefence.SAM_SA_10_S_300_Grumble_TEL_C: AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR, AirDefence.S_300PS_5P85D_ln: AirDefence.S_300PS_40B6M_tr,
AirDefence.SAM_Hawk_LN_M192: AirDefence.SAM_Hawk_TR__AN_MPQ_46, AirDefence.Hawk_ln: AirDefence.Hawk_tr,
AirDefence.SAM_Patriot_LN: AirDefence.SAM_Patriot_STR, AirDefence.Patriot_ln: AirDefence.Patriot_str,
AirDefence.SAM_SA_2_S_75_Guideline_LN: AirDefence.SAM_SA_2_S_75_Fan_Song_TR, AirDefence.S_75M_Volhov: AirDefence.SNR_75V,
AirDefence.SAM_Rapier_LN: AirDefence.SAM_Rapier_Blindfire_TR, AirDefence.Rapier_fsa_launcher: AirDefence.Rapier_fsa_blindfire_radar,
AirDefence.HQ_7_Self_Propelled_LN: AirDefence.HQ_7_Self_Propelled_STR, AirDefence.HQ_7_LN_SP: AirDefence.HQ_7_STR_SP,
} }
UNITS_WITH_RADAR = { UNITS_WITH_RADAR = {
# Radars # Radars
AirDefence.SAM_SA_19_Tunguska_Grison, AirDefence._2S6_Tunguska,
AirDefence.SAM_SA_11_Buk_Gadfly_Fire_Dome_TEL, AirDefence.SA_11_Buk_LN_9A310M1,
AirDefence.SAM_SA_8_Osa_Gecko_TEL, AirDefence.Osa_9A33_ln,
AirDefence.SAM_SA_15_Tor_Gauntlet, AirDefence.Tor_9A331,
AirDefence.SPAAA_Gepard, AirDefence.Gepard,
AirDefence.SPAAA_Vulcan_M163, AirDefence.Vulcan,
AirDefence.SAM_Roland_ADS, AirDefence.Roland_ADS,
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish, AirDefence.ZSU_23_4_Shilka,
AirDefence.EWR_1L13, AirDefence._1L13_EWR,
AirDefence.SAM_SA_6_Kub_Straight_Flush_STR, AirDefence.Kub_1S91_str,
AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR, AirDefence.S_300PS_40B6M_tr,
AirDefence.SAM_SA_10_S_300_Grumble_Clam_Shell_SR, AirDefence.S_300PS_40B6MD_sr,
AirDefence.EWR_55G6, AirDefence._55G6_EWR,
AirDefence.SAM_SA_10_S_300_Grumble_Big_Bird_SR, AirDefence.S_300PS_64H6E_sr,
AirDefence.SAM_SA_11_Buk_Gadfly_Snow_Drift_SR, AirDefence.SA_11_Buk_SR_9S18M1,
AirDefence.MCC_SR_Sborka_Dog_Ear_SR, AirDefence.Dog_Ear_radar,
AirDefence.SAM_Hawk_TR__AN_MPQ_46, AirDefence.Hawk_tr,
AirDefence.SAM_Hawk_SR__AN_MPQ_50, AirDefence.Hawk_sr,
AirDefence.SAM_Patriot_STR, AirDefence.Patriot_str,
AirDefence.SAM_Hawk_CWAR_AN_MPQ_55, AirDefence.Hawk_cwar,
AirDefence.SAM_P19_Flat_Face_SR__SA_2_3, AirDefence.P_19_s_125_sr,
AirDefence.SAM_Roland_EWR, AirDefence.Roland_Radar,
AirDefence.SAM_SA_3_S_125_Low_Blow_TR, AirDefence.Snr_s_125_tr,
AirDefence.SAM_SA_2_S_75_Fan_Song_TR, AirDefence.SNR_75V,
AirDefence.SAM_Rapier_Blindfire_TR, AirDefence.Rapier_fsa_blindfire_radar,
AirDefence.HQ_7_Self_Propelled_LN, AirDefence.HQ_7_LN_SP,
AirDefence.HQ_7_Self_Propelled_STR, AirDefence.HQ_7_STR_SP,
AirDefence.EWR_FuMG_401_Freya_LZ, AirDefence.FuMG_401,
AirDefence.EWR_FuSe_65_Würzburg_Riese, AirDefence.FuSe_65,
# Ships # Ships
CVN_70_Carl_Vinson, VINSON,
FFG_Oliver_Hazzard_Perry, PERRY,
CG_Ticonderoga, TICONDEROG,
Corvette_1124_4_Grisha, ALBATROS,
CV_1143_5_Admiral_Kuznetsov, KUZNECOW,
Corvette_1241_1_Molniya, MOLNIYA,
Cruiser_1164_Moskva, MOSCOW,
Frigate_11540_Neustrashimy, NEUSTRASH,
Battlecruiser_1144_2_Pyotr_Velikiy, PIOTR,
Frigate_1135M_Rezky, REZKY,
CV_1143_5_Admiral_Kuznetsov_2017, CV_1143_5,
CVN_74_John_C__Stennis, Stennis,
CVN_71_Theodore_Roosevelt, CVN_71,
CVN_72_Abraham_Lincoln, CVN_72,
CVN_73_George_Washington, CVN_73,
DDG_Arleigh_Burke_IIa, USS_Arleigh_Burke_IIa,
LHA_1_Tarawa, LHA_Tarawa,
Type_052B_Destroyer, Type_052B,
Type_054A_Frigate, Type_054A,
Type_052C_Destroyer, Type_052C,
} }

View File

@@ -4,13 +4,13 @@ import datetime
import inspect import inspect
import logging import logging
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass, field
from typing import Dict, Iterator, Optional, Set, Tuple, Type, Union, cast from typing import Dict, Iterator, Optional, Set, Tuple, Union, cast
from dcs.unitgroup import FlyingGroup from dcs.unitgroup import FlyingGroup
from dcs.unittype import FlyingType
from dcs.weapons_data import Weapons, weapon_ids from dcs.weapons_data import Weapons, weapon_ids
from game.dcs.aircrafttype import AircraftType
PydcsWeapon = Dict[str, Union[int, str]] PydcsWeapon = Dict[str, Union[int, str]]
PydcsWeaponAssignment = Tuple[int, PydcsWeapon] PydcsWeaponAssignment = Tuple[int, PydcsWeapon]
@@ -21,8 +21,8 @@ class Weapon:
"""Wraps a pydcs weapon dict in a hashable type.""" """Wraps a pydcs weapon dict in a hashable type."""
cls_id: str cls_id: str
name: str name: str = field(compare=False)
weight: int weight: int = field(compare=False)
def available_on(self, date: datetime.date) -> bool: def available_on(self, date: datetime.date) -> bool:
introduction_year = WEAPON_INTRODUCTION_YEARS.get(self) introduction_year = WEAPON_INTRODUCTION_YEARS.get(self)
@@ -97,12 +97,12 @@ class Pylon:
yield weapon yield weapon
@classmethod @classmethod
def for_aircraft(cls, aircraft: Type[FlyingType], number: int) -> Pylon: def for_aircraft(cls, aircraft: AircraftType, number: int) -> Pylon:
# In pydcs these are all arbitrary inner classes of the aircraft type. # In pydcs these are all arbitrary inner classes of the aircraft type.
# The only way to identify them is by their name. # The only way to identify them is by their name.
pylons = [ pylons = [
v v
for v in aircraft.__dict__.values() for v in aircraft.dcs_unit_type.__dict__.values()
if inspect.isclass(v) and v.__name__.startswith("Pylon") if inspect.isclass(v) and v.__name__.startswith("Pylon")
] ]
@@ -121,8 +121,8 @@ class Pylon:
return cls(number, allowed) return cls(number, allowed)
@classmethod @classmethod
def iter_pylons(cls, aircraft: Type[FlyingType]) -> Iterator[Pylon]: def iter_pylons(cls, aircraft: AircraftType) -> Iterator[Pylon]:
for pylon in sorted(list(aircraft.pylons)): for pylon in sorted(list(aircraft.dcs_unit_type.pylons)):
yield cls.for_aircraft(aircraft, pylon) yield cls.for_aircraft(aircraft, pylon)

1347
game/db.py

File diff suppressed because it is too large Load Diff

248
game/dcs/aircrafttype.py Normal file
View File

@@ -0,0 +1,248 @@
from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass
from functools import cached_property
from pathlib import Path
from typing import ClassVar, Type, Iterator, TYPE_CHECKING, Optional, Any
import yaml
from dcs.helicopters import helicopter_map
from dcs.planes import plane_map
from dcs.unittype import FlyingType
from game.dcs.unittype import UnitType
from game.radio.channels import (
ChannelNamer,
RadioChannelAllocator,
CommonRadioChannelAllocator,
HueyChannelNamer,
SCR522ChannelNamer,
ViggenChannelNamer,
ViperChannelNamer,
TomcatChannelNamer,
MirageChannelNamer,
SingleRadioChannelNamer,
FarmerRadioChannelAllocator,
SCR522RadioChannelAllocator,
ViggenRadioChannelAllocator,
NoOpChannelAllocator,
)
from game.utils import Distance, Speed, feet, kph, knots
if TYPE_CHECKING:
from gen.aircraft import FlightData
from gen import AirSupport, RadioFrequency, RadioRegistry
from gen.radios import Radio
@dataclass(frozen=True)
class RadioConfig:
inter_flight: Optional[Radio]
intra_flight: Optional[Radio]
channel_allocator: Optional[RadioChannelAllocator]
channel_namer: Type[ChannelNamer]
@classmethod
def from_data(cls, data: dict[str, Any]) -> RadioConfig:
return RadioConfig(
cls.make_radio(data.get("inter_flight", None)),
cls.make_radio(data.get("intra_flight", None)),
cls.make_allocator(data.get("channels", {})),
cls.make_namer(data.get("channels", {})),
)
@classmethod
def make_radio(cls, name: Optional[str]) -> Optional[Radio]:
from gen.radios import get_radio
if name is None:
return None
return get_radio(name)
@classmethod
def make_allocator(cls, data: dict[str, Any]) -> Optional[RadioChannelAllocator]:
try:
alloc_type = data["type"]
except KeyError:
return None
allocator_type: Type[RadioChannelAllocator] = {
"SCR-522": SCR522RadioChannelAllocator,
"common": CommonRadioChannelAllocator,
"farmer": FarmerRadioChannelAllocator,
"noop": NoOpChannelAllocator,
"viggen": ViggenRadioChannelAllocator,
}[alloc_type]
return allocator_type.from_cfg(data)
@classmethod
def make_namer(cls, config: dict[str, Any]) -> Type[ChannelNamer]:
return {
"SCR-522": SCR522ChannelNamer,
"default": ChannelNamer,
"huey": HueyChannelNamer,
"mirage": MirageChannelNamer,
"single": SingleRadioChannelNamer,
"tomcat": TomcatChannelNamer,
"viggen": ViggenChannelNamer,
"viper": ViperChannelNamer,
}[config.get("namer", "default")]
@dataclass(frozen=True)
class PatrolConfig:
altitude: Optional[Distance]
speed: Optional[Speed]
@classmethod
def from_data(cls, data: dict[str, Any]) -> PatrolConfig:
altitude = data.get("altitude", None)
speed = data.get("altitude", None)
return PatrolConfig(
feet(altitude) if altitude is not None else None,
knots(speed) if speed is not None else None,
)
@dataclass(frozen=True)
class AircraftType(UnitType[FlyingType]):
carrier_capable: bool
lha_capable: bool
always_keeps_gun: bool
# If true, the aircraft does not use the guns as the last resort weapons, but as a main weapon.
# It'll RTB when it doesn't have gun ammo left.
gunfighter: bool
max_group_size: int
patrol_altitude: Optional[Distance]
patrol_speed: Optional[Speed]
intra_flight_radio: Optional[Radio]
channel_allocator: Optional[RadioChannelAllocator]
channel_namer: Type[ChannelNamer]
_by_name: ClassVar[dict[str, AircraftType]] = {}
_by_unit_type: ClassVar[dict[Type[FlyingType], list[AircraftType]]] = defaultdict(
list
)
_loaded: ClassVar[bool] = False
def __str__(self) -> str:
return self.name
@property
def dcs_id(self) -> str:
return self.dcs_unit_type.id
@property
def flyable(self) -> bool:
return self.dcs_unit_type.flyable
@cached_property
def max_speed(self) -> Speed:
return kph(self.dcs_unit_type.max_speed)
def alloc_flight_radio(self, radio_registry: RadioRegistry) -> RadioFrequency:
from gen.radios import ChannelInUseError, MHz
if self.intra_flight_radio is not None:
return radio_registry.alloc_for_radio(self.intra_flight_radio)
freq = MHz(self.dcs_unit_type.radio_frequency)
try:
radio_registry.reserve(freq)
except ChannelInUseError:
pass
return freq
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
) -> None:
if self.channel_allocator is not None:
self.channel_allocator.assign_channels_for_flight(flight, air_support)
def channel_name(self, radio_id: int, channel_id: int) -> str:
return self.channel_namer.channel_name(radio_id, channel_id)
def __setstate__(self, state: dict[str, Any]) -> None:
# Update any existing models with new data on load.
updated = AircraftType.named(state["name"])
state.update(updated.__dict__)
self.__dict__.update(state)
@classmethod
def register(cls, aircraft_type: AircraftType) -> None:
cls._by_name[aircraft_type.name] = aircraft_type
cls._by_unit_type[aircraft_type.dcs_unit_type].append(aircraft_type)
@classmethod
def named(cls, name: str) -> AircraftType:
if not cls._loaded:
cls._load_all()
return cls._by_name[name]
@classmethod
def for_dcs_type(cls, dcs_unit_type: Type[FlyingType]) -> Iterator[AircraftType]:
if not cls._loaded:
cls._load_all()
yield from cls._by_unit_type[dcs_unit_type]
@staticmethod
def _each_unit_type() -> Iterator[Type[FlyingType]]:
yield from helicopter_map.values()
yield from plane_map.values()
@classmethod
def _load_all(cls) -> None:
for unit_type in cls._each_unit_type():
for data in cls._each_variant_of(unit_type):
cls.register(data)
cls._loaded = True
@classmethod
def _each_variant_of(cls, aircraft: Type[FlyingType]) -> Iterator[AircraftType]:
data_path = Path("resources/units/aircraft") / f"{aircraft.id}.yaml"
if not data_path.exists():
logging.warning(f"No data for {aircraft.id}; it will not be available")
return
with data_path.open() as data_file:
data = yaml.safe_load(data_file)
try:
price = data["price"]
except KeyError as ex:
raise KeyError(f"Missing required price field: {data_path}") from ex
radio_config = RadioConfig.from_data(data.get("radios", {}))
patrol_config = PatrolConfig.from_data(data.get("patrol", {}))
try:
introduction = data["introduced"]
if introduction is None:
introduction = "N/A"
except KeyError:
introduction = "No data."
for variant in data.get("variants", [aircraft.id]):
yield AircraftType(
dcs_unit_type=aircraft,
name=variant,
description=data.get("description", "No data."),
year_introduced=introduction,
country_of_origin=data.get("origin", "No data."),
manufacturer=data.get("manufacturer", "No data."),
role=data.get("role", "No data."),
price=price,
carrier_capable=data.get("carrier_capable", False),
lha_capable=data.get("lha_capable", False),
always_keeps_gun=data.get("always_keeps_gun", False),
gunfighter=data.get("gunfighter", False),
max_group_size=data.get("max_group_size", aircraft.group_size_max),
patrol_altitude=patrol_config.altitude,
patrol_speed=patrol_config.speed,
intra_flight_radio=radio_config.intra_flight,
channel_allocator=radio_config.channel_allocator,
channel_namer=radio_config.channel_namer,
)

View File

@@ -0,0 +1,97 @@
from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass
from pathlib import Path
from typing import Type, Optional, ClassVar, Iterator
import yaml
from dcs.unittype import VehicleType
from dcs.vehicles import vehicle_map
from game.data.groundunitclass import GroundUnitClass
from game.dcs.unittype import UnitType
@dataclass(frozen=True)
class GroundUnitType(UnitType[VehicleType]):
unit_class: Optional[GroundUnitClass]
spawn_weight: int
_by_name: ClassVar[dict[str, GroundUnitType]] = {}
_by_unit_type: ClassVar[
dict[Type[VehicleType], list[GroundUnitType]]
] = defaultdict(list)
_loaded: ClassVar[bool] = False
def __str__(self) -> str:
return self.name
@property
def dcs_id(self) -> str:
return self.dcs_unit_type.id
@classmethod
def register(cls, aircraft_type: GroundUnitType) -> None:
cls._by_name[aircraft_type.name] = aircraft_type
cls._by_unit_type[aircraft_type.dcs_unit_type].append(aircraft_type)
@classmethod
def named(cls, name: str) -> GroundUnitType:
if not cls._loaded:
cls._load_all()
return cls._by_name[name]
@classmethod
def for_dcs_type(cls, dcs_unit_type: Type[VehicleType]) -> Iterator[GroundUnitType]:
if not cls._loaded:
cls._load_all()
yield from cls._by_unit_type[dcs_unit_type]
@staticmethod
def _each_unit_type() -> Iterator[Type[VehicleType]]:
yield from vehicle_map.values()
@classmethod
def _load_all(cls) -> None:
for unit_type in cls._each_unit_type():
for data in cls._each_variant_of(unit_type):
cls.register(data)
cls._loaded = True
@classmethod
def _each_variant_of(cls, vehicle: Type[VehicleType]) -> Iterator[GroundUnitType]:
data_path = Path("resources/units/ground_units") / f"{vehicle.id}.yaml"
if not data_path.exists():
logging.warning(f"No data for {vehicle.id}; it will not be available")
return
with data_path.open() as data_file:
data = yaml.safe_load(data_file)
try:
introduction = data["introduced"]
if introduction is None:
introduction = "N/A"
except KeyError:
introduction = "No data."
class_name = data.get("class")
unit_class: Optional[GroundUnitClass] = None
if class_name is not None:
unit_class = GroundUnitClass(class_name)
for variant in data.get("variants", [vehicle.id]):
yield GroundUnitType(
dcs_unit_type=vehicle,
unit_class=unit_class,
spawn_weight=data.get("spawn_weight", 0),
name=variant,
description=data.get("description", "No data."),
year_introduced=introduction,
country_of_origin=data.get("origin", "No data."),
manufacturer=data.get("manufacturer", "No data."),
role=data.get("role", "No data."),
price=data.get("price", 1),
)

26
game/dcs/unittype.py Normal file
View File

@@ -0,0 +1,26 @@
from dataclasses import dataclass
from functools import cached_property
from typing import TypeVar, Generic, Type
from dcs.unittype import UnitType as DcsUnitType
DcsUnitTypeT = TypeVar("DcsUnitTypeT", bound=DcsUnitType)
@dataclass(frozen=True)
class UnitType(Generic[DcsUnitTypeT]):
dcs_unit_type: Type[DcsUnitTypeT]
name: str
description: str
year_introduced: str
country_of_origin: str
manufacturer: str
role: str
price: int
def __str__(self) -> str:
return self.name
@cached_property
def eplrs_capable(self) -> bool:
return getattr(self.dcs_unit_type, "eplrs", False)

View File

@@ -14,13 +14,12 @@ from typing import (
Dict, Dict,
Iterator, Iterator,
List, List,
Type,
TYPE_CHECKING, TYPE_CHECKING,
) )
from dcs.unittype import FlyingType, UnitType
from game import db from game import db
from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
from game.theater import Airfield, ControlPoint from game.theater import Airfield, ControlPoint
from game.transfers import CargoShip from game.transfers import CargoShip
from game.unitmap import ( from game.unitmap import (
@@ -49,8 +48,8 @@ class AirLosses:
def losses(self) -> Iterator[FlyingUnit]: def losses(self) -> Iterator[FlyingUnit]:
return itertools.chain(self.player, self.enemy) return itertools.chain(self.player, self.enemy)
def by_type(self, player: bool) -> Dict[Type[FlyingType], int]: def by_type(self, player: bool) -> Dict[AircraftType, int]:
losses_by_type: Dict[Type[FlyingType], int] = defaultdict(int) losses_by_type: Dict[AircraftType, int] = defaultdict(int)
losses = self.player if player else self.enemy losses = self.player if player else self.enemy
for loss in losses: for loss in losses:
losses_by_type[loss.flight.unit_type] += 1 losses_by_type[loss.flight.unit_type] += 1
@@ -182,8 +181,8 @@ class Debriefing:
def casualty_count(self, control_point: ControlPoint) -> int: def casualty_count(self, control_point: ControlPoint) -> int:
return len([x for x in self.front_line_losses if x.origin == control_point]) return len([x for x in self.front_line_losses if x.origin == control_point])
def front_line_losses_by_type(self, player: bool) -> Dict[Type[UnitType], int]: def front_line_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]:
losses_by_type: Dict[Type[UnitType], int] = defaultdict(int) losses_by_type: dict[GroundUnitType, int] = defaultdict(int)
if player: if player:
losses = self.ground_losses.player_front_line losses = self.ground_losses.player_front_line
else: else:
@@ -192,8 +191,8 @@ class Debriefing:
losses_by_type[loss.unit_type] += 1 losses_by_type[loss.unit_type] += 1
return losses_by_type return losses_by_type
def convoy_losses_by_type(self, player: bool) -> Dict[Type[UnitType], int]: def convoy_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]:
losses_by_type: Dict[Type[UnitType], int] = defaultdict(int) losses_by_type: dict[GroundUnitType, int] = defaultdict(int)
if player: if player:
losses = self.ground_losses.player_convoy losses = self.ground_losses.player_convoy
else: else:
@@ -202,8 +201,8 @@ class Debriefing:
losses_by_type[loss.unit_type] += 1 losses_by_type[loss.unit_type] += 1
return losses_by_type return losses_by_type
def cargo_ship_losses_by_type(self, player: bool) -> Dict[Type[UnitType], int]: def cargo_ship_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]:
losses_by_type: Dict[Type[UnitType], int] = defaultdict(int) losses_by_type: dict[GroundUnitType, int] = defaultdict(int)
if player: if player:
ships = self.ground_losses.player_cargo_ships ships = self.ground_losses.player_cargo_ships
else: else:
@@ -213,8 +212,8 @@ class Debriefing:
losses_by_type[unit_type] += count losses_by_type[unit_type] += count
return losses_by_type return losses_by_type
def airlift_losses_by_type(self, player: bool) -> Dict[Type[UnitType], int]: def airlift_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]:
losses_by_type: Dict[Type[UnitType], int] = defaultdict(int) losses_by_type: dict[GroundUnitType, int] = defaultdict(int)
if player: if player:
losses = self.ground_losses.player_airlifts losses = self.ground_losses.player_airlifts
else: else:
@@ -383,15 +382,21 @@ class PollDebriefingFileThread(threading.Thread):
else: else:
last_modified = 0 last_modified = 0
while not self.stopped(): while not self.stopped():
if ( try:
os.path.isfile("state.json") if (
and os.path.getmtime("state.json") > last_modified os.path.isfile("state.json")
): and os.path.getmtime("state.json") > last_modified
with open("state.json", "r") as json_file: ):
json_data = json.load(json_file) with open("state.json", "r") as json_file:
debriefing = Debriefing(json_data, self.game, self.unit_map) json_data = json.load(json_file)
self.callback(debriefing) debriefing = Debriefing(json_data, self.game, self.unit_map)
break self.callback(debriefing)
break
except json.JSONDecodeError:
logging.exception(
"Failed to decode state.json. Probably attempted read while DCS "
"was still writing the file. Will retry in 5 seconds."
)
time.sleep(5) time.sleep(5)

View File

@@ -14,6 +14,7 @@ from game.operation.operation import Operation
from game.theater import ControlPoint from game.theater import ControlPoint
from gen import AirTaskingOrder from gen import AirTaskingOrder
from gen.ground_forces.combat_stance import CombatStance from gen.ground_forces.combat_stance import CombatStance
from ..dcs.groundunittype import GroundUnitType
from ..unitmap import UnitMap from ..unitmap import UnitMap
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -53,7 +54,7 @@ class Event:
@property @property
def is_player_attacking(self) -> bool: def is_player_attacking(self) -> bool:
return self.attacker_name == self.game.player_name return self.attacker_name == self.game.player_faction.name
@property @property
def tasks(self) -> List[Type[Task]]: def tasks(self) -> List[Type[Task]]:
@@ -122,7 +123,7 @@ class Event:
def commit_air_losses(self, debriefing: Debriefing) -> None: def commit_air_losses(self, debriefing: Debriefing) -> None:
for loss in debriefing.air_losses.losses: for loss in debriefing.air_losses.losses:
if ( if loss.pilot is not None and (
not loss.pilot.player not loss.pilot.player
or not self.game.settings.invulnerable_player_pilots or not self.game.settings.invulnerable_player_pilots
): ):
@@ -434,12 +435,12 @@ class Event:
moved_units[frontline_unit] = int(count * move_factor) moved_units[frontline_unit] = int(count * move_factor)
total_units_redeployed = total_units_redeployed + int(count * move_factor) total_units_redeployed = total_units_redeployed + int(count * move_factor)
destination.base.commision_units(moved_units) destination.base.commission_units(moved_units)
source.base.commit_losses(moved_units) source.base.commit_losses(moved_units)
# Also transfer pending deliveries. # Also transfer pending deliveries.
for unit_type, count in source.pending_unit_deliveries.units.items(): for unit_type, count in source.pending_unit_deliveries.units.items():
if not issubclass(unit_type, VehicleType): if not isinstance(unit_type, GroundUnitType):
continue continue
if count <= 0: if count <= 0:
# Don't transfer *sales*... # Don't transfer *sales*...

View File

@@ -1,15 +1,13 @@
from __future__ import annotations from __future__ import annotations
from game.data.groundunitclass import GroundUnitClass
import itertools
import logging import logging
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Optional, Dict, Type, List, Any, cast from typing import Optional, Dict, Type, List, Any, Iterator
import dcs import dcs
from dcs.countries import country_dict from dcs.countries import country_dict
from dcs.planes import plane_map from dcs.unittype import ShipType, UnitType
from dcs.unittype import FlyingType, ShipType, VehicleType, UnitType
from dcs.vehicles import Armor, Unarmed, Infantry, Artillery, AirDefence
from game.data.building_data import ( from game.data.building_data import (
WW2_ALLIES_BUILDINGS, WW2_ALLIES_BUILDINGS,
@@ -23,7 +21,9 @@ from game.data.doctrine import (
COLDWAR_DOCTRINE, COLDWAR_DOCTRINE,
WWII_DOCTRINE, WWII_DOCTRINE,
) )
from pydcs_extensions.mod_units import MODDED_VEHICLES, MODDED_AIRPLANES from game.data.groundunitclass import GroundUnitClass
from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
@dataclass @dataclass
@@ -45,25 +45,25 @@ class Faction:
description: str = field(default="") description: str = field(default="")
# Available aircraft # Available aircraft
aircrafts: List[Type[FlyingType]] = field(default_factory=list) aircrafts: List[AircraftType] = field(default_factory=list)
# Available awacs aircraft # Available awacs aircraft
awacs: List[Type[FlyingType]] = field(default_factory=list) awacs: List[AircraftType] = field(default_factory=list)
# Available tanker aircraft # Available tanker aircraft
tankers: List[Type[FlyingType]] = field(default_factory=list) tankers: List[AircraftType] = field(default_factory=list)
# Available frontline units # Available frontline units
frontline_units: List[Type[VehicleType]] = field(default_factory=list) frontline_units: List[GroundUnitType] = field(default_factory=list)
# Available artillery units # Available artillery units
artillery_units: List[Type[VehicleType]] = field(default_factory=list) artillery_units: List[GroundUnitType] = field(default_factory=list)
# Infantry units used # Infantry units used
infantry_units: List[Type[VehicleType]] = field(default_factory=list) infantry_units: List[GroundUnitType] = field(default_factory=list)
# Logistics units used # Logistics units used
logistics_units: List[Type[VehicleType]] = field(default_factory=list) logistics_units: List[GroundUnitType] = field(default_factory=list)
# Possible SAMS site generators for this faction # Possible SAMS site generators for this faction
air_defenses: List[str] = field(default_factory=list) air_defenses: List[str] = field(default_factory=list)
@@ -114,7 +114,7 @@ class Faction:
has_jtac: bool = field(default=False) has_jtac: bool = field(default=False)
# Unit to use as JTAC for this faction # Unit to use as JTAC for this faction
jtac_unit: Optional[Type[FlyingType]] = field(default=None) jtac_unit: Optional[AircraftType] = field(default=None)
# doctrine # doctrine
doctrine: Doctrine = field(default=MODERN_DOCTRINE) doctrine: Doctrine = field(default=MODERN_DOCTRINE)
@@ -123,7 +123,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(default_factory=dict) liveries_overrides: Dict[AircraftType, List[str]] = field(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
@@ -134,15 +134,11 @@ class Faction:
#: both will use it. #: both will use it.
unrestricted_satnav: bool = False unrestricted_satnav: bool = False
def has_access_to_unittype(self, unitclass: GroundUnitClass) -> bool: def has_access_to_unittype(self, unit_class: GroundUnitClass) -> bool:
has_access = False for vehicle in itertools.chain(self.frontline_units, self.artillery_units):
for vehicle in unitclass.unit_list: if vehicle.unit_class is unit_class:
if vehicle in self.frontline_units:
return True return True
if vehicle in self.artillery_units: return False
return True
return has_access
@classmethod @classmethod
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction: def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
@@ -163,16 +159,26 @@ class Faction:
faction.authors = json.get("authors", "") faction.authors = json.get("authors", "")
faction.description = json.get("description", "") faction.description = json.get("description", "")
faction.aircrafts = load_all_aircraft(json.get("aircrafts", [])) faction.aircrafts = [AircraftType.named(n) for n in json.get("aircrafts", [])]
faction.awacs = load_all_aircraft(json.get("awacs", [])) faction.awacs = [AircraftType.named(n) for n in json.get("awacs", [])]
faction.tankers = load_all_aircraft(json.get("tankers", [])) faction.tankers = [AircraftType.named(n) for n in json.get("tankers", [])]
faction.aircrafts = list(set(faction.aircrafts + faction.awacs)) faction.aircrafts = list(
set(faction.aircrafts + faction.awacs + faction.tankers)
)
faction.frontline_units = load_all_vehicles(json.get("frontline_units", [])) faction.frontline_units = [
faction.artillery_units = load_all_vehicles(json.get("artillery_units", [])) GroundUnitType.named(n) for n in json.get("frontline_units", [])
faction.infantry_units = load_all_vehicles(json.get("infantry_units", [])) ]
faction.logistics_units = load_all_vehicles(json.get("logistics_units", [])) faction.artillery_units = [
GroundUnitType.named(n) for n in json.get("artillery_units", [])
]
faction.infantry_units = [
GroundUnitType.named(n) for n in json.get("infantry_units", [])
]
faction.logistics_units = [
GroundUnitType.named(n) for n in json.get("logistics_units", [])
]
faction.ewrs = json.get("ewrs", []) faction.ewrs = json.get("ewrs", [])
@@ -196,7 +202,7 @@ class Faction:
faction.has_jtac = json.get("has_jtac", False) faction.has_jtac = json.get("has_jtac", False)
jtac_name = json.get("jtac_unit", None) jtac_name = json.get("jtac_unit", None)
if jtac_name is not None: if jtac_name is not None:
faction.jtac_unit = load_aircraft(jtac_name) faction.jtac_unit = AircraftType.named(jtac_name)
else: else:
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))
@@ -230,87 +236,110 @@ class Faction:
# Load liveries override # Load liveries override
faction.liveries_overrides = {} faction.liveries_overrides = {}
liveries_overrides = json.get("liveries_overrides", {}) liveries_overrides = json.get("liveries_overrides", {})
for k, v in liveries_overrides.items(): for name, livery in liveries_overrides.items():
k = load_aircraft(k) aircraft = AircraftType.named(name)
if k is not None: faction.liveries_overrides[aircraft] = [s.lower() for s in livery]
faction.liveries_overrides[k] = [s.lower() for s in v]
faction.unrestricted_satnav = json.get("unrestricted_satnav", False) faction.unrestricted_satnav = json.get("unrestricted_satnav", False)
return faction return faction
@property @property
def units(self) -> List[Type[UnitType]]: def ground_units(self) -> Iterator[GroundUnitType]:
return ( yield from self.artillery_units
self.infantry_units yield from self.frontline_units
+ self.aircrafts yield from self.logistics_units
+ self.awacs
+ self.artillery_units
+ self.frontline_units
+ self.tankers
+ self.logistics_units
)
def infantry_with_class(
self, unit_class: GroundUnitClass
) -> Iterator[GroundUnitType]:
for unit in self.infantry_units:
if unit.unit_class is unit_class:
yield unit
def unit_loader(unit: str, class_repository: List[Any]) -> Optional[Type[UnitType]]: def apply_mod_settings(self, mod_settings) -> Faction:
""" # aircraft
Find unit by name if not mod_settings.a4_skyhawk:
:param unit: Unit name as string self.remove_aircraft("A-4E-C")
:param class_repository: Repository of classes (Either a module, a class, or a list of classes) if not mod_settings.hercules:
:return: The unit as a PyDCS type self.remove_aircraft("Hercules")
""" if not mod_settings.f22_raptor:
if unit is None: self.remove_aircraft("F-22A")
return None if not mod_settings.jas39_gripen:
elif unit in plane_map.keys(): self.remove_aircraft("JAS39Gripen")
return plane_map[unit] self.remove_aircraft("JAS39Gripen_AG")
else: if not mod_settings.su57_felon:
for mother_class in class_repository: self.remove_aircraft("Su-57")
if getattr(mother_class, unit, None) is not None: # frenchpack
return getattr(mother_class, unit) if not mod_settings.frenchpack:
if type(mother_class) is list: self.remove_vehicle("AMX10RCR")
for m in mother_class: self.remove_vehicle("SEPAR")
if m.__name__ == unit: self.remove_vehicle("ERC")
return m self.remove_vehicle("M120")
logging.error(f"FACTION ERROR : Unable to find {unit} in pydcs") self.remove_vehicle("AA20")
return None self.remove_vehicle("TRM2000")
self.remove_vehicle("TRM2000_Citerne")
self.remove_vehicle("TRM2000_AA20")
self.remove_vehicle("TRMMISTRAL")
self.remove_vehicle("VABH")
self.remove_vehicle("VAB_RADIO")
self.remove_vehicle("VAB_50")
self.remove_vehicle("VIB_VBR")
self.remove_vehicle("VAB_HOT")
self.remove_vehicle("VAB_MORTIER")
self.remove_vehicle("VBL50")
self.remove_vehicle("VBLANF1")
self.remove_vehicle("VBL-radio")
self.remove_vehicle("VBAE")
self.remove_vehicle("VBAE_MMP")
self.remove_vehicle("AMX-30B2")
self.remove_vehicle("Tracma")
self.remove_vehicle("JTACFP")
self.remove_vehicle("SHERIDAN")
self.remove_vehicle("Leclerc_XXI")
self.remove_vehicle("Toyota_bleu")
self.remove_vehicle("Toyota_vert")
self.remove_vehicle("Toyota_desert")
self.remove_vehicle("Kamikaze")
self.remove_vehicle("AMX1375")
self.remove_vehicle("AMX1390")
self.remove_vehicle("VBCI")
self.remove_vehicle("T62")
self.remove_vehicle("T64BV")
self.remove_vehicle("T72M")
self.remove_vehicle("KORNET")
# high digit sams
if not mod_settings.high_digit_sams:
self.remove_air_defenses("SA10BGenerator")
self.remove_air_defenses("SA12Generator")
self.remove_air_defenses("SA20Generator")
self.remove_air_defenses("SA20BGenerator")
self.remove_air_defenses("SA23Generator")
self.remove_air_defenses("SA17Generator")
self.remove_air_defenses("KS19Generator")
return self
def remove_aircraft(self, name):
for i in self.aircrafts:
if i.dcs_unit_type.id == name:
self.aircrafts.remove(i)
def load_aircraft(name: str) -> Optional[Type[FlyingType]]: def remove_air_defenses(self, name):
return cast( for i in self.air_defenses:
Optional[FlyingType], if i == name:
unit_loader(name, [dcs.planes, dcs.helicopters, MODDED_AIRPLANES]), self.air_defenses.remove(i)
)
def remove_vehicle(self, name):
def load_all_aircraft(data) -> List[Type[FlyingType]]: for i in self.frontline_units:
items = [] if i.dcs_unit_type.id == name:
for name in data: self.frontline_units.remove(i)
item = load_aircraft(name)
if item is not None:
items.append(item)
return items
def load_vehicle(name: str) -> Optional[Type[VehicleType]]:
return cast(
Optional[FlyingType],
unit_loader(
name, [Infantry, Unarmed, Armor, AirDefence, Artillery, MODDED_VEHICLES]
),
)
def load_all_vehicles(data) -> List[Type[VehicleType]]:
items = []
for name in data:
item = load_vehicle(name)
if item is not None:
items.append(item)
return items
def load_ship(name: str) -> Optional[Type[ShipType]]: def load_ship(name: str) -> Optional[Type[ShipType]]:
return cast(Optional[FlyingType], unit_loader(name, [dcs.ships])) if (ship := getattr(dcs.ships, name, None)) is not None:
return ship
logging.error(f"FACTION ERROR : Unable to find {name} in dcs.ships")
return None
def load_all_ships(data) -> List[Type[ShipType]]: def load_all_ships(data) -> List[Type[ShipType]]:

View File

@@ -1,22 +1,24 @@
from game.dcs.aircrafttype import AircraftType
import itertools import itertools
import logging import logging
import random import random
import sys import sys
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from enum import Enum from enum import Enum
from typing import Any, Dict, List, Iterator from typing import Any, List
from dcs.action import Coalition from dcs.action import Coalition
from dcs.mapping import Point from dcs.mapping import Point
from dcs.task import CAP, CAS, PinpointStrike from dcs.task import CAP, CAS, PinpointStrike
from dcs.vehicles import AirDefence from dcs.vehicles import AirDefence
from pydcs_extensions.a4ec.a4ec import A_4E_C
from faker import Faker from faker import Faker
from game import db from game import db
from game.inventory import GlobalAircraftInventory from game.inventory import GlobalAircraftInventory
from game.models.game_stats import GameStats from game.models.game_stats import GameStats
from game.plugins import LuaPluginManager from game.plugins import LuaPluginManager
from game.theater.theatergroundobject import MissileSiteGroundObject from gen import aircraft, naming
from gen.ato import AirTaskingOrder from gen.ato import AirTaskingOrder
from gen.conflictgen import Conflict from gen.conflictgen import Conflict
from gen.flights.ai_flight_planner import CoalitionMissionPlanner from gen.flights.ai_flight_planner import CoalitionMissionPlanner
@@ -34,7 +36,7 @@ from .navmesh import NavMesh
from .procurement import AircraftProcurementRequest, ProcurementAi from .procurement import AircraftProcurementRequest, ProcurementAi
from .profiling import logged_duration from .profiling import logged_duration
from .settings import Settings, AutoAtoBehavior from .settings import Settings, AutoAtoBehavior
from .squadrons import Pilot, AirWing from .squadrons import AirWing
from .theater import ConflictTheater from .theater import ConflictTheater
from .theater.bullseye import Bullseye from .theater.bullseye import Bullseye
from .theater.transitnetwork import TransitNetwork, TransitNetworkBuilder from .theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
@@ -86,8 +88,8 @@ class TurnState(Enum):
class Game: class Game:
def __init__( def __init__(
self, self,
player_name: str, player_faction: Faction,
enemy_name: str, enemy_faction: Faction,
theater: ConflictTheater, theater: ConflictTheater,
start_date: datetime, start_date: datetime,
settings: Settings, settings: Settings,
@@ -97,10 +99,10 @@ class Game:
self.settings = settings self.settings = settings
self.events: List[Event] = [] self.events: List[Event] = []
self.theater = theater self.theater = theater
self.player_name = player_name self.player_faction = player_faction
self.player_country = db.FACTIONS[player_name].country self.player_country = player_faction.country
self.enemy_name = enemy_name self.enemy_faction = enemy_faction
self.enemy_country = db.FACTIONS[enemy_name].country self.enemy_country = enemy_faction.country
# pass_turn() will be called when initialization is complete which will # pass_turn() will be called when initialization is complete which will
# increment this to turn 0 before it reaches the player. # increment this to turn 0 before it reaches the player.
self.turn = -1 self.turn = -1
@@ -108,7 +110,7 @@ class Game:
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)
self.ground_planners: Dict[int, GroundPlanner] = {} self.ground_planners: dict[int, GroundPlanner] = {}
self.informations = [] self.informations = []
self.informations.append(Information("Game Start", "-" * 40, 0)) self.informations.append(Information("Game Start", "-" * 40, 0))
# Culling Zones are for areas around points of interest that contain things we may not wish to cull. # Culling Zones are for areas around points of interest that contain things we may not wish to cull.
@@ -119,6 +121,7 @@ class Game:
self.enemy_budget = enemy_budget self.enemy_budget = enemy_budget
self.current_unit_id = 0 self.current_unit_id = 0
self.current_group_id = 0 self.current_group_id = 0
self.name_generator = naming.namegen
self.conditions = self.generate_conditions() self.conditions = self.generate_conditions()
@@ -148,7 +151,7 @@ class Game:
self.on_load(game_still_initializing=True) self.on_load(game_still_initializing=True)
def __getstate__(self) -> Dict[str, Any]: def __getstate__(self) -> dict[str, Any]:
state = self.__dict__.copy() state = self.__dict__.copy()
# Avoid persisting any volatile types that can be deterministically # Avoid persisting any volatile types that can be deterministically
# recomputed on load for the sake of save compatibility. # recomputed on load for the sake of save compatibility.
@@ -160,7 +163,7 @@ class Game:
del state["red_faker"] del state["red_faker"]
return state return state
def __setstate__(self, state: Dict[str, Any]) -> None: def __setstate__(self, state: dict[str, Any]) -> None:
self.__dict__.update(state) self.__dict__.update(state)
# Regenerate any state that was not persisted. # Regenerate any state that was not persisted.
self.on_load() self.on_load()
@@ -200,14 +203,6 @@ class Game:
else: else:
self.enemy_country = "Russia" self.enemy_country = "Russia"
@property
def player_faction(self) -> Faction:
return db.FACTIONS[self.player_name]
@property
def enemy_faction(self) -> Faction:
return db.FACTIONS[self.enemy_name]
def faction_for(self, player: bool) -> Faction: def faction_for(self, player: bool) -> Faction:
if player: if player:
return self.player_faction return self.player_faction
@@ -247,8 +242,8 @@ class Game:
player_cp, player_cp,
enemy_cp, enemy_cp,
enemy_cp.position, enemy_cp.position,
self.player_name, self.player_faction.name,
self.enemy_name, self.enemy_faction.name,
) )
) )
@@ -294,12 +289,20 @@ class Game:
return ( return (
event event
and event.attacker_name and event.attacker_name
and event.attacker_name == self.player_name and event.attacker_name == self.player_faction.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")
def on_load(self, game_still_initializing: bool = False) -> None: def on_load(self, game_still_initializing: bool = False) -> None:
if not hasattr(self, "name_generator"):
self.name_generator = naming.namegen
# Hack: Replace the global name generator state with the state from the save
# game.
#
# We need to persist this state so that names generated after game load don't
# conflict with those generated before exit.
naming.namegen = self.name_generator
LuaPluginManager.load_settings(self.settings) LuaPluginManager.load_settings(self.settings)
ObjectiveDistanceCache.set_theater(self.theater) ObjectiveDistanceCache.set_theater(self.theater)
self.compute_conflicts_position() self.compute_conflicts_position()
@@ -327,15 +330,18 @@ class Game:
# one hop ahead. ControlPoint.process_turn handles unit deliveries. # one hop ahead. ControlPoint.process_turn handles unit deliveries.
self.transfers.perform_transfers() self.transfers.perform_transfers()
# Needs to happen *before* planning transfers so we don't cancel the # Needs to happen *before* planning transfers so we don't cancel them.
self.reset_ato() self.reset_ato()
for control_point in self.theater.controlpoints: for control_point in self.theater.controlpoints:
control_point.process_turn(self) control_point.process_turn(self)
if not skipped and self.turn > 1: self.blue_air_wing.replenish()
self.red_air_wing.replenish()
if not skipped:
for cp in self.theater.player_points(): for cp in self.theater.player_points():
cp.base.affect_strength(+PLAYER_BASE_STRENGTH_RECOVERY) cp.base.affect_strength(+PLAYER_BASE_STRENGTH_RECOVERY)
else: elif self.turn > 1:
for cp in self.theater.player_points(): for cp in self.theater.player_points():
if not cp.is_carrier and not cp.is_lha: if not cp.is_carrier and not cp.is_lha:
cp.base.affect_strength(-PLAYER_BASE_STRENGTH_RECOVERY) cp.base.affect_strength(-PLAYER_BASE_STRENGTH_RECOVERY)

View File

@@ -6,6 +6,7 @@ from typing import Dict, Iterable, Iterator, Set, Tuple, TYPE_CHECKING, Type
from dcs.unittype import FlyingType from dcs.unittype import FlyingType
from game.dcs.aircrafttype import AircraftType
from gen.flights.flight import Flight from gen.flights.flight import Flight
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -17,9 +18,9 @@ class ControlPointAircraftInventory:
def __init__(self, control_point: ControlPoint) -> None: def __init__(self, control_point: ControlPoint) -> None:
self.control_point = control_point self.control_point = control_point
self.inventory: Dict[Type[FlyingType], int] = defaultdict(int) self.inventory: Dict[AircraftType, int] = defaultdict(int)
def add_aircraft(self, aircraft: Type[FlyingType], count: int) -> None: def add_aircraft(self, aircraft: AircraftType, count: int) -> None:
"""Adds aircraft to the inventory. """Adds aircraft to the inventory.
Args: Args:
@@ -28,7 +29,7 @@ class ControlPointAircraftInventory:
""" """
self.inventory[aircraft] += count self.inventory[aircraft] += count
def remove_aircraft(self, aircraft: Type[FlyingType], count: int) -> None: def remove_aircraft(self, aircraft: AircraftType, count: int) -> None:
"""Removes aircraft from the inventory. """Removes aircraft from the inventory.
Args: Args:
@@ -42,12 +43,12 @@ class ControlPointAircraftInventory:
available = self.inventory[aircraft] available = self.inventory[aircraft]
if available < count: if available < count:
raise ValueError( raise ValueError(
f"Cannot remove {count} {aircraft.id} from " f"Cannot remove {count} {aircraft} from "
f"{self.control_point.name}. Only have {available}." f"{self.control_point.name}. Only have {available}."
) )
self.inventory[aircraft] -= count self.inventory[aircraft] -= count
def available(self, aircraft: Type[FlyingType]) -> int: def available(self, aircraft: AircraftType) -> int:
"""Returns the number of available aircraft of the given type. """Returns the number of available aircraft of the given type.
Args: Args:
@@ -59,14 +60,14 @@ class ControlPointAircraftInventory:
return 0 return 0
@property @property
def types_available(self) -> Iterator[Type[FlyingType]]: def types_available(self) -> Iterator[AircraftType]:
"""Iterates over all available aircraft types.""" """Iterates over all available aircraft types."""
for aircraft, count in self.inventory.items(): for aircraft, count in self.inventory.items():
if count > 0: if count > 0:
yield aircraft yield aircraft
@property @property
def all_aircraft(self) -> Iterator[Tuple[Type[FlyingType], int]]: def all_aircraft(self) -> Iterator[Tuple[AircraftType, int]]:
"""Iterates over all available aircraft types, including amounts.""" """Iterates over all available aircraft types, including amounts."""
for aircraft, count in self.inventory.items(): for aircraft, count in self.inventory.items():
if count > 0: if count > 0:
@@ -107,9 +108,9 @@ class GlobalAircraftInventory:
return self.inventories[control_point] return self.inventories[control_point]
@property @property
def available_types_for_player(self) -> Iterator[Type[FlyingType]]: def available_types_for_player(self) -> Iterator[AircraftType]:
"""Iterates over all aircraft types available to the player.""" """Iterates over all aircraft types available to the player."""
seen: Set[Type[FlyingType]] = set() seen: Set[AircraftType] = set()
for control_point, inventory in self.inventories.items(): for control_point, inventory in self.inventories.items():
if control_point.captured: if control_point.captured:
for aircraft in inventory.types_available: for aircraft in inventory.types_available:

View File

@@ -17,7 +17,7 @@ from dcs.triggers import TriggerStart
from game.plugins import LuaPluginManager from game.plugins import LuaPluginManager
from game.theater.theatergroundobject import TheaterGroundObject from game.theater.theatergroundobject import TheaterGroundObject
from gen import Conflict, FlightType, VisualGenerator from gen import Conflict, FlightType, VisualGenerator
from gen.aircraft import AIRCRAFT_DATA, AircraftConflictGenerator, FlightData from gen.aircraft import AircraftConflictGenerator, FlightData
from gen.airfields import AIRFIELD_DATA from gen.airfields import AIRFIELD_DATA
from gen.airsupportgen import AirSupport, AirSupportConflictGenerator from gen.airsupportgen import AirSupport, AirSupportConflictGenerator
from gen.armor import GroundConflictGenerator, JtacInfo from gen.armor import GroundConflictGenerator, JtacInfo
@@ -77,8 +77,8 @@ class Operation:
yield Conflict( yield Conflict(
cls.game.theater, cls.game.theater,
frontline, frontline,
cls.game.player_name, cls.game.player_faction.name,
cls.game.enemy_name, cls.game.enemy_faction.name,
cls.game.player_country, cls.game.player_country,
cls.game.enemy_country, cls.game.enemy_country,
frontline.position, frontline.position,
@@ -95,8 +95,8 @@ class Operation:
return Conflict( return Conflict(
cls.game.theater, cls.game.theater,
FrontLine(player_cp, enemy_cp), FrontLine(player_cp, enemy_cp),
cls.game.player_name, cls.game.player_faction.name,
cls.game.enemy_name, cls.game.enemy_faction.name,
cls.game.player_country, cls.game.player_country,
cls.game.enemy_country, cls.game.enemy_country,
mid_point, mid_point,
@@ -215,23 +215,7 @@ class Operation:
for flight in flights: for flight in flights:
if not flight.client_units: if not flight.client_units:
continue continue
cls.assign_channels_to_flight(flight, air_support) flight.aircraft_type.assign_channels_for_flight(flight, air_support)
@staticmethod
def assign_channels_to_flight(flight: FlightData, air_support: AirSupport) -> None:
"""Assigns preset radio channels for a client flight."""
airframe = flight.aircraft_type
try:
aircraft_data = AIRCRAFT_DATA[airframe.id]
except KeyError:
logging.warning(f"No aircraft data for {airframe.id}")
return
if aircraft_data.channel_allocator is not None:
aircraft_data.channel_allocator.assign_channels_for_flight(
flight, air_support
)
@classmethod @classmethod
def _create_tacan_registry( def _create_tacan_registry(
@@ -375,6 +359,7 @@ class Operation:
cls.game.settings, cls.game.settings,
cls.game, cls.game,
cls.radio_registry, cls.radio_registry,
cls.tacan_registry,
cls.unit_map, cls.unit_map,
air_support=cls.airsupportgen.air_support, air_support=cls.airsupportgen.air_support,
) )
@@ -404,8 +389,8 @@ class Operation:
player_cp = front_line.blue_cp player_cp = front_line.blue_cp
enemy_cp = front_line.red_cp enemy_cp = front_line.red_cp
conflict = Conflict.frontline_cas_conflict( conflict = Conflict.frontline_cas_conflict(
cls.game.player_name, cls.game.player_faction.name,
cls.game.enemy_name, cls.game.enemy_faction.name,
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),
front_line, front_line,
@@ -593,8 +578,7 @@ class Operation:
zone = data["zone"] zone = data["zone"]
laserCode = data["laserCode"] laserCode = data["laserCode"]
dcsUnit = data["dcsUnit"] dcsUnit = data["dcsUnit"]
lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', zone='{zone}', laserCode='{laserCode}', dcsUnit='{dcsUnit}' }}, \n" lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', zone={repr(zone)}, laserCode='{laserCode}', dcsUnit='{dcsUnit}' }}, \n"
# lua += f" {{name='{dcsGroupName}', description='JTAC {callsign} ', information='Laser:{laserCode}', jtac={laserCode} }}, \n"
lua += "}" lua += "}"
# Process the Target Points # Process the Target Points

View File

@@ -2,16 +2,18 @@ import logging
import os import os
import pickle import pickle
import shutil import shutil
from pathlib import Path
from typing import Optional from typing import Optional
_dcs_saved_game_folder: Optional[str] = None _dcs_saved_game_folder: Optional[str] = None
_file_abs_path = None
def setup(user_folder: str): def setup(user_folder: str):
global _dcs_saved_game_folder global _dcs_saved_game_folder
_dcs_saved_game_folder = user_folder _dcs_saved_game_folder = user_folder
_file_abs_path = os.path.join(base_path(), "default.liberation") if not save_dir().exists():
save_dir().mkdir(parents=True)
def base_path() -> str: def base_path() -> str:
@@ -20,16 +22,20 @@ def base_path() -> str:
return _dcs_saved_game_folder return _dcs_saved_game_folder
def save_dir() -> Path:
return Path(base_path()) / "Liberation" / "Saves"
def _temporary_save_file() -> str: def _temporary_save_file() -> str:
return os.path.join(base_path(), "tmpsave.liberation") return str(save_dir() / "tmpsave.liberation")
def _autosave_path() -> str: def _autosave_path() -> str:
return os.path.join(base_path(), "autosave.liberation") return str(save_dir() / "autosave.liberation")
def mission_path_for(name: str) -> str: def mission_path_for(name: str) -> str:
return os.path.join(base_path(), "Missions", "{}".format(name)) return os.path.join(base_path(), "Missions", name)
def load_game(path): def load_game(path):

View File

@@ -3,12 +3,12 @@ from __future__ import annotations
import math import math
import random import random
from dataclasses import dataclass from dataclasses import dataclass
from typing import Iterator, List, Optional, TYPE_CHECKING, Tuple, Type from typing import Iterator, List, Optional, TYPE_CHECKING, Tuple
from dcs.unittype import FlyingType, VehicleType
from game import db from game import db
from game.data.groundunitclass import GroundUnitClass from game.data.groundunitclass import GroundUnitClass
from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
from game.factions.faction import Faction from game.factions.faction import Faction
from game.theater import ControlPoint, MissionTarget from game.theater import ControlPoint, MissionTarget
from game.utils import Distance from game.utils import Distance
@@ -59,6 +59,18 @@ class ProcurementAi:
def calculate_ground_unit_budget_share(self) -> float: def calculate_ground_unit_budget_share(self) -> float:
armor_investment = 0 armor_investment = 0
aircraft_investment = 0 aircraft_investment = 0
# faction has no ground units
if (
len(self.faction.artillery_units) == 0
and len(self.faction.frontline_units) == 0
):
return 0
# faction has no planes
if len(self.faction.aircrafts) == 0:
return 1
for cp in self.owned_points: for cp in self.owned_points:
cp_ground_units = cp.allocated_ground_units(self.game.transfers) cp_ground_units = cp.allocated_ground_units(self.game.transfers)
armor_investment += cp_ground_units.total_value armor_investment += cp_ground_units.total_value
@@ -113,7 +125,7 @@ class ProcurementAi:
if available % 2 == 0: if available % 2 == 0:
continue continue
inventory.remove_aircraft(aircraft, 1) inventory.remove_aircraft(aircraft, 1)
total += db.PRICES[aircraft] total += aircraft.price
return total return total
def repair_runways(self, budget: float) -> float: def repair_runways(self, budget: float) -> float:
@@ -135,12 +147,17 @@ class ProcurementAi:
def affordable_ground_unit_of_class( def affordable_ground_unit_of_class(
self, budget: float, unit_class: GroundUnitClass self, budget: float, unit_class: GroundUnitClass
) -> Optional[Type[VehicleType]]: ) -> Optional[GroundUnitType]:
faction_units = set(self.faction.frontline_units) | set( faction_units = set(self.faction.frontline_units) | set(
self.faction.artillery_units self.faction.artillery_units
) )
of_class = set(unit_class.unit_list) & faction_units of_class = {u for u in faction_units if u.unit_class is unit_class}
affordable_units = [u for u in of_class if db.PRICES[u] <= budget]
# faction has no access to needed unit type, take a random unit
if not of_class:
of_class = faction_units
affordable_units = [u for u in of_class if u.price <= budget]
if not affordable_units: if not affordable_units:
return None return None
return random.choice(affordable_units) return random.choice(affordable_units)
@@ -162,7 +179,7 @@ class ProcurementAi:
# Can't afford any more units. # Can't afford any more units.
break break
budget -= db.PRICES[unit] budget -= unit.price
cp.pending_unit_deliveries.order({unit: 1}) cp.pending_unit_deliveries.order({unit: 1})
return budget return budget
@@ -198,12 +215,12 @@ class ProcurementAi:
airbase: ControlPoint, airbase: ControlPoint,
number: int, number: int,
max_price: float, max_price: float,
) -> Optional[Type[FlyingType]]: ) -> Optional[AircraftType]:
best_choice: Optional[Type[FlyingType]] = None best_choice: Optional[AircraftType] = None
for unit in aircraft_for_task(task): for unit in aircraft_for_task(task):
if unit not in self.faction.aircrafts: if unit not in self.faction.aircrafts:
continue continue
if db.PRICES[unit] * number > max_price: if unit.price * number > max_price:
continue continue
if not airbase.can_operate(unit): if not airbase.can_operate(unit):
continue continue
@@ -224,7 +241,7 @@ class ProcurementAi:
def affordable_aircraft_for( def affordable_aircraft_for(
self, request: AircraftProcurementRequest, airbase: ControlPoint, budget: float self, request: AircraftProcurementRequest, airbase: ControlPoint, budget: float
) -> Optional[Type[FlyingType]]: ) -> Optional[AircraftType]:
return self._affordable_aircraft_for_task( return self._affordable_aircraft_for_task(
request.task_capability, airbase, request.number, budget request.task_capability, airbase, request.number, budget
) )
@@ -242,7 +259,7 @@ class ProcurementAi:
# able to operate expensive aircraft. # able to operate expensive aircraft.
continue continue
budget -= db.PRICES[unit] * request.number budget -= unit.price * request.number
airbase.pending_unit_deliveries.order({unit: request.number}) airbase.pending_unit_deliveries.order({unit: request.number})
return budget, True return budget, True
return budget, False return budget, False
@@ -343,9 +360,9 @@ class ProcurementAi:
class_cost = 0 class_cost = 0
total_cost = 0 total_cost = 0
for unit_type, count in allocations.all.items(): for unit_type, count in allocations.all.items():
cost = db.PRICES[unit_type] * count cost = unit_type.price * count
total_cost += cost total_cost += cost
if unit_type in unit_class: if unit_type.unit_class is unit_class:
class_cost += cost class_cost += cost
if not total_cost: if not total_cost:
return 0 return 0

298
game/radio/channels.py Normal file
View File

@@ -0,0 +1,298 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Optional, Any, TYPE_CHECKING
if TYPE_CHECKING:
from gen import FlightData, AirSupport
class RadioChannelAllocator:
"""Base class for radio channel allocators."""
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
) -> None:
"""Assigns mission frequencies to preset channels for the flight."""
raise NotImplementedError
@classmethod
def from_cfg(cls, cfg: dict[str, Any]) -> RadioChannelAllocator:
return cls()
@classmethod
def name(cls) -> str:
raise NotImplementedError
@dataclass(frozen=True)
class CommonRadioChannelAllocator(RadioChannelAllocator):
"""Radio channel allocator suitable for most aircraft.
Most of the aircraft with preset channels available have one or more radios
with 20 or more channels available (typically per-radio, but this is not the
case for the JF-17).
"""
#: Index of the radio used for intra-flight communications. Matches the
#: index of the panel_radio field of the pydcs.dcs.planes object.
inter_flight_radio_index: Optional[int]
#: Index of the radio used for intra-flight communications. Matches the
#: index of the panel_radio field of the pydcs.dcs.planes object.
intra_flight_radio_index: Optional[int]
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
) -> None:
if self.intra_flight_radio_index is not None:
flight.assign_channel(
self.intra_flight_radio_index, 1, flight.intra_flight_channel
)
if self.inter_flight_radio_index is None:
return
# For cases where the inter-flight and intra-flight radios share presets
# (the JF-17 only has one set of channels, even though it can use two
# channels simultaneously), start assigning inter-flight channels at 2.
radio_id = self.inter_flight_radio_index
if self.intra_flight_radio_index == radio_id:
first_channel = 2
else:
first_channel = 1
last_channel = flight.num_radio_channels(radio_id)
channel_alloc = iter(range(first_channel, last_channel + 1))
if flight.departure.atc is not None:
flight.assign_channel(radio_id, next(channel_alloc), flight.departure.atc)
# TODO: If there ever are multiple AWACS, limit to mission relevant.
for awacs in air_support.awacs:
flight.assign_channel(radio_id, next(channel_alloc), awacs.freq)
if flight.arrival != flight.departure and flight.arrival.atc is not None:
flight.assign_channel(radio_id, next(channel_alloc), flight.arrival.atc)
try:
# TODO: Skip incompatible tankers.
for tanker in air_support.tankers:
flight.assign_channel(radio_id, next(channel_alloc), tanker.freq)
if flight.divert is not None and flight.divert.atc is not None:
flight.assign_channel(radio_id, next(channel_alloc), flight.divert.atc)
except StopIteration:
# Any remaining channels are nice-to-haves, but not necessary for
# the few aircraft with a small number of channels available.
pass
@classmethod
def from_cfg(cls, cfg: dict[str, Any]) -> CommonRadioChannelAllocator:
return CommonRadioChannelAllocator(
inter_flight_radio_index=cfg["inter_flight_radio_index"],
intra_flight_radio_index=cfg["intra_flight_radio_index"],
)
@classmethod
def name(cls) -> str:
return "common"
@dataclass(frozen=True)
class NoOpChannelAllocator(RadioChannelAllocator):
"""Channel allocator for aircraft that don't support preset channels."""
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
) -> None:
pass
@classmethod
def name(cls) -> str:
return "noop"
@dataclass(frozen=True)
class FarmerRadioChannelAllocator(RadioChannelAllocator):
"""Preset channel allocator for the MiG-19P."""
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
) -> None:
# The Farmer only has 6 preset channels. It also only has a VHF radio,
# and currently our ATC data and AWACS are only in the UHF band.
radio_id = 1
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
# TODO: Assign 4-6 to VHF frequencies of departure, arrival, and divert.
# TODO: Assign 2 and 3 to AWACS if it is VHF.
@classmethod
def name(cls) -> str:
return "farmer"
@dataclass(frozen=True)
class ViggenRadioChannelAllocator(RadioChannelAllocator):
"""Preset channel allocator for the AJS37."""
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
) -> None:
# The Viggen's preset channels are handled differently from other
# aircraft. The aircraft automatically configures channels for every
# allied flight in the game (including AWACS) and for every airfield. As
# such, we don't need to allocate any of those. There are seven presets
# we can modify, however: three channels for the main radio intended for
# communication with wingmen, and four emergency channels for the backup
# radio. We'll set the first channel of the main radio to the
# intra-flight channel, and the first three emergency channels to each
# of the flight plan's airfields. The fourth emergency channel is always
# the guard channel.
radio_id = 1
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
if flight.departure.atc is not None:
flight.assign_channel(radio_id, 4, flight.departure.atc)
if flight.arrival.atc is not None:
flight.assign_channel(radio_id, 5, flight.arrival.atc)
# TODO: Assign divert to 6 when we support divert airfields.
@classmethod
def name(cls) -> str:
return "viggen"
@dataclass(frozen=True)
class SCR522RadioChannelAllocator(RadioChannelAllocator):
"""Preset channel allocator for the SCR522 WW2 radios. (4 channels)"""
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
) -> None:
radio_id = 1
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
if flight.departure.atc is not None:
flight.assign_channel(radio_id, 2, flight.departure.atc)
if flight.arrival.atc is not None:
flight.assign_channel(radio_id, 3, flight.arrival.atc)
# TODO : Some GCI on Channel 4 ?
@classmethod
def name(cls) -> str:
return "SCR-522"
class ChannelNamer:
"""Base class allowing channel name customization per-aircraft.
Most aircraft will want to customize this behavior, but the default is
reasonable for any aircraft with numbered radios.
"""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
"""Returns the name of the channel for the given radio and channel."""
return f"COMM{radio_id} Ch {channel_id}"
@classmethod
def name(cls) -> str:
return "default"
class SingleRadioChannelNamer(ChannelNamer):
"""Channel namer for the aircraft with only a single radio.
Aircraft like the MiG-19P and the MiG-21bis only have a single radio, so
it's not necessary for us to name the radio when naming the channel.
"""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
return f"Ch {channel_id}"
@classmethod
def name(cls) -> str:
return "single"
class HueyChannelNamer(ChannelNamer):
"""Channel namer for the UH-1H."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
return f"COM3 Ch {channel_id}"
@classmethod
def name(cls) -> str:
return "huey"
class MirageChannelNamer(ChannelNamer):
"""Channel namer for the M-2000."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
radio_name = ["V/UHF", "UHF"][radio_id - 1]
return f"{radio_name} Ch {channel_id}"
@classmethod
def name(cls) -> str:
return "mirage"
class TomcatChannelNamer(ChannelNamer):
"""Channel namer for the F-14."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
radio_name = ["UHF", "VHF/UHF"][radio_id - 1]
return f"{radio_name} Ch {channel_id}"
@classmethod
def name(cls) -> str:
return "tomcat"
class ViggenChannelNamer(ChannelNamer):
"""Channel namer for the AJS37."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
if channel_id >= 4:
channel_letter = "EFGH"[channel_id - 4]
return f"FR 24 {channel_letter}"
return f"FR 22 Special {channel_id}"
@classmethod
def name(cls) -> str:
return "viggen"
class ViperChannelNamer(ChannelNamer):
"""Channel namer for the F-16."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
return f"COM{radio_id} Ch {channel_id}"
@classmethod
def name(cls) -> str:
return "viper"
class SCR522ChannelNamer(ChannelNamer):
"""
Channel namer for P-51 & P-47D
"""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
if channel_id > 3:
return "?"
else:
return f"Button " + "ABCD"[channel_id - 1]
@classmethod
def name(cls) -> str:
return "SCR-522"

View File

@@ -20,6 +20,7 @@ class Settings:
# Difficulty settings # Difficulty settings
player_skill: str = "Good" player_skill: str = "Good"
enemy_skill: str = "Average" enemy_skill: str = "Average"
ai_pilot_levelling: bool = True
enemy_vehicle_skill: str = "Average" enemy_vehicle_skill: str = "Average"
map_coalition_visibility: ForcedOptions.Views = ForcedOptions.Views.All map_coalition_visibility: ForcedOptions.Views = ForcedOptions.Views.All
labels: str = "Full" labels: str = "Full"
@@ -33,6 +34,18 @@ class Settings:
player_income_multiplier: float = 1.0 player_income_multiplier: float = 1.0
enemy_income_multiplier: float = 1.0 enemy_income_multiplier: float = 1.0
#: Feature flag for squadron limits.
enable_squadron_pilot_limits: bool = False
#: The maximum number of pilots a squadron can have at one time. Changing this after
#: the campaign has started will have no immediate effect; pilots already in the
#: squadron will not be removed if the limit is lowered and pilots will not be
#: immediately created if the limit is raised.
squadron_pilot_limit: int = 12
#: The number of pilots a squadron can replace per turn.
squadron_replenishment_rate: int = 4
default_start_type: str = "Cold" default_start_type: str = "Cold"
# Mission specific # Mission specific
@@ -44,6 +57,7 @@ class Settings:
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 = True disable_legacy_aewc: bool = True
disable_legacy_tanker: bool = True
generate_dark_kneeboard: bool = False generate_dark_kneeboard: bool = False
invulnerable_player_pilots: bool = True invulnerable_player_pilots: bool = True
auto_ato_behavior: AutoAtoBehavior = AutoAtoBehavior.Default auto_ato_behavior: AutoAtoBehavior = AutoAtoBehavior.Default

View File

@@ -8,7 +8,6 @@ from dataclasses import dataclass, field
from enum import unique, Enum from enum import unique, Enum
from pathlib import Path from pathlib import Path
from typing import ( from typing import (
Type,
Tuple, Tuple,
TYPE_CHECKING, TYPE_CHECKING,
Optional, Optional,
@@ -17,10 +16,9 @@ from typing import (
) )
import yaml import yaml
from dcs.unittype import FlyingType
from faker import Faker from faker import Faker
from game.db import flying_type_from_name from game.dcs.aircrafttype import AircraftType
from game.settings import AutoAtoBehavior from game.settings import AutoAtoBehavior
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -76,14 +74,23 @@ class Pilot:
@dataclass @dataclass
class Squadron: class Squadron:
name: str name: str
nickname: str nickname: Optional[str]
country: str country: str
role: str role: str
aircraft: Type[FlyingType] aircraft: AircraftType
livery: Optional[str] livery: Optional[str]
mission_types: tuple[FlightType, ...] mission_types: tuple[FlightType, ...]
pilots: list[Pilot]
available_pilots: list[Pilot] = field(init=False, hash=False, compare=False) #: The pool of pilots that have not yet been assigned to the squadron. This only
#: happens when a preset squadron defines more preset pilots than the squadron limit
#: allows. This pool will be consumed before random pilots are generated.
pilot_pool: list[Pilot]
current_roster: list[Pilot] = field(default_factory=list, init=False, hash=False)
available_pilots: list[Pilot] = field(
default_factory=list, init=False, hash=False, compare=False
)
auto_assignable_mission_types: set[FlightType] = field( auto_assignable_mission_types: set[FlightType] = field(
init=False, hash=False, compare=False init=False, hash=False, compare=False
) )
@@ -95,18 +102,29 @@ class Squadron:
player: bool player: bool
def __post_init__(self) -> None: def __post_init__(self) -> None:
self.available_pilots = list(self.active_pilots) if any(p.status is not PilotStatus.Active for p in self.pilot_pool):
raise ValueError("Squadrons can only be created with active pilots.")
self._recruit_pilots(self.game.settings.squadron_pilot_limit)
self.auto_assignable_mission_types = set(self.mission_types) self.auto_assignable_mission_types = set(self.mission_types)
def __str__(self) -> str: def __str__(self) -> str:
if self.nickname is None:
return self.name
return f'{self.name} "{self.nickname}"' return f'{self.name} "{self.nickname}"'
@property
def pilot_limits_enabled(self) -> bool:
return self.game.settings.enable_squadron_pilot_limits
def claim_new_pilot_if_allowed(self) -> Optional[Pilot]:
if self.pilot_limits_enabled:
return None
self._recruit_pilots(1)
return self.available_pilots.pop()
def claim_available_pilot(self) -> Optional[Pilot]: def claim_available_pilot(self) -> Optional[Pilot]:
# No pilots available, so the preference is irrelevant. Create a new pilot and
# return it.
if not self.available_pilots: if not self.available_pilots:
self.enlist_new_pilots(1) return self.claim_new_pilot_if_allowed()
return self.available_pilots.pop()
# For opfor, so player/AI option is irrelevant. # For opfor, so player/AI option is irrelevant.
if not self.player: if not self.player:
@@ -127,11 +145,12 @@ class Squadron:
# No pilot was found that matched the user's preference. # No pilot was found that matched the user's preference.
# #
# If they chose to *never* assign players and only players remain in the pool, # If they chose to *never* assign players and only players remain in the pool,
# we cannot fill the slot with the available pilots. Recruit a new one. # we cannot fill the slot with the available pilots.
# #
# If they prefer players and we're out of players, just return an AI pilot. # If they only *prefer* players and we're out of players, just return an AI
# pilot.
if not prefer_players: if not prefer_players:
self.enlist_new_pilots(1) return self.claim_new_pilot_if_allowed()
return self.available_pilots.pop() return self.available_pilots.pop()
def claim_pilot(self, pilot: Pilot) -> None: def claim_pilot(self, pilot: Pilot) -> None:
@@ -151,23 +170,48 @@ class Squadron:
# repopulating the same size flight from the same squadron. # repopulating the same size flight from the same squadron.
self.available_pilots.extend(reversed(pilots)) self.available_pilots.extend(reversed(pilots))
def enlist_new_pilots(self, count: int) -> None: def _recruit_pilots(self, count: int) -> None:
new_pilots = [Pilot(self.faker.name()) for _ in range(count)] new_pilots = self.pilot_pool[:count]
self.pilots.extend(new_pilots) self.pilot_pool = self.pilot_pool[count:]
count -= len(new_pilots)
new_pilots.extend([Pilot(self.faker.name()) for _ in range(count)])
self.current_roster.extend(new_pilots)
self.available_pilots.extend(new_pilots) self.available_pilots.extend(new_pilots)
def replenish_lost_pilots(self) -> None:
if not self.pilot_limits_enabled:
return
replenish_count = min(
self.game.settings.squadron_replenishment_rate,
self._number_of_unfilled_pilot_slots,
)
if replenish_count > 0:
self._recruit_pilots(replenish_count)
def return_all_pilots(self) -> None: def return_all_pilots(self) -> None:
self.available_pilots = list(self.active_pilots) self.available_pilots = list(self.active_pilots)
@staticmethod
def send_on_leave(pilot: Pilot) -> None:
pilot.send_on_leave()
def return_from_leave(self, pilot: Pilot):
if not self.has_unfilled_pilot_slots:
raise RuntimeError(
f"Cannot return {pilot} from leave because {self} is full"
)
pilot.return_from_leave()
@property @property
def faker(self) -> Faker: def faker(self) -> Faker:
return self.game.faker_for(self.player) return self.game.faker_for(self.player)
def _pilots_with_status(self, status: PilotStatus) -> list[Pilot]: def _pilots_with_status(self, status: PilotStatus) -> list[Pilot]:
return [p for p in self.pilots if p.status == status] return [p for p in self.current_roster if p.status == status]
def _pilots_without_status(self, status: PilotStatus) -> list[Pilot]: def _pilots_without_status(self, status: PilotStatus) -> list[Pilot]:
return [p for p in self.pilots if p.status != status] return [p for p in self.current_roster if p.status != status]
@property @property
def active_pilots(self) -> list[Pilot]: def active_pilots(self) -> list[Pilot]:
@@ -178,27 +222,47 @@ class Squadron:
return self._pilots_with_status(PilotStatus.OnLeave) return self._pilots_with_status(PilotStatus.OnLeave)
@property @property
def number_of_pilots_including_dead(self) -> int: def number_of_pilots_including_inactive(self) -> int:
return len(self.pilots) return len(self.current_roster)
@property @property
def number_of_living_pilots(self) -> int: def _number_of_unfilled_pilot_slots(self) -> int:
return len(self._pilots_without_status(PilotStatus.Dead)) return self.game.settings.squadron_pilot_limit - len(self.active_pilots)
@property
def number_of_available_pilots(self) -> int:
return len(self.available_pilots)
def can_provide_pilots(self, count: int) -> bool:
return not self.pilot_limits_enabled or self.number_of_available_pilots >= count
@property
def has_available_pilots(self) -> bool:
return not self.pilot_limits_enabled or bool(self.available_pilots)
@property
def has_unfilled_pilot_slots(self) -> bool:
return not self.pilot_limits_enabled or self._number_of_unfilled_pilot_slots > 0
def can_auto_assign(self, task: FlightType) -> bool:
return task in self.auto_assignable_mission_types
def pilot_at_index(self, index: int) -> Pilot: def pilot_at_index(self, index: int) -> Pilot:
return self.pilots[index] return self.current_roster[index]
@classmethod @classmethod
def from_yaml(cls, path: Path, game: Game, player: bool) -> Squadron: def from_yaml(cls, path: Path, game: Game, player: bool) -> Squadron:
from gen.flights.ai_flight_planner_db import tasks_for_aircraft from gen.flights.ai_flight_planner_db import tasks_for_aircraft
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
with path.open() as squadron_file: with path.open(encoding="utf8") as squadron_file:
data = yaml.safe_load(squadron_file) data = yaml.safe_load(squadron_file)
unit_type = flying_type_from_name(data["aircraft"]) name = data["aircraft"]
if unit_type is None: try:
raise KeyError(f"Could not find any aircraft with the ID {unit_type}") unit_type = AircraftType.named(name)
except KeyError as ex:
raise KeyError(f"Could not find any aircraft named {name}") from ex
pilots = [Pilot(n, player=False) for n in data.get("pilots", [])] pilots = [Pilot(n, player=False) for n in data.get("pilots", [])]
pilots.extend([Pilot(n, player=True) for n in data.get("players", [])]) pilots.extend([Pilot(n, player=True) for n in data.get("players", [])])
@@ -215,13 +279,13 @@ class Squadron:
return Squadron( return Squadron(
name=data["name"], name=data["name"],
nickname=data["nickname"], nickname=data.get("nickname"),
country=data["country"], country=data["country"],
role=data["role"], role=data["role"],
aircraft=unit_type, aircraft=unit_type,
livery=data.get("livery"), livery=data.get("livery"),
mission_types=tuple(mission_types), mission_types=tuple(mission_types),
pilots=pilots, pilot_pool=pilots,
game=game, game=game,
player=player, player=player,
) )
@@ -245,8 +309,8 @@ class SquadronLoader:
yield Path(persistency.base_path()) / "Liberation/Squadrons" yield Path(persistency.base_path()) / "Liberation/Squadrons"
yield Path("resources/squadrons") yield Path("resources/squadrons")
def load(self) -> dict[Type[FlyingType], list[Squadron]]: def load(self) -> dict[AircraftType, list[Squadron]]:
squadrons: dict[Type[FlyingType], list[Squadron]] = defaultdict(list) squadrons: dict[AircraftType, list[Squadron]] = defaultdict(list)
country = self.game.country_for(self.player) country = self.game.country_for(self.player)
faction = self.game.faction_for(self.player) faction = self.game.faction_for(self.player)
any_country = country.startswith("Combined Joint Task Forces ") any_country = country.startswith("Combined Joint Task Forces ")
@@ -311,21 +375,35 @@ class AirWing:
aircraft=aircraft, aircraft=aircraft,
livery=None, livery=None,
mission_types=tuple(tasks_for_aircraft(aircraft)), mission_types=tuple(tasks_for_aircraft(aircraft)),
pilots=[], pilot_pool=[],
game=game, game=game,
player=player, player=player,
) )
] ]
def squadrons_for(self, aircraft: Type[FlyingType]) -> Sequence[Squadron]: def squadrons_for(self, aircraft: AircraftType) -> Sequence[Squadron]:
return self.squadrons[aircraft] return self.squadrons[aircraft]
def squadrons_for_task(self, task: FlightType) -> Iterator[Squadron]: def can_auto_plan(self, task: FlightType) -> bool:
try:
next(self.auto_assignable_for_task(task))
return True
except StopIteration:
return False
def auto_assignable_for_task(self, task: FlightType) -> Iterator[Squadron]:
for squadron in self.iter_squadrons(): for squadron in self.iter_squadrons():
if task in squadron.mission_types: if squadron.can_auto_assign(task):
yield squadron yield squadron
def squadron_for(self, aircraft: Type[FlyingType]) -> Squadron: def auto_assignable_for_task_with_type(
self, aircraft: AircraftType, task: FlightType
) -> Iterator[Squadron]:
for squadron in self.squadrons_for(aircraft):
if squadron.can_auto_assign(task) and squadron.has_available_pilots:
yield squadron
def squadron_for(self, aircraft: AircraftType) -> Squadron:
return self.squadrons_for(aircraft)[0] return self.squadrons_for(aircraft)[0]
def iter_squadrons(self) -> Iterator[Squadron]: def iter_squadrons(self) -> Iterator[Squadron]:
@@ -334,6 +412,10 @@ class AirWing:
def squadron_at_index(self, index: int) -> Squadron: def squadron_at_index(self, index: int) -> Squadron:
return list(self.iter_squadrons())[index] return list(self.iter_squadrons())[index]
def replenish(self) -> None:
for squadron in self.iter_squadrons():
squadron.replenish_lost_pilots()
def reset(self) -> None: def reset(self) -> None:
for squadron in self.iter_squadrons(): for squadron in self.iter_squadrons():
squadron.return_all_pilots() squadron.return_all_pilots()

View File

@@ -1,20 +1,10 @@
import itertools import itertools
import logging import logging
import math from typing import Any
import typing
from typing import Dict, Type
from dcs.task import AWACS, CAP, CAS, Embarking, PinpointStrike, Task, Transport from game.dcs.aircrafttype import AircraftType
from dcs.unittype import FlyingType, UnitType, VehicleType from game.dcs.groundunittype import GroundUnitType
from dcs.vehicles import AirDefence, Armor from game.dcs.unittype import UnitType
from game import db
from game.db import PRICES
STRENGTH_AA_ASSEMBLE_MIN = 0.2
PLANES_SCRAMBLE_MIN_BASE = 2
PLANES_SCRAMBLE_MAX_BASE = 8
PLANES_SCRAMBLE_FACTOR = 0.3
BASE_MAX_STRENGTH = 1 BASE_MAX_STRENGTH = 1
BASE_MIN_STRENGTH = 0 BASE_MIN_STRENGTH = 0
@@ -22,11 +12,8 @@ BASE_MIN_STRENGTH = 0
class Base: class Base:
def __init__(self): def __init__(self):
self.aircraft: Dict[Type[FlyingType], int] = {} self.aircraft: dict[AircraftType, int] = {}
self.armor: Dict[Type[VehicleType], int] = {} self.armor: dict[GroundUnitType, int] = {}
# TODO: Appears unused.
self.aa: Dict[AirDefence, int] = {}
self.commision_points: Dict[Type, float] = {}
self.strength = 1 self.strength = 1
@property @property
@@ -41,145 +28,52 @@ class Base:
def total_armor_value(self) -> int: def total_armor_value(self) -> int:
total = 0 total = 0
for unit_type, count in self.armor.items(): for unit_type, count in self.armor.items():
try: total += unit_type.price * count
total += PRICES[unit_type] * count
except KeyError:
logging.exception(f"No price found for {unit_type.id}")
return total return total
@property def total_units_of_type(self, unit_type: UnitType) -> int:
def total_aa(self) -> int:
return sum(self.aa.values())
def total_units(self, task: Task) -> int:
return sum( return sum(
[ [
c c
for t, c in itertools.chain( for t, c in itertools.chain(self.aircraft.items(), self.armor.items())
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:
return sum(
[
c
for t, c in itertools.chain(
self.aircraft.items(), self.armor.items(), self.aa.items()
)
if t == unit_type if t == unit_type
] ]
) )
@property def commission_units(self, units: dict[Any, int]):
def all_units(self):
return itertools.chain(
self.aircraft.items(), self.armor.items(), self.aa.items()
)
def _find_best_unit(
self, available_units: Dict[UnitType, int], for_type: Task, count: int
) -> Dict[UnitType, int]:
if count <= 0:
logging.warning("{}: no units for {}".format(self, for_type))
return {}
sorted_units = [
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)
result: Dict[UnitType, int] = {}
for unit_type in sorted_units:
existing_count = available_units[unit_type] # type: int
if not existing_count:
continue
if count <= 0:
break
result_unit_count = min(count, existing_count)
count -= result_unit_count
assert result_unit_count > 0
result[unit_type] = result.get(unit_type, 0) + result_unit_count
logging.info("{} for {} ({}): {}".format(self, for_type, count, result))
return result
def _find_best_planes(
self, for_type: Task, count: int
) -> typing.Dict[FlyingType, int]:
return self._find_best_unit(self.aircraft, for_type, count)
def _find_best_armor(self, for_type: Task, count: int) -> typing.Dict[Armor, int]:
return self._find_best_unit(self.armor, for_type, count)
def append_commision_points(self, for_type, points: float) -> int:
self.commision_points[for_type] = (
self.commision_points.get(for_type, 0) + points
)
points = self.commision_points[for_type]
if points >= 1:
self.commision_points[for_type] = points - math.floor(points)
return int(math.floor(points))
return 0
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.armor = {k: v for k, v in self.armor.items() if k in applicable_units}
def commision_units(self, units: typing.Dict[typing.Any, int]):
for unit_type, unit_count in units.items(): for unit_type, unit_count in units.items():
if unit_count <= 0: if unit_count <= 0:
continue continue
for_task = db.unit_task(unit_type) target_dict: dict[Any, int]
if isinstance(unit_type, AircraftType):
target_dict = None
if (
for_task == AWACS
or for_task == CAS
or for_task == CAP
or for_task == Embarking
or for_task == Transport
):
target_dict = self.aircraft target_dict = self.aircraft
elif for_task == PinpointStrike: elif isinstance(unit_type, GroundUnitType):
target_dict = self.armor target_dict = self.armor
elif for_task == AirDefence:
target_dict = self.aa
if target_dict is not None:
target_dict[unit_type] = target_dict.get(unit_type, 0) + unit_count
else: else:
logging.error("Unable to determine target dict for " + str(unit_type)) logging.error(f"Unexpected unit type of {unit_type}")
return
def commit_losses(self, units_lost: typing.Dict[typing.Any, int]): target_dict[unit_type] = target_dict.get(unit_type, 0) + unit_count
def commit_losses(self, units_lost: dict[Any, int]):
for unit_type, count in units_lost.items(): for unit_type, count in units_lost.items():
target_dict: dict[Any, int]
if unit_type in self.aircraft: if unit_type in self.aircraft:
target_array = self.aircraft target_dict = self.aircraft
elif unit_type in self.armor: elif unit_type in self.armor:
target_array = self.armor target_dict = self.armor
else: else:
print("Base didn't find event type {}".format(unit_type)) print("Base didn't find event type {}".format(unit_type))
continue continue
if unit_type not in target_array: if unit_type not in target_dict:
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_dict[unit_type] = max(target_dict[unit_type] - count, 0)
if target_array[unit_type] == 0: if target_dict[unit_type] == 0:
del target_array[unit_type] del target_dict[unit_type]
def affect_strength(self, amount): def affect_strength(self, amount):
self.strength += amount self.strength += amount
@@ -190,55 +84,3 @@ class Base:
def set_strength_to_minimum(self) -> None: def set_strength_to_minimum(self) -> None:
self.strength = BASE_MIN_STRENGTH self.strength = BASE_MIN_STRENGTH
def scramble_count(self, multiplier: float, task: Task = None) -> int:
if task:
count = sum(
[v for k, v in self.aircraft.items() if db.unit_task(k) == task]
)
else:
count = self.total_aircraft
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,
)
def assemble_count(self):
return int(self.total_armor * 0.5)
def assemble_aa_count(self) -> int:
# previous logic removed because we always want the full air defense capabilities.
return self.total_aa
def scramble_sweep(self, multiplier: float) -> typing.Dict[FlyingType, int]:
return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP))
def scramble_last_defense(self):
# return as many CAP-capable aircraft as we can since this is the last defense of the base
# (but not more than 20 - that's just nuts)
return self._find_best_planes(CAP, min(self.total_aircraft, 20))
def scramble_cas(self, multiplier: float) -> typing.Dict[FlyingType, int]:
return self._find_best_planes(CAS, self.scramble_count(multiplier, CAS))
def scramble_interceptors(self, multiplier: float) -> typing.Dict[FlyingType, int]:
return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP))
def assemble_attack(self) -> typing.Dict[Armor, int]:
return self._find_best_armor(PinpointStrike, self.assemble_count())
def assemble_defense(self) -> typing.Dict[Armor, int]:
count = int(self.total_armor * min(self.strength + 0.5, 1))
return self._find_best_armor(PinpointStrike, count)
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(),
)

View File

@@ -16,10 +16,10 @@ from dcs.country import Country
from dcs.mapping import Point from dcs.mapping import Point
from dcs.planes import F_15C from dcs.planes import F_15C
from dcs.ships import ( from dcs.ships import (
Bulker_Handy_Wind, HandyWind,
CVN_74_John_C__Stennis, Stennis,
DDG_Arleigh_Burke_IIa, USS_Arleigh_Burke_IIa,
LHA_1_Tarawa, LHA_Tarawa,
) )
from dcs.statics import Fortification, Warehouse from dcs.statics import Fortification, Warehouse
from dcs.terrain import ( from dcs.terrain import (
@@ -77,53 +77,53 @@ class MizCampaignLoader:
OFF_MAP_UNIT_TYPE = F_15C.id OFF_MAP_UNIT_TYPE = F_15C.id
CV_UNIT_TYPE = CVN_74_John_C__Stennis.id CV_UNIT_TYPE = Stennis.id
LHA_UNIT_TYPE = LHA_1_Tarawa.id LHA_UNIT_TYPE = LHA_Tarawa.id
FRONT_LINE_UNIT_TYPE = Armor.APC_M113.id FRONT_LINE_UNIT_TYPE = Armor.M_113.id
SHIPPING_LANE_UNIT_TYPE = Bulker_Handy_Wind.id SHIPPING_LANE_UNIT_TYPE = HandyWind.id
FOB_UNIT_TYPE = Unarmed.Truck_SKP_11_Mobile_ATC.id FOB_UNIT_TYPE = Unarmed.SKP_11.id
FARP_HELIPAD = "SINGLE_HELIPAD" FARP_HELIPAD = "SINGLE_HELIPAD"
OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id
SHIP_UNIT_TYPE = DDG_Arleigh_Burke_IIa.id SHIP_UNIT_TYPE = USS_Arleigh_Burke_IIa.id
MISSILE_SITE_UNIT_TYPE = MissilesSS.SSM_SS_1C_Scud_B.id MISSILE_SITE_UNIT_TYPE = MissilesSS.Scud_B.id
COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.AShM_SS_N_2_Silkworm.id COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.Hy_launcher.id
# Multiple options for air defenses so campaign designers can more accurately see # Multiple options for air defenses so campaign designers can more accurately see
# the coverage of their IADS for the expected type. # the coverage of their IADS for the expected type.
LONG_RANGE_SAM_UNIT_TYPES = { LONG_RANGE_SAM_UNIT_TYPES = {
AirDefence.SAM_Patriot_LN.id, AirDefence.Patriot_ln.id,
AirDefence.SAM_SA_10_S_300_Grumble_TEL_C.id, AirDefence.S_300PS_5P85C_ln.id,
AirDefence.SAM_SA_10_S_300_Grumble_TEL_D.id, AirDefence.S_300PS_5P85D_ln.id,
} }
MEDIUM_RANGE_SAM_UNIT_TYPES = { MEDIUM_RANGE_SAM_UNIT_TYPES = {
AirDefence.SAM_Hawk_LN_M192.id, AirDefence.Hawk_ln.id,
AirDefence.SAM_SA_2_S_75_Guideline_LN.id, AirDefence.S_75M_Volhov.id,
AirDefence.SAM_SA_3_S_125_Goa_LN.id, AirDefence._5p73_s_125_ln.id,
} }
SHORT_RANGE_SAM_UNIT_TYPES = { SHORT_RANGE_SAM_UNIT_TYPES = {
AirDefence.SAM_Avenger__Stinger.id, AirDefence.M1097_Avenger.id,
AirDefence.SAM_Rapier_LN.id, AirDefence.Rapier_fsa_launcher.id,
AirDefence.SAM_SA_19_Tunguska_Grison.id, AirDefence._2S6_Tunguska.id,
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL.id, AirDefence.Strela_1_9P31.id,
} }
AAA_UNIT_TYPES = { AAA_UNIT_TYPES = {
AirDefence.AAA_8_8cm_Flak_18.id, AirDefence.Flak18.id,
AirDefence.SPAAA_Vulcan_M163.id, AirDefence.Vulcan.id,
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish.id, AirDefence.ZSU_23_4_Shilka.id,
} }
EWR_UNIT_TYPE = AirDefence.EWR_1L13.id EWR_UNIT_TYPE = AirDefence._1L13_EWR.id
ARMOR_GROUP_UNIT_TYPE = Armor.MBT_M1A2_Abrams.id ARMOR_GROUP_UNIT_TYPE = Armor.M_1_Abrams.id
FACTORY_UNIT_TYPE = Fortification.Workshop_A.id FACTORY_UNIT_TYPE = Fortification.Workshop_A.id
AMMUNITION_DEPOT_UNIT_TYPE = Warehouse.Ammunition_depot.id AMMUNITION_DEPOT_UNIT_TYPE = Warehouse._Ammunition_depot.id
STRIKE_TARGET_UNIT_TYPE = Fortification.Tech_combine.id STRIKE_TARGET_UNIT_TYPE = Fortification.Tech_combine.id

View File

@@ -16,7 +16,6 @@ from typing import (
Optional, Optional,
Set, Set,
TYPE_CHECKING, TYPE_CHECKING,
Type,
Union, Union,
Sequence, Sequence,
Iterable, Iterable,
@@ -25,14 +24,13 @@ from typing import (
from dcs.mapping import Point from dcs.mapping import Point
from dcs.ships import ( from dcs.ships import (
CVN_74_John_C__Stennis, Stennis,
CV_1143_5_Admiral_Kuznetsov, KUZNECOW,
LHA_1_Tarawa, LHA_Tarawa,
Type_071_Amphibious_Transport_Dock, Type_071,
) )
from dcs.terrain.terrain import Airport, ParkingSlot from dcs.terrain.terrain import Airport, ParkingSlot
from dcs.unit import Unit from dcs.unit import Unit
from dcs.unittype import FlyingType, VehicleType
from game import db from game import db
from game.point_with_heading import PointWithHeading from game.point_with_heading import PointWithHeading
@@ -46,7 +44,8 @@ from .theatergroundobject import (
GenericCarrierGroundObject, GenericCarrierGroundObject,
TheaterGroundObject, TheaterGroundObject,
) )
from ..db import PRICES from ..dcs.aircrafttype import AircraftType
from ..dcs.groundunittype import GroundUnitType
from ..utils import nautical_miles from ..utils import nautical_miles
from ..weather import Conditions from ..weather import Conditions
@@ -125,19 +124,19 @@ class PresetLocations:
@dataclass(frozen=True) @dataclass(frozen=True)
class AircraftAllocations: class AircraftAllocations:
present: dict[Type[FlyingType], int] present: dict[AircraftType, int]
ordered: dict[Type[FlyingType], int] ordered: dict[AircraftType, int]
transferring: dict[Type[FlyingType], int] transferring: dict[AircraftType, int]
@property @property
def total_value(self) -> int: def total_value(self) -> int:
total: int = 0 total: int = 0
for unit_type, count in self.present.items(): for unit_type, count in self.present.items():
total += PRICES[unit_type] * count total += unit_type.price * count
for unit_type, count in self.ordered.items(): for unit_type, count in self.ordered.items():
total += PRICES[unit_type] * count total += unit_type.price * count
for unit_type, count in self.transferring.items(): for unit_type, count in self.transferring.items():
total += PRICES[unit_type] * count total += unit_type.price * count
return total return total
@@ -160,13 +159,13 @@ class AircraftAllocations:
@dataclass(frozen=True) @dataclass(frozen=True)
class GroundUnitAllocations: class GroundUnitAllocations:
present: dict[Type[VehicleType], int] present: dict[GroundUnitType, int]
ordered: dict[Type[VehicleType], int] ordered: dict[GroundUnitType, int]
transferring: dict[Type[VehicleType], int] transferring: dict[GroundUnitType, int]
@property @property
def all(self) -> dict[Type[VehicleType], int]: def all(self) -> dict[GroundUnitType, int]:
combined: dict[Type[VehicleType], int] = defaultdict(int) combined: dict[GroundUnitType, int] = defaultdict(int)
for unit_type, count in itertools.chain( for unit_type, count in itertools.chain(
self.present.items(), self.ordered.items(), self.transferring.items() self.present.items(), self.ordered.items(), self.transferring.items()
): ):
@@ -177,11 +176,11 @@ class GroundUnitAllocations:
def total_value(self) -> int: def total_value(self) -> int:
total: int = 0 total: int = 0
for unit_type, count in self.present.items(): for unit_type, count in self.present.items():
total += PRICES[unit_type] * count total += unit_type.price * count
for unit_type, count in self.ordered.items(): for unit_type, count in self.ordered.items():
total += PRICES[unit_type] * count total += unit_type.price * count
for unit_type, count in self.transferring.items(): for unit_type, count in self.transferring.items():
total += PRICES[unit_type] * count total += unit_type.price * count
return total return total
@@ -486,14 +485,14 @@ class ControlPoint(MissionTarget, 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, Stennis,
CV_1143_5_Admiral_Kuznetsov, KUZNECOW,
]: ]:
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:
for u in group.units: for u in group.units:
if db.unit_type_from_name(u.type) in [LHA_1_Tarawa]: if db.unit_type_from_name(u.type) in [LHA_Tarawa]:
return group.name return group.name
return None return None
@@ -541,27 +540,19 @@ class ControlPoint(MissionTarget, ABC):
while self.base.armor: while self.base.armor:
unit_type, count = self.base.armor.popitem() unit_type, count = self.base.armor.popitem()
for _ in range(count): for _ in range(count):
destination.control_point.base.commision_units({unit_type: 1}) destination.control_point.base.commission_units({unit_type: 1})
destination = heapq.heappushpop(destinations, destination) destination = heapq.heappushpop(destinations, destination)
def capture_aircraft( def capture_aircraft(self, game: Game, airframe: AircraftType, count: int) -> None:
self, game: Game, airframe: Type[FlyingType], count: int value = airframe.price * count
) -> None:
try:
value = PRICES[airframe] * count
except KeyError:
logging.exception(f"Unknown price for {airframe.id}")
return
game.adjust_budget(value, player=not self.captured) game.adjust_budget(value, player=not self.captured)
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 {airframe}"
f"{airframe.id}. {count} aircraft have been captured and sold for " f"{count} aircraft have been captured and sold for ${value}M."
f"${value}M."
) )
def aircraft_retreat_destination( def aircraft_retreat_destination(
self, game: Game, airframe: Type[FlyingType] self, game: Game, airframe: AircraftType
) -> 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.
@@ -579,17 +570,17 @@ class ControlPoint(MissionTarget, ABC):
return None return None
def _retreat_air_units( def _retreat_air_units(
self, game: Game, airframe: Type[FlyingType], count: int self, game: Game, airframe: AircraftType, count: int
) -> None: ) -> None:
while count: while count:
logging.debug(f"Retreating {count} {airframe.id} from {self.name}") logging.debug(f"Retreating {count} {airframe} from {self.name}")
destination = self.aircraft_retreat_destination(game, airframe) destination = self.aircraft_retreat_destination(game, airframe)
if destination is None: if destination is None:
self.capture_aircraft(game, airframe, count) self.capture_aircraft(game, airframe, count)
return return
parking = destination.unclaimed_parking(game) parking = destination.unclaimed_parking(game)
transfer_amount = min([parking, count]) transfer_amount = min([parking, count])
destination.base.commision_units({airframe: transfer_amount}) destination.base.commission_units({airframe: transfer_amount})
count -= transfer_amount count -= transfer_amount
def retreat_air_units(self, game: Game) -> None: def retreat_air_units(self, game: Game) -> None:
@@ -618,16 +609,16 @@ class ControlPoint(MissionTarget, ABC):
self.base.set_strength_to_minimum() self.base.set_strength_to_minimum()
@abstractmethod @abstractmethod
def can_operate(self, aircraft: Type[FlyingType]) -> bool: def can_operate(self, aircraft: AircraftType) -> bool:
... ...
def aircraft_transferring(self, game: Game) -> dict[Type[FlyingType], int]: def aircraft_transferring(self, game: Game) -> dict[AircraftType, int]:
if self.captured: if self.captured:
ato = game.blue_ato ato = game.blue_ato
else: else:
ato = game.red_ato ato = game.red_ato
transferring: defaultdict[Type[FlyingType], int] = defaultdict(int) transferring: defaultdict[AircraftType, int] = defaultdict(int)
for package in ato.packages: for package in ato.packages:
for flight in package.flights: for flight in package.flights:
if flight.departure == flight.arrival: if flight.departure == flight.arrival:
@@ -692,7 +683,7 @@ class ControlPoint(MissionTarget, ABC):
def allocated_aircraft(self, game: Game) -> AircraftAllocations: def allocated_aircraft(self, game: Game) -> AircraftAllocations:
on_order = {} on_order = {}
for unit_bought, count in self.pending_unit_deliveries.units.items(): for unit_bought, count in self.pending_unit_deliveries.units.items():
if issubclass(unit_bought, FlyingType): if isinstance(unit_bought, AircraftType):
on_order[unit_bought] = count on_order[unit_bought] = count
return AircraftAllocations( return AircraftAllocations(
@@ -704,10 +695,10 @@ class ControlPoint(MissionTarget, ABC):
) -> GroundUnitAllocations: ) -> GroundUnitAllocations:
on_order = {} on_order = {}
for unit_bought, count in self.pending_unit_deliveries.units.items(): for unit_bought, count in self.pending_unit_deliveries.units.items():
if issubclass(unit_bought, VehicleType): if isinstance(unit_bought, GroundUnitType):
on_order[unit_bought] = count on_order[unit_bought] = count
transferring: dict[Type[VehicleType], int] = defaultdict(int) transferring: dict[GroundUnitType, int] = defaultdict(int)
for transfer in transfers: for transfer in transfers:
if transfer.destination == self: if transfer.destination == self:
for unit_type, count in transfer.units.items(): for unit_type, count in transfer.units.items():
@@ -788,7 +779,7 @@ class Airfield(ControlPoint):
self.airport = airport self.airport = airport
self._runway_status = RunwayStatus() self._runway_status = RunwayStatus()
def can_operate(self, aircraft: FlyingType) -> bool: def can_operate(self, aircraft: AircraftType) -> bool:
# TODO: Allow helicopters. # TODO: Allow helicopters.
# Need to implement ground spawns so the helos don't use the runway. # Need to implement ground spawns so the helos don't use the runway.
# TODO: Allow harrier. # TODO: Allow harrier.
@@ -810,6 +801,7 @@ class Airfield(ControlPoint):
if self.is_friendly(for_player): if self.is_friendly(for_player):
yield from [ yield from [
FlightType.AEWC, FlightType.AEWC,
FlightType.REFUELING,
# TODO: FlightType.INTERCEPTION # TODO: FlightType.INTERCEPTION
# TODO: FlightType.LOGISTICS # TODO: FlightType.LOGISTICS
] ]
@@ -899,10 +891,10 @@ class NavalControlPoint(ControlPoint, ABC):
for group in self.find_main_tgo().groups: for group in self.find_main_tgo().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, Stennis,
LHA_1_Tarawa, LHA_Tarawa,
CV_1143_5_Admiral_Kuznetsov, KUZNECOW,
Type_071_Amphibious_Transport_Dock, Type_071,
]: ]:
return True return True
return False return False
@@ -959,7 +951,10 @@ class Carrier(NavalControlPoint):
yield from super().mission_types(for_player) yield from super().mission_types(for_player)
if self.is_friendly(for_player): if self.is_friendly(for_player):
yield FlightType.AEWC yield from [
FlightType.AEWC,
FlightType.REFUELING,
]
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")
@@ -968,8 +963,8 @@ class Carrier(NavalControlPoint):
def is_carrier(self): def is_carrier(self):
return True return True
def can_operate(self, aircraft: FlyingType) -> bool: def can_operate(self, aircraft: AircraftType) -> bool:
return aircraft in db.CARRIER_CAPABLE return aircraft.carrier_capable
@property @property
def total_aircraft_parking(self) -> int: def total_aircraft_parking(self) -> int:
@@ -1002,8 +997,8 @@ class Lha(NavalControlPoint):
def is_lha(self) -> bool: def is_lha(self) -> bool:
return True return True
def can_operate(self, aircraft: FlyingType) -> bool: def can_operate(self, aircraft: AircraftType) -> bool:
return aircraft in db.LHA_CAPABLE return aircraft.lha_capable
@property @property
def total_aircraft_parking(self) -> int: def total_aircraft_parking(self) -> int:
@@ -1042,7 +1037,7 @@ class OffMapSpawn(ControlPoint):
def total_aircraft_parking(self) -> int: def total_aircraft_parking(self) -> int:
return 1000 return 1000
def can_operate(self, aircraft: FlyingType) -> bool: def can_operate(self, aircraft: AircraftType) -> bool:
return True return True
@property @property
@@ -1113,7 +1108,7 @@ class Fob(ControlPoint):
def total_aircraft_parking(self) -> int: def total_aircraft_parking(self) -> int:
return 0 return 0
def can_operate(self, aircraft: FlyingType) -> bool: def can_operate(self, aircraft: AircraftType) -> bool:
return False return False
@property @property

View File

@@ -81,6 +81,7 @@ class FrontLine(MissionTarget):
yield from [ yield from [
FlightType.CAS, FlightType.CAS,
FlightType.AEWC, FlightType.AEWC,
FlightType.REFUELING
# TODO: FlightType.TROOP_TRANSPORT # TODO: FlightType.TROOP_TRANSPORT
# TODO: FlightType.EVAC # TODO: FlightType.EVAC
] ]

View File

@@ -78,20 +78,33 @@ class GeneratorSettings:
no_enemy_navy: bool no_enemy_navy: bool
@dataclass
class ModSettings:
a4_skyhawk: bool = False
f22_raptor: bool = False
hercules: bool = False
jas39_gripen: bool = False
su57_felon: bool = False
frenchpack: bool = False
high_digit_sams: bool = False
class GameGenerator: class GameGenerator:
def __init__( def __init__(
self, self,
player: str, player: Faction,
enemy: str, enemy: Faction,
theater: ConflictTheater, theater: ConflictTheater,
settings: Settings, settings: Settings,
generator_settings: GeneratorSettings, generator_settings: GeneratorSettings,
mod_settings: ModSettings,
) -> None: ) -> None:
self.player = player self.player = player
self.enemy = enemy self.enemy = enemy
self.theater = theater self.theater = theater
self.settings = settings self.settings = settings
self.generator_settings = generator_settings self.generator_settings = generator_settings
self.mod_settings = mod_settings
def generate(self) -> Game: def generate(self) -> Game:
with logged_duration("TGO population"): with logged_duration("TGO population"):
@@ -99,8 +112,8 @@ class GameGenerator:
namegen.reset() namegen.reset()
self.prepare_theater() self.prepare_theater()
game = Game( game = Game(
player_name=self.player, player_faction=self.player.apply_mod_settings(self.mod_settings),
enemy_name=self.enemy, enemy_faction=self.enemy.apply_mod_settings(self.mod_settings),
theater=self.theater, theater=self.theater,
start_date=self.generator_settings.start_date, start_date=self.generator_settings.start_date,
settings=self.settings, settings=self.settings,
@@ -159,9 +172,9 @@ class ControlPointGroundObjectGenerator:
@property @property
def faction_name(self) -> str: def faction_name(self) -> str:
if self.control_point.captured: if self.control_point.captured:
return self.game.player_name return self.game.player_faction.name
else: else:
return self.game.enemy_name return self.game.enemy_faction.name
@property @property
def faction(self) -> Faction: def faction(self) -> Faction:

View File

@@ -597,7 +597,8 @@ class EwrGroundObject(TheaterGroundObject):
@property @property
def group_name(self) -> str: def group_name(self) -> str:
# Prefix the group names with the side color so Skynet can find them. # Prefix the group names with the side color so Skynet can find them.
return f"{self.faction_color}|{super().group_name}" # Use Group Id and uppercase EWR
return f"{self.faction_color}|EWR|{self.group_id}"
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

View File

@@ -6,20 +6,19 @@ from collections import defaultdict
from dataclasses import dataclass, field from dataclasses import dataclass, field
from functools import singledispatchmethod from functools import singledispatchmethod
from typing import ( from typing import (
Dict,
Generic, Generic,
Iterator, Iterator,
List, List,
Optional, Optional,
TYPE_CHECKING, TYPE_CHECKING,
Type,
TypeVar, TypeVar,
Sequence, Sequence,
) )
from dcs.mapping import Point from dcs.mapping import Point
from dcs.unittype import FlyingType, VehicleType
from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
from game.procurement import AircraftProcurementRequest from game.procurement import AircraftProcurementRequest
from game.squadrons import Squadron from game.squadrons import Squadron
from game.theater import ControlPoint, MissionTarget from game.theater import ControlPoint, MissionTarget
@@ -29,7 +28,7 @@ from game.theater.transitnetwork import (
) )
from game.utils import meters, nautical_miles from game.utils import meters, nautical_miles
from gen.ato import Package from gen.ato import Package
from gen.flights.ai_flight_planner_db import TRANSPORT_CAPABLE from gen.flights.ai_flight_planner_db import TRANSPORT_CAPABLE, aircraft_for_task
from gen.flights.closestairfields import ObjectiveDistanceCache from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.flights.flight import Flight, FlightType from gen.flights.flight import Flight, FlightType
from gen.flights.flightplan import FlightPlanBuilder from gen.flights.flightplan import FlightPlanBuilder
@@ -72,10 +71,18 @@ class TransferOrder:
player: bool = field(init=False) player: bool = field(init=False)
#: The units being transferred. #: The units being transferred.
units: Dict[Type[VehicleType], int] units: dict[GroundUnitType, int]
transport: Optional[Transport] = field(default=None) transport: Optional[Transport] = field(default=None)
def __str__(self) -> str:
"""Returns the text that should be displayed for the transfer."""
count = self.size
origin = self.origin.name
destination = self.destination.name
description = "Transfer" if self.player else "Enemy transfer"
return f"{description} of {count} units from {origin} to {destination}"
def __post_init__(self) -> None: def __post_init__(self) -> None:
self.position = self.origin self.position = self.origin
self.player = self.origin.is_friendly(to_player=True) self.player = self.origin.is_friendly(to_player=True)
@@ -89,27 +96,27 @@ class TransferOrder:
def kill_all(self) -> None: def kill_all(self) -> None:
self.units.clear() self.units.clear()
def kill_unit(self, unit_type: Type[VehicleType]) -> None: def kill_unit(self, unit_type: GroundUnitType) -> None:
if unit_type not in self.units or not self.units[unit_type]: if unit_type not in self.units or not self.units[unit_type]:
raise KeyError(f"{self.destination} has no {unit_type} remaining") raise KeyError(f"{self} has no {unit_type} remaining")
self.units[unit_type] -= 1 self.units[unit_type] -= 1
@property @property
def size(self) -> int: def size(self) -> int:
return sum(c for c in self.units.values()) return sum(self.units.values())
def iter_units(self) -> Iterator[Type[VehicleType]]: def iter_units(self) -> Iterator[GroundUnitType]:
for unit_type, count in self.units.items(): for unit_type, count in self.units.items():
for _ in range(count): for _ in range(count):
yield unit_type yield unit_type
@property @property
def completed(self) -> bool: def completed(self) -> bool:
return self.destination == self.position or not self.units return self.destination == self.position or not self.size
def disband_at(self, location: ControlPoint) -> None: def disband_at(self, location: ControlPoint) -> None:
logging.info(f"Units halting at {location}.") logging.info(f"Units halting at {location}.")
location.base.commision_units(self.units) location.base.commission_units(self.units)
self.units.clear() self.units.clear()
@property @property
@@ -120,15 +127,17 @@ class TransferOrder:
) )
return self.transport.destination return self.transport.destination
def proceed(self) -> None: def find_escape_route(self) -> Optional[ControlPoint]:
if self.transport is None: if self.transport is not None:
return return self.transport.find_escape_route()
return None
def proceed(self) -> None:
if not self.destination.is_friendly(self.player): if not self.destination.is_friendly(self.player):
logging.info(f"Transfer destination {self.destination} was captured.") logging.info(f"Transfer destination {self.destination} was captured.")
if self.position.is_friendly(self.player): if self.position.is_friendly(self.player):
self.disband_at(self.position) self.disband_at(self.position)
elif (escape_route := self.transport.find_escape_route()) is not None: elif (escape_route := self.find_escape_route()) is not None:
self.disband_at(escape_route) self.disband_at(escape_route)
else: else:
logging.info( logging.info(
@@ -138,6 +147,9 @@ class TransferOrder:
self.kill_all() self.kill_all()
return return
if self.transport is None:
return
self.position = self.next_stop self.position = self.next_stop
self.transport = None self.transport = None
@@ -156,7 +168,7 @@ class Airlift(Transport):
self.flight = flight self.flight = flight
@property @property
def units(self) -> Dict[Type[VehicleType], int]: def units(self) -> dict[GroundUnitType, int]:
return self.transfer.units return self.transfer.units
@property @property
@@ -191,9 +203,9 @@ class AirliftPlanner:
self.package = Package(target=next_stop, auto_asap=True) self.package = Package(target=next_stop, auto_asap=True)
def compatible_with_mission( def compatible_with_mission(
self, unit_type: Type[FlyingType], airfield: ControlPoint self, unit_type: AircraftType, airfield: ControlPoint
) -> bool: ) -> bool:
if not unit_type in TRANSPORT_CAPABLE: if unit_type not in aircraft_for_task(FlightType.TRANSPORT):
return False return False
if not self.transfer.origin.can_operate(unit_type): if not self.transfer.origin.can_operate(unit_type):
return False return False
@@ -201,7 +213,7 @@ class AirliftPlanner:
return False return False
# Cargo planes have no maximum range. # Cargo planes have no maximum range.
if not unit_type.helicopter: if not unit_type.dcs_unit_type.helicopter:
return True return True
# A helicopter that is transport capable and able to operate at both bases. Need # A helicopter that is transport capable and able to operate at both bases. Need
@@ -227,36 +239,46 @@ class AirliftPlanner:
distance_cache = ObjectiveDistanceCache.get_closest_airfields( distance_cache = ObjectiveDistanceCache.get_closest_airfields(
self.transfer.position self.transfer.position
) )
air_wing = self.game.air_wing_for(self.for_player)
for cp in distance_cache.closest_airfields: for cp in distance_cache.closest_airfields:
if cp.captured != self.for_player: if cp.captured != self.for_player:
continue continue
inventory = self.game.aircraft_inventory.for_control_point(cp) inventory = self.game.aircraft_inventory.for_control_point(cp)
for unit_type, available in inventory.all_aircraft: for unit_type, available in inventory.all_aircraft:
squadrons = [ squadrons = air_wing.auto_assignable_for_task_with_type(
s unit_type, FlightType.TRANSPORT
for s in self.game.air_wing_for(self.for_player).squadrons_for( )
unit_type for squadron in squadrons:
) if self.compatible_with_mission(unit_type, cp):
if FlightType.TRANSPORT in s.auto_assignable_mission_types while (
] available
if not squadrons: and squadron.has_available_pilots
continue and self.transfer.transport is None
squadron = squadrons[0] ):
if self.compatible_with_mission(unit_type, cp): flight_size = self.create_airlift_flight(
while available and self.transfer.transport is None: squadron, inventory
flight_size = self.create_airlift_flight(squadron, inventory) )
available -= flight_size available -= flight_size
if self.package.flights: if self.package.flights:
self.game.ato_for(self.for_player).add_package(self.package) self.game.ato_for(self.for_player).add_package(self.package)
def create_airlift_flight( def create_airlift_flight(
self, squadron: Squadron, inventory: ControlPointAircraftInventory self, squadron: Squadron, inventory: ControlPointAircraftInventory
) -> int: ) -> int:
available = inventory.available(squadron.aircraft) available_aircraft = inventory.available(squadron.aircraft)
capacity_each = 1 if squadron.aircraft.helicopter else 2 capacity_each = 1 if squadron.aircraft.dcs_unit_type.helicopter else 2
required = math.ceil(self.transfer.size / capacity_each) required = math.ceil(self.transfer.size / capacity_each)
flight_size = min(required, available, squadron.aircraft.group_size_max) flight_size = min(
required,
available_aircraft,
squadron.aircraft.dcs_unit_type.group_size_max,
)
# TODO: Use number_of_available_pilots directly once feature flag is gone.
# The number of currently available pilots is not relevant when pilot limits
# are disabled.
if not squadron.can_provide_pilots(flight_size):
flight_size = squadron.number_of_available_pilots
capacity = flight_size * capacity_each capacity = flight_size * capacity_each
if capacity < self.transfer.size: if capacity < self.transfer.size:
@@ -308,7 +330,7 @@ class MultiGroupTransport(MissionTarget, Transport):
transfer.transport = None transfer.transport = None
self.transfers.remove(transfer) self.transfers.remove(transfer)
def kill_unit(self, unit_type: Type[VehicleType]) -> None: def kill_unit(self, unit_type: GroundUnitType) -> None:
for transfer in self.transfers: for transfer in self.transfers:
try: try:
transfer.kill_unit(unit_type) transfer.kill_unit(unit_type)
@@ -328,16 +350,21 @@ class MultiGroupTransport(MissionTarget, Transport):
@property @property
def size(self) -> int: def size(self) -> int:
return sum(sum(t.units.values()) for t in self.transfers) return sum(t.size for t in self.transfers)
@property @property
def units(self) -> Dict[Type[VehicleType], int]: def units(self) -> dict[GroundUnitType, int]:
units: Dict[Type[VehicleType], int] = defaultdict(int) units: dict[GroundUnitType, int] = defaultdict(int)
for transfer in self.transfers: for transfer in self.transfers:
for unit_type, count in transfer.units.items(): for unit_type, count in transfer.units.items():
units[unit_type] += count units[unit_type] += count
return units return units
def iter_units(self) -> Iterator[GroundUnitType]:
for unit_type, count in self.units.items():
for _ in range(count):
yield unit_type
@property @property
def player_owned(self) -> bool: def player_owned(self) -> bool:
return self.origin.captured return self.origin.captured
@@ -403,8 +430,8 @@ TransportType = TypeVar("TransportType", bound=MultiGroupTransport)
class TransportMap(Generic[TransportType]): class TransportMap(Generic[TransportType]):
def __init__(self) -> None: def __init__(self) -> None:
# Dict of origin -> destination -> transport. # Dict of origin -> destination -> transport.
self.transports: Dict[ self.transports: dict[
ControlPoint, Dict[ControlPoint, TransportType] ControlPoint, dict[ControlPoint, TransportType]
] = defaultdict(dict) ] = defaultdict(dict)
def create_transport( def create_transport(
@@ -562,7 +589,7 @@ class PendingTransfers:
if transfer.transport is not None: if transfer.transport is not None:
self.cancel_transport(transfer.transport, transfer) self.cancel_transport(transfer.transport, transfer)
self.pending_transfers.remove(transfer) self.pending_transfers.remove(transfer)
transfer.origin.base.commision_units(transfer.units) transfer.origin.base.commission_units(transfer.units)
def perform_transfers(self) -> None: def perform_transfers(self) -> None:
incomplete = [] incomplete = []
@@ -581,7 +608,10 @@ class PendingTransfers:
def order_airlift_assets(self) -> None: def order_airlift_assets(self) -> None:
for control_point in self.game.theater.controlpoints: for control_point in self.game.theater.controlpoints:
self.order_airlift_assets_at(control_point) if self.game.air_wing_for(control_point.captured).can_auto_plan(
FlightType.TRANSPORT
):
self.order_airlift_assets_at(control_point)
@staticmethod @staticmethod
def desired_airlift_capacity(control_point: ControlPoint) -> int: def desired_airlift_capacity(control_point: ControlPoint) -> int:
@@ -589,10 +619,10 @@ class PendingTransfers:
def current_airlift_capacity(self, control_point: ControlPoint) -> int: def current_airlift_capacity(self, control_point: ControlPoint) -> int:
inventory = self.game.aircraft_inventory.for_control_point(control_point) inventory = self.game.aircraft_inventory.for_control_point(control_point)
squadrons = self.game.air_wing_for(control_point.captured).squadrons_for_task( squadrons = self.game.air_wing_for(
FlightType.TRANSPORT control_point.captured
) ).auto_assignable_for_task(FlightType.TRANSPORT)
unit_types = {s.aircraft for s in squadrons}.intersection(TRANSPORT_CAPABLE) unit_types = {s.aircraft for s in squadrons}
return sum( return sum(
count count
for unit_type, count in inventory.all_aircraft for unit_type, count in inventory.all_aircraft

View File

@@ -3,12 +3,11 @@ from __future__ import annotations
import logging import logging
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass
from typing import Dict, Optional, TYPE_CHECKING, Type from typing import Optional, TYPE_CHECKING, Any
from dcs.unittype import UnitType, VehicleType
from game.theater import ControlPoint from game.theater import ControlPoint
from .db import PRICES from .dcs.groundunittype import GroundUnitType
from .dcs.unittype import UnitType
from .theater.transitnetwork import ( from .theater.transitnetwork import (
NoPathError, NoPathError,
TransitNetwork, TransitNetwork,
@@ -29,16 +28,16 @@ class PendingUnitDeliveries:
self.destination = destination self.destination = destination
# Maps unit type to order quantity. # Maps unit type to order quantity.
self.units: Dict[Type[UnitType], int] = defaultdict(int) self.units: dict[UnitType, int] = defaultdict(int)
def __str__(self) -> str: def __str__(self) -> str:
return f"Pending delivery to {self.destination}" return f"Pending delivery to {self.destination}"
def order(self, units: Dict[Type[UnitType], int]) -> None: def order(self, units: dict[UnitType, int]) -> None:
for k, v in units.items(): for k, v in units.items():
self.units[k] += v self.units[k] += v
def sell(self, units: Dict[Type[UnitType], int]) -> None: def sell(self, units: dict[UnitType, int]) -> None:
for k, v in units.items(): for k, v in units.items():
self.units[k] -= v self.units[k] -= v
@@ -46,24 +45,28 @@ class PendingUnitDeliveries:
self.refund(game, self.units) self.refund(game, self.units)
self.units = defaultdict(int) self.units = defaultdict(int)
def refund(self, game: Game, units: Dict[Type[UnitType], int]) -> None: def refund_ground_units(self, game: Game) -> None:
ground_units: dict[UnitType[Any], int] = {
u: self.units[u] for u in self.units.keys() if isinstance(u, GroundUnitType)
}
self.refund(game, ground_units)
for gu in ground_units.keys():
del self.units[gu]
def refund(self, game: Game, units: dict[UnitType, int]) -> None:
for unit_type, count in units.items(): for unit_type, count in units.items():
try: logging.info(f"Refunding {count} {unit_type} at {self.destination.name}")
price = PRICES[unit_type] game.adjust_budget(
except KeyError: unit_type.price * count, player=self.destination.captured
logging.error(f"Could not refund {unit_type.id}, price unknown") )
continue
logging.info(f"Refunding {count} {unit_type.id} at {self.destination.name}") def pending_orders(self, unit_type: UnitType) -> int:
game.adjust_budget(price * count, player=self.destination.captured)
def pending_orders(self, unit_type: Type[UnitType]) -> int:
pending_units = self.units.get(unit_type) pending_units = self.units.get(unit_type)
if pending_units is None: if pending_units is None:
pending_units = 0 pending_units = 0
return pending_units return pending_units
def available_next_turn(self, unit_type: Type[UnitType]) -> int: def available_next_turn(self, unit_type: UnitType) -> int:
current_units = self.destination.base.total_units_of_type(unit_type) current_units = self.destination.base.total_units_of_type(unit_type)
return self.pending_orders(unit_type) + current_units return self.pending_orders(unit_type) + current_units
@@ -74,18 +77,16 @@ class PendingUnitDeliveries:
f"{self.destination.name} lost its source for ground unit " f"{self.destination.name} lost its source for ground unit "
"reinforcements. Refunding purchase price." "reinforcements. Refunding purchase price."
) )
self.refund_all(game) self.refund_ground_units(game)
return
bought_units: Dict[Type[UnitType], int] = {} bought_units: dict[UnitType, int] = {}
units_needing_transfer: Dict[Type[VehicleType], int] = {} units_needing_transfer: dict[GroundUnitType, int] = {}
sold_units: Dict[Type[UnitType], int] = {} sold_units: dict[UnitType, int] = {}
for unit_type, count in self.units.items(): for unit_type, count in self.units.items():
coalition = "Ally" if self.destination.captured else "Enemy" coalition = "Ally" if self.destination.captured else "Enemy"
name = unit_type.id d: dict[Any, int]
if ( if (
issubclass(unit_type, VehicleType) isinstance(unit_type, GroundUnitType)
and self.destination != ground_unit_source and self.destination != ground_unit_source
): ):
source = ground_unit_source source = ground_unit_source
@@ -97,22 +98,27 @@ class PendingUnitDeliveries:
if count >= 0: if count >= 0:
d[unit_type] = count d[unit_type] = count
game.message( game.message(
f"{coalition} reinforcements: {name} x {count} at {source}" f"{coalition} reinforcements: {unit_type} x {count} at {source}"
) )
else: else:
sold_units[unit_type] = -count sold_units[unit_type] = -count
game.message(f"{coalition} sold: {name} x {-count} at {source}") game.message(f"{coalition} sold: {unit_type} x {-count} at {source}")
self.units = defaultdict(int) self.units = defaultdict(int)
self.destination.base.commision_units(bought_units) self.destination.base.commission_units(bought_units)
self.destination.base.commit_losses(sold_units) self.destination.base.commit_losses(sold_units)
if units_needing_transfer: if units_needing_transfer:
ground_unit_source.base.commision_units(units_needing_transfer) if ground_unit_source is None:
raise RuntimeError(
f"ground unit source could not be found for {self.destination} but still tried to "
f"transfer units to there"
)
ground_unit_source.base.commission_units(units_needing_transfer)
self.create_transfer(game, ground_unit_source, units_needing_transfer) self.create_transfer(game, ground_unit_source, units_needing_transfer)
def create_transfer( def create_transfer(
self, game: Game, source: ControlPoint, units: Dict[Type[VehicleType], int] self, game: Game, source: ControlPoint, units: dict[GroundUnitType, int]
) -> None: ) -> None:
game.transfers.new_transfer(TransferOrder(source, self.destination, units)) game.transfers.new_transfer(TransferOrder(source, self.destination, units))

View File

@@ -2,13 +2,12 @@
import itertools import itertools
import math import math
from dataclasses import dataclass from dataclasses import dataclass
from typing import Dict, Optional, Type from typing import Dict, Optional
from dcs.unit import Unit from dcs.unit import Unit
from dcs.unitgroup import FlyingGroup, Group, VehicleGroup from dcs.unitgroup import FlyingGroup, Group, VehicleGroup
from dcs.unittype import VehicleType
from game import db from game.dcs.groundunittype import GroundUnitType
from game.squadrons import Pilot from game.squadrons import Pilot
from game.theater import Airfield, ControlPoint, TheaterGroundObject from game.theater import Airfield, ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import BuildingGroundObject, SceneryGroundObject from game.theater.theatergroundobject import BuildingGroundObject, SceneryGroundObject
@@ -19,12 +18,12 @@ from gen.flights.flight import Flight
@dataclass(frozen=True) @dataclass(frozen=True)
class FlyingUnit: class FlyingUnit:
flight: Flight flight: Flight
pilot: Pilot pilot: Optional[Pilot]
@dataclass(frozen=True) @dataclass(frozen=True)
class FrontLineUnit: class FrontLineUnit:
unit_type: Type[VehicleType] unit_type: GroundUnitType
origin: ControlPoint origin: ControlPoint
@@ -37,13 +36,13 @@ class GroundObjectUnit:
@dataclass(frozen=True) @dataclass(frozen=True)
class ConvoyUnit: class ConvoyUnit:
unit_type: Type[VehicleType] unit_type: GroundUnitType
convoy: Convoy convoy: Convoy
@dataclass(frozen=True) @dataclass(frozen=True)
class AirliftUnits: class AirliftUnits:
cargo: tuple[Type[VehicleType], ...] cargo: tuple[GroundUnitType, ...]
transfer: TransferOrder transfer: TransferOrder
@@ -70,8 +69,6 @@ class UnitMap:
name = str(unit.name) name = str(unit.name)
if name in self.aircraft: if name in self.aircraft:
raise RuntimeError(f"Duplicate unit name: {name}") raise RuntimeError(f"Duplicate unit name: {name}")
if pilot is None:
raise ValueError(f"{name} has no pilot assigned")
self.aircraft[name] = FlyingUnit(flight, pilot) self.aircraft[name] = FlyingUnit(flight, pilot)
if flight.cargo is not None: if flight.cargo is not None:
self.add_airlift_units(group, flight.cargo) self.add_airlift_units(group, flight.cargo)
@@ -87,20 +84,15 @@ class UnitMap:
def airfield(self, name: str) -> Optional[Airfield]: def airfield(self, name: str) -> Optional[Airfield]:
return self.airfields.get(name, None) return self.airfields.get(name, None)
def add_front_line_units(self, group: Group, origin: ControlPoint) -> None: def add_front_line_units(
self, group: Group, origin: ControlPoint, unit_type: GroundUnitType
) -> None:
for unit in group.units: for unit in group.units:
# 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__.
name = str(unit.name) name = str(unit.name)
if name in self.front_line_units: if name in self.front_line_units:
raise RuntimeError(f"Duplicate front line unit: {name}") raise RuntimeError(f"Duplicate front line unit: {name}")
unit_type = db.unit_type_from_name(unit.type)
if unit_type is None:
raise RuntimeError(f"Unknown unit type: {unit.type}")
if not issubclass(unit_type, VehicleType):
raise RuntimeError(
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]:
@@ -143,19 +135,12 @@ class UnitMap:
return self.ground_object_units.get(name, None) return self.ground_object_units.get(name, None)
def add_convoy_units(self, group: Group, convoy: Convoy) -> None: def add_convoy_units(self, group: Group, convoy: Convoy) -> None:
for unit in group.units: for unit, unit_type in zip(group.units, convoy.iter_units()):
# 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__.
name = str(unit.name) name = str(unit.name)
if name in self.convoys: if name in self.convoys:
raise RuntimeError(f"Duplicate convoy unit: {name}") raise RuntimeError(f"Duplicate convoy unit: {name}")
unit_type = db.unit_type_from_name(unit.type)
if unit_type is None:
raise RuntimeError(f"Unknown unit type: {unit.type}")
if not issubclass(unit_type, VehicleType):
raise RuntimeError(
f"{name} is a {unit_type.__name__}, expected a VehicleType"
)
self.convoys[name] = ConvoyUnit(unit_type, convoy) self.convoys[name] = ConvoyUnit(unit_type, convoy)
def convoy_unit(self, name: str) -> Optional[ConvoyUnit]: def convoy_unit(self, name: str) -> Optional[ConvoyUnit]:

View File

@@ -58,6 +58,10 @@ class Distance:
def from_nautical_miles(cls, value: float) -> Distance: def from_nautical_miles(cls, value: float) -> Distance:
return cls(value * NM_TO_METERS) return cls(value * NM_TO_METERS)
@classmethod
def inf(cls) -> Distance:
return cls.from_meters(math.inf)
def __add__(self, other: Distance) -> Distance: def __add__(self, other: Distance) -> Distance:
return meters(self.meters + other.meters) return meters(self.meters + other.meters)

View File

@@ -2,7 +2,7 @@ from pathlib import Path
def _build_version_string() -> str: def _build_version_string() -> str:
components = ["3.0"] components = ["4.0.0"]
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:
@@ -70,9 +70,9 @@ VERSION = _build_version_string()
#: Version 4.2 #: Version 4.2
#: * Adds support for AAA objectives. Place with any of the following units (either red #: * Adds support for AAA objectives. Place with any of the following units (either red
#: or blue): #: or blue):
#: * AAA_8_8cm_Flak_18, #: * Flak18,
#: * SPAAA_Vulcan_M163, #: * Vulcan,
#: * SPAAA_ZSU_23_4_Shilka_Gun_Dish, #: * ZSU_23_4_Shilka,
#: #:
#: Version 5.0 #: Version 5.0
#: * Ammunition Depots objective locations are now predetermined using the "Ammunition #: * Ammunition Depots objective locations are now predetermined using the "Ammunition
@@ -87,4 +87,13 @@ VERSION = _build_version_string()
#: Version 6.0 #: Version 6.0
#: * Random objective generation no is longer supported. Fixed objective locations were #: * Random objective generation no is longer supported. Fixed objective locations were
#: added in 4.1. #: added in 4.1.
CAMPAIGN_FORMAT_VERSION = (6, 0) #:
#: Version 6.1
#: * Support for new Syrian airfields in DCS 2.7.2.7910.1 (Cyprus update).
#:
#: Version 7.0
#: * DCS 2.7.2.7910.1 (Cyprus update) changed the IDs of scenery strike targets. Any
#: mission using map buildings as strike targets must check and potentially recreate
#: all those objectives. This definitely affects all Syria campaigns, other maps are
#: not yet verified.
CAMPAIGN_FORMAT_VERSION = (7, 0)

View File

@@ -12,30 +12,17 @@ from dcs.action import AITaskPush, ActivateGroup
from dcs.condition import CoalitionHasAirdrome, TimeAfter from dcs.condition import CoalitionHasAirdrome, TimeAfter
from dcs.country import Country from dcs.country import Country
from dcs.flyingunit import FlyingUnit from dcs.flyingunit import FlyingUnit
from dcs.helicopters import UH_1H, helicopter_map
from dcs.mapping import Point from dcs.mapping import Point
from dcs.mission import Mission, StartType from dcs.mission import Mission, StartType
from dcs.planes import ( from dcs.planes import (
AJS37, AJS37,
B_17G, B_17G,
B_52H, B_52H,
Bf_109K_4,
C_101CC, C_101CC,
C_101EB, C_101EB,
FW_190A8,
FW_190D9,
F_14B, F_14B,
I_16,
JF_17, JF_17,
Ju_88A4,
P_47D_30,
P_47D_30bl1,
P_47D_40,
P_51D,
P_51D_30_NA,
PlaneType, PlaneType,
SpitfireLFMkIX,
SpitfireLFMkIXCW,
Su_33, Su_33,
Tu_22M3, Tu_22M3,
) )
@@ -43,6 +30,7 @@ from dcs.point import MovingPoint, PointAction
from dcs.task import ( from dcs.task import (
AWACS, AWACS,
AWACSTaskAction, AWACSTaskAction,
ActivateBeaconCommand,
AntishipStrike, AntishipStrike,
AttackGroup, AttackGroup,
Bombing, Bombing,
@@ -61,8 +49,10 @@ from dcs.task import (
OptReactOnThreat, OptReactOnThreat,
OptRestrictJettison, OptRestrictJettison,
OrbitAction, OrbitAction,
Refueling,
RunwayAttack, RunwayAttack,
StartCommand, StartCommand,
Tanker,
Targets, Targets,
Transport, Transport,
WeaponType, WeaponType,
@@ -72,15 +62,14 @@ from dcs.terrain.terrain import Airport, NoParkingSlotError
from dcs.triggers import Event, TriggerOnce, TriggerRule from dcs.triggers import Event, TriggerOnce, TriggerRule
from dcs.unit import Unit, Skill from dcs.unit import Unit, Skill
from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup
from dcs.unittype import FlyingType, UnitType from dcs.unittype import FlyingType
from game import db from game import db
from game.data.cap_capabilities_db import GUNFIGHTERS
from game.data.weapons import Pylon from game.data.weapons import Pylon
from game.db import GUN_RELIANT_AIRFRAMES from game.dcs.aircrafttype import AircraftType
from game.factions.faction import Faction from game.factions.faction import Faction
from game.settings import Settings from game.settings import Settings
from game.squadrons import Pilot, Squadron from game.squadrons import Pilot
from game.theater.controlpoint import ( from game.theater.controlpoint import (
Airfield, Airfield,
ControlPoint, ControlPoint,
@@ -101,15 +90,17 @@ from gen.flights.flight import (
FlightWaypoint, FlightWaypoint,
FlightWaypointType, FlightWaypointType,
) )
from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio from gen.radios import RadioFrequency, RadioRegistry
from gen.runways import RunwayData from gen.runways import RunwayData
from .airsupportgen import AirSupport, AwacsInfo from gen.tacan import TacanBand, TacanRegistry
from .airsupportgen import AirSupport, AwacsInfo, TankerInfo
from .callsigns import callsign_for_support_unit from .callsigns import callsign_for_support_unit
from .flights.flightplan import ( from .flights.flightplan import (
AwacsFlightPlan, AwacsFlightPlan,
CasFlightPlan, CasFlightPlan,
LoiterFlightPlan, LoiterFlightPlan,
PatrollingFlightPlan, PatrollingFlightPlan,
RefuelingFlightPlan,
SweepFlightPlan, SweepFlightPlan,
) )
from .flights.traveltime import GroundSpeed, TotEstimator from .flights.traveltime import GroundSpeed, TotEstimator
@@ -125,16 +116,6 @@ RTB_ALTITUDE = meters(800)
RTB_DISTANCE = 5000 RTB_DISTANCE = 5000
HELI_ALT = 500 HELI_ALT = 500
# Note that fallback radio channels will *not* be reserved. It's possible that
# flights using these will overlap with other channels. This is because we would
# need to make sure we fell back to a frequency that is not used by any beacon
# or ATC, which we don't have the information to predict. Deal with the minor
# annoyance for now since we'll be fleshing out radio info soon enough.
ALLIES_WW2_CHANNEL = MHz(124)
GERMAN_WW2_CHANNEL = MHz(40)
HELICOPTER_CHANNEL = MHz(127)
UHF_FALLBACK_CHANNEL = MHz(251)
TARGET_WAYPOINTS = ( TARGET_WAYPOINTS = (
FlightWaypointType.TARGET_GROUP_LOC, FlightWaypointType.TARGET_GROUP_LOC,
FlightWaypointType.TARGET_POINT, FlightWaypointType.TARGET_POINT,
@@ -142,121 +123,6 @@ TARGET_WAYPOINTS = (
) )
# TODO: Get radio information for all the special cases.
def get_fallback_channel(unit_type: UnitType) -> RadioFrequency:
if unit_type in helicopter_map.values() and unit_type != UH_1H:
return HELICOPTER_CHANNEL
german_ww2_aircraft = [
Bf_109K_4,
FW_190A8,
FW_190D9,
Ju_88A4,
]
if unit_type in german_ww2_aircraft:
return GERMAN_WW2_CHANNEL
allied_ww2_aircraft = [
I_16,
P_47D_30,
P_47D_30bl1,
P_47D_40,
P_51D,
P_51D_30_NA,
SpitfireLFMkIX,
SpitfireLFMkIXCW,
]
if unit_type in allied_ww2_aircraft:
return ALLIES_WW2_CHANNEL
return UHF_FALLBACK_CHANNEL
class ChannelNamer:
"""Base class allowing channel name customization per-aircraft.
Most aircraft will want to customize this behavior, but the default is
reasonable for any aircraft with numbered radios.
"""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
"""Returns the name of the channel for the given radio and channel."""
return f"COMM{radio_id} Ch {channel_id}"
class SingleRadioChannelNamer(ChannelNamer):
"""Channel namer for the aircraft with only a single radio.
Aircraft like the MiG-19P and the MiG-21bis only have a single radio, so
it's not necessary for us to name the radio when naming the channel.
"""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
return f"Ch {channel_id}"
class HueyChannelNamer(ChannelNamer):
"""Channel namer for the UH-1H."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
return f"COM3 Ch {channel_id}"
class MirageChannelNamer(ChannelNamer):
"""Channel namer for the M-2000."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
radio_name = ["V/UHF", "UHF"][radio_id - 1]
return f"{radio_name} Ch {channel_id}"
class TomcatChannelNamer(ChannelNamer):
"""Channel namer for the F-14."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
radio_name = ["UHF", "VHF/UHF"][radio_id - 1]
return f"{radio_name} Ch {channel_id}"
class ViggenChannelNamer(ChannelNamer):
"""Channel namer for the AJS37."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
if channel_id >= 4:
channel_letter = "EFGH"[channel_id - 4]
return f"FR 24 {channel_letter}"
return f"FR 22 Special {channel_id}"
class ViperChannelNamer(ChannelNamer):
"""Channel namer for the F-16."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
return f"COM{radio_id} Ch {channel_id}"
class SCR522ChannelNamer(ChannelNamer):
"""
Channel namer for P-51 & P-47D
"""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
if channel_id > 3:
return "?"
else:
return f"Button " + "ABCD"[channel_id - 1]
@dataclass(frozen=True) @dataclass(frozen=True)
class ChannelAssignment: class ChannelAssignment:
radio_id: int radio_id: int
@@ -270,9 +136,6 @@ class FlightData:
#: The package that the flight belongs to. #: The package that the flight belongs to.
package: Package package: Package
#: The country that the flight belongs to.
country: str
flight_type: FlightType flight_type: FlightType
#: All units in the flight. #: All units in the flight.
@@ -313,7 +176,7 @@ class FlightData:
def __init__( def __init__(
self, self,
package: Package, package: Package,
country: str, aircraft_type: AircraftType,
flight_type: FlightType, flight_type: FlightType,
units: List[FlyingUnit], units: List[FlyingUnit],
size: int, size: int,
@@ -329,7 +192,7 @@ class FlightData:
custom_name: Optional[str], custom_name: Optional[str],
) -> None: ) -> None:
self.package = package self.package = package
self.country = country self.aircraft_type = aircraft_type
self.flight_type = flight_type self.flight_type = flight_type
self.units = units self.units = units
self.size = size self.size = size
@@ -351,11 +214,6 @@ class FlightData:
"""List of playable units in the flight.""" """List of playable units in the flight."""
return [u for u in self.units if u.is_human()] return [u for u in self.units if u.is_human()]
@property
def aircraft_type(self) -> FlyingType:
"""Returns the type of aircraft in this flight."""
return self.units[0].unit_type
def num_radio_channels(self, radio_id: int) -> int: def num_radio_channels(self, radio_id: int) -> int:
"""Returns the number of preset channels for the given radio.""" """Returns the number of preset channels for the given radio."""
# Note: pydcs only initializes the radio presets for client slots. # Note: pydcs only initializes the radio presets for client slots.
@@ -381,302 +239,6 @@ class FlightData:
) )
class RadioChannelAllocator:
"""Base class for radio channel allocators."""
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
) -> None:
"""Assigns mission frequencies to preset channels for the flight."""
raise NotImplementedError
@dataclass(frozen=True)
class CommonRadioChannelAllocator(RadioChannelAllocator):
"""Radio channel allocator suitable for most aircraft.
Most of the aircraft with preset channels available have one or more radios
with 20 or more channels available (typically per-radio, but this is not the
case for the JF-17).
"""
#: Index of the radio used for intra-flight communications. Matches the
#: index of the panel_radio field of the pydcs.dcs.planes object.
inter_flight_radio_index: Optional[int]
#: Index of the radio used for intra-flight communications. Matches the
#: index of the panel_radio field of the pydcs.dcs.planes object.
intra_flight_radio_index: Optional[int]
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
) -> None:
if self.intra_flight_radio_index is not None:
flight.assign_channel(
self.intra_flight_radio_index, 1, flight.intra_flight_channel
)
if self.inter_flight_radio_index is None:
return
# For cases where the inter-flight and intra-flight radios share presets
# (the JF-17 only has one set of channels, even though it can use two
# channels simultaneously), start assigning inter-flight channels at 2.
radio_id = self.inter_flight_radio_index
if self.intra_flight_radio_index == radio_id:
first_channel = 2
else:
first_channel = 1
last_channel = flight.num_radio_channels(radio_id)
channel_alloc = iter(range(first_channel, last_channel + 1))
if flight.departure.atc is not None:
flight.assign_channel(radio_id, next(channel_alloc), flight.departure.atc)
# TODO: If there ever are multiple AWACS, limit to mission relevant.
for awacs in air_support.awacs:
flight.assign_channel(radio_id, next(channel_alloc), awacs.freq)
if flight.arrival != flight.departure and flight.arrival.atc is not None:
flight.assign_channel(radio_id, next(channel_alloc), flight.arrival.atc)
try:
# TODO: Skip incompatible tankers.
for tanker in air_support.tankers:
flight.assign_channel(radio_id, next(channel_alloc), tanker.freq)
if flight.divert is not None and flight.divert.atc is not None:
flight.assign_channel(radio_id, next(channel_alloc), flight.divert.atc)
except StopIteration:
# Any remaining channels are nice-to-haves, but not necessary for
# the few aircraft with a small number of channels available.
pass
@dataclass(frozen=True)
class NoOpChannelAllocator(RadioChannelAllocator):
"""Channel allocator for aircraft that don't support preset channels."""
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
) -> None:
pass
@dataclass(frozen=True)
class FarmerRadioChannelAllocator(RadioChannelAllocator):
"""Preset channel allocator for the MiG-19P."""
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
) -> None:
# The Farmer only has 6 preset channels. It also only has a VHF radio,
# and currently our ATC data and AWACS are only in the UHF band.
radio_id = 1
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
# TODO: Assign 4-6 to VHF frequencies of departure, arrival, and divert.
# TODO: Assign 2 and 3 to AWACS if it is VHF.
@dataclass(frozen=True)
class ViggenRadioChannelAllocator(RadioChannelAllocator):
"""Preset channel allocator for the AJS37."""
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
) -> None:
# The Viggen's preset channels are handled differently from other
# aircraft. The aircraft automatically configures channels for every
# allied flight in the game (including AWACS) and for every airfield. As
# such, we don't need to allocate any of those. There are seven presets
# we can modify, however: three channels for the main radio intended for
# communication with wingmen, and four emergency channels for the backup
# radio. We'll set the first channel of the main radio to the
# intra-flight channel, and the first three emergency channels to each
# of the flight plan's airfields. The fourth emergency channel is always
# the guard channel.
radio_id = 1
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
if flight.departure.atc is not None:
flight.assign_channel(radio_id, 4, flight.departure.atc)
if flight.arrival.atc is not None:
flight.assign_channel(radio_id, 5, flight.arrival.atc)
# TODO: Assign divert to 6 when we support divert airfields.
@dataclass(frozen=True)
class SCR522RadioChannelAllocator(RadioChannelAllocator):
"""Preset channel allocator for the SCR522 WW2 radios. (4 channels)"""
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
) -> None:
radio_id = 1
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
if flight.departure.atc is not None:
flight.assign_channel(radio_id, 2, flight.departure.atc)
if flight.arrival.atc is not None:
flight.assign_channel(radio_id, 3, flight.arrival.atc)
# TODO : Some GCI on Channel 4 ?
@dataclass(frozen=True)
class AircraftData:
"""Additional aircraft data not exposed by pydcs."""
#: The type of radio used for inter-flight communications.
inter_flight_radio: Radio
#: The type of radio used for intra-flight communications.
intra_flight_radio: Radio
#: The radio preset channel allocator, if the aircraft supports channel
#: presets. If the aircraft does not support preset channels, this will be
#: None.
channel_allocator: Optional[RadioChannelAllocator]
#: Defines how channels should be named when printed in the kneeboard.
channel_namer: Type[ChannelNamer] = ChannelNamer
# Indexed by the id field of the pydcs PlaneType.
AIRCRAFT_DATA: Dict[str, AircraftData] = {
"A-10C": AircraftData(
inter_flight_radio=get_radio("AN/ARC-164"),
# VHF for intraflight is not accepted anymore by DCS
# (see https://forums.eagle.ru/showthread.php?p=4499738).
intra_flight_radio=get_radio("AN/ARC-164"),
channel_allocator=NoOpChannelAllocator(),
),
"AJS37": AircraftData(
# The AJS37 has somewhat unique radio configuration. Two backup radio
# (FR 24) can only operate simultaneously with the main radio in guard
# mode. As such, we only use the main radio for both inter- and intra-
# flight communication.
inter_flight_radio=get_radio("FR 22"),
intra_flight_radio=get_radio("FR 22"),
channel_allocator=ViggenRadioChannelAllocator(),
channel_namer=ViggenChannelNamer,
),
"AV8BNA": AircraftData(
inter_flight_radio=get_radio("AN/ARC-210"),
intra_flight_radio=get_radio("AN/ARC-210"),
channel_allocator=CommonRadioChannelAllocator(
inter_flight_radio_index=2, intra_flight_radio_index=1
),
),
"F-14B": AircraftData(
inter_flight_radio=get_radio("AN/ARC-159"),
intra_flight_radio=get_radio("AN/ARC-182"),
channel_allocator=CommonRadioChannelAllocator(
inter_flight_radio_index=1, intra_flight_radio_index=2
),
channel_namer=TomcatChannelNamer,
),
"F-16C_50": AircraftData(
inter_flight_radio=get_radio("AN/ARC-164"),
intra_flight_radio=get_radio("AN/ARC-222"),
# COM2 is the AN/ARC-222, which is the VHF radio we want to use for
# intra-flight communication to leave COM1 open for UHF inter-flight.
channel_allocator=CommonRadioChannelAllocator(
inter_flight_radio_index=1, intra_flight_radio_index=2
),
channel_namer=ViperChannelNamer,
),
"FA-18C_hornet": AircraftData(
inter_flight_radio=get_radio("AN/ARC-210"),
intra_flight_radio=get_radio("AN/ARC-210"),
# DCS will clobber channel 1 of the first radio compatible with the
# flight's assigned frequency. Since the F/A-18's two radios are both
# AN/ARC-210s, radio 1 will be compatible regardless of which frequency
# is assigned, so we must use radio 1 for the intra-flight radio.
channel_allocator=CommonRadioChannelAllocator(
inter_flight_radio_index=2, intra_flight_radio_index=1
),
),
"JF-17": AircraftData(
inter_flight_radio=get_radio("R&S M3AR UHF"),
intra_flight_radio=get_radio("R&S M3AR VHF"),
channel_allocator=CommonRadioChannelAllocator(
inter_flight_radio_index=1, intra_flight_radio_index=1
),
# Same naming pattern as the Viper, so just reuse that.
channel_namer=ViperChannelNamer,
),
"Ka-50": AircraftData(
inter_flight_radio=get_radio("R-800L1"),
intra_flight_radio=get_radio("R-800L1"),
# The R-800L1 doesn't have preset channels, and the other radio is for
# communications with FAC and ground units, which don't currently have
# radios assigned, so no channels to configure.
channel_allocator=NoOpChannelAllocator(),
),
"M-2000C": AircraftData(
inter_flight_radio=get_radio("TRT ERA 7000 V/UHF"),
intra_flight_radio=get_radio("TRT ERA 7200 UHF"),
channel_allocator=CommonRadioChannelAllocator(
inter_flight_radio_index=1, intra_flight_radio_index=2
),
channel_namer=MirageChannelNamer,
),
"MiG-15bis": AircraftData(
inter_flight_radio=get_radio("RSI-6K HF"),
intra_flight_radio=get_radio("RSI-6K HF"),
channel_allocator=NoOpChannelAllocator(),
),
"MiG-19P": AircraftData(
inter_flight_radio=get_radio("RSIU-4V"),
intra_flight_radio=get_radio("RSIU-4V"),
channel_allocator=FarmerRadioChannelAllocator(),
channel_namer=SingleRadioChannelNamer,
),
"MiG-21Bis": AircraftData(
inter_flight_radio=get_radio("RSIU-5V"),
intra_flight_radio=get_radio("RSIU-5V"),
channel_allocator=CommonRadioChannelAllocator(
inter_flight_radio_index=1, intra_flight_radio_index=1
),
channel_namer=SingleRadioChannelNamer,
),
"P-51D": AircraftData(
inter_flight_radio=get_radio("SCR522"),
intra_flight_radio=get_radio("SCR522"),
channel_allocator=CommonRadioChannelAllocator(
inter_flight_radio_index=1, intra_flight_radio_index=1
),
channel_namer=SCR522ChannelNamer,
),
"UH-1H": AircraftData(
inter_flight_radio=get_radio("AN/ARC-51BX"),
# Ideally this would use the AN/ARC-131 because that radio is supposed
# to be used for flight comms, but DCS won't allow it as the flight's
# frequency, nor will it allow the AN/ARC-134.
intra_flight_radio=get_radio("AN/ARC-51BX"),
channel_allocator=CommonRadioChannelAllocator(
inter_flight_radio_index=1, intra_flight_radio_index=1
),
channel_namer=HueyChannelNamer,
),
"F-22A": AircraftData(
inter_flight_radio=get_radio("SCR-522"),
intra_flight_radio=get_radio("SCR-522"),
channel_allocator=None,
channel_namer=SCR522ChannelNamer,
),
"JAS39Gripen": AircraftData(
inter_flight_radio=get_radio("R&S Series 6000"),
intra_flight_radio=get_radio("R&S Series 6000"),
channel_allocator=None,
),
}
AIRCRAFT_DATA["A-10C_2"] = AIRCRAFT_DATA["A-10C"]
AIRCRAFT_DATA["P-51D-30-NA"] = AIRCRAFT_DATA["P-51D"]
AIRCRAFT_DATA["P-47D-30"] = AIRCRAFT_DATA["P-51D"]
AIRCRAFT_DATA["JAS39Gripen_AG"] = AIRCRAFT_DATA["JAS39Gripen"]
class AircraftConflictGenerator: class AircraftConflictGenerator:
def __init__( def __init__(
self, self,
@@ -684,6 +246,7 @@ class AircraftConflictGenerator:
settings: Settings, settings: Settings,
game: Game, game: Game,
radio_registry: RadioRegistry, radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,
unit_map: UnitMap, unit_map: UnitMap,
air_support: AirSupport, air_support: AirSupport,
) -> None: ) -> None:
@@ -691,6 +254,7 @@ class AircraftConflictGenerator:
self.game = game self.game = game
self.settings = settings self.settings = settings
self.radio_registry = radio_registry self.radio_registry = radio_registry
self.tacan_registy = tacan_registry
self.unit_map = unit_map self.unit_map = unit_map
self.flights: List[FlightData] = [] self.flights: List[FlightData] = []
self.air_support = air_support self.air_support = air_support
@@ -710,21 +274,6 @@ class AircraftConflictGenerator:
total += flight.client_count total += flight.client_count
return total return total
def get_intra_flight_channel(self, airframe: UnitType) -> RadioFrequency:
"""Allocates an intra-flight channel to a group.
Args:
airframe: The type of aircraft a channel should be allocated for.
Returns:
The frequency of the intra-flight channel.
"""
try:
aircraft_data = AIRCRAFT_DATA[airframe.id]
return self.radio_registry.alloc_for_radio(aircraft_data.intra_flight_radio)
except KeyError:
return get_fallback_channel(airframe)
@staticmethod @staticmethod
def _start_type(start_type: str) -> StartType: def _start_type(start_type: str) -> StartType:
if start_type == "Runway": if start_type == "Runway":
@@ -754,7 +303,10 @@ class AircraftConflictGenerator:
current_level = levels.index(base_skill) current_level = levels.index(base_skill)
missions_for_skill_increase = 4 missions_for_skill_increase = 4
increase = pilot.record.missions_flown // missions_for_skill_increase increase = pilot.record.missions_flown // missions_for_skill_increase
new_level = min(current_level + increase, len(levels) - 1) capped_increase = min(current_level + increase, len(levels) - 1)
new_level = (capped_increase, current_level)[
self.game.settings.ai_pilot_levelling
]
return levels[new_level] return levels[new_level]
def set_skill(self, unit: FlyingUnit, pilot: Optional[Pilot], blue: bool) -> None: def set_skill(self, unit: FlyingUnit, pilot: Optional[Pilot], blue: bool) -> None:
@@ -824,10 +376,13 @@ class AircraftConflictGenerator:
OptReactOnThreat(OptReactOnThreat.Values.EvadeFire) OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)
) )
if flight.flight_type == FlightType.AEWC: if (
flight.flight_type == FlightType.AEWC
or flight.flight_type == FlightType.REFUELING
):
channel = self.radio_registry.alloc_uhf() channel = self.radio_registry.alloc_uhf()
else: else:
channel = self.get_intra_flight_channel(unit_type) channel = flight.unit_type.alloc_flight_radio(self.radio_registry)
group.set_frequency(channel.mhz) group.set_frequency(channel.mhz)
divert = None divert = None
@@ -837,7 +392,7 @@ class AircraftConflictGenerator:
self.flights.append( self.flights.append(
FlightData( FlightData(
package=package, package=package,
country=self.game.faction_for(player=flight.departure.captured).country, aircraft_type=flight.unit_type,
flight_type=flight.flight_type, flight_type=flight.flight_type,
units=group.units, units=group.units,
size=len(group.units), size=len(group.units),
@@ -879,6 +434,23 @@ class AircraftConflictGenerator:
) )
) )
if isinstance(flight.flight_plan, RefuelingFlightPlan):
callsign = callsign_for_support_unit(group)
tacan = self.tacan_registy.alloc_for_band(TacanBand.Y)
self.air_support.tankers.append(
TankerInfo(
group_name=str(group.name),
callsign=callsign,
variant=flight.unit_type.name,
freq=channel,
tacan=tacan,
start_time=flight.flight_plan.patrol_start_time,
end_time=flight.flight_plan.patrol_end_time,
blue=flight.departure.captured,
)
)
def _generate_at_airport( def _generate_at_airport(
self, self,
name: str, name: str,
@@ -929,7 +501,7 @@ class AircraftConflictGenerator:
group = self.m.flight_group( group = self.m.flight_group(
country=side, country=side,
name=name, name=name,
aircraft_type=flight.unit_type, aircraft_type=flight.unit_type.dcs_unit_type,
airport=None, airport=None,
position=pos, position=pos,
altitude=alt.meters, altitude=alt.meters,
@@ -1063,7 +635,7 @@ class AircraftConflictGenerator:
control_point: Airfield, control_point: Airfield,
country: Country, country: Country,
faction: Faction, faction: Faction,
aircraft: Type[FlyingType], aircraft: AircraftType,
number: int, number: int,
) -> None: ) -> None:
for _ in range(number): for _ in range(number):
@@ -1085,7 +657,7 @@ class AircraftConflictGenerator:
group = self._generate_at_airport( group = self._generate_at_airport(
name=namegen.next_aircraft_name(country, control_point.id, flight), name=namegen.next_aircraft_name(country, control_point.id, flight),
side=country, side=country,
unit_type=aircraft, unit_type=aircraft.dcs_unit_type,
count=1, count=1,
start_type="Cold", start_type="Cold",
airport=control_point.airport, airport=control_point.airport,
@@ -1159,7 +731,7 @@ class AircraftConflictGenerator:
group = self._generate_at_group( group = self._generate_at_group(
name=name, name=name,
side=country, side=country,
unit_type=flight.unit_type, unit_type=flight.unit_type.dcs_unit_type,
count=flight.count, count=flight.count,
start_type=flight.start_type, start_type=flight.start_type,
at=self.m.find_group(group_name), at=self.m.find_group(group_name),
@@ -1172,7 +744,7 @@ class AircraftConflictGenerator:
group = self._generate_at_airport( group = self._generate_at_airport(
name=name, name=name,
side=country, side=country,
unit_type=flight.unit_type, unit_type=flight.unit_type.dcs_unit_type,
count=flight.count, count=flight.count,
start_type=flight.start_type, start_type=flight.start_type,
airport=cp.airport, airport=cp.airport,
@@ -1214,7 +786,7 @@ class AircraftConflictGenerator:
if flight.client_count > 0: if flight.client_count > 0:
return True return True
return flight.unit_type in GUN_RELIANT_AIRFRAMES return flight.unit_type.always_keeps_gun
def configure_behavior( def configure_behavior(
self, self,
@@ -1253,9 +825,8 @@ class AircraftConflictGenerator:
@staticmethod @staticmethod
def configure_eplrs(group: FlyingGroup, flight: Flight) -> None: def configure_eplrs(group: FlyingGroup, flight: Flight) -> None:
if hasattr(flight.unit_type, "eplrs"): if flight.unit_type.eplrs_capable:
if flight.unit_type.eplrs: group.points[0].tasks.append(EPLRS(group.id))
group.points[0].tasks.append(EPLRS(group.id))
def configure_cap( def configure_cap(
self, self,
@@ -1267,7 +838,7 @@ class AircraftConflictGenerator:
group.task = CAP.name group.task = CAP.name
self._setup_group(group, package, flight, dynamic_runways) self._setup_group(group, package, flight, dynamic_runways)
if flight.unit_type not in GUNFIGHTERS: if not flight.unit_type.gunfighter:
ammo_type = OptRTBOnOutOfAmmo.Values.AAM ammo_type = OptRTBOnOutOfAmmo.Values.AAM
else: else:
ammo_type = OptRTBOnOutOfAmmo.Values.Cannon ammo_type = OptRTBOnOutOfAmmo.Values.Cannon
@@ -1284,7 +855,7 @@ class AircraftConflictGenerator:
group.task = FighterSweep.name group.task = FighterSweep.name
self._setup_group(group, package, flight, dynamic_runways) self._setup_group(group, package, flight, dynamic_runways)
if flight.unit_type not in GUNFIGHTERS: if not flight.unit_type.gunfighter:
ammo_type = OptRTBOnOutOfAmmo.Values.AAM ammo_type = OptRTBOnOutOfAmmo.Values.AAM
else: else:
ammo_type = OptRTBOnOutOfAmmo.Values.Cannon ammo_type = OptRTBOnOutOfAmmo.Values.Cannon
@@ -1457,6 +1028,32 @@ class AircraftConflictGenerator:
group.points[0].tasks.append(AWACSTaskAction()) group.points[0].tasks.append(AWACSTaskAction())
def configure_refueling(
self,
group: FlyingGroup,
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
) -> None:
group.task = Refueling.name
if not isinstance(flight.flight_plan, RefuelingFlightPlan):
logging.error(
f"Cannot configure racetrack refueling tasks for {flight} because it "
"does not have an racetrack refueling flight plan."
)
return
self._setup_group(group, package, flight, dynamic_runways)
self.configure_behavior(
flight,
group,
react_on_threat=OptReactOnThreat.Values.EvadeFire,
roe=OptROE.Values.WeaponHold,
restrict_jettison=True,
)
def configure_escort( def configure_escort(
self, self,
group: FlyingGroup, group: FlyingGroup,
@@ -1535,6 +1132,8 @@ class AircraftConflictGenerator:
self.configure_sweep(group, package, flight, dynamic_runways) self.configure_sweep(group, package, flight, dynamic_runways)
elif flight_type == FlightType.AEWC: elif flight_type == FlightType.AEWC:
self.configure_awacs(group, package, flight, dynamic_runways) self.configure_awacs(group, package, flight, dynamic_runways)
elif flight_type == FlightType.REFUELING:
self.configure_refueling(group, package, flight, dynamic_runways)
elif flight_type in [FlightType.CAS, FlightType.BAI]: elif flight_type in [FlightType.CAS, FlightType.BAI]:
self.configure_cas(group, package, flight, dynamic_runways) self.configure_cas(group, package, flight, dynamic_runways)
elif flight_type == FlightType.DEAD: elif flight_type == FlightType.DEAD:
@@ -1581,7 +1180,7 @@ class AircraftConflictGenerator:
# under the current flight plans. # under the current flight plans.
# TODO: Make this smarter, it currently selects a random unit in the group for target, # TODO: Make this smarter, it currently selects a random unit in the group for target,
# this could be updated to make it pick the "best" two targets in the group. # this could be updated to make it pick the "best" two targets in the group.
if flight.unit_type is AJS37 and flight.client_count: if flight.unit_type.dcs_unit_type is AJS37 and flight.client_count:
viggen_target_points = [ viggen_target_points = [
(idx, point) (idx, point)
for idx, point in enumerate(filtered_points) for idx, point in enumerate(filtered_points)
@@ -1602,7 +1201,7 @@ class AircraftConflictGenerator:
for idx, point in enumerate(filtered_points): for idx, point in enumerate(filtered_points):
PydcsWaypointBuilder.for_waypoint( PydcsWaypointBuilder.for_waypoint(
point, group, package, flight, self.m point, group, package, flight, self.m, self.air_support
).build() ).build()
# Set here rather than when the FlightData is created so they waypoints # Set here rather than when the FlightData is created so they waypoints
@@ -1676,12 +1275,14 @@ class PydcsWaypointBuilder:
package: Package, package: Package,
flight: Flight, flight: Flight,
mission: Mission, mission: Mission,
air_support: AirSupport,
) -> None: ) -> None:
self.waypoint = waypoint self.waypoint = waypoint
self.group = group self.group = group
self.package = package self.package = package
self.flight = flight self.flight = flight
self.mission = mission self.mission = mission
self.air_support = air_support
def build(self) -> MovingPoint: def build(self) -> MovingPoint:
waypoint = self.group.add_waypoint( waypoint = self.group.add_waypoint(
@@ -1717,6 +1318,7 @@ class PydcsWaypointBuilder:
package: Package, package: Package,
flight: Flight, flight: Flight,
mission: Mission, mission: Mission,
air_support: AirSupport,
) -> PydcsWaypointBuilder: ) -> PydcsWaypointBuilder:
builders = { builders = {
FlightWaypointType.DROP_OFF: CargoStopBuilder, FlightWaypointType.DROP_OFF: CargoStopBuilder,
@@ -1736,15 +1338,16 @@ class PydcsWaypointBuilder:
FlightWaypointType.PICKUP: CargoStopBuilder, FlightWaypointType.PICKUP: CargoStopBuilder,
} }
builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder) builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder)
return builder(waypoint, group, package, flight, mission) return builder(waypoint, group, package, flight, mission, air_support)
def _viggen_client_tot(self) -> bool: def _viggen_client_tot(self) -> bool:
"""Viggen player aircraft consider any waypoint with a TOT set to be a target ("M") waypoint. """Viggen player aircraft consider any waypoint with a TOT set to be a target ("M") waypoint.
If the flight is a player controlled Viggen flight, no TOT should be set on any waypoint except actual target waypoints. If the flight is a player controlled Viggen flight, no TOT should be set on any waypoint except actual target waypoints.
""" """
if (self.flight.client_count > 0 and self.flight.unit_type == AJS37) and ( if (
self.waypoint.waypoint_type not in TARGET_WAYPOINTS self.flight.client_count > 0
): and self.flight.unit_type.dcs_unit_type == AJS37
) and (self.waypoint.waypoint_type not in TARGET_WAYPOINTS):
return True return True
else: else:
return False return False
@@ -2119,6 +1722,8 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
# is their first priority and they will not engage any targets because # is their first priority and they will not engage any targets because
# they're fully focused on orbiting. If the STE task is first, they will # they're fully focused on orbiting. If the STE task is first, they will
# engage targets if available and orbit if they find nothing to shoot. # engage targets if available and orbit if they find nothing to shoot.
if self.flight.flight_type is FlightType.REFUELING:
self.configure_refueling_actions(waypoint)
# TODO: Move the properties of this task into the flight plan? # TODO: Move the properties of this task into the flight plan?
# CAP is the only current user of this so it's not a big deal, but might # CAP is the only current user of this so it's not a big deal, but might
@@ -2133,17 +1738,48 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
) )
) )
racetrack = ControlledTask( # TODO: Set orbit speeds for all race tracks and remove this special case.
OrbitAction( if isinstance(flight_plan, RefuelingFlightPlan):
orbit = OrbitAction(
altitude=waypoint.alt,
pattern=OrbitAction.OrbitPattern.RaceTrack,
speed=int(flight_plan.patrol_speed.kph),
)
else:
orbit = OrbitAction(
altitude=waypoint.alt, pattern=OrbitAction.OrbitPattern.RaceTrack altitude=waypoint.alt, pattern=OrbitAction.OrbitPattern.RaceTrack
) )
)
racetrack = ControlledTask(orbit)
self.set_waypoint_tot(waypoint, flight_plan.patrol_start_time) self.set_waypoint_tot(waypoint, flight_plan.patrol_start_time)
racetrack.stop_after_time(int(flight_plan.patrol_end_time.total_seconds())) racetrack.stop_after_time(int(flight_plan.patrol_end_time.total_seconds()))
waypoint.add_task(racetrack) waypoint.add_task(racetrack)
return waypoint return waypoint
def configure_refueling_actions(self, waypoint: MovingPoint) -> None:
waypoint.add_task(Tanker())
if self.flight.unit_type.dcs_unit_type.tacan:
tanker_info = self.air_support.tankers[-1]
tacan = tanker_info.tacan
tacan_callsign = {
"Texaco": "TEX",
"Arco": "ARC",
"Shell": "SHL",
}.get(tanker_info.callsign)
waypoint.add_task(
ActivateBeaconCommand(
tacan.number,
tacan.band.value,
tacan_callsign,
bearing=True,
unit_id=self.group.units[0].id,
aa=True,
)
)
class RaceTrackEndBuilder(PydcsWaypointBuilder): class RaceTrackEndBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint: def build(self) -> MovingPoint:

View File

@@ -966,7 +966,96 @@ AIRFIELD_DATA = {
runway_length=8871, runway_length=8871,
atc=AtcData(MHz(3, 775), MHz(38, 450), MHz(120, 100), MHz(250, 50)), atc=AtcData(MHz(3, 775), MHz(38, 450), MHz(120, 100), MHz(250, 50)),
ils={ ils={
"28": ("IGNP", MHz(109, 10)), "28": ("IGNP", MHz(109, 100)),
},
),
"Gecitkale": AirfieldData(
theater="Syria",
icao="LCGK",
elevation=147,
runway_length=8156,
vor=("GKE", MHz(114, 300)),
atc=AtcData(MHz(3, 775), MHz(4, 800), MHz(40, 500), MHz(252, 50)),
),
"Kingsfield": AirfieldData(
theater="Syria",
icao="CY-0004",
elevation=270,
runway_length=3069,
atc=AtcData(MHz(4, 650), MHz(40, 200), MHz(121), MHz(251, 750)),
),
"Larnaca": AirfieldData(
theater="Syria",
icao="LCRE",
elevation=16,
runway_length=8009,
vor=("LCA", MHz(112, 80)),
atc=AtcData(MHz(4, 700), MHz(40, 300), MHz(121, 200), MHz(251, 850)),
ils={
"22": ("ILC", MHz(110, 300)),
},
),
"Ercan": AirfieldData(
theater="Syria",
icao="LCEN",
elevation=383,
runway_length=7559,
vor=("ECN", MHz(117)),
atc=AtcData(MHz(4, 750), MHz(40, 400), MHz(120, 200), MHz(251, 950)),
),
"Lakatamia": AirfieldData(
theater="Syria",
icao="CY-0001",
elevation=757,
runway_length=1230,
atc=AtcData(MHz(4, 725), MHz(40, 350), MHz(120, 200), MHz(251, 900)),
),
"Nicosia": AirfieldData(
theater="Syria",
icao="LCNC",
elevation=716,
runway_length=0,
),
"Pinarbashi": AirfieldData(
theater="Syria",
icao="CY-0003",
elevation=770,
runway_length=3364,
atc=AtcData(MHz(4, 825), MHz(40, 550), MHz(121), MHz(252, 100)),
),
"Akrotiri": AirfieldData(
theater="Syria",
icao="LCRA",
elevation=62,
runway_length=8276,
tacan=TacanChannel(107, TacanBand.X),
tacan_callsign="AKR",
vor=("AKR", MHz(116)),
atc=AtcData(MHz(4, 625), MHz(40, 150), MHz(128), MHz(251, 700)),
ils={
"28": ("IAK", MHz(109, 700)),
},
),
"Paphos": AirfieldData(
theater="Syria",
icao="LCPH",
elevation=40,
runway_length=8425,
vor=("PHA", MHz(117, 900)),
atc=AtcData(MHz(4, 675), MHz(40, 250), MHz(119, 900), MHz(251, 800)),
ils={
"29": ("IPA", MHz(108, 900)),
},
),
"Gazipasa": AirfieldData(
theater="Syria",
icao="LTFG",
elevation=36,
runway_length=6885,
vor=("GZP", MHz(114, 200)),
atc=AtcData(MHz(4, 600), MHz(40, 100), MHz(119, 250), MHz(251, 650)),
ils={
"8": ("IGZP", MHz(108, 500)),
}, },
), ),
# NTTR # NTTR

View File

@@ -16,6 +16,7 @@ from dcs.task import (
) )
from game import db from game import db
from .flights.ai_flight_planner_db import AEWC_CAPABLE
from .naming import namegen from .naming import namegen
from .callsigns import callsign_for_support_unit from .callsigns import callsign_for_support_unit
from .conflictgen import Conflict from .conflictgen import Conflict
@@ -53,6 +54,8 @@ class TankerInfo:
variant: str variant: str
freq: RadioFrequency freq: RadioFrequency
tacan: TacanChannel tacan: TacanChannel
start_time: Optional[timedelta]
end_time: Optional[timedelta]
blue: bool blue: bool
@@ -99,120 +102,126 @@ class AirSupportConflictGenerator:
else self.conflict.red_cp else self.conflict.red_cp
) )
fallback_tanker_number = 0 if not self.game.settings.disable_legacy_tanker:
fallback_tanker_number = 0
for i, tanker_unit_type in enumerate( for i, tanker_unit_type in enumerate(
db.find_unittype(Refueling, self.conflict.attackers_side) self.game.faction_for(player=True).tankers
): ):
alt, airspeed = self._get_tanker_params(tanker_unit_type) # TODO: Make loiter altitude a property of the unit type.
variant = db.unit_type_name(tanker_unit_type) alt, airspeed = self._get_tanker_params(tanker_unit_type.dcs_unit_type)
freq = self.radio_registry.alloc_uhf()
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y)
tanker_heading = (
self.conflict.red_cp.position.heading_between_point(
self.conflict.blue_cp.position
)
+ TANKER_HEADING_OFFSET * i
)
tanker_position = player_cp.position.point_from_heading(
tanker_heading, TANKER_DISTANCE
)
tanker_group = self.mission.refuel_flight(
country=self.mission.country(self.game.player_country),
name=namegen.next_tanker_name(
self.mission.country(self.game.player_country), tanker_unit_type
),
airport=None,
plane_type=tanker_unit_type,
position=tanker_position,
altitude=alt,
race_distance=58000,
frequency=freq.mhz,
start_type=StartType.Warm,
speed=airspeed,
tacanchannel=str(tacan),
)
tanker_group.set_frequency(freq.mhz)
callsign = callsign_for_support_unit(tanker_group)
tacan_callsign = {
"Texaco": "TEX",
"Arco": "ARC",
"Shell": "SHL",
}.get(callsign)
if tacan_callsign is None:
# The dict above is all the callsigns currently in the game, but
# non-Western countries don't use the callsigns and instead just
# use numbers. It's possible that none of those nations have
# TACAN compatible refueling aircraft, but fallback just in
# case.
tacan_callsign = f"TK{fallback_tanker_number}"
fallback_tanker_number += 1
if tanker_unit_type != IL_78M:
# Override PyDCS tacan channel.
tanker_group.points[0].tasks.pop()
tanker_group.points[0].tasks.append(
ActivateBeaconCommand(
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(SetImmortalCommand(True))
self.air_support.tankers.append(
TankerInfo(
str(tanker_group.name),
callsign,
variant,
freq,
tacan,
blue=True,
)
)
if not self.game.settings.disable_legacy_aewc:
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() freq = self.radio_registry.alloc_uhf()
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y)
awacs_flight = self.mission.awacs_flight( tanker_heading = (
self.conflict.red_cp.position.heading_between_point(
self.conflict.blue_cp.position
)
+ TANKER_HEADING_OFFSET * i
)
tanker_position = player_cp.position.point_from_heading(
tanker_heading, TANKER_DISTANCE
)
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_awacs_name( name=namegen.next_tanker_name(
self.mission.country(self.game.player_country) self.mission.country(self.game.player_country), tanker_unit_type
), ),
plane_type=awacs_unit,
altitude=AWACS_ALT,
airport=None, airport=None,
position=self.conflict.position.random_point_within( plane_type=tanker_unit_type,
AWACS_DISTANCE, AWACS_DISTANCE position=tanker_position,
), altitude=alt,
race_distance=58000,
frequency=freq.mhz, frequency=freq.mhz,
start_type=StartType.Warm, start_type=StartType.Warm,
speed=airspeed,
tacanchannel=str(tacan),
) )
awacs_flight.set_frequency(freq.mhz) tanker_group.set_frequency(freq.mhz)
awacs_flight.points[0].tasks.append(SetInvisibleCommand(True)) callsign = callsign_for_support_unit(tanker_group)
awacs_flight.points[0].tasks.append(SetImmortalCommand(True)) tacan_callsign = {
"Texaco": "TEX",
"Arco": "ARC",
"Shell": "SHL",
}.get(callsign)
if tacan_callsign is None:
# The dict above is all the callsigns currently in the game, but
# non-Western countries don't use the callsigns and instead just
# use numbers. It's possible that none of those nations have
# TACAN compatible refueling aircraft, but fallback just in
# case.
tacan_callsign = f"TK{fallback_tanker_number}"
fallback_tanker_number += 1
self.air_support.awacs.append( if tanker_unit_type != IL_78M:
AwacsInfo( # Override PyDCS tacan channel.
group_name=str(awacs_flight.name), tanker_group.points[0].tasks.pop()
callsign=callsign_for_support_unit(awacs_flight), tanker_group.points[0].tasks.append(
freq=freq, ActivateBeaconCommand(
depature_location=None, tacan.number,
start_time=None, tacan.band.value,
end_time=None, tacan_callsign,
True,
tanker_group.units[0].id,
True,
)
)
tanker_group.points[0].tasks.append(SetInvisibleCommand(True))
tanker_group.points[0].tasks.append(SetImmortalCommand(True))
self.air_support.tankers.append(
TankerInfo(
str(tanker_group.name),
callsign,
tanker_unit_type.name,
freq,
tacan,
blue=True, blue=True,
) )
) )
else:
if not self.game.settings.disable_legacy_aewc:
possible_awacs = [
a
for a in self.game.faction_for(player=True).aircrafts
if a in AEWC_CAPABLE
]
if not possible_awacs:
logging.warning("No AWACS for faction") logging.warning("No AWACS for faction")
return
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))
awacs_flight.points[0].tasks.append(SetImmortalCommand(True))
self.air_support.awacs.append(
AwacsInfo(
group_name=str(awacs_flight.name),
callsign=callsign_for_support_unit(awacs_flight),
freq=freq,
depature_location=None,
start_time=None,
end_time=None,
blue=True,
)
)

View File

@@ -10,7 +10,6 @@ from dcs.action import AITaskPush
from dcs.condition import GroupLifeLess, Or, TimeAfter, UnitDamaged from dcs.condition import GroupLifeLess, Or, TimeAfter, UnitDamaged
from dcs.country import Country from dcs.country import Country
from dcs.mapping import Point from dcs.mapping import Point
from dcs.planes import MQ_9_Reaper
from dcs.point import PointAction from dcs.point import PointAction
from dcs.task import ( from dcs.task import (
EPLRS, EPLRS,
@@ -26,18 +25,18 @@ from dcs.task import (
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
from dcs.unittype import VehicleType
from game import db from game.data.groundunitclass import GroundUnitClass
from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
from game.theater.controlpoint import ControlPoint
from game.unitmap import UnitMap 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 gen.ground_forces.ai_ground_planner import ( from gen.ground_forces.ai_ground_planner import (
DISTANCE_FROM_FRONTLINE, DISTANCE_FROM_FRONTLINE,
CombatGroup, CombatGroup,
CombatGroupRole, CombatGroupRole,
) )
from .callsigns import callsign_for_support_unit from .callsigns import callsign_for_support_unit
from .conflictgen import Conflict from .conflictgen import Conflict
from .ground_forces.combat_stance import CombatStance from .ground_forces.combat_stance import CombatStance
@@ -174,14 +173,14 @@ class GroundConflictGenerator:
n = "JTAC" + str(self.conflict.blue_cp.id) + str(self.conflict.red_cp.id) n = "JTAC" + str(self.conflict.blue_cp.id) + str(self.conflict.red_cp.id)
code = 1688 - len(self.jtacs) code = 1688 - len(self.jtacs)
utype = MQ_9_Reaper utype = self.game.player_faction.jtac_unit
if self.game.player_faction.jtac_unit is not None: if self.game.player_faction.jtac_unit is None:
utype = self.game.player_faction.jtac_unit utype = AircraftType.named("MQ-9 Reaper")
jtac = self.mission.flight_group( jtac = self.mission.flight_group(
country=self.mission.country(self.game.player_country), country=self.mission.country(self.game.player_country),
name=n, name=n,
aircraft_type=utype, aircraft_type=utype.dcs_unit_type,
position=position[0], position=position[0],
airport=None, airport=None,
altitude=5000, altitude=5000,
@@ -225,23 +224,22 @@ class GroundConflictGenerator:
else: else:
cp = self.conflict.red_cp cp = self.conflict.red_cp
if is_player: faction = self.game.faction_for(is_player)
faction = self.game.player_name
else:
faction = self.game.enemy_name
# Disable infantry unit gen if disabled # Disable infantry unit gen if disabled
if not self.game.settings.perf_infantry: if not self.game.settings.perf_infantry:
if self.game.settings.manpads: if self.game.settings.manpads:
# 50% of armored units protected by manpad # 50% of armored units protected by manpad
if random.choice([True, False]): if random.choice([True, False]):
manpads = db.find_manpad(faction) manpads = list(faction.infantry_with_class(GroundUnitClass.Manpads))
if len(manpads) > 0: if manpads:
u = random.choice(manpads) u = random.choices(
manpads, weights=[m.spawn_weight for m in manpads]
)[0]
self.mission.vehicle_group( self.mission.vehicle_group(
side, side,
namegen.next_infantry_name(side, cp.id, u), namegen.next_infantry_name(side, cp.id, u),
u, u.dcs_unit_type,
position=infantry_position, position=infantry_position,
group_size=1, group_size=1,
heading=forward_heading, heading=forward_heading,
@@ -249,30 +247,38 @@ class GroundConflictGenerator:
) )
return return
possible_infantry_units = db.find_infantry( possible_infantry_units = set(
faction, allow_manpad=self.game.settings.manpads faction.infantry_with_class(GroundUnitClass.Infantry)
) )
if len(possible_infantry_units) == 0: if self.game.settings.manpads:
possible_infantry_units |= set(
faction.infantry_with_class(GroundUnitClass.Manpads)
)
if not possible_infantry_units:
return return
u = random.choice(possible_infantry_units) infantry_choices = list(possible_infantry_units)
units = random.choices(
infantry_choices,
weights=[u.spawn_weight for u in infantry_choices],
k=INFANTRY_GROUP_SIZE,
)
self.mission.vehicle_group( self.mission.vehicle_group(
side, side,
namegen.next_infantry_name(side, cp.id, u), namegen.next_infantry_name(side, cp.id, units[0]),
u, units[0].dcs_unit_type,
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,
) )
for i in range(INFANTRY_GROUP_SIZE): for unit in units[1:]:
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), namegen.next_infantry_name(side, cp.id, unit),
u, unit.dcs_unit_type,
position=position, position=position,
group_size=1, group_size=1,
heading=forward_heading, heading=forward_heading,
@@ -312,7 +318,7 @@ class GroundConflictGenerator:
) )
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, gen_group.size * 10, 100)
fire_task.number = 2 if stance != CombatStance.RETREAT else 1 fire_task.number = 2 if stance != CombatStance.RETREAT else 1
dcs_group.add_trigger_action(fire_task) dcs_group.add_trigger_action(fire_task)
artillery_trigger.add_action(AITaskPush(dcs_group.id, len(dcs_group.tasks))) artillery_trigger.add_action(AITaskPush(dcs_group.id, len(dcs_group.tasks)))
@@ -502,7 +508,7 @@ class GroundConflictGenerator:
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 group.unit_type.eplrs_capable:
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:
@@ -673,7 +679,7 @@ class GroundConflictGenerator:
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
""" """
# TODO: Update to return a list of groups instead of a single point # TODO: Update to return a list of groups instead of a single point
rng = group.units[0].threat_range rng = getattr(group.unit_type.dcs_unit_type, "threat_range", 0)
if not enemy_groups: if not enemy_groups:
return None return None
for _ in range(10): for _ in range(10):
@@ -690,7 +696,7 @@ class GroundConflictGenerator:
""" """
For artilery group, decide the distance from frontline with the range of the unit For artilery group, decide the distance from frontline with the range of the unit
""" """
rg = group.units[0].threat_range - 7500 rg = getattr(group.unit_type.dcs_unit_type, "threat_range", 0) - 7500
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],
@@ -723,7 +729,7 @@ class GroundConflictGenerator:
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]]:
@@ -754,10 +760,9 @@ class GroundConflictGenerator:
if final_position is not None: if final_position is not None:
g = self._generate_group( g = self._generate_group(
self.mission.country(country), self.mission.country(country),
group.units[0], group.unit_type,
len(group.units), group.size,
final_position, final_position,
distance_from_frontline,
heading=opposite_heading(spawn_heading), heading=opposite_heading(spawn_heading),
) )
if is_player: if is_player:
@@ -781,10 +786,9 @@ class GroundConflictGenerator:
def _generate_group( def _generate_group(
self, self,
side: Country, side: Country,
unit: VehicleType, unit_type: GroundUnitType,
count: int, count: int,
at: Point, at: Point,
distance_from_frontline,
move_formation: PointAction = PointAction.OffRoad, move_formation: PointAction = PointAction.OffRoad,
heading=0, heading=0,
) -> VehicleGroup: ) -> VehicleGroup:
@@ -794,18 +798,17 @@ class GroundConflictGenerator:
else: else:
cp = self.conflict.red_cp cp = self.conflict.red_cp
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), namegen.next_unit_name(side, cp.id, unit_type),
unit, unit_type.dcs_unit_type,
position=at, position=at,
group_size=count, group_size=count,
heading=heading, heading=heading,
move_formation=move_formation, move_formation=move_formation,
) )
self.unit_map.add_front_line_units(group, cp) self.unit_map.add_front_line_units(group, cp, unit_type)
for c in range(count): for c in range(count):
vehicle: Vehicle = group.units[c] vehicle: Vehicle = group.units[c]

View File

@@ -168,9 +168,10 @@ class Package:
# likely to be the main task than others. For example, a package with # likely to be the main task than others. For example, a package with
# only CAP flights is a CAP package, a flight with CAP and strike is a # only CAP flights is a CAP package, a flight with CAP and strike is a
# strike package, a flight with CAP and DEAD is a DEAD package, and a # strike package, a flight with CAP and DEAD is a DEAD package, and a
# flight with strike and SEAD is an OCA/Strike package. The type of # flight with strike and SEAD is an OCA/Strike package. This list defines the
# package is determined by the highest priority flight in the package. # priority order for package task names. The package's primary task will be the
task_priorities = [ # first task in this list that matches a flight in the package.
tasks_by_priority = [
FlightType.CAS, FlightType.CAS,
FlightType.STRIKE, FlightType.STRIKE,
FlightType.ANTISHIP, FlightType.ANTISHIP,
@@ -183,10 +184,11 @@ class Package:
FlightType.TARCAP, FlightType.TARCAP,
FlightType.BARCAP, FlightType.BARCAP,
FlightType.AEWC, FlightType.AEWC,
FlightType.REFUELING,
FlightType.SWEEP, FlightType.SWEEP,
FlightType.ESCORT, FlightType.ESCORT,
] ]
for task in task_priorities: for task in tasks_by_priority:
if flight_counts[task]: if flight_counts[task]:
return task return task

View File

@@ -4,7 +4,7 @@ import itertools
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from dcs import Mission from dcs import Mission
from dcs.ships import Bulker_Handy_Wind from dcs.ships import HandyWind
from dcs.unitgroup import ShipGroup from dcs.unitgroup import ShipGroup
from game.transfers import CargoShip from game.transfers import CargoShip
@@ -35,7 +35,7 @@ class CargoShipGenerator:
group = self.mission.ship_group( group = self.mission.ship_group(
country, country,
ship.name, ship.name,
Bulker_Handy_Wind, HandyWind,
position=waypoints[0], position=waypoints[0],
group_size=1, group_size=1,
) )

View File

@@ -13,7 +13,7 @@ class SilkwormGenerator(GroupGenerator):
positions = self.get_circular_position(5, launcher_distance=120, coverage=180) positions = self.get_circular_position(5, launcher_distance=120, coverage=180)
self.add_unit( self.add_unit(
MissilesSS.AShM_Silkworm_SR, MissilesSS.Silkworm_SR,
"SR#0", "SR#0",
self.position.x, self.position.x,
self.position.y, self.position.y,
@@ -23,7 +23,7 @@ class SilkwormGenerator(GroupGenerator):
# Launchers # Launchers
for i, p in enumerate(positions): for i, p in enumerate(positions):
self.add_unit( self.add_unit(
MissilesSS.AShM_SS_N_2_Silkworm, MissilesSS.Silkworm_SR,
"Missile#" + str(i), "Missile#" + str(i),
p[0], p[0],
p[1], p[1],
@@ -32,7 +32,7 @@ class SilkwormGenerator(GroupGenerator):
# Commander # Commander
self.add_unit( self.add_unit(
Unarmed.Truck_KAMAZ_43101, Unarmed.KAMAZ_Truck,
"KAMAZ#0", "KAMAZ#0",
self.position.x - 35, self.position.x - 35,
self.position.y - 20, self.position.y - 20,
@@ -41,7 +41,7 @@ class SilkwormGenerator(GroupGenerator):
# Shorad # Shorad
self.add_unit( self.add_unit(
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish, AirDefence.ZSU_23_4_Shilka,
"SHILKA#0", "SHILKA#0",
self.position.x - 55, self.position.x - 55,
self.position.y - 38, self.position.y - 38,
@@ -50,7 +50,7 @@ class SilkwormGenerator(GroupGenerator):
# Shorad 2 # Shorad 2
self.add_unit( self.add_unit(
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL, AirDefence.Strela_1_9P31,
"STRELA#0", "STRELA#0",
self.position.x + 200, self.position.x + 200,
self.position.y + 15, self.position.y + 15,

View File

@@ -153,6 +153,8 @@ class Conflict:
if theater.is_on_land(pos): if theater.is_on_land(pos):
return pos return pos
pos = initial.point_from_heading(opposite_heading(heading), distance) pos = initial.point_from_heading(opposite_heading(heading), distance)
if theater.is_on_land(pos):
return pos
if coerce: if coerce:
pos = theater.nearest_land_pos(initial) pos = theater.nearest_land_pos(initial)
return pos return pos

View File

@@ -1,15 +1,15 @@
from __future__ import annotations from __future__ import annotations
import itertools import itertools
from typing import Dict, TYPE_CHECKING, Type from typing import TYPE_CHECKING
from dcs import Mission from dcs import Mission
from dcs.mapping import Point from dcs.mapping import Point
from dcs.point import PointAction from dcs.point import PointAction
from dcs.unit import Vehicle from dcs.unit import Vehicle
from dcs.unitgroup import VehicleGroup from dcs.unitgroup import VehicleGroup
from dcs.unittype import VehicleType
from game.dcs.groundunittype import GroundUnitType
from game.transfers import Convoy from game.transfers import Convoy
from game.unitmap import UnitMap from game.unitmap import UnitMap
from game.utils import kph from game.utils import kph
@@ -50,7 +50,7 @@ class ConvoyGenerator:
self, self,
name: str, name: str,
position: Point, position: Point,
units: Dict[Type[VehicleType], int], units: dict[GroundUnitType, int],
for_player: bool, for_player: bool,
) -> VehicleGroup: ) -> VehicleGroup:
country = self.mission.country( country = self.mission.country(
@@ -63,7 +63,7 @@ class ConvoyGenerator:
group = self.mission.vehicle_group( group = self.mission.vehicle_group(
country, country,
name, name,
main_unit_type, main_unit_type.dcs_unit_type,
position=position, position=position,
group_size=main_unit_count, group_size=main_unit_count,
move_formation=PointAction.OnRoad, move_formation=PointAction.OnRoad,
@@ -76,7 +76,7 @@ class ConvoyGenerator:
for unit_type, count in unit_types[1:]: for unit_type, count in unit_types[1:]:
for i in range(count): for i in range(count):
v = self.mission.vehicle( v = self.mission.vehicle(
f"{name} Unit #{next(unit_name_counter)}", unit_type f"{name} Unit #{next(unit_name_counter)}", unit_type.dcs_unit_type
) )
v.position.x = position.x v.position.x = position.x
v.position.y = next(y) v.position.y = next(y)

View File

@@ -1,8 +1,11 @@
import random import random
from dcs.vehicles import Armor from dcs.unitgroup import VehicleGroup
from game import db from game import db, Game
from game.data.groundunitclass import GroundUnitClass
from game.dcs.groundunittype import GroundUnitType
from game.theater.theatergroundobject import VehicleGroupGroundObject
from gen.defenses.armored_group_generator import ( from gen.defenses.armored_group_generator import (
ArmoredGroupGenerator, ArmoredGroupGenerator,
FixedSizeArmorGroupGenerator, FixedSizeArmorGroupGenerator,
@@ -14,8 +17,14 @@ 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
""" """
armor_types = (
GroundUnitClass.Apc,
GroundUnitClass.Atgm,
GroundUnitClass.Ifv,
GroundUnitClass.Tank,
)
possible_unit = [ possible_unit = [
u for u in db.FACTIONS[faction].frontline_units if u in Armor.__dict__.values() u for u in db.FACTIONS[faction].frontline_units if u.unit_class in armor_types
] ]
if len(possible_unit) > 0: if len(possible_unit) > 0:
unit_type = random.choice(possible_unit) unit_type = random.choice(possible_unit)
@@ -23,7 +32,9 @@ def generate_armor_group(faction: str, game, ground_object):
return None return None
def generate_armor_group_of_type(game, ground_object, unit_type): def generate_armor_group_of_type(
game: Game, ground_object: VehicleGroupGroundObject, unit_type: GroundUnitType
) -> VehicleGroup:
""" """
This generate a group of ground units of given type This generate a group of ground units of given type
:return: Generated group :return: Generated group
@@ -33,7 +44,12 @@ def generate_armor_group_of_type(game, ground_object, unit_type):
return generator.get_generated_group() return generator.get_generated_group()
def generate_armor_group_of_type_and_size(game, ground_object, unit_type, size: int): def generate_armor_group_of_type_and_size(
game: Game,
ground_object: VehicleGroupGroundObject,
unit_type: GroundUnitType,
size: int,
) -> VehicleGroup:
""" """
This generate a group of ground units of given type and size This generate a group of ground units of given type and size
:return: Generated group :return: Generated group

View File

@@ -1,15 +1,22 @@
import random import random
from game import Game
from game.dcs.groundunittype import GroundUnitType
from game.theater.theatergroundobject import VehicleGroupGroundObject
from gen.sam.group_generator import GroupGenerator from gen.sam.group_generator import GroupGenerator
class ArmoredGroupGenerator(GroupGenerator): class ArmoredGroupGenerator(GroupGenerator):
def __init__(self, game, ground_object, unit_type): def __init__(
super(ArmoredGroupGenerator, self).__init__(game, ground_object) self,
game: Game,
ground_object: VehicleGroupGroundObject,
unit_type: GroundUnitType,
) -> None:
super().__init__(game, ground_object)
self.unit_type = unit_type self.unit_type = unit_type
def generate(self): def generate(self) -> None:
grid_x = random.randint(2, 3) grid_x = random.randint(2, 3)
grid_y = random.randint(1, 2) grid_y = random.randint(1, 2)
@@ -20,7 +27,7 @@ class ArmoredGroupGenerator(GroupGenerator):
for j in range(grid_y): for j in range(grid_y):
index = index + 1 index = index + 1
self.add_unit( self.add_unit(
self.unit_type, self.unit_type.dcs_unit_type,
"Armor#" + str(index), "Armor#" + str(index),
self.position.x + spacing * i, self.position.x + spacing * i,
self.position.y + spacing * j, self.position.y + spacing * j,
@@ -29,8 +36,14 @@ class ArmoredGroupGenerator(GroupGenerator):
class FixedSizeArmorGroupGenerator(GroupGenerator): class FixedSizeArmorGroupGenerator(GroupGenerator):
def __init__(self, game, ground_object, unit_type, size): def __init__(
super(FixedSizeArmorGroupGenerator, self).__init__(game, ground_object) self,
game: Game,
ground_object: VehicleGroupGroundObject,
unit_type: GroundUnitType,
size: int,
) -> None:
super().__init__(game, ground_object)
self.unit_type = unit_type self.unit_type = unit_type
self.size = size self.size = size
@@ -41,7 +54,7 @@ class FixedSizeArmorGroupGenerator(GroupGenerator):
for i in range(self.size): for i in range(self.size):
index = index + 1 index = index + 1
self.add_unit( self.add_unit(
self.unit_type, self.unit_type.dcs_unit_type,
"Armor#" + str(index), "Armor#" + str(index),
self.position.x + spacing * i, self.position.x + spacing * i,
self.position.y, self.position.y,

View File

@@ -2,7 +2,7 @@ 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 from dcs.ships import USS_Arleigh_Burke_IIa, TICONDEROG
class CarrierGroupGenerator(ShipGroupGenerator): class CarrierGroupGenerator(ShipGroupGenerator):
@@ -22,7 +22,7 @@ class CarrierGroupGenerator(ShipGroupGenerator):
# Add Arleigh Burke escort # Add Arleigh Burke escort
self.add_unit( self.add_unit(
DDG_Arleigh_Burke_IIa, USS_Arleigh_Burke_IIa,
"USS Ramage", "USS Ramage",
self.position.x + 6482, self.position.x + 6482,
self.position.y + 6667, self.position.y + 6667,
@@ -30,7 +30,7 @@ class CarrierGroupGenerator(ShipGroupGenerator):
) )
self.add_unit( self.add_unit(
DDG_Arleigh_Burke_IIa, USS_Arleigh_Burke_IIa,
"USS Mitscher", "USS Mitscher",
self.position.x - 7963, self.position.x - 7963,
self.position.y + 7037, self.position.y + 7037,
@@ -38,7 +38,7 @@ class CarrierGroupGenerator(ShipGroupGenerator):
) )
self.add_unit( self.add_unit(
DDG_Arleigh_Burke_IIa, USS_Arleigh_Burke_IIa,
"USS Forrest Sherman", "USS Forrest Sherman",
self.position.x - 7408, self.position.x - 7408,
self.position.y - 7408, self.position.y - 7408,
@@ -46,7 +46,7 @@ class CarrierGroupGenerator(ShipGroupGenerator):
) )
self.add_unit( self.add_unit(
DDG_Arleigh_Burke_IIa, USS_Arleigh_Burke_IIa,
"USS Lassen", "USS Lassen",
self.position.x + 8704, self.position.x + 8704,
self.position.y - 6296, self.position.y - 6296,
@@ -56,7 +56,7 @@ class CarrierGroupGenerator(ShipGroupGenerator):
# Add Ticonderoga escort # Add Ticonderoga escort
if self.heading >= 180: if self.heading >= 180:
self.add_unit( self.add_unit(
CG_Ticonderoga, TICONDEROG,
"USS Hué City", "USS Hué City",
self.position.x + 2222, self.position.x + 2222,
self.position.y - 3333, self.position.y - 3333,
@@ -64,7 +64,7 @@ class CarrierGroupGenerator(ShipGroupGenerator):
) )
else: else:
self.add_unit( self.add_unit(
CG_Ticonderoga, TICONDEROG,
"USS Hué City", "USS Hué City",
self.position.x - 3333, self.position.x - 3333,
self.position.y + 2222, self.position.y + 2222,

View File

@@ -5,9 +5,9 @@ from typing import TYPE_CHECKING
from dcs.ships import ( from dcs.ships import (
Type_052C_Destroyer, Type_052C,
Type_052B_Destroyer, Type_052B,
Type_054A_Frigate, Type_054A,
) )
from game.factions.faction import Faction from game.factions.faction import Faction
@@ -30,14 +30,14 @@ class ChineseNavyGroupGenerator(ShipGroupGenerator):
if include_frigate: if include_frigate:
self.add_unit( self.add_unit(
Type_054A_Frigate, Type_054A,
"FF1", "FF1",
self.position.x + 1200, self.position.x + 1200,
self.position.y + 900, self.position.y + 900,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
Type_054A_Frigate, Type_054A,
"FF2", "FF2",
self.position.x + 1200, self.position.x + 1200,
self.position.y - 900, self.position.y - 900,
@@ -45,7 +45,7 @@ class ChineseNavyGroupGenerator(ShipGroupGenerator):
) )
if include_dd: if include_dd:
dd_type = random.choice([Type_052C_Destroyer, Type_052B_Destroyer]) dd_type = random.choice([Type_052C, Type_052B])
self.add_unit( self.add_unit(
dd_type, dd_type,
"DD1", "DD1",
@@ -69,5 +69,5 @@ class Type54GroupGenerator(DDGroupGenerator):
self, game: Game, ground_object: TheaterGroundObject, faction: Faction self, game: Game, ground_object: TheaterGroundObject, faction: Faction
): ):
super(Type54GroupGenerator, self).__init__( super(Type54GroupGenerator, self).__init__(
game, ground_object, faction, Type_054A_Frigate game, ground_object, faction, Type_054A
) )

View File

@@ -1,12 +1,12 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Type
from game.factions.faction import Faction from game.factions.faction import Faction
from game.theater.theatergroundobject import TheaterGroundObject 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 FFG_Oliver_Hazzard_Perry, DDG_Arleigh_Burke_IIa from dcs.ships import PERRY, USS_Arleigh_Burke_IIa
if TYPE_CHECKING: if TYPE_CHECKING:
from game.game import Game from game.game import Game
@@ -18,7 +18,7 @@ class DDGroupGenerator(ShipGroupGenerator):
game: Game, game: Game,
ground_object: TheaterGroundObject, ground_object: TheaterGroundObject,
faction: Faction, faction: Faction,
ddtype: ShipType, ddtype: Type[ShipType],
): ):
super(DDGroupGenerator, self).__init__(game, ground_object, faction) super(DDGroupGenerator, self).__init__(game, ground_object, faction)
self.ddtype = ddtype self.ddtype = ddtype
@@ -46,7 +46,7 @@ class OliverHazardPerryGroupGenerator(DDGroupGenerator):
self, game: Game, ground_object: TheaterGroundObject, faction: Faction self, game: Game, ground_object: TheaterGroundObject, faction: Faction
): ):
super(OliverHazardPerryGroupGenerator, self).__init__( super(OliverHazardPerryGroupGenerator, self).__init__(
game, ground_object, faction, FFG_Oliver_Hazzard_Perry game, ground_object, faction, PERRY
) )
@@ -55,5 +55,5 @@ class ArleighBurkeGroupGenerator(DDGroupGenerator):
self, game: Game, ground_object: TheaterGroundObject, faction: Faction self, game: Game, ground_object: TheaterGroundObject, faction: Faction
): ):
super(ArleighBurkeGroupGenerator, self).__init__( super(ArleighBurkeGroupGenerator, self).__init__(
game, ground_object, faction, DDG_Arleigh_Burke_IIa game, ground_object, faction, USS_Arleigh_Burke_IIa
) )

View File

@@ -1,4 +1,4 @@
from dcs.ships import FAC_La_Combattante_IIa from dcs.ships import La_Combattante_II
from game.factions.faction import Faction from game.factions.faction import Faction
from game.theater import TheaterGroundObject from game.theater import TheaterGroundObject
@@ -8,5 +8,5 @@ from gen.fleet.dd_group import DDGroupGenerator
class LaCombattanteIIGroupGenerator(DDGroupGenerator): class LaCombattanteIIGroupGenerator(DDGroupGenerator):
def __init__(self, game, ground_object: TheaterGroundObject, faction: Faction): def __init__(self, game, ground_object: TheaterGroundObject, faction: Faction):
super(LaCombattanteIIGroupGenerator, self).__init__( super(LaCombattanteIIGroupGenerator, self).__init__(
game, ground_object, faction, FAC_La_Combattante_IIa game, ground_object, faction, La_Combattante_II
) )

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 (
Corvette_1124_4_Grisha, ALBATROS,
Corvette_1241_1_Molniya, MOLNIYA,
Frigate_11540_Neustrashimy, NEUSTRASH,
Frigate_1135M_Rezky, REZKY,
Cruiser_1164_Moskva, MOSCOW,
SSK_877V_Kilo, KILO,
SSK_641B_Tango, SOM,
) )
from gen.fleet.dd_group import DDGroupGenerator from gen.fleet.dd_group import DDGroupGenerator
@@ -37,9 +37,7 @@ class RussianNavyGroupGenerator(ShipGroupGenerator):
include_frigate = True include_frigate = True
if include_frigate: if include_frigate:
frigate_type = random.choice( frigate_type = random.choice([ALBATROS, MOLNIYA])
[Corvette_1124_4_Grisha, Corvette_1241_1_Molniya]
)
self.add_unit( self.add_unit(
frigate_type, frigate_type,
"FF1", "FF1",
@@ -56,7 +54,7 @@ class RussianNavyGroupGenerator(ShipGroupGenerator):
) )
if include_dd: if include_dd:
dd_type = random.choice([Frigate_11540_Neustrashimy, Frigate_1135M_Rezky]) dd_type = random.choice([NEUSTRASH, REZKY])
self.add_unit( self.add_unit(
dd_type, dd_type,
"DD1", "DD1",
@@ -76,7 +74,7 @@ class RussianNavyGroupGenerator(ShipGroupGenerator):
# 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/dcs-liberation/dcs_liberation/issues/567 # See https://github.com/dcs-liberation/dcs_liberation/issues/567
self.add_unit( self.add_unit(
Cruiser_1164_Moskva, MOSCOW,
"CC1", "CC1",
self.position.x, self.position.x,
self.position.y, self.position.y,
@@ -91,7 +89,7 @@ class GrishaGroupGenerator(DDGroupGenerator):
self, game: Game, ground_object: TheaterGroundObject, faction: Faction self, game: Game, ground_object: TheaterGroundObject, faction: Faction
): ):
super(GrishaGroupGenerator, self).__init__( super(GrishaGroupGenerator, self).__init__(
game, ground_object, faction, Corvette_1124_4_Grisha game, ground_object, faction, ALBATROS
) )
@@ -100,7 +98,7 @@ class MolniyaGroupGenerator(DDGroupGenerator):
self, game: Game, ground_object: TheaterGroundObject, faction: Faction self, game: Game, ground_object: TheaterGroundObject, faction: Faction
): ):
super(MolniyaGroupGenerator, self).__init__( super(MolniyaGroupGenerator, self).__init__(
game, ground_object, faction, Corvette_1241_1_Molniya game, ground_object, faction, MOLNIYA
) )
@@ -108,15 +106,11 @@ class KiloSubGroupGenerator(DDGroupGenerator):
def __init__( def __init__(
self, game: Game, ground_object: TheaterGroundObject, faction: Faction self, game: Game, ground_object: TheaterGroundObject, faction: Faction
): ):
super(KiloSubGroupGenerator, self).__init__( super(KiloSubGroupGenerator, self).__init__(game, ground_object, faction, KILO)
game, ground_object, faction, SSK_877V_Kilo
)
class TangoSubGroupGenerator(DDGroupGenerator): class TangoSubGroupGenerator(DDGroupGenerator):
def __init__( def __init__(
self, game: Game, ground_object: TheaterGroundObject, faction: Faction self, game: Game, ground_object: TheaterGroundObject, faction: Faction
): ):
super(TangoSubGroupGenerator, self).__init__( super(TangoSubGroupGenerator, self).__init__(game, ground_object, faction, SOM)
game, ground_object, faction, SSK_641B_Tango
)

View File

@@ -1,6 +1,6 @@
import random import random
from dcs.ships import Boat_Schnellboot_type_S130 from dcs.ships import Schnellboot_type_S130
from gen.sam.group_generator import ShipGroupGenerator from gen.sam.group_generator import ShipGroupGenerator
@@ -10,7 +10,7 @@ class SchnellbootGroupGenerator(ShipGroupGenerator):
for i in range(random.randint(2, 4)): for i in range(random.randint(2, 4)):
self.add_unit( self.add_unit(
Boat_Schnellboot_type_S130, Schnellboot_type_S130,
"Schnellboot" + str(i), "Schnellboot" + str(i),
self.position.x + i * random.randint(100, 250), self.position.x + i * random.randint(100, 250),
self.position.y + (random.randint(100, 200) - 100), self.position.y + (random.randint(100, 200) - 100),

View File

@@ -1,6 +1,6 @@
import random import random
from dcs.ships import U_boat_VIIC_U_flak from dcs.ships import Uboat_VIIC
from gen.sam.group_generator import ShipGroupGenerator from gen.sam.group_generator import ShipGroupGenerator
@@ -10,7 +10,7 @@ class UBoatGroupGenerator(ShipGroupGenerator):
for i in range(random.randint(1, 4)): for i in range(random.randint(1, 4)):
self.add_unit( self.add_unit(
U_boat_VIIC_U_flak, Uboat_VIIC,
"Uboat" + str(i), "Uboat" + str(i),
self.position.x + i * random.randint(100, 250), self.position.x + i * random.randint(100, 250),
self.position.y + (random.randint(100, 200) - 100), self.position.y + (random.randint(100, 200) - 100),

View File

@@ -1,6 +1,6 @@
import random import random
from dcs.ships import LS_Samuel_Chase, LST_Mk_II from dcs.ships import USS_Samuel_Chase, LST_Mk2
from gen.sam.group_generator import ShipGroupGenerator from gen.sam.group_generator import ShipGroupGenerator
@@ -10,7 +10,7 @@ class WW2LSTGroupGenerator(ShipGroupGenerator):
# Add LS Samuel Chase # Add LS Samuel Chase
self.add_unit( self.add_unit(
LS_Samuel_Chase, USS_Samuel_Chase,
"SamuelChase", "SamuelChase",
self.position.x, self.position.x,
self.position.y, self.position.y,
@@ -19,7 +19,7 @@ class WW2LSTGroupGenerator(ShipGroupGenerator):
for i in range(1, random.randint(3, 4)): for i in range(1, random.randint(3, 4)):
self.add_unit( self.add_unit(
LST_Mk_II, LST_Mk2,
"LST" + str(i), "LST" + str(i),
self.position.x + i * random.randint(800, 1200), self.position.x + i * random.randint(800, 1200),
self.position.y, self.position.y,

View File

@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
import math
import operator import operator
import random import random
from collections import defaultdict from collections import defaultdict
@@ -16,14 +17,10 @@ from typing import (
Set, Set,
TYPE_CHECKING, TYPE_CHECKING,
Tuple, Tuple,
Type,
TypeVar, TypeVar,
Union,
) )
from dcs.unittype import FlyingType from game.dcs.aircrafttype import AircraftType
from game.factions.faction import Faction
from game.infos.information import Information from game.infos.information import Information
from game.procurement import AircraftProcurementRequest from game.procurement import AircraftProcurementRequest
from game.profiling import logged_duration, MultiEventTracer from game.profiling import logged_duration, MultiEventTracer
@@ -166,8 +163,6 @@ class AircraftAllocator:
flight.max_distance flight.max_distance
) )
# Prefer using squadrons with pilots first
best_understaffed: Optional[Tuple[ControlPoint, Squadron]] = None
for airfield in airfields_in_range: for airfield in airfields_in_range:
if not airfield.is_friendly(self.is_player): if not airfield.is_friendly(self.is_player):
continue continue
@@ -179,24 +174,14 @@ class AircraftAllocator:
continue continue
# Valid location with enough aircraft available. Find a squadron to fit # Valid location with enough aircraft available. Find a squadron to fit
# the role. # the role.
for squadron in self.air_wing.squadrons_for(aircraft): squadrons = self.air_wing.auto_assignable_for_task_with_type(
if task not in squadron.auto_assignable_mission_types: aircraft, task
continue )
if len(squadron.available_pilots) >= flight.num_aircraft: for squadron in squadrons:
if squadron.can_provide_pilots(flight.num_aircraft):
inventory.remove_aircraft(aircraft, flight.num_aircraft) inventory.remove_aircraft(aircraft, flight.num_aircraft)
return airfield, squadron return airfield, squadron
return None
# A compatible squadron that doesn't have enough pilots. Remember it
# as a fallback in case we find no better choices.
if best_understaffed is None:
best_understaffed = airfield, squadron
if best_understaffed is not None:
airfield, squadron = best_understaffed
self.global_inventory.for_control_point(airfield).remove_aircraft(
squadron.aircraft, flight.num_aircraft
)
return best_understaffed
class PackageBuilder: class PackageBuilder:
@@ -255,7 +240,7 @@ class PackageBuilder:
return True return True
def find_divert_field( def find_divert_field(
self, aircraft: Type[FlyingType], arrival: ControlPoint self, aircraft: AircraftType, arrival: ControlPoint
) -> Optional[ControlPoint]: ) -> Optional[ControlPoint]:
divert_limit = nautical_miles(150) divert_limit = nautical_miles(150)
for airfield in self.closest_airfields.operational_airfields_within( for airfield in self.closest_airfields.operational_airfields_within(
@@ -524,6 +509,24 @@ class ObjectiveFinder:
raise RuntimeError("Found no friendly control points. You probably lost.") raise RuntimeError("Found no friendly control points. You probably lost.")
return farthest return farthest
def closest_friendly_control_point(self) -> ControlPoint:
"""Finds the friendly control point that is closest to any threats."""
threat_zones = self.game.threat_zone_for(not self.is_player)
closest = None
min_distance = meters(math.inf)
for cp in self.friendly_control_points():
if isinstance(cp, OffMapSpawn):
continue
distance = threat_zones.distance_to_threat(cp.position)
if distance < min_distance:
closest = cp
min_distance = distance
if closest is None:
raise RuntimeError("Found no friendly control points. You probably lost.")
return closest
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 ( return (
@@ -581,7 +584,8 @@ 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) MAX_AWEC_RANGE = Distance.inf()
MAX_TANKER_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
@@ -600,14 +604,7 @@ class CoalitionMissionPlanner:
also possible for the player to exclude mission types from their squadron also possible for the player to exclude mission types from their squadron
designs. designs.
""" """
all_compatible = aircraft_for_task(mission_type) return self.game.air_wing_for(self.is_player).can_auto_plan(mission_type)
for squadron in self.game.air_wing_for(self.is_player).iter_squadrons():
if (
squadron.aircraft in all_compatible
and mission_type in squadron.auto_assignable_mission_types
):
return True
return False
def critical_missions(self) -> Iterator[ProposedMission]: def critical_missions(self) -> Iterator[ProposedMission]:
"""Identifies the most important missions to plan this turn. """Identifies the most important missions to plan this turn.
@@ -628,6 +625,11 @@ class CoalitionMissionPlanner:
asap=True, asap=True,
) )
yield ProposedMission(
self.objective_finder.closest_friendly_control_point(),
[ProposedFlight(FlightType.REFUELING, 1, self.MAX_TANKER_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 CAP in such a way, that it is established during the whole desired mission length # Plan CAP in such a way, that it is established during the whole desired mission length
@@ -842,7 +844,7 @@ 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", f"{available} {aircraft.id} from {cp}") self.message("Unused aircraft", f"{available} {aircraft} from {cp}")
def plan_flight( def plan_flight(
self, self,

View File

@@ -8,6 +8,7 @@ from dcs.helicopters import (
CH_47D, CH_47D,
CH_53E, CH_53E,
Ka_50, Ka_50,
Mi_24P,
Mi_24V, Mi_24V,
Mi_26, Mi_26,
Mi_28N, Mi_28N,
@@ -51,10 +52,13 @@ from dcs.planes import (
F_5E_3, F_5E_3,
F_86F_Sabre, F_86F_Sabre,
IL_76MD, IL_76MD,
I_16, IL_78M,
JF_17, JF_17,
J_11A, J_11A,
Ju_88A4, Ju_88A4,
KC130,
KC135MPRS,
KC_135,
KJ_2000, KJ_2000,
L_39ZA, L_39ZA,
MQ_9_Reaper, MQ_9_Reaper,
@@ -77,6 +81,7 @@ from dcs.planes import (
P_51D_30_NA, P_51D_30_NA,
RQ_1A_Predator, RQ_1A_Predator,
S_3B, S_3B,
S_3B_Tanker,
SpitfireLFMkIX, SpitfireLFMkIX,
SpitfireLFMkIXCW, SpitfireLFMkIXCW,
Su_17M4, Su_17M4,
@@ -100,12 +105,12 @@ from dcs.planes import (
) )
from dcs.unittype import FlyingType from dcs.unittype import FlyingType
from game.dcs.aircrafttype import AircraftType
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.jas39.jas39 import JAS39Gripen, JAS39Gripen_AG
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.jas39.jas39 import JAS39Gripen, JAS39Gripen_AG
from pydcs_extensions.su57.su57 import Su_57 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
@@ -204,6 +209,7 @@ CAS_CAPABLE = [
SA342L, SA342L,
Ka_50, Ka_50,
Mi_28N, Mi_28N,
Mi_24P,
Mi_24V, Mi_24V,
Mi_8MT, Mi_8MT,
UH_1H, UH_1H,
@@ -212,7 +218,6 @@ CAS_CAPABLE = [
F_5E_3, F_5E_3,
F_86F_Sabre, F_86F_Sabre,
C_101CC, C_101CC,
MB_339PAN,
L_39ZA, L_39ZA,
A_20G, A_20G,
Ju_88A4, Ju_88A4,
@@ -325,7 +330,6 @@ STRIKE_CAPABLE = [
MiG_15bis, MiG_15bis,
F_5E_3, F_5E_3,
F_86F_Sabre, F_86F_Sabre,
MB_339PAN,
C_101CC, C_101CC,
L_39ZA, L_39ZA,
B_17G, B_17G,
@@ -401,8 +405,17 @@ AEWC_CAPABLE = [
KJ_2000, KJ_2000,
] ]
# Priority is given to the tankers that can carry the most fuel.
REFUELING_CAPABALE = [
KC_135,
KC135MPRS,
IL_78M,
KC130,
S_3B_Tanker,
]
def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
def dcs_types_for_task(task: FlightType) -> list[Type[FlyingType]]:
cap_missions = (FlightType.BARCAP, FlightType.TARCAP, FlightType.SWEEP) cap_missions = (FlightType.BARCAP, FlightType.TARCAP, FlightType.SWEEP)
if task in cap_missions: if task in cap_missions:
return CAP_CAPABLE return CAP_CAPABLE
@@ -428,6 +441,8 @@ def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
return CAP_CAPABLE return CAP_CAPABLE
elif task == FlightType.AEWC: elif task == FlightType.AEWC:
return AEWC_CAPABLE return AEWC_CAPABLE
elif task == FlightType.REFUELING:
return REFUELING_CAPABALE
elif task == FlightType.TRANSPORT: elif task == FlightType.TRANSPORT:
return TRANSPORT_CAPABLE return TRANSPORT_CAPABLE
else: else:
@@ -435,7 +450,15 @@ def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
return [] return []
def tasks_for_aircraft(aircraft: Type[FlyingType]) -> list[FlightType]: def aircraft_for_task(task: FlightType) -> list[AircraftType]:
dcs_types = dcs_types_for_task(task)
types: list[AircraftType] = []
for dcs_type in dcs_types:
types.extend(AircraftType.for_dcs_type(dcs_type))
return types
def tasks_for_aircraft(aircraft: AircraftType) -> list[FlightType]:
tasks = [] tasks = []
for task in FlightType: for task in FlightType:
if aircraft in aircraft_for_task(task): if aircraft in aircraft_for_task(task):

View File

@@ -1,16 +1,15 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from enum import Enum from enum import Enum
from typing import List, Optional, TYPE_CHECKING, Type, Union from typing import List, Optional, TYPE_CHECKING, Union
from dcs.mapping import Point from dcs.mapping import Point
from dcs.point import MovingPoint, PointAction from dcs.point import MovingPoint, PointAction
from dcs.unit import Unit from dcs.unit import Unit
from dcs.unittype import FlyingType
from game import db from game import db
from game.dcs.aircrafttype import AircraftType
from game.squadrons import Pilot, Squadron from game.squadrons import Pilot, Squadron
from game.theater.controlpoint import ControlPoint, MissionTarget from game.theater.controlpoint import ControlPoint, MissionTarget
from game.utils import Distance, meters from game.utils import Distance, meters
@@ -69,6 +68,7 @@ class FlightType(Enum):
AEWC = "AEW&C" AEWC = "AEW&C"
TRANSPORT = "Transport" TRANSPORT = "Transport"
SEAD_ESCORT = "SEAD Escort" SEAD_ESCORT = "SEAD Escort"
REFUELING = "Refueling"
def __str__(self) -> str: def __str__(self) -> str:
return self.value return self.value
@@ -299,7 +299,7 @@ class Flight:
return self.roster.player_count return self.roster.player_count
@property @property
def unit_type(self) -> Type[FlyingType]: def unit_type(self) -> AircraftType:
return self.squadron.aircraft return self.squadron.aircraft
@property @property
@@ -324,13 +324,11 @@ class Flight:
self.roster.clear() self.roster.clear()
def __repr__(self): def __repr__(self):
name = db.unit_type_name(self.unit_type)
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 {self.unit_type}"
return f"[{self.flight_type}] {self.count} x {name}" return f"[{self.flight_type}] {self.count} x {self.unit_type}"
def __str__(self): def __str__(self):
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 {self.unit_type}"
return f"[{self.flight_type}] {self.count} x {name}" return f"[{self.flight_type}] {self.count} x {self.unit_type}"

View File

@@ -16,12 +16,10 @@ from functools import cached_property
from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple
from dcs.mapping import Point from dcs.mapping import Point
from dcs.planes import E_3A, E_2C, A_50, KJ_2000
from dcs.unit import Unit from dcs.unit import Unit
from shapely.geometry import Point as ShapelyPoint from shapely.geometry import Point as ShapelyPoint
from game.data.doctrine import Doctrine from game.data.doctrine import Doctrine
from game.squadrons import Pilot
from game.theater import ( from game.theater import (
Airfield, Airfield,
ControlPoint, ControlPoint,
@@ -29,9 +27,10 @@ from game.theater import (
MissionTarget, MissionTarget,
SamGroundObject, SamGroundObject,
TheaterGroundObject, TheaterGroundObject,
NavalControlPoint,
) )
from game.theater.theatergroundobject import EwrGroundObject from game.theater.theatergroundobject import EwrGroundObject, NavalGroundObject
from game.utils import Distance, Speed, feet, meters, nautical_miles from game.utils import Distance, Speed, feet, meters, nautical_miles, knots
from .closestairfields import ObjectiveDistanceCache from .closestairfields import ObjectiveDistanceCache
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
from .traveltime import GroundSpeed, TravelTime from .traveltime import GroundSpeed, TravelTime
@@ -769,6 +768,28 @@ class AwacsFlightPlan(LoiterFlightPlan):
return self.push_time return self.push_time
@dataclass(frozen=True)
class RefuelingFlightPlan(PatrollingFlightPlan):
takeoff: FlightWaypoint
land: FlightWaypoint
divert: Optional[FlightWaypoint]
bullseye: FlightWaypoint
#: Racetrack speed.
patrol_speed: Speed
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.takeoff
yield from self.nav_to
yield self.patrol_start
yield self.patrol_end
yield from self.nav_from
yield self.land
if self.divert is not None:
yield self.divert
yield self.bullseye
@dataclass(frozen=True) @dataclass(frozen=True)
class AirliftFlightPlan(FlightPlan): class AirliftFlightPlan(FlightPlan):
takeoff: FlightWaypoint takeoff: FlightWaypoint
@@ -919,6 +940,8 @@ class FlightPlanBuilder:
return self.generate_aewc(flight) return self.generate_aewc(flight)
elif task == FlightType.TRANSPORT: elif task == FlightType.TRANSPORT:
return self.generate_transport(flight) return self.generate_transport(flight)
elif task == FlightType.REFUELING:
return self.generate_refueling_racetrack(flight)
raise PlanningError(f"{task} flight plan generation not implemented") raise PlanningError(f"{task} flight plan generation not implemented")
def regenerate_package_waypoints(self) -> None: def regenerate_package_waypoints(self) -> None:
@@ -1059,15 +1082,8 @@ class FlightPlanBuilder:
orbit_location = self.aewc_orbit(location) orbit_location = self.aewc_orbit(location)
# As high as possible to maximize detection and on-station time. if flight.unit_type.patrol_altitude is not None:
if flight.unit_type == E_2C: patrol_alt = flight.unit_type.patrol_altitude
patrol_alt = feet(30000)
elif flight.unit_type == E_3A:
patrol_alt = feet(35000)
elif flight.unit_type == A_50:
patrol_alt = feet(33000)
elif flight.unit_type == KJ_2000:
patrol_alt = feet(40000)
else: else:
patrol_alt = feet(25000) patrol_alt = feet(25000)
@@ -1131,12 +1147,9 @@ class FlightPlanBuilder:
from game.transfers import CargoShip from game.transfers import CargoShip
if isinstance(location, ControlPoint): if isinstance(location, NavalControlPoint):
if not location.is_fleet: targets = self.anti_ship_targets_for_tgo(location.find_main_tgo())
raise InvalidObjectiveLocation(flight.flight_type, location) elif isinstance(location, NavalGroundObject):
# The first group generated will be the carrier group itself.
targets = self.anti_ship_targets_for_tgo(location.ground_objects[0])
elif isinstance(location, TheaterGroundObject):
targets = self.anti_ship_targets_for_tgo(location) targets = self.anti_ship_targets_for_tgo(location)
elif isinstance(location, CargoShip): elif isinstance(location, CargoShip):
targets = [StrikeTarget(location.name, location)] targets = [StrikeTarget(location.name, location)]
@@ -1612,6 +1625,74 @@ class FlightPlanBuilder:
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
) )
def generate_refueling_racetrack(self, flight: Flight) -> RefuelingFlightPlan:
location = self.package.target
closest_boundary = self.threat_zones.closest_boundary(location.position)
heading_to_threat_boundary = location.position.heading_between_point(
closest_boundary
)
distance_to_threat = meters(
location.position.distance_to_point(closest_boundary)
)
orbit_heading = heading_to_threat_boundary
# Station 70nm outside the threat zone.
threat_buffer = nautical_miles(70)
if self.threat_zones.threatened(location.position):
orbit_distance = distance_to_threat + threat_buffer
else:
orbit_distance = distance_to_threat - threat_buffer
racetrack_center = location.position.point_from_heading(
orbit_heading, orbit_distance.meters
)
racetrack_half_distance = Distance.from_nautical_miles(20).meters
racetrack_start = racetrack_center.point_from_heading(
orbit_heading + 90, racetrack_half_distance
)
racetrack_end = racetrack_center.point_from_heading(
orbit_heading - 90, racetrack_half_distance
)
builder = WaypointBuilder(flight, self.game, self.is_player)
tanker_type = flight.unit_type
if tanker_type.patrol_altitude is not None:
altitude = tanker_type.patrol_altitude
else:
altitude = feet(21000)
if tanker_type.patrol_speed is not None:
speed = tanker_type.patrol_speed
else:
# ~280 knots IAS at 21000.
speed = knots(400)
racetrack = builder.race_track(racetrack_start, racetrack_end, altitude)
return RefuelingFlightPlan(
package=self.package,
flight=flight,
takeoff=builder.takeoff(flight.departure),
nav_to=builder.nav_path(
flight.departure.position, racetrack_start, altitude
),
nav_from=builder.nav_path(racetrack_end, flight.arrival.position, altitude),
patrol_start=racetrack[0],
patrol_end=racetrack[1],
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert),
bullseye=builder.bullseye(),
patrol_duration=timedelta(hours=1),
patrol_speed=speed,
# TODO: Factor out a common base of the combat and non-combat race-tracks.
# No harm in setting this, but we ought to clean up a bit.
engagement_distance=meters(0),
)
@staticmethod @staticmethod
def target_waypoint( def target_waypoint(
flight: Flight, builder: WaypointBuilder, target: StrikeTarget flight: Flight, builder: WaypointBuilder, target: StrikeTarget

View File

@@ -1,11 +1,10 @@
from __future__ import annotations from __future__ import annotations
import datetime import datetime
from typing import Optional, List, Iterator, Type, TYPE_CHECKING, Mapping from typing import Optional, List, Iterator, TYPE_CHECKING, Mapping
from dcs.unittype import FlyingType
from game.data.weapons import Weapon, Pylon from game.data.weapons import Weapon, Pylon
from game.dcs.aircrafttype import AircraftType
if TYPE_CHECKING: if TYPE_CHECKING:
from gen.flights.flight import Flight from gen.flights.flight import Flight
@@ -27,9 +26,7 @@ class Loadout:
def derive_custom(self, name: str) -> Loadout: def derive_custom(self, name: str) -> Loadout:
return Loadout(name, self.pylons, self.date, is_custom=True) return Loadout(name, self.pylons, self.date, is_custom=True)
def degrade_for_date( def degrade_for_date(self, unit_type: AircraftType, date: datetime.date) -> Loadout:
self, unit_type: Type[FlyingType], date: datetime.date
) -> Loadout:
if self.date is not None and self.date <= date: if self.date is not None and self.date <= date:
return Loadout(self.name, self.pylons, self.date) return Loadout(self.name, self.pylons, self.date)
@@ -61,7 +58,7 @@ class Loadout:
# {"CLSID": class ID, "num": pylon number} # {"CLSID": class ID, "num": pylon number}
# "tasks": List (as a dict) of task IDs the payload is used by. # "tasks": List (as a dict) of task IDs the payload is used by.
# } # }
payloads = flight.unit_type.load_payloads() payloads = flight.unit_type.dcs_unit_type.load_payloads()
for payload in payloads.values(): for payload in payloads.values():
name = payload["name"] name = payload["name"]
pylons = payload["pylons"] pylons = payload["pylons"]
@@ -126,8 +123,8 @@ class Loadout:
for name in cls.default_loadout_names_for(flight): for name in cls.default_loadout_names_for(flight):
# This operation is cached, but must be called before load_by_name will # This operation is cached, but must be called before load_by_name will
# work. # work.
flight.unit_type.load_payloads() flight.unit_type.dcs_unit_type.load_payloads()
payload = flight.unit_type.loadout_by_name(name) payload = flight.unit_type.dcs_unit_type.loadout_by_name(name)
if payload is not None: if payload is not None:
return Loadout( return Loadout(
name, name,

View File

@@ -25,16 +25,13 @@ 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):
raise TypeError("Flight has non-flying unit")
# TODO: Expose both a cruise speed and target speed. # TODO: Expose both a cruise speed and target speed.
# The cruise speed can be used for ascent, hold, join, and RTB to save # The cruise speed can be used for ascent, hold, join, and RTB to save
# on fuel, but mission speed will be fast enough to keep the flight # on fuel, but mission speed will be fast enough to keep the flight
# safer. # safer.
# DCS's max speed is in kph at 0 MSL. # DCS's max speed is in kph at 0 MSL.
max_speed = kph(flight.unit_type.max_speed) max_speed = flight.unit_type.max_speed
if max_speed > SPEED_OF_SOUND_AT_SEA_LEVEL: if max_speed > SPEED_OF_SOUND_AT_SEA_LEVEL:
# Aircraft is supersonic. Limit to mach 0.85 to conserve fuel and # Aircraft is supersonic. Limit to mach 0.85 to conserve fuel and
# account for heavily loaded jets. # account for heavily loaded jets.

View File

@@ -32,6 +32,8 @@ class ForcedOptionsGenerator:
self.mission.forced_options.labels = ForcedOptions.Labels.Abbreviate self.mission.forced_options.labels = ForcedOptions.Labels.Abbreviate
elif self.game.settings.labels == "Dot Only": elif self.game.settings.labels == "Dot Only":
self.mission.forced_options.labels = ForcedOptions.Labels.DotOnly self.mission.forced_options.labels = ForcedOptions.Labels.DotOnly
elif self.game.settings.labels == "Neutral Dot":
self.mission.forced_options.labels = ForcedOptions.Labels.NeutralDot
elif self.game.settings.labels == "Off": elif self.game.settings.labels == "Off":
self.mission.forced_options.labels = ForcedOptions.Labels.None_ self.mission.forced_options.labels = ForcedOptions.Labels.None_

View File

@@ -3,11 +3,9 @@ import random
from enum import Enum from enum import Enum
from typing import Dict, List from typing import Dict, List
from dcs.unittype import VehicleType
from game.theater import ControlPoint
from game.data.groundunitclass import GroundUnitClass from game.data.groundunitclass import GroundUnitClass
from game.dcs.groundunittype import GroundUnitType
from game.theater import ControlPoint
from gen.ground_forces.combat_stance import CombatStance from gen.ground_forces.combat_stance import CombatStance
MAX_COMBAT_GROUP_PER_CP = 10 MAX_COMBAT_GROUP_PER_CP = 10
@@ -48,17 +46,19 @@ GROUP_SIZES_BY_COMBAT_STANCE = {
class CombatGroup: class CombatGroup:
def __init__(self, role: CombatGroupRole): def __init__(
self.units: List[VehicleType] = [] self, role: CombatGroupRole, unit_type: GroundUnitType, size: int
) -> None:
self.unit_type = unit_type
self.size = size
self.role = role self.role = role
self.assigned_enemy_cp = None self.assigned_enemy_cp = None
self.start_position = None self.start_position = None
def __str__(self): def __str__(self):
s = "" s = f"ROLE : {self.role}\n"
s += "ROLE : " + str(self.role) + "\n" if self.size:
if len(self.units) > 0: s += f"UNITS {self.unit_type} * {self.size}"
s += "UNITS " + self.units[0].name + " * " + str(len(self.units))
return s return s
@@ -97,28 +97,29 @@ class GroundPlanner:
# Create combat groups and assign them randomly to each enemy CP # Create combat groups and assign them randomly to each enemy CP
for unit_type in self.cp.base.armor: for unit_type in self.cp.base.armor:
if unit_type in GroundUnitClass.Tank: unit_class = unit_type.unit_class
if unit_class is GroundUnitClass.Tank:
collection = self.tank_groups collection = self.tank_groups
role = CombatGroupRole.TANK role = CombatGroupRole.TANK
elif unit_type in GroundUnitClass.Apc: elif unit_class is GroundUnitClass.Apc:
collection = self.apc_group collection = self.apc_group
role = CombatGroupRole.APC role = CombatGroupRole.APC
elif unit_type in GroundUnitClass.Artillery: elif unit_class is GroundUnitClass.Artillery:
collection = self.art_group collection = self.art_group
role = CombatGroupRole.ARTILLERY role = CombatGroupRole.ARTILLERY
elif unit_type in GroundUnitClass.Ifv: elif unit_class is GroundUnitClass.Ifv:
collection = self.ifv_group collection = self.ifv_group
role = CombatGroupRole.IFV role = CombatGroupRole.IFV
elif unit_type in GroundUnitClass.Logistics: elif unit_class is GroundUnitClass.Logistics:
collection = self.logi_groups collection = self.logi_groups
role = CombatGroupRole.LOGI role = CombatGroupRole.LOGI
elif unit_type in GroundUnitClass.Atgm: elif unit_class is GroundUnitClass.Atgm:
collection = self.atgm_group collection = self.atgm_group
role = CombatGroupRole.ATGM role = CombatGroupRole.ATGM
elif unit_type in GroundUnitClass.Shorads: elif unit_class is GroundUnitClass.Shorads:
collection = self.shorad_groups collection = self.shorad_groups
role = CombatGroupRole.SHORAD role = CombatGroupRole.SHORAD
elif unit_type in GroundUnitClass.Recon: elif unit_class is GroundUnitClass.Recon:
collection = self.recon_groups collection = self.recon_groups
role = CombatGroupRole.RECON role = CombatGroupRole.RECON
else: else:
@@ -137,17 +138,17 @@ class GroundPlanner:
while available > 0: while available > 0:
if role == CombatGroupRole.SHORAD: if role == CombatGroupRole.SHORAD:
n = 1 count = 1
else: else:
n = random.choice(group_size_choice) count = random.choice(group_size_choice)
if n > available: if count > available:
if available >= 2: if available >= 2:
n = 2 count = 2
else: else:
n = 1 count = 1
available -= n available -= count
group = CombatGroup(role) group = CombatGroup(role, unit_type, count)
if len(self.connected_enemy_cp) > 0: if len(self.connected_enemy_cp) > 0:
enemy_cp = random.choice(self.connected_enemy_cp).id enemy_cp = random.choice(self.connected_enemy_cp).id
self.units_per_cp[enemy_cp].append(group) self.units_per_cp[enemy_cp].append(group)
@@ -155,9 +156,6 @@ class GroundPlanner:
else: else:
self.reserve.append(group) self.reserve.append(group)
group.assigned_enemy_cp = "__reserve__" group.assigned_enemy_cp = "__reserve__"
for i in range(n):
group.units.append(unit_type)
collection.append(group) collection.append(group)
if remaining_available_frontline_units == 0: if remaining_available_frontline_units == 0:

View File

@@ -32,15 +32,15 @@ from typing import Dict, List, Optional, TYPE_CHECKING, Tuple, Iterator
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
from dcs.mission import Mission from dcs.mission import Mission
from dcs.unit import Unit from dcs.unit import Unit
from dcs.unittype import FlyingType
from tabulate import tabulate from tabulate import tabulate
from game.data.alic import AlicCodes from game.data.alic import AlicCodes
from game.db import unit_type_from_name from game.db import unit_type_from_name
from game.dcs.aircrafttype import AircraftType
from game.theater import ConflictTheater, TheaterGroundObject, LatLon from game.theater import ConflictTheater, TheaterGroundObject, LatLon
from game.theater.bullseye import Bullseye from game.theater.bullseye import Bullseye
from game.utils import meters from game.utils import meters
from .aircraft import AIRCRAFT_DATA, FlightData from .aircraft import FlightData
from .airsupportgen import AwacsInfo, TankerInfo from .airsupportgen import AwacsInfo, TankerInfo
from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator
from .flights.flight import FlightWaypoint, FlightWaypointType, FlightType from .flights.flight import FlightWaypoint, FlightWaypointType, FlightType
@@ -142,7 +142,8 @@ class KneeboardPage:
"""Writes the kneeboard page to the given path.""" """Writes the kneeboard page to the given path."""
raise NotImplementedError raise NotImplementedError
def format_ll(self, ll: LatLon) -> str: @staticmethod
def format_ll(ll: LatLon) -> str:
ns = "N" if ll.latitude >= 0 else "S" ns = "N" if ll.latitude >= 0 else "S"
ew = "E" if ll.longitude >= 0 else "W" ew = "E" if ll.longitude >= 0 else "W"
return f"{ll.latitude:.4}°{ns} {ll.longitude:.4}°{ew}" return f"{ll.latitude:.4}°{ns} {ll.longitude:.4}°{ew}"
@@ -355,8 +356,9 @@ class BriefingPage(KneeboardPage):
if channel is None: if channel is None:
return str(frequency) return str(frequency)
namer = AIRCRAFT_DATA[self.flight.aircraft_type.id].channel_namer channel_name = self.flight.aircraft_type.channel_name(
channel_name = namer.channel_name(channel.radio_id, channel.channel) channel.radio_id, channel.channel
)
return f"{channel_name}\n{frequency}" return f"{channel_name}\n{frequency}"
@@ -452,9 +454,10 @@ class SupportPage(KneeboardPage):
if channel is None: if channel is None:
return str(frequency) return str(frequency)
namer = AIRCRAFT_DATA[self.flight.aircraft_type.id].channel_namer channel_name = self.flight.aircraft_type.channel_name(
channel_name = namer.channel_name(channel.radio_id, channel.channel) channel.radio_id, channel.channel
return f"{channel_name} {frequency}" )
return f"{channel_name}\n{frequency}"
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:
@@ -565,14 +568,14 @@ class KneeboardGenerator(MissionInfoGenerator):
temp_dir = Path("kneeboards") temp_dir = Path("kneeboards")
temp_dir.mkdir(exist_ok=True) temp_dir.mkdir(exist_ok=True)
for aircraft, pages in self.pages_by_airframe().items(): for aircraft, pages in self.pages_by_airframe().items():
aircraft_dir = temp_dir / aircraft.id aircraft_dir = temp_dir / aircraft.dcs_unit_type.id
aircraft_dir.mkdir(exist_ok=True) aircraft_dir.mkdir(exist_ok=True)
for idx, page in enumerate(pages): for idx, page in enumerate(pages):
page_path = aircraft_dir / f"page{idx:02}.png" page_path = aircraft_dir / f"page{idx:02}.png"
page.write(page_path) page.write(page_path)
self.mission.add_aircraft_kneeboard(aircraft, page_path) self.mission.add_aircraft_kneeboard(aircraft.dcs_unit_type, page_path)
def pages_by_airframe(self) -> Dict[FlyingType, List[KneeboardPage]]: def pages_by_airframe(self) -> Dict[AircraftType, List[KneeboardPage]]:
"""Returns a list of kneeboard pages per airframe in the mission. """Returns a list of kneeboard pages per airframe in the mission.
Only client flights will be included, but because DCS does not support Only client flights will be included, but because DCS does not support
@@ -583,7 +586,7 @@ class KneeboardGenerator(MissionInfoGenerator):
A dict mapping aircraft types to the list of kneeboard pages for A dict mapping aircraft types to the list of kneeboard pages for
that aircraft. that aircraft.
""" """
all_flights: Dict[FlyingType, List[KneeboardPage]] = defaultdict(list) all_flights: Dict[AircraftType, List[KneeboardPage]] = defaultdict(list)
for flight in self.flights: for flight in self.flights:
if not flight.client_units: if not flight.client_units:
continue continue

View File

@@ -14,21 +14,21 @@ class ScudGenerator(GroupGenerator):
# Scuds # Scuds
self.add_unit( self.add_unit(
MissilesSS.SSM_SS_1C_Scud_B, MissilesSS.Scud_B,
"V1#0", "V1#0",
self.position.x, self.position.x,
self.position.y + random.randint(1, 8), self.position.y + random.randint(1, 8),
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
MissilesSS.SSM_SS_1C_Scud_B, MissilesSS.Scud_B,
"V1#1", "V1#1",
self.position.x + 50, self.position.x + 50,
self.position.y + random.randint(1, 8), self.position.y + random.randint(1, 8),
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
MissilesSS.SSM_SS_1C_Scud_B, MissilesSS.Scud_B,
"V1#2", "V1#2",
self.position.x + 100, self.position.x + 100,
self.position.y + random.randint(1, 8), self.position.y + random.randint(1, 8),
@@ -37,7 +37,7 @@ class ScudGenerator(GroupGenerator):
# Commander # Commander
self.add_unit( self.add_unit(
Unarmed.LUV_UAZ_469_Jeep, Unarmed.UAZ_469,
"Kubel#0", "Kubel#0",
self.position.x - 35, self.position.x - 35,
self.position.y - 20, self.position.y - 20,
@@ -46,7 +46,7 @@ class ScudGenerator(GroupGenerator):
# Shorad # Shorad
self.add_unit( self.add_unit(
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish, AirDefence.ZSU_23_4_Shilka,
"SHILKA#0", "SHILKA#0",
self.position.x - 55, self.position.x - 55,
self.position.y - 38, self.position.y - 38,
@@ -54,7 +54,7 @@ class ScudGenerator(GroupGenerator):
) )
self.add_unit( self.add_unit(
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL, AirDefence.Strela_1_9P31,
"STRELA#0", "STRELA#0",
self.position.x + 200, self.position.x + 200,
self.position.y + 15, self.position.y + 15,

View File

@@ -14,21 +14,21 @@ class V1GroupGenerator(GroupGenerator):
# Ramps # Ramps
self.add_unit( self.add_unit(
MissilesSS.SSM_V_1_Launcher, MissilesSS.V1_launcher,
"V1#0", "V1#0",
self.position.x, self.position.x,
self.position.y + random.randint(1, 8), self.position.y + random.randint(1, 8),
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
MissilesSS.SSM_V_1_Launcher, MissilesSS.V1_launcher,
"V1#1", "V1#1",
self.position.x + 50, self.position.x + 50,
self.position.y + random.randint(1, 8), self.position.y + random.randint(1, 8),
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
MissilesSS.SSM_V_1_Launcher, MissilesSS.V1_launcher,
"V1#2", "V1#2",
self.position.x + 100, self.position.x + 100,
self.position.y + random.randint(1, 8), self.position.y + random.randint(1, 8),
@@ -37,7 +37,7 @@ class V1GroupGenerator(GroupGenerator):
# Commander # Commander
self.add_unit( self.add_unit(
Unarmed.LUV_Kubelwagen_82, Unarmed.Kubelwagen_82,
"Kubel#0", "Kubel#0",
self.position.x - 35, self.position.x - 35,
self.position.y - 20, self.position.y - 20,
@@ -45,9 +45,7 @@ class V1GroupGenerator(GroupGenerator):
) )
# Self defense flak # Self defense flak
flak_unit = random.choice( flak_unit = random.choice([AirDefence.Flak38, AirDefence.Flak30])
[AirDefence.AAA_Flak_Vierling_38_Quad_20mm, AirDefence.AAA_Flak_38_20mm]
)
self.add_unit( self.add_unit(
flak_unit, flak_unit,
@@ -58,7 +56,7 @@ class V1GroupGenerator(GroupGenerator):
) )
self.add_unit( self.add_unit(
Unarmed.Truck_Opel_Blitz, Unarmed.Blitz_36_6700A,
"Blitz#0", "Blitz#0",
self.position.x + 200, self.position.x + 200,
self.position.y + 15, self.position.y + 15,

View File

@@ -3,10 +3,9 @@ import time
from typing import List from typing import List
from dcs.country import Country from dcs.country import Country
from dcs.unittype import UnitType
from game import db
from game.dcs.aircrafttype import AircraftType
from game.dcs.unittype import UnitType
from gen.flights.flight import Flight from gen.flights.flight import Flight
ALPHA_MILITARY = [ ALPHA_MILITARY = [
@@ -290,14 +289,14 @@ class NameGenerator:
country.id, country.id,
cls.aircraft_number, cls.aircraft_number,
parent_base_id, parent_base_id,
db.unit_type_name(flight.unit_type), flight.unit_type.name,
) )
@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( return "unit|{}|{}|{}|{}|".format(
country.id, cls.number, parent_base_id, db.unit_type_name(unit_type) country.id, cls.number, parent_base_id, unit_type.name
) )
@classmethod @classmethod
@@ -309,7 +308,7 @@ class NameGenerator:
country.id, country.id,
cls.infantry_number, cls.infantry_number,
parent_base_id, parent_base_id,
db.unit_type_name(unit_type), unit_type.name,
) )
@classmethod @classmethod
@@ -318,11 +317,9 @@ class NameGenerator:
return "awacs|{}|{}|0|".format(country.id, cls.number) return "awacs|{}|{}|0|".format(country.id, cls.number)
@classmethod @classmethod
def next_tanker_name(cls, country: Country, unit_type: UnitType): def next_tanker_name(cls, country: Country, unit_type: AircraftType):
cls.number += 1 cls.number += 1
return "tanker|{}|{}|0|{}".format( return "tanker|{}|{}|0|{}".format(country.id, cls.number, unit_type.name)
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):

View File

@@ -153,7 +153,7 @@ def get_radio(name: str) -> Radio:
for radio in RADIOS: for radio in RADIOS:
if radio.name == name: if radio.name == name:
return radio return radio
raise KeyError raise KeyError(f"Unknown radio: {name}")
class RadioRegistry: class RadioRegistry:

View File

@@ -27,7 +27,7 @@ class BoforsGenerator(AirDefenseGroupGenerator):
for j in range(grid_y): for j in range(grid_y):
index = index + 1 index = index + 1
self.add_unit( self.add_unit(
AirDefence.AAA_Bofors_40mm, AirDefence.Bofors40,
"AAA#" + str(index), "AAA#" + str(index),
self.position.x + spacing * i, self.position.x + spacing * i,
self.position.y + spacing * j, self.position.y + spacing * j,

View File

@@ -8,12 +8,12 @@ from gen.sam.airdefensegroupgenerator import (
) )
GFLAK = [ GFLAK = [
AirDefence.AAA_Flak_Vierling_38_Quad_20mm, AirDefence.Flak38,
AirDefence.AAA_8_8cm_Flak_18, AirDefence.Flak18,
AirDefence.AAA_8_8cm_Flak_36, AirDefence.Flak36,
AirDefence.AAA_8_8cm_Flak_37, AirDefence.Flak37,
AirDefence.AAA_8_8cm_Flak_41, AirDefence.Flak41,
AirDefence.AAA_Flak_38_20mm, AirDefence.Flak30,
] ]
@@ -53,7 +53,7 @@ class FlakGenerator(AirDefenseGroupGenerator):
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( self.add_unit(
AirDefence.SL_Flakscheinwerfer_37, AirDefence.Flakscheinwerfer_37,
"SearchLight#" + str(index), "SearchLight#" + str(index),
pos[0], pos[0],
pos[1], pos[1],
@@ -62,14 +62,14 @@ class FlakGenerator(AirDefenseGroupGenerator):
# Support # Support
self.add_unit( self.add_unit(
AirDefence.PU_Maschinensatz_33, AirDefence.Maschinensatz_33,
"MC33#", "MC33#",
self.position.x - 20, self.position.x - 20,
self.position.y - 20, self.position.y - 20,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
AirDefence.AAA_SP_Kdo_G_40, AirDefence.KDO_Mod40,
"KDO#", "KDO#",
self.position.x - 25, self.position.x - 25,
self.position.y - 20, self.position.y - 20,
@@ -78,7 +78,7 @@ class FlakGenerator(AirDefenseGroupGenerator):
# Commander # Commander
self.add_unit( self.add_unit(
Unarmed.LUV_Kubelwagen_82, Unarmed.Kubelwagen_82,
"Kubel#", "Kubel#",
self.position.x - 35, self.position.x - 35,
self.position.y - 20, self.position.y - 20,
@@ -89,7 +89,7 @@ class FlakGenerator(AirDefenseGroupGenerator):
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( self.add_unit(
Unarmed.Truck_Opel_Blitz, Unarmed.Blitz_36_6700A,
"BLITZ#" + str(index), "BLITZ#" + str(index),
self.position.x + 125 + 15 * i + random.randint(1, 5), self.position.x + 125 + 15 * i + random.randint(1, 5),
self.position.y + 15 * j + random.randint(1, 5), self.position.y + 15 * j + random.randint(1, 5),

View File

@@ -25,7 +25,7 @@ class Flak18Generator(AirDefenseGroupGenerator):
for j in range(2): for j in range(2):
index = index + 1 index = index + 1
self.add_unit( self.add_unit(
AirDefence.AAA_8_8cm_Flak_18, AirDefence.Flak18,
"AAA#" + str(index), "AAA#" + str(index),
self.position.x + spacing * i + random.randint(1, 5), self.position.x + spacing * i + random.randint(1, 5),
self.position.y + spacing * j + random.randint(1, 5), self.position.y + spacing * j + random.randint(1, 5),
@@ -34,7 +34,7 @@ class Flak18Generator(AirDefenseGroupGenerator):
# Add a commander truck # Add a commander truck
self.add_unit( self.add_unit(
Unarmed.Truck_Opel_Blitz, Unarmed.Blitz_36_6700A,
"Blitz#", "Blitz#",
self.position.x - 35, self.position.x - 35,
self.position.y - 20, self.position.y - 20,

View File

@@ -21,7 +21,7 @@ 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( self.add_unit(
AirDefence.AAA_QF_3_7, AirDefence.QF_37_AA,
"AA#" + str(i), "AA#" + str(i),
position[0], position[0],
position[1], position[1],
@@ -31,7 +31,7 @@ class AllyWW2FlakGenerator(AirDefenseGroupGenerator):
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( self.add_unit(
AirDefence.AAA_M1_37mm, AirDefence.M1_37mm,
"AA#" + str(4 + i), "AA#" + str(4 + i),
position[0], position[0],
position[1], position[1],
@@ -41,7 +41,7 @@ class AllyWW2FlakGenerator(AirDefenseGroupGenerator):
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( self.add_unit(
AirDefence.AAA_M45_Quadmount_HB_12_7mm, AirDefence.M45_Quadmount,
"AA#" + str(12 + i), "AA#" + str(12 + i),
position[0], position[0],
position[1], position[1],
@@ -50,28 +50,28 @@ class AllyWW2FlakGenerator(AirDefenseGroupGenerator):
# Add a commander truck # Add a commander truck
self.add_unit( self.add_unit(
Unarmed.Car_Willys_Jeep, Unarmed.Willys_MB,
"CMD#1", "CMD#1",
self.position.x, self.position.x,
self.position.y - 20, self.position.y - 20,
random.randint(0, 360), random.randint(0, 360),
) )
self.add_unit( self.add_unit(
Unarmed.Carrier_M30_Cargo, Unarmed.M30_CC,
"LOG#1", "LOG#1",
self.position.x, self.position.x,
self.position.y + 20, self.position.y + 20,
random.randint(0, 360), random.randint(0, 360),
) )
self.add_unit( self.add_unit(
Unarmed.Tractor_M4_Hi_Speed, Unarmed.M4_Tractor,
"LOG#2", "LOG#2",
self.position.x + 20, self.position.x + 20,
self.position.y, self.position.y,
random.randint(0, 360), random.randint(0, 360),
) )
self.add_unit( self.add_unit(
Unarmed.Truck_Bedford, Unarmed.Bedford_MWD,
"LOG#3", "LOG#3",
self.position.x - 20, self.position.x - 20,
self.position.y, self.position.y,

View File

@@ -21,7 +21,7 @@ class ZSU57Generator(AirDefenseGroupGenerator):
) )
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit( self.add_unit(
AirDefence.SPAAA_ZSU_57_2, AirDefence.ZSU_57_2,
"SPAA#" + str(i), "SPAA#" + str(i),
position[0], position[0],
position[1], position[1],

View File

@@ -27,7 +27,7 @@ class ZU23InsurgentGenerator(AirDefenseGroupGenerator):
for j in range(grid_y): for j in range(grid_y):
index = index + 1 index = index + 1
self.add_unit( self.add_unit(
AirDefence.AAA_ZU_23_Insurgent_Closed_Emplacement, AirDefence.ZU_23_Closed_Insurgent,
"AAA#" + str(index), "AAA#" + str(index),
self.position.x + spacing * i, self.position.x + spacing * i,
self.position.y + spacing * j, self.position.y + spacing * j,

View File

@@ -29,7 +29,7 @@ class EarlyColdWarFlakGenerator(AirDefenseGroupGenerator):
for j in range(2): for j in range(2):
index = index + 1 index = index + 1
self.add_unit( self.add_unit(
AirDefence.AAA_8_8cm_Flak_18, AirDefence.Flak18,
"AAA#" + str(index), "AAA#" + str(index),
self.position.x + spacing * i + random.randint(1, 5), self.position.x + spacing * i + random.randint(1, 5),
self.position.y + spacing * j + random.randint(1, 5), self.position.y + spacing * j + random.randint(1, 5),
@@ -38,14 +38,14 @@ class EarlyColdWarFlakGenerator(AirDefenseGroupGenerator):
# Medium range guns # Medium range guns
self.add_unit( self.add_unit(
AirDefence.AAA_S_60_57mm, AirDefence.S_60_Type59_Artillery,
"SHO#1", "SHO#1",
self.position.x - 40, self.position.x - 40,
self.position.y - 40, self.position.y - 40,
self.heading + 180, self.heading + 180,
), ),
self.add_unit( self.add_unit(
AirDefence.AAA_S_60_57mm, AirDefence.S_60_Type59_Artillery,
"SHO#2", "SHO#2",
self.position.x + spacing * 2 + 40, self.position.x + spacing * 2 + 40,
self.position.y + spacing + 40, self.position.y + spacing + 40,
@@ -54,14 +54,14 @@ class EarlyColdWarFlakGenerator(AirDefenseGroupGenerator):
# Short range guns # Short range guns
self.add_unit( self.add_unit(
AirDefence.AAA_ZU_23_Closed_Emplacement, AirDefence.ZU_23_Emplacement_Closed,
"SHO#3", "SHO#3",
self.position.x - 80, self.position.x - 80,
self.position.y - 40, self.position.y - 40,
self.heading + 180, self.heading + 180,
), ),
self.add_unit( self.add_unit(
AirDefence.AAA_ZU_23_Closed_Emplacement, AirDefence.ZU_23_Emplacement_Closed,
"SHO#4", "SHO#4",
self.position.x + spacing * 2 + 80, self.position.x + spacing * 2 + 80,
self.position.y + spacing + 40, self.position.y + spacing + 40,
@@ -70,7 +70,7 @@ class EarlyColdWarFlakGenerator(AirDefenseGroupGenerator):
# Add a truck # Add a truck
self.add_unit( self.add_unit(
Unarmed.Truck_KAMAZ_43101, Unarmed.KAMAZ_Truck,
"Truck#", "Truck#",
self.position.x - 60, self.position.x - 60,
self.position.y - 20, self.position.y - 20,
@@ -102,7 +102,7 @@ class ColdWarFlakGenerator(AirDefenseGroupGenerator):
for j in range(2): for j in range(2):
index = index + 1 index = index + 1
self.add_unit( self.add_unit(
AirDefence.AAA_8_8cm_Flak_18, AirDefence.Flak18,
"AAA#" + str(index), "AAA#" + str(index),
self.position.x + spacing * i + random.randint(1, 5), self.position.x + spacing * i + random.randint(1, 5),
self.position.y + spacing * j + random.randint(1, 5), self.position.y + spacing * j + random.randint(1, 5),
@@ -111,14 +111,14 @@ class ColdWarFlakGenerator(AirDefenseGroupGenerator):
# Medium range guns # Medium range guns
self.add_unit( self.add_unit(
AirDefence.AAA_S_60_57mm, AirDefence.S_60_Type59_Artillery,
"SHO#1", "SHO#1",
self.position.x - 40, self.position.x - 40,
self.position.y - 40, self.position.y - 40,
self.heading + 180, self.heading + 180,
), ),
self.add_unit( self.add_unit(
AirDefence.AAA_S_60_57mm, AirDefence.S_60_Type59_Artillery,
"SHO#2", "SHO#2",
self.position.x + spacing * 2 + 40, self.position.x + spacing * 2 + 40,
self.position.y + spacing + 40, self.position.y + spacing + 40,
@@ -127,14 +127,14 @@ class ColdWarFlakGenerator(AirDefenseGroupGenerator):
# Short range guns # Short range guns
self.add_unit( self.add_unit(
AirDefence.AAA_ZU_23_Closed_Emplacement, AirDefence.ZU_23_Emplacement_Closed,
"SHO#3", "SHO#3",
self.position.x - 80, self.position.x - 80,
self.position.y - 40, self.position.y - 40,
self.heading + 180, self.heading + 180,
), ),
self.add_unit( self.add_unit(
AirDefence.AAA_ZU_23_Closed_Emplacement, AirDefence.ZU_23_Emplacement_Closed,
"SHO#4", "SHO#4",
self.position.x + spacing * 2 + 80, self.position.x + spacing * 2 + 80,
self.position.y + spacing + 40, self.position.y + spacing + 40,
@@ -143,7 +143,7 @@ class ColdWarFlakGenerator(AirDefenseGroupGenerator):
# Add a P19 Radar for EWR # Add a P19 Radar for EWR
self.add_unit( self.add_unit(
AirDefence.SAM_P19_Flat_Face_SR__SA_2_3, AirDefence.P_19_s_125_sr,
"SR#0", "SR#0",
self.position.x - 60, self.position.x - 60,
self.position.y - 20, self.position.y - 20,

View File

@@ -1,3 +1,5 @@
from typing import Type
from dcs.vehicles import AirDefence from dcs.vehicles import AirDefence
from dcs.unittype import VehicleType from dcs.unittype import VehicleType
@@ -5,7 +7,7 @@ from gen.sam.group_generator import GroupGenerator
class EwrGenerator(GroupGenerator): class EwrGenerator(GroupGenerator):
unit_type: VehicleType unit_type: Type[VehicleType]
@classmethod @classmethod
def name(cls) -> str: def name(cls) -> str:
@@ -25,13 +27,13 @@ class EwrGenerator(GroupGenerator):
class BoxSpringGenerator(EwrGenerator): class BoxSpringGenerator(EwrGenerator):
"""1L13 "Box Spring" EWR.""" """1L13 "Box Spring" EWR."""
unit_type = AirDefence.EWR_1L13 unit_type = AirDefence._1L13_EWR
class TallRackGenerator(EwrGenerator): class TallRackGenerator(EwrGenerator):
"""55G6 "Tall Rack" EWR.""" """55G6 "Tall Rack" EWR."""
unit_type = AirDefence.EWR_55G6 unit_type = AirDefence._55G6_EWR
class DogEarGenerator(EwrGenerator): class DogEarGenerator(EwrGenerator):
@@ -40,7 +42,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.MCC_SR_Sborka_Dog_Ear_SR unit_type = AirDefence.Dog_Ear_radar
class RolandEwrGenerator(EwrGenerator): class RolandEwrGenerator(EwrGenerator):
@@ -49,7 +51,7 @@ class RolandEwrGenerator(EwrGenerator):
This is the Roland search radar, but used as an early warning radar. This is the Roland search radar, but used as an early warning radar.
""" """
unit_type = AirDefence.SAM_Roland_EWR unit_type = AirDefence.Roland_Radar
class FlatFaceGenerator(EwrGenerator): class FlatFaceGenerator(EwrGenerator):
@@ -58,7 +60,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_P19_Flat_Face_SR__SA_2_3 unit_type = AirDefence.P_19_s_125_sr
class PatriotEwrGenerator(EwrGenerator): class PatriotEwrGenerator(EwrGenerator):
@@ -67,7 +69,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 unit_type = AirDefence.Patriot_str
class BigBirdGenerator(EwrGenerator): class BigBirdGenerator(EwrGenerator):
@@ -76,7 +78,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_300_Grumble_Big_Bird_SR unit_type = AirDefence.S_300PS_64H6E_sr
class SnowDriftGenerator(EwrGenerator): class SnowDriftGenerator(EwrGenerator):
@@ -85,7 +87,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_Gadfly_Snow_Drift_SR unit_type = AirDefence.SA_11_Buk_SR_9S18M1
class StraightFlushGenerator(EwrGenerator): class StraightFlushGenerator(EwrGenerator):
@@ -94,7 +96,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_Straight_Flush_STR unit_type = AirDefence.Kub_1S91_str
class HawkEwrGenerator(EwrGenerator): class HawkEwrGenerator(EwrGenerator):
@@ -103,4 +105,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.Hawk_sr

View File

@@ -18,7 +18,7 @@ class FreyaGenerator(AirDefenseGroupGenerator):
# 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( self.add_unit(
AirDefence.EWR_FuMG_401_Freya_LZ, AirDefence.FuMG_401,
"EWR#1", "EWR#1",
self.position.x, self.position.x,
self.position.y, self.position.y,
@@ -28,7 +28,7 @@ class FreyaGenerator(AirDefenseGroupGenerator):
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( self.add_unit(
AirDefence.AAA_Flak_Vierling_38_Quad_20mm, AirDefence.Flak38,
"AA#" + str(i), "AA#" + str(i),
position[0], position[0],
position[1], position[1],
@@ -38,7 +38,7 @@ class FreyaGenerator(AirDefenseGroupGenerator):
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( self.add_unit(
AirDefence.AAA_8_8cm_Flak_18, AirDefence.Flak18,
"AA#" + str(4 + i), "AA#" + str(4 + i),
position[0], position[0],
position[1], position[1],
@@ -47,58 +47,58 @@ class FreyaGenerator(AirDefenseGroupGenerator):
# Command/Logi # Command/Logi
self.add_unit( self.add_unit(
Unarmed.LUV_Kubelwagen_82, Unarmed.Kubelwagen_82,
"Kubel#1", "Kubel#1",
self.position.x - 20, self.position.x - 20,
self.position.y - 20, self.position.y - 20,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
Unarmed.Carrier_Sd_Kfz_7_Tractor, Unarmed.Sd_Kfz_7,
"Sdkfz#1", "Sdkfz#1",
self.position.x + 20, self.position.x + 20,
self.position.y + 22, self.position.y + 22,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
Unarmed.LUV_Kettenrad, Unarmed.Sd_Kfz_2,
"Sdkfz#2", "Sdkfz#2",
self.position.x - 22, self.position.x - 22,
self.position.y + 20, self.position.y + 20,
self.heading, self.heading,
) )
# PU_Maschinensatz_33 and Kdo.g 40 Telemeter # Maschinensatz_33 and Kdo.g 40 Telemeter
self.add_unit( self.add_unit(
AirDefence.PU_Maschinensatz_33, AirDefence.Maschinensatz_33,
"Energy#1", "Energy#1",
self.position.x + 20, self.position.x + 20,
self.position.y - 20, self.position.y - 20,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
AirDefence.AAA_SP_Kdo_G_40, AirDefence.KDO_Mod40,
"Telemeter#1", "Telemeter#1",
self.position.x + 20, self.position.x + 20,
self.position.y - 10, self.position.y - 10,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
Infantry.Infantry_Mauser_98, Infantry.Soldier_mauser98,
"Inf#1", "Inf#1",
self.position.x + 20, self.position.x + 20,
self.position.y - 14, self.position.y - 14,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
Infantry.Infantry_Mauser_98, Infantry.Soldier_mauser98,
"Inf#2", "Inf#2",
self.position.x + 20, self.position.x + 20,
self.position.y - 22, self.position.y - 22,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
Infantry.Infantry_Mauser_98, Infantry.Soldier_mauser98,
"Inf#3", "Inf#3",
self.position.x + 20, self.position.x + 20,
self.position.y - 24, self.position.y - 24,

View File

@@ -20,7 +20,7 @@ class AvengerGenerator(AirDefenseGroupGenerator):
num_launchers = random.randint(2, 3) num_launchers = random.randint(2, 3)
self.add_unit( self.add_unit(
Unarmed.Truck_M818_6x6, Unarmed.M_818,
"TRUCK", "TRUCK",
self.position.x, self.position.x,
self.position.y, self.position.y,
@@ -31,7 +31,7 @@ class AvengerGenerator(AirDefenseGroupGenerator):
) )
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit( self.add_unit(
AirDefence.SAM_Avenger__Stinger, AirDefence.M1097_Avenger,
"SPAA#" + str(i), "SPAA#" + str(i),
position[0], position[0],
position[1], position[1],

View File

@@ -20,7 +20,7 @@ class ChaparralGenerator(AirDefenseGroupGenerator):
num_launchers = random.randint(2, 4) num_launchers = random.randint(2, 4)
self.add_unit( self.add_unit(
Unarmed.Truck_M818_6x6, Unarmed.M_818,
"TRUCK", "TRUCK",
self.position.x, self.position.x,
self.position.y, self.position.y,
@@ -31,7 +31,7 @@ class ChaparralGenerator(AirDefenseGroupGenerator):
) )
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit( self.add_unit(
AirDefence.SAM_Chaparral_M48, AirDefence.M48_Chaparral,
"SPAA#" + str(i), "SPAA#" + str(i),
position[0], position[0],
position[1], position[1],

View File

@@ -18,7 +18,7 @@ class GepardGenerator(AirDefenseGroupGenerator):
def generate(self): def generate(self):
self.add_unit( self.add_unit(
AirDefence.SPAAA_Gepard, AirDefence.Gepard,
"SPAAA", "SPAAA",
self.position.x, self.position.x,
self.position.y, self.position.y,
@@ -26,14 +26,14 @@ class GepardGenerator(AirDefenseGroupGenerator):
) )
if random.randint(0, 1) == 1: if random.randint(0, 1) == 1:
self.add_unit( self.add_unit(
AirDefence.SPAAA_Gepard, AirDefence.Gepard,
"SPAAA2", "SPAAA2",
self.position.x, self.position.x,
self.position.y, self.position.y,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
Unarmed.Truck_M818_6x6, Unarmed.M_818,
"TRUCK", "TRUCK",
self.position.x + 80, self.position.x + 80,
self.position.y, self.position.y,

View File

@@ -2,7 +2,6 @@ import random
from typing import Dict, Iterable, List, Optional, Sequence, Set, Type from typing import Dict, Iterable, List, Optional, Sequence, Set, Type
from dcs.unitgroup import VehicleGroup from dcs.unitgroup import VehicleGroup
from dcs.vehicles import AirDefence
from game import Game from game import Game
from game.factions.faction import Faction from game.factions.faction import Faction
@@ -104,41 +103,6 @@ SAM_MAP: Dict[str, Type[AirDefenseGroupGenerator]] = {
} }
SAM_PRICES = {
AirDefence.SAM_Hawk_Platoon_Command_Post__PCP: 35,
AirDefence.AAA_ZU_23_Emplacement: 10,
AirDefence.AAA_ZU_23_Closed_Emplacement: 10,
AirDefence.SPAAA_ZU_23_2_Mounted_Ural_375: 10,
AirDefence.SPAAA_ZU_23_2_Insurgent_Mounted_Ural_375: 10,
AirDefence.AAA_ZU_23_Insurgent_Closed_Emplacement: 10,
AirDefence.AAA_ZU_23_Insurgent_Emplacement: 10,
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish: 10,
AirDefence.SPAAA_Vulcan_M163: 15,
AirDefence.SAM_Linebacker___Bradley_M6: 20,
AirDefence.SAM_Rapier_LN: 20,
AirDefence.SAM_Avenger__Stinger: 22,
AirDefence.SPAAA_Gepard: 24,
AirDefence.SAM_Roland_ADS: 40,
AirDefence.SAM_Patriot_LN: 85,
AirDefence.SAM_Patriot_EPP_III: 85,
AirDefence.SAM_Chaparral_M48: 25,
AirDefence.AAA_Bofors_40mm: 15,
AirDefence.AAA_8_8cm_Flak_36: 15,
AirDefence.SAM_SA_2_S_75_Guideline_LN: 30,
AirDefence.SAM_SA_3_S_125_Goa_LN: 35,
AirDefence.SAM_SA_6_Kub_Gainful_TEL: 45,
AirDefence.SAM_SA_8_Osa_Gecko_TEL: 30,
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL: 25,
AirDefence.SAM_SA_10_S_300_Grumble_TEL_C: 80,
AirDefence.SAM_SA_10_S_300_Grumble_C2: 80,
AirDefence.SAM_SA_11_Buk_Gadfly_Fire_Dome_TEL: 60,
AirDefence.SAM_SA_13_Strela_10M3_Gopher_TEL: 30,
AirDefence.SAM_SA_15_Tor_Gauntlet: 40,
AirDefence.SAM_SA_19_Tunguska_Grison: 35,
AirDefence.HQ_7_Self_Propelled_LN: 35,
}
def get_faction_possible_sams_generator( def get_faction_possible_sams_generator(
faction: Faction, faction: Faction,
) -> List[Type[AirDefenseGroupGenerator]]: ) -> List[Type[AirDefenseGroupGenerator]]:

View File

@@ -19,21 +19,21 @@ class HawkGenerator(AirDefenseGroupGenerator):
def generate(self): def generate(self):
self.add_unit( self.add_unit(
AirDefence.SAM_Hawk_SR__AN_MPQ_50, AirDefence.Hawk_sr,
"SR", "SR",
self.position.x + 20, self.position.x + 20,
self.position.y, self.position.y,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
AirDefence.SAM_Hawk_Platoon_Command_Post__PCP, AirDefence.Hawk_pcp,
"PCP", "PCP",
self.position.x, self.position.x,
self.position.y, self.position.y,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
AirDefence.SAM_Hawk_TR__AN_MPQ_46, AirDefence.Hawk_tr,
"TR", "TR",
self.position.x + 40, self.position.x + 40,
self.position.y, self.position.y,
@@ -44,7 +44,7 @@ class HawkGenerator(AirDefenseGroupGenerator):
aa_group = self.add_auxiliary_group("AA") aa_group = self.add_auxiliary_group("AA")
self.add_unit_to_group( self.add_unit_to_group(
aa_group, aa_group,
AirDefence.SPAAA_Vulcan_M163, AirDefence.Vulcan,
"AAA", "AAA",
self.position + Point(20, 30), self.position + Point(20, 30),
self.heading, self.heading,
@@ -57,7 +57,7 @@ class HawkGenerator(AirDefenseGroupGenerator):
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit( self.add_unit(
AirDefence.SAM_Hawk_LN_M192, AirDefence.Hawk_ln,
"LN#" + str(i), "LN#" + str(i),
position[0], position[0],
position[1], position[1],

View File

@@ -19,14 +19,14 @@ class HQ7Generator(AirDefenseGroupGenerator):
def generate(self): def generate(self):
self.add_unit( self.add_unit(
AirDefence.HQ_7_Self_Propelled_STR, AirDefence.HQ_7_STR_SP,
"STR", "STR",
self.position.x, self.position.x,
self.position.y, self.position.y,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
AirDefence.HQ_7_Self_Propelled_LN, AirDefence.HQ_7_LN_SP,
"LN", "LN",
self.position.x + 20, self.position.x + 20,
self.position.y, self.position.y,
@@ -37,14 +37,14 @@ class HQ7Generator(AirDefenseGroupGenerator):
aa_group = self.add_auxiliary_group("AA") aa_group = self.add_auxiliary_group("AA")
self.add_unit_to_group( self.add_unit_to_group(
aa_group, aa_group,
AirDefence.SPAAA_ZU_23_2_Mounted_Ural_375, AirDefence.Ural_375_ZU_23,
"AAA1", "AAA1",
self.position + Point(20, 30), self.position + Point(20, 30),
self.heading, self.heading,
) )
self.add_unit_to_group( self.add_unit_to_group(
aa_group, aa_group,
AirDefence.SPAAA_ZU_23_2_Mounted_Ural_375, AirDefence.Ural_375_ZU_23,
"AAA2", "AAA2",
self.position - Point(20, 30), self.position - Point(20, 30),
self.heading, self.heading,
@@ -57,7 +57,7 @@ class HQ7Generator(AirDefenseGroupGenerator):
) )
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit( self.add_unit(
AirDefence.HQ_7_Self_Propelled_LN, AirDefence.HQ_7_LN_SP,
"LN#" + str(i), "LN#" + str(i),
position[0], position[0],
position[1], position[1],

View File

@@ -20,7 +20,7 @@ class LinebackerGenerator(AirDefenseGroupGenerator):
num_launchers = random.randint(2, 4) num_launchers = random.randint(2, 4)
self.add_unit( self.add_unit(
Unarmed.Truck_M818_6x6, Unarmed.M_818,
"TRUCK", "TRUCK",
self.position.x, self.position.x,
self.position.y, self.position.y,
@@ -31,7 +31,7 @@ class LinebackerGenerator(AirDefenseGroupGenerator):
) )
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit( self.add_unit(
AirDefence.SAM_Linebacker___Bradley_M6, AirDefence.M6_Linebacker,
"M6#" + str(i), "M6#" + str(i),
position[0], position[0],
position[1], position[1],

View File

@@ -20,35 +20,35 @@ class PatriotGenerator(AirDefenseGroupGenerator):
def generate(self): def generate(self):
# Command Post # Command Post
self.add_unit( self.add_unit(
AirDefence.SAM_Patriot_STR, AirDefence.Patriot_str,
"STR", "STR",
self.position.x + 30, self.position.x + 30,
self.position.y + 30, self.position.y + 30,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
AirDefence.SAM_Patriot_CR__AMG_AN_MRC_137, AirDefence.Patriot_AMG,
"MRC", "MRC",
self.position.x, self.position.x,
self.position.y, self.position.y,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
AirDefence.SAM_Patriot_ECS, AirDefence.Patriot_ECS,
"MSQ", "MSQ",
self.position.x + 30, self.position.x + 30,
self.position.y, self.position.y,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
AirDefence.SAM_Patriot_C2_ICC, AirDefence.Patriot_cp,
"ICC", "ICC",
self.position.x + 60, self.position.x + 60,
self.position.y, self.position.y,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
AirDefence.SAM_Patriot_EPP_III, AirDefence.Patriot_EPP,
"EPP", "EPP",
self.position.x, self.position.x,
self.position.y + 30, self.position.y + 30,
@@ -61,7 +61,7 @@ class PatriotGenerator(AirDefenseGroupGenerator):
) )
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit( self.add_unit(
AirDefence.SAM_Patriot_LN, AirDefence.Patriot_ln,
"LN#" + str(i), "LN#" + str(i),
position[0], position[0],
position[1], position[1],
@@ -77,7 +77,7 @@ class PatriotGenerator(AirDefenseGroupGenerator):
for i, (x, y, heading) in enumerate(positions): for i, (x, y, heading) in enumerate(positions):
self.add_unit_to_group( self.add_unit_to_group(
aa_group, aa_group,
AirDefence.SPAAA_Vulcan_M163, AirDefence.Vulcan,
f"SPAAA#{i}", f"SPAAA#{i}",
Point(x, y), Point(x, y),
heading, heading,

View File

@@ -18,14 +18,14 @@ class RapierGenerator(AirDefenseGroupGenerator):
def generate(self): def generate(self):
self.add_unit( self.add_unit(
AirDefence.SAM_Rapier_Blindfire_TR, AirDefence.Rapier_fsa_blindfire_radar,
"BT", "BT",
self.position.x, self.position.x,
self.position.y, self.position.y,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
AirDefence.SAM_Rapier_Tracker, AirDefence.Rapier_fsa_optical_tracker_unit,
"OT", "OT",
self.position.x + 20, self.position.x + 20,
self.position.y, self.position.y,
@@ -39,7 +39,7 @@ class RapierGenerator(AirDefenseGroupGenerator):
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit( self.add_unit(
AirDefence.SAM_Rapier_LN, AirDefence.Rapier_fsa_launcher,
"LN#" + str(i), "LN#" + str(i),
position[0], position[0],
position[1], position[1],

View File

@@ -16,21 +16,21 @@ class RolandGenerator(AirDefenseGroupGenerator):
def generate(self): def generate(self):
self.add_unit( self.add_unit(
AirDefence.SAM_Roland_EWR, AirDefence.Roland_Radar,
"EWR", "EWR",
self.position.x + 40, self.position.x + 40,
self.position.y, self.position.y,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
AirDefence.SAM_Roland_ADS, AirDefence.Roland_ADS,
"ADS", "ADS",
self.position.x, self.position.x,
self.position.y, self.position.y,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
Unarmed.Truck_M818_6x6, Unarmed.M_818,
"TRUCK", "TRUCK",
self.position.x + 80, self.position.x + 80,
self.position.y, self.position.y,

View File

@@ -22,13 +22,13 @@ class SA10Generator(AirDefenseGroupGenerator):
def __init__(self, game: Game, ground_object: SamGroundObject): def __init__(self, game: Game, ground_object: SamGroundObject):
super().__init__(game, ground_object) super().__init__(game, ground_object)
self.sr1 = AirDefence.SAM_SA_10_S_300_Grumble_Clam_Shell_SR self.sr1 = AirDefence.S_300PS_40B6MD_sr
self.sr2 = AirDefence.SAM_SA_10_S_300_Grumble_Big_Bird_SR self.sr2 = AirDefence.S_300PS_64H6E_sr
self.cp = AirDefence.SAM_SA_10_S_300_Grumble_C2 self.cp = AirDefence.S_300PS_54K6_cp
self.tr1 = AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR self.tr1 = AirDefence.S_300PS_40B6M_tr
self.tr2 = AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR self.tr2 = AirDefence.S_300PS_40B6M_tr
self.ln1 = AirDefence.SAM_SA_10_S_300_Grumble_TEL_C self.ln1 = AirDefence.S_300PS_5P85C_ln
self.ln2 = AirDefence.SAM_SA_10_S_300_Grumble_TEL_D self.ln2 = AirDefence.S_300PS_5P85D_ln
def generate(self): def generate(self):
# Search Radar # Search Radar
@@ -84,7 +84,7 @@ class SA10Generator(AirDefenseGroupGenerator):
for i, (x, y, heading) in enumerate(positions): for i, (x, y, heading) in enumerate(positions):
self.add_unit_to_group( self.add_unit_to_group(
aa_group, aa_group,
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish, AirDefence.ZSU_23_4_Shilka,
f"AA#{i}", f"AA#{i}",
Point(x, y), Point(x, y),
heading, heading,
@@ -109,7 +109,7 @@ class Tier2SA10Generator(SA10Generator):
for i, (x, y, heading) in enumerate(positions): for i, (x, y, heading) in enumerate(positions):
self.add_unit_to_group( self.add_unit_to_group(
pd_group, pd_group,
AirDefence.SAM_SA_15_Tor_Gauntlet, AirDefence.Tor_9A331,
f"PD#{i}", f"PD#{i}",
Point(x, y), Point(x, y),
heading, heading,
@@ -131,7 +131,7 @@ class Tier3SA10Generator(SA10Generator):
for i, (x, y, heading) in enumerate(positions): for i, (x, y, heading) in enumerate(positions):
self.add_unit_to_group( self.add_unit_to_group(
aa_group, aa_group,
AirDefence.SAM_SA_19_Tunguska_Grison, AirDefence._2S6_Tunguska,
f"AA#{i}", f"AA#{i}",
Point(x, y), Point(x, y),
heading, heading,
@@ -146,7 +146,7 @@ class Tier3SA10Generator(SA10Generator):
for i, (x, y, heading) in enumerate(positions): for i, (x, y, heading) in enumerate(positions):
self.add_unit_to_group( self.add_unit_to_group(
pd_group, pd_group,
AirDefence.SAM_SA_15_Tor_Gauntlet, AirDefence.Tor_9A331,
f"PD#{i}", f"PD#{i}",
Point(x, y), Point(x, y),
heading, heading,

View File

@@ -18,14 +18,14 @@ class SA11Generator(AirDefenseGroupGenerator):
def generate(self): def generate(self):
self.add_unit( self.add_unit(
AirDefence.SAM_SA_11_Buk_Gadfly_Snow_Drift_SR, AirDefence.SA_11_Buk_SR_9S18M1,
"SR", "SR",
self.position.x + 20, self.position.x + 20,
self.position.y, self.position.y,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
AirDefence.SAM_SA_11_Buk_Gadfly_C2, AirDefence.SA_11_Buk_CC_9S470M1,
"CC", "CC",
self.position.x, self.position.x,
self.position.y, self.position.y,
@@ -39,7 +39,7 @@ class SA11Generator(AirDefenseGroupGenerator):
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit( self.add_unit(
AirDefence.SAM_SA_11_Buk_Gadfly_Fire_Dome_TEL, AirDefence.SA_11_Buk_LN_9A310M1,
"LN#" + str(i), "LN#" + str(i),
position[0], position[0],
position[1], position[1],

View File

@@ -18,14 +18,14 @@ class SA13Generator(AirDefenseGroupGenerator):
def generate(self): def generate(self):
self.add_unit( self.add_unit(
Unarmed.LUV_UAZ_469_Jeep, Unarmed.UAZ_469,
"UAZ", "UAZ",
self.position.x, self.position.x,
self.position.y, self.position.y,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
Unarmed.Truck_KAMAZ_43101, Unarmed.KAMAZ_Truck,
"TRUCK", "TRUCK",
self.position.x + 40, self.position.x + 40,
self.position.y, self.position.y,
@@ -38,7 +38,7 @@ class SA13Generator(AirDefenseGroupGenerator):
) )
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit( self.add_unit(
AirDefence.SAM_SA_13_Strela_10M3_Gopher_TEL, AirDefence.Strela_10M3,
"LN#" + str(i), "LN#" + str(i),
position[0], position[0],
position[1], position[1],

View File

@@ -16,21 +16,21 @@ class SA15Generator(AirDefenseGroupGenerator):
def generate(self): def generate(self):
self.add_unit( self.add_unit(
AirDefence.SAM_SA_15_Tor_Gauntlet, AirDefence.Tor_9A331,
"ADS", "ADS",
self.position.x, self.position.x,
self.position.y, self.position.y,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
Unarmed.LUV_UAZ_469_Jeep, Unarmed.UAZ_469,
"EWR", "EWR",
self.position.x + 40, self.position.x + 40,
self.position.y, self.position.y,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
Unarmed.Truck_KAMAZ_43101, Unarmed.KAMAZ_Truck,
"TRUCK", "TRUCK",
self.position.x + 80, self.position.x + 80,
self.position.y, self.position.y,

View File

@@ -17,14 +17,14 @@ class SA17Generator(AirDefenseGroupGenerator):
def generate(self): def generate(self):
self.add_unit( self.add_unit(
AirDefence.SAM_SA_11_Buk_Gadfly_Snow_Drift_SR, AirDefence.SA_11_Buk_SR_9S18M1,
"SR", "SR",
self.position.x + 20, self.position.x + 20,
self.position.y, self.position.y,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
AirDefence.SAM_SA_11_Buk_Gadfly_C2, AirDefence.SA_11_Buk_CC_9S470M1,
"CC", "CC",
self.position.x, self.position.x,
self.position.y, self.position.y,

View File

@@ -21,7 +21,7 @@ class SA19Generator(AirDefenseGroupGenerator):
if num_launchers == 1: if num_launchers == 1:
self.add_unit( self.add_unit(
AirDefence.SAM_SA_19_Tunguska_Grison, AirDefence._2S6_Tunguska,
"LN#0", "LN#0",
self.position.x, self.position.x,
self.position.y, self.position.y,
@@ -33,7 +33,7 @@ class SA19Generator(AirDefenseGroupGenerator):
) )
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit( self.add_unit(
AirDefence.SAM_SA_19_Tunguska_Grison, AirDefence._2S6_Tunguska,
"LN#" + str(i), "LN#" + str(i),
position[0], position[0],
position[1], position[1],

View File

@@ -18,14 +18,14 @@ class SA2Generator(AirDefenseGroupGenerator):
def generate(self): def generate(self):
self.add_unit( self.add_unit(
AirDefence.SAM_P19_Flat_Face_SR__SA_2_3, AirDefence.P_19_s_125_sr,
"SR", "SR",
self.position.x, self.position.x,
self.position.y, self.position.y,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
AirDefence.SAM_SA_2_S_75_Fan_Song_TR, AirDefence.SNR_75V,
"TR", "TR",
self.position.x + 20, self.position.x + 20,
self.position.y, self.position.y,
@@ -39,7 +39,7 @@ class SA2Generator(AirDefenseGroupGenerator):
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit( self.add_unit(
AirDefence.SAM_SA_2_S_75_Guideline_LN, AirDefence.S_75M_Volhov,
"LN#" + str(i), "LN#" + str(i),
position[0], position[0],
position[1], position[1],

View File

@@ -18,14 +18,14 @@ class SA3Generator(AirDefenseGroupGenerator):
def generate(self): def generate(self):
self.add_unit( self.add_unit(
AirDefence.SAM_P19_Flat_Face_SR__SA_2_3, AirDefence.P_19_s_125_sr,
"SR", "SR",
self.position.x, self.position.x,
self.position.y, self.position.y,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
AirDefence.SAM_SA_3_S_125_Low_Blow_TR, AirDefence.Snr_s_125_tr,
"TR", "TR",
self.position.x + 20, self.position.x + 20,
self.position.y, self.position.y,
@@ -39,7 +39,7 @@ class SA3Generator(AirDefenseGroupGenerator):
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit( self.add_unit(
AirDefence.SAM_SA_3_S_125_Goa_LN, AirDefence._5p73_s_125_ln,
"LN#" + str(i), "LN#" + str(i),
position[0], position[0],
position[1], position[1],

View File

@@ -18,7 +18,7 @@ class SA6Generator(AirDefenseGroupGenerator):
def generate(self): def generate(self):
self.add_unit( self.add_unit(
AirDefence.SAM_SA_6_Kub_Straight_Flush_STR, AirDefence.Kub_1S91_str,
"STR", "STR",
self.position.x, self.position.x,
self.position.y, self.position.y,
@@ -32,7 +32,7 @@ class SA6Generator(AirDefenseGroupGenerator):
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit( self.add_unit(
AirDefence.SAM_SA_6_Kub_Gainful_TEL, AirDefence.Kub_2P25_ln,
"LN#" + str(i), "LN#" + str(i),
position[0], position[0],
position[1], position[1],

View File

@@ -16,14 +16,14 @@ class SA8Generator(AirDefenseGroupGenerator):
def generate(self): def generate(self):
self.add_unit( self.add_unit(
AirDefence.SAM_SA_8_Osa_Gecko_TEL, AirDefence.Osa_9A33_ln,
"OSA", "OSA",
self.position.x, self.position.x,
self.position.y, self.position.y,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
AirDefence.SAM_SA_8_Osa_LD_9T217, AirDefence.SA_8_Osa_LD_9T217,
"LD", "LD",
self.position.x + 20, self.position.x + 20,
self.position.y, self.position.y,

View File

@@ -18,14 +18,14 @@ class SA9Generator(AirDefenseGroupGenerator):
def generate(self): def generate(self):
self.add_unit( self.add_unit(
Unarmed.LUV_UAZ_469_Jeep, Unarmed.UAZ_469,
"UAZ", "UAZ",
self.position.x, self.position.x,
self.position.y, self.position.y,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
Unarmed.Truck_KAMAZ_43101, Unarmed.KAMAZ_Truck,
"TRUCK", "TRUCK",
self.position.x + 40, self.position.x + 40,
self.position.y, self.position.y,
@@ -38,7 +38,7 @@ class SA9Generator(AirDefenseGroupGenerator):
) )
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit( self.add_unit(
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL, AirDefence.Strela_1_9P31,
"LN#" + str(i), "LN#" + str(i),
position[0], position[0],
position[1], position[1],

View File

@@ -18,7 +18,7 @@ class VulcanGenerator(AirDefenseGroupGenerator):
def generate(self): def generate(self):
self.add_unit( self.add_unit(
AirDefence.SPAAA_Vulcan_M163, AirDefence.Vulcan,
"SPAAA", "SPAAA",
self.position.x, self.position.x,
self.position.y, self.position.y,
@@ -26,14 +26,14 @@ class VulcanGenerator(AirDefenseGroupGenerator):
) )
if random.randint(0, 1) == 1: if random.randint(0, 1) == 1:
self.add_unit( self.add_unit(
AirDefence.SPAAA_Vulcan_M163, AirDefence.Vulcan,
"SPAAA2", "SPAAA2",
self.position.x, self.position.x,
self.position.y, self.position.y,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
Unarmed.Truck_M818_6x6, Unarmed.M_818,
"TRUCK", "TRUCK",
self.position.x + 80, self.position.x + 80,
self.position.y, self.position.y,

View File

@@ -24,7 +24,7 @@ class ZSU23Generator(AirDefenseGroupGenerator):
) )
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit( self.add_unit(
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish, AirDefence.ZSU_23_4_Shilka,
"SPAA#" + str(i), "SPAA#" + str(i),
position[0], position[0],
position[1], position[1],

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