Compare commits

..

626 Commits
2.4.0 ... 3.0.0

Author SHA1 Message Date
Dan Albert
00ca7d0b4d Merge pull request #1208 from dcs-liberation/develop-3.x
Release 3.0.0.
2021-06-10 23:58:41 -07:00
Dan Albert
64c426653c Merge branch 'master' into develop-3.x 2021-06-10 23:46:18 -07:00
Mustang-25
a8960c9bbe Add the First Lebanon War Historical Campaign.
(cherry picked from commit 75e3b4cc84)
2021-06-10 17:39:00 -07:00
Florian
72282845e8 added missing units
(cherry picked from commit 78cd17e279)
2021-06-10 17:37:26 -07:00
Khopa
e64aff4e91 Removed helipad from golan heights campaign to avoid capture trigger error 2021-06-10 23:27:19 +02:00
Dan Albert
e192e54c90 Fix CAS commit range display.
CAS commits around the target, not its flight plan.

(cherry picked from commit 40aa7734e1)
2021-06-09 21:52:02 -07:00
Dan Albert
39b0599b7b Fix engagement distance display.
(cherry picked from commit a9dacf4a29)
2021-06-09 21:45:20 -07:00
Dan Albert
45b40e4aa3 Update Operation Mole Cricket.
https://github.com/dcs-liberation/dcs_liberation/issues/1203
(cherry picked from commit 0594e1148e)
2021-06-09 21:27:22 -07:00
Dan Albert
9887a8ff83 Add Northern Russia campaign.
https://github.com/dcs-liberation/dcs_liberation/issues/1202
(cherry picked from commit 9eacd1563f)
2021-06-09 21:27:21 -07:00
Dan Albert
8d3556aa4b Update mission start guidance.
(cherry picked from commit 66f82b6ff9)
2021-06-09 19:21:18 -07:00
Florian
a59c01bcfe added texts for all units
(cherry picked from commit eb6206ea57)
2021-06-09 19:10:02 -07:00
Brock Greman
fb72962f74 Fixing display of "sunny" during clear conditions at night.
(cherry picked from commit 3ad51cafa8)
2021-06-09 19:09:40 -07:00
Dan Albert
ed1dacfe7c 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

(cherry picked from commit 0e68884493)
2021-06-09 19:09:12 -07:00
Florian
794de0fcbb added missing units to price table
(cherry picked from commit 366190ee99)
2021-06-09 19:04:32 -07:00
Dan Albert
9d71b2e727 Make campaign names consistent.
(cherry picked from commit 42d56a324f)
2021-06-09 18:59:05 -07:00
docofmur
5b8f626651 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

(cherry picked from commit 7d1f1ea2f7)
2021-06-09 18:59:04 -07:00
Dan Albert
461f4b82a9 Add Around the Mountain campaign.
https://github.com/dcs-liberation/dcs_liberation/issues/1199
(cherry picked from commit 30cab8e3a7)
2021-06-09 18:54:15 -07:00
Dan Albert
15653d0628 Add Operation Allied Sword campaign and factions.
https://github.com/dcs-liberation/dcs_liberation/issues/1196
(cherry picked from commit e0e2162c6d)
2021-06-09 18:54:14 -07:00
Dan Albert
dffc631b87 Add Humble Helper campaign and factions.
https://github.com/dcs-liberation/dcs_liberation/issues/1197
(cherry picked from commit f1582fcc10)
2021-06-09 18:54:12 -07:00
Dan Albert
17efb48b2e Make enable_and_reset not half lie.
https://github.com/dcs-liberation/dcs_liberation/issues/1185
(cherry picked from commit b8c14d69c3)
2021-06-08 21:20:15 -07:00
Dan Albert
7e85825d2b Fix typo in Incirlik runway data.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1188

(cherry picked from commit 725b5083c7)
2021-06-08 21:15:04 -07:00
Dan Albert
798591b980 Fix repeated JTACs after multiple generations.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1191

(cherry picked from commit 87dd6b19bf)
2021-06-08 20:56:41 -07:00
Schneefl0cke
4a52af298c Add Recon combat role.
(cherry picked from commit e4c9d8799e)
2021-06-08 20:47:41 -07:00
dependabot[bot]
fe886a754e 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>
(cherry picked from commit bc938db7f9)
2021-06-08 20:47:31 -07:00
bgreman
0220fa4ff6 Gripen mod support. 2021-06-08 19:47:44 -07:00
Dan Albert
e7336d8608 Fix hangar status display.
(cherry picked from commit d8881e2734)
2021-06-06 17:13:00 -07:00
Dan Albert
d77a174ac1 Label the player checkbox in the roster editor.
(cherry picked from commit 45869c428e)
2021-06-06 13:43:55 -07:00
Khopa
6348317893 Added a small WW2 campaign on Normandy map (Replacing the former Normandy Small campaign). 2021-06-06 18:49:11 +02:00
Khopa
a516cd2f80 Added a small WW2 campaign on Normandy map (Replacing the former Normandy Small campaign). 2021-06-06 18:45:02 +02:00
Dan Albert
e1aa3e9d0e Suppress events fired while rebuilding model.
(cherry picked from commit d316e13fa6)
2021-06-05 15:21:58 -07:00
Dan Albert
d1d1acf6e0 Hide incompatible campaigns by default.
https://github.com/dcs-liberation/dcs_liberation/issues/1178
(cherry picked from commit 1ea98a6ed1)
2021-06-05 15:15:53 -07:00
Dan Albert
3e43414d9c Make the new package dialog modal.
In the *new* package dialog, a package has been created and may have
aircraft assigned to it, but it is not a part of the ATO until the user
saves it.

Other actions (modifying settings, closing some other dialogs like the
base menu) can cause a Game update which will forcibly close this window
without either accepting or rejecting it, so we neither save the package
nor release any allocated units.

While it would be preferable to be able to update this dialog as needed
in the event of game updates, the quick fix is to just not allow
interaction with other UI elements until the new package has either been
finalized or canceled.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1027
2021-06-05 14:21:31 -07:00
Dan Albert
6d682d509f Restore former turn 0 budget split. 2021-06-05 14:09:10 -07:00
Florian
3a592aee8b Split purchase budget based on investment ratio.
The AI purchaser will aim to have a 50/50 ground/air investment mix.
This allows it to overspend on one category if significant losses were
taken the previous turn.

The total purchase amount is still limited, so if the bases are full
when only 10% of the investment is in ground units, the full budget for
the turn will still go to air.
2021-06-05 14:07:18 -07:00
Dan Albert
b74f60fe0e Stagger packages in units of seconds, not minutes.
Missions with very large numbers of packages and short mission windows
would raise an exception here because we couldn't schedule more
frequently than once a minute. Switch to using seconds instead of
minutes to avoid that problem. If there are more packages than there are
seconds in the mission the game is broken for other reasons.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1154
2021-06-05 13:50:28 -07:00
Dan Albert
34f3a50234 Fix UI quirks when reopening custom loadout.
* Disable the selector.
* Select the default loadout rather than the first one so unchecking the
  custom box goes back to the default loadout.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1152
2021-06-05 13:40:11 -07:00
Dan Albert
6094179a40 Show pending ground unit count in the base menu.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1161
2021-06-05 13:24:12 -07:00
Dan Albert
e3bc2688ba Revert accidental change to IR campaign.
Used this for debugging airlifts and accidentally included it.
2021-06-05 12:46:19 -07:00
Dan Albert
96cdea2a94 Load two units per cargo plane.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1029
2021-06-05 12:21:34 -07:00
Dan Albert
cb159e3341 Fix canceling transfers.
singledispatchmethod only overloads on the first argument.
2021-06-05 12:00:43 -07:00
Dan Albert
136e776b03 Add map markers for each building in the group.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1126
2021-06-04 18:07:12 -07:00
Dan Albert
a0833e8943 Allow selection of auto-assigned mission types.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1176
2021-06-04 17:50:21 -07:00
Dan Albert
8bb1b1da7c Fix squadron roster size.
This was excluding a number of pilots from the end of the roster equal
to the number of losses the squadron had suffered.
2021-06-04 17:01:29 -07:00
Dan Albert
558502d8ea Convert flight creator to pilot roster.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1143
2021-06-04 17:01:29 -07:00
Khopa
8edb952800 Updated campaign "Golan Heights" to version 6.0 2021-06-05 01:24:08 +02:00
Mustang-25
f3d79e58db Add the Battle for the UAE Campaign.
The Battle for the UAE is a follow-on campaign to the Battle for Abu Dhabi.
2021-06-03 23:30:02 -07:00
Dan Albert
f26ff085e1 Add a new bug template for campaign update "PRs". 2021-06-03 19:08:00 -07:00
Brock Greman
7ea550738e Sorts flights in the base menu by mission start time. Also makes base menu dialog bigger. 2021-06-03 15:32:49 -07:00
Brock Greman
6b1048590f Fixes issue where only first a/c in a flight would show in air inventory 2021-06-03 14:46:12 -07:00
Schneefl0cke
203f0d3851 Introduce recon unit type, adjust ratios. 2021-06-02 23:00:35 -07:00
Dan Albert
d9c38a716c Move settings and stats to the toolbar.
https://github.com/dcs-liberation/dcs_liberation/issues/1146
2021-06-01 22:59:15 -07:00
Mustang-25
24709d01bd Add the Operation Mole Cricket 2010 Campaign. 2021-06-01 22:13:23 -07:00
Dan Albert
2dc2681f84 Attach bug URL to a TODO. 2021-06-01 20:16:33 -07:00
Dan Albert
d53a39860e Update Black Sea to latest campaign format. 2021-06-01 18:49:33 -07:00
SnappyComebacks
ad2f084112 Added ammunition depot changes to 3.0 changelog. 2021-05-31 22:20:38 -07:00
Dan Albert
d59c42ed3f Fix generation crash for large campaigns.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1162
2021-05-31 21:02:46 -07:00
Dan Albert
e022ffee62 Auto-ASAP player missions. 2021-05-31 20:05:04 -07:00
Dan Albert
77ddd5ed78 Don't include off-map spawns for AEW&C planning. 2021-05-31 20:00:10 -07:00
Dan Albert
8604faffe6 Make EWR sites purchasable.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/968
2021-05-31 16:33:55 -07:00
Dan Albert
45919200c4 Depopulate captured objectives.
Buildings are left to be captured, but the retreating coalition now
destroys their abandoned equipment.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1158
2021-05-31 15:43:20 -07:00
Dan Albert
d498bb9cff Give AEW&C a significant buffer from threats. 2021-05-31 15:33:21 -07:00
Dan Albert
389f60786a Fix moving carriers. 2021-05-31 15:13:56 -07:00
Dan Albert
2d0929cd69 Plan AEW&C in safer locations.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1119
2021-05-31 14:57:25 -07:00
Dan Albert
e94ebd6ed2 Invert AEW&C default behavior.
This is working well. Make it the default, but don't remove it since we
don't have HAVCAP yet.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1118
2021-05-31 14:42:22 -07:00
Dan Albert
77373606fe Fix crash in startup. 2021-05-31 14:36:25 -07:00
Dan Albert
284f2bc323 Show runway status on the new map.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1105
2021-05-31 14:18:27 -07:00
Dan Albert
355e6e1d15 Fix several cases of wrongly using broken runways.
The usual symptom here was the game breaking when a carrier is
destroyed. The carrier would no longer be operational but missions would
be assigned there that could not generate flight plans.
2021-05-31 14:13:33 -07:00
Dan Albert
f6909d2f98 Use airfield icons for off-map spawns.
Nothing else is really appropriate, and these are meant to represent
off-map airfields.
2021-05-31 13:23:47 -07:00
Dan Albert
c42974f7b3 Update FOB icon.
Also removes the possibility of generating FOBs that are not FOBs.
2021-05-31 13:14:45 -07:00
Khopa
230d80a2a5 Updated campaign "Russia Small" campaign to version 6.0 2021-05-31 19:52:06 +02:00
Dan Albert
551038b295 Fix TGO purchase UI.
Enable buying and selling of armor groups. Don't allow armor groups to
be replaced with air defenses or vice versa. These are a different TGO
type and this has always been a thing that will break the flight
planner.
2021-05-30 21:17:55 -07:00
Dan Albert
4055b06e71 Clean up and rename ControlPoint.for_airbase. 2021-05-30 21:17:12 -07:00
Dan Albert
6616359baf Remove "base defenses" UI features.
There's no such thing any more. There are just objectives that are
closer to the CP than the others.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1130
2021-05-30 21:07:48 -07:00
Dan Albert
a5336bbe56 Note dropping random objectives in the changelog. 2021-05-30 20:51:18 -07:00
Dan Albert
871e7f7a50 Remove random objective generation.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1076
2021-05-30 20:47:20 -07:00
Dan Albert
d1c7146a47 Add cheat options back to front lines.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1108.
2021-05-30 20:16:54 -07:00
Dan Albert
30f6220c3e Remove the old map. 2021-05-30 19:57:14 -07:00
Dan Albert
acd3e87996 Remove accidentally preserved debug log in UI. 2021-05-30 19:46:38 -07:00
Dan Albert
8c8814d07e Add culling display option to the new map.
https://github.com/dcs-liberation/dcs_liberation/issues/1097
2021-05-30 19:32:08 -07:00
Dan Albert
417fc3af5b Allow objects near missile launchers to be culled.
We want the scud to not be culled, but we should still cull things
nearby. Rather than making the scud the center of a 2.5km unculled zone,
just exclude missile objectives from culling.
2021-05-30 19:29:59 -07:00
Dan Albert
2218733da4 Add exclusion zone display to the new map.
https://github.com/dcs-liberation/dcs_liberation/issues/1097
2021-05-30 18:50:53 -07:00
Brock Greman
9d1060975e Fixes the ugly border on the Air Inventory View 2021-05-30 16:47:18 -07:00
Dan Albert
82281e2477 Fix inventory handling when adjusting flight size.
We need to resize the flight before attempting to claim the aircraft, or
we'll reclaim the original number of aircraft rather than the new size.
2021-05-30 15:46:54 -07:00
Dan Albert
d0976c45e9 Add pilot assignements to the inventory table. 2021-05-30 14:28:58 -07:00
bgreman
a888397bef Add a global air inventory view to air wing dialog.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/428.
2021-05-30 13:40:51 -07:00
Brock Greman
7b2bb4a128 Show ground unit supply info in the base menu.
Update the base UI to have a hint about ground unit deployment limits
and a matching tooltip for how it is calculated.
2021-05-30 13:15:06 -07:00
SnappyComebacks
d440dc00f1 Purchase reserves at front lines.
This changes the ground unit purchase behavior such that the supply
limit is exceeded by 30%, with the extra units kept in reserve.

The old hard cap of 50 units is no longer needed, since the ammo supply
now does the same task, so that's been removed.
2021-05-30 13:04:18 -07:00
Florian
d61382f4e2 Maintain composition when buying ground units.
Unit composition is defined by the doctrine. The most understaffed CP
will now get the most underrepresented unit type. Previously a random
understaffed CP would get a random unit type.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1057.
2021-05-30 12:52:35 -07:00
Khopa
d4fe893539 Updated "Russia Small" campaign to 5.0 (ammo depots) 2021-05-30 17:58:56 +02:00
Khopa
1af95955b6 Base menu UI : Added ammo depots & factory information (WIP UX) 2021-05-30 17:49:15 +02:00
Khopa
a43e926dd2 Updated campaign "Golan Height" for v5.0 (Ammo depots) 2021-05-30 16:59:02 +02:00
Khopa
ff49046bfa Updated locales for a few factions 2021-05-30 16:57:51 +02:00
SnappyComebacks
95b0b851a5 Limit front line size with ammo depots.
This limit is determined by the number of buildings that belong to Ammo
Depots at the front line's connected Control Point. The limit increases
for every surviving building at ammo depot objectives.

There is a lower limit to the number of units that will spawn, so that
if there are no surviving ammo depot buildings at a control point, there
will still be some ground conflict.
2021-05-29 13:25:23 -07:00
Dan Albert
077ca19912 Add ammo depots to Abu Dhabi and Inherent Resolve.
IR gets one per base to maintain the old behavior. Abu Dhabi has a bit
more variety, with major bases like Al Dhafra and Bandar Abbas getting
two and FOBs getting none.
2021-05-29 12:36:05 -07:00
Brock Greman
089cc23648 Fixing duplicate connected CPs between Beslan and Modok and Beslan and Nalchik. 2021-05-29 03:05:22 -07:00
Dan Albert
e6b9a73d03 Improve AI air defense target prioritization.
Target the air defenses whose *threat ranges* come closest to friendly
bases rather than the closest sites themselves. In other words, the
SA-10 that is 5 miles behind the SA-6 will now be the priority.

This also treats EWRs a bit differently. If they are not protected by a
SAM their detection range will be used for determining their "threat"
range. Otherwise a heuristic is used to determine whether or not they
can be safely attacked without encroaching on the covering SAM.
2021-05-28 19:27:02 -07:00
Dan Albert
cea264e871 Remove special case behavior for FOB missions.
The only difference from the main CP types was that it didn't support
AEW&C (which shouldn't have been on the main ControlPoint class anyway)
and add strike.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1132
2021-05-28 18:02:57 -07:00
Dan Albert
d0bde7b016 Check for interesection when localizing.
Apparently it is possible to place an objective exactly on the boundary
of a navpoly.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1140
2021-05-28 17:26:46 -07:00
Dan Albert
5b271df66f Propagate planning error for package waypoints.
We did this for the flight plan itself, but did not if the package
waypoints failed to generate.

https://github.com/dcs-liberation/dcs_liberation/issues/1140
2021-05-28 17:26:31 -07:00
Dan Albert
bc7faee880 Add navmesh map mode to the new map.
https://github.com/dcs-liberation/dcs_liberation/issues/1097
2021-05-28 17:00:33 -07:00
Dan Albert
a2abdcf5d3 Ensure that a transit path exists for recruitment.
Networks can be disconnected even by airlift because FOBs are not
airports.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1147
2021-05-28 16:28:50 -07:00
Dan Albert
d4e843983d Add more tracing for turn processing.
Most of the time (1-2 seconds) is going to flight plan layout. There
don't seem to be any easy opportunities for improvement.
2021-05-28 15:31:33 -07:00
Dan Albert
6e41c36a44 Fix replacing unassigned pilot slots. 2021-05-27 22:51:11 -07:00
Dan Albert
1fe3451120 Set locales for some factions.
Far from complete.
2021-05-27 21:37:04 -07:00
Dan Albert
bc4a95d0a5 Un-WIP squadrons in the changelog.
This is feature complete.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-27 21:05:01 -07:00
Dan Albert
14dc6d1604 Add squadron selector to flight creator.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1142
2021-05-27 21:00:32 -07:00
Dan Albert
1795ed7617 Limit squadron tasks to those of the aircraft.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-27 20:27:45 -07:00
Dan Albert
e8edb31be3 Revert "Don't assign pilots to unused aircraft."
The unitmap needs to account for this.

This reverts commit dae3835eb0.
2021-05-27 20:02:39 -07:00
Dan Albert
58fd30e6ad Add the 15th Airlift Squadron. 2021-05-27 19:58:45 -07:00
Dan Albert
9a34ada258 Clear last turn's procurement requests each turn.
Without this, *this turn's* urgent requests are lower priority than last
turn's stretch goals. The requests are remade every turn so we lose
nothing by removing this.

Bug was introduced by f69450e2ae, so this
doesn't affect 2.5.
2021-05-27 19:58:45 -07:00
Dan Albert
748a752e29 Fix pydcs loadout cache for command line launcher. 2021-05-27 19:58:45 -07:00
Dan Albert
37748ef3bd Obey squadron mission types when planning airlift.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-27 19:58:45 -07:00
Dan Albert
d41007de8e Add VFA-192. 2021-05-27 19:58:45 -07:00
Dan Albert
45befd440c Consider squadron for task capability checking.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-27 19:58:45 -07:00
Dan Albert
a2c10f1c7a Check for compatible squadrons when buying planes.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-27 19:58:45 -07:00
Dan Albert
d7768f86d3 Obey squadron mission types in auto-planning.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-27 19:58:45 -07:00
Dan Albert
dae3835eb0 Don't assign pilots to unused aircraft.
These "flights" are only created so that we can spawn the aircraft on
the ramp for OCA strikes. They shouldn't have pilots assigned.
2021-05-27 19:58:45 -07:00
Dan Albert
e9b5784d30 Update player slot advice for update UI.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-27 17:35:54 -07:00
Dan Albert
1521f0a9b1 Add on-leave toggle for pilots.
Pilots on leave will not be assignable to any flights (but will not be
unassigned from any already scheduled this turn).

https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-27 17:09:09 -07:00
Dan Albert
9a9c351f47 Copy the list of random names as was intended.
Every name generated depopulated the global list which made reset do
nothing. Large numbers of TGOs (or generation of many campaigns) would
drain the list fully and new squadrons would no longer have a name list
to pull from.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1144
2021-05-27 16:32:21 -07:00
Dan Albert
4ec11ddea5 Auto-ASAP player packages based on preferences.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-27 16:04:51 -07:00
Dan Albert
f619b6b9fc Choose player pilots based on player preference.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-27 16:00:27 -07:00
Dan Albert
bcccb3206d Connect auto-ATO disable option.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/315
2021-05-27 15:43:47 -07:00
Dan Albert
11a8ff7f70 Add settings UI for auto-ato behavior.
https://github.com/dcs-liberation/dcs_liberation/issues/276
https://github.com/dcs-liberation/dcs_liberation/issues/315
2021-05-27 15:41:08 -07:00
Dan Albert
f6ab1aad77 Clean up signal handling in pilot roster editor. 2021-05-27 15:06:36 -07:00
Dan Albert
5a732acf64 Fix creating new fights to create player pilots.
This is just a hack that automatically converts the assigned pilots to
players if needed. This really needs to be replaced with a roster editor
like the flight edit screen has, but that also requires squadron
selection, which isn't a thing we're ready for yet.

https://github.com/dcs-liberation/dcs_liberation/issues/1139
2021-05-27 14:15:27 -07:00
Dan Albert
e4e06e0a6e Add player toggle to flight settings.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-27 02:18:57 -07:00
Dan Albert
28f20d47d3 Add player pilot invulnerability option.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-27 01:40:23 -07:00
Dan Albert
82ce688a0d Allow players to be defined in the squadron file.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-27 01:15:52 -07:00
Dan Albert
f36757b650 Add livery selection for predefined squadrons.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-27 00:56:05 -07:00
Dan Albert
ac4a7441e9 Add predefined squadron support.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-27 00:23:40 -07:00
Dan Albert
9091afe682 Remove very dead campaign.
This was never updated to the miz format. Patches welcome if someone
wants to bring it up to date.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1129
2021-05-26 23:05:57 -07:00
Dan Albert
e2034b19e7 Update missing clients advice.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-26 20:41:22 -07:00
Dan Albert
1b7a225f9d Replace client count with player pilots.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-26 20:33:23 -07:00
Dan Albert
a52043ef29 Allow toggling player/AI pilot state.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-26 20:33:15 -07:00
Dan Albert
b38d271f10 Show player/AI status for pilots.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-26 19:47:53 -07:00
Dan Albert
e480519855 Increase skill level for experienced pilots.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-26 19:34:24 -07:00
Dan Albert
8b8d1e87e7 Track missions flown for each pilot.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-26 19:04:06 -07:00
Dan Albert
cd6de191d1 Track pilot deaths.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-26 18:54:07 -07:00
Dan Albert
8b8e018521 Fix cases where pilots were not returned.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-26 18:49:19 -07:00
Dan Albert
5277beede3 Show active and available pilots in air wing view.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-26 18:18:22 -07:00
Dan Albert
57a2457050 Show aircraft type in the squadron list.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-26 17:49:53 -07:00
Dan Albert
2f8656d54f Generate random squadron nicknames.
A little weird that the animal names aren't plural, but good enough.

https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-26 17:49:53 -07:00
Dan Albert
49102e510d Disallow creating missions with missing pilots.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-26 17:49:53 -07:00
Dan Albert
e7b8548698 Clean up more delegates.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-26 17:49:53 -07:00
Dan Albert
9c2bad85d5 Show number of missing pilots in the UI.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-26 17:49:53 -07:00
Dan Albert
4147d2f684 Initial squadrons implementation.
Doesn't actually do anything yet, but squadrons are created for each
aircraft type and pilots will be created as needed to fill flights.

https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-26 17:49:53 -07:00
Dan Albert
6b30f47588 Fix airlifts always using player country. 2021-05-26 16:48:36 -07:00
Dan Albert
e49da6afd6 Inject the saved games path from preferences.
pydcs can't guess the saved games path accurately for all users, so
inject the path that they've told us is correct to work around that.

It seems pydcs has two problems:

1. Only `DCS` is checked, not `DCS.openbeta`.
2. Only `%USERPROFILE%/Saved Games` is used, so if the user has moved
   their Saved Games directory (but not their whole user profile) pydcs
   cannot find the location.

https://docs.microsoft.com/en-us/windows/win32/shell/knownfolderid would
be the starting place for fixing problem 2 properly. 1 is just a matter
of trying both.
2021-05-26 16:02:54 -07:00
Dan Albert
6fa0a29249 Update to Python 3.9.
This is what I've been using locally for ages and it seems to work fine.
2021-05-26 13:15:27 -07:00
bgreman
c163e2c981 Inject mod plane weapons into pydcs.
Adds a simple injector that iterates over attrs of an input class and
injects things that look like custom weapons into pydcs's weapons
classes.

Also updated all current mod aircraft configs to perform the injection.
2021-05-26 12:49:49 -07:00
Brock Greman
372bf9d97f Fixing F-22 loadouts 2021-05-26 01:46:29 -07:00
Dan Albert
619d5dd1b9 Teach sweep to care about multi-role too.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1124
2021-05-25 22:58:09 -07:00
Dan Albert
4939faf5fa Schedule SEAD with a one minute lead time. 2021-05-25 22:53:57 -07:00
bgreman
205e4aa707 Add ability to toggle between own/enemy intel. 2021-05-25 22:23:33 -07:00
Dan Albert
81ce7fbb62 Fix handling of empty polys in the new UI.
This was copied from the Qt map and tweaked, but the use cases are
slightly different so this needs to return an empty list for an empty
polygon instead of None.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1121
2021-05-25 19:19:45 -07:00
Dan Albert
de9651533f Load map.js explicitly from canvas.html.
Without this it's hard to get to map.js in the debug tools since Chrome
doesn't know about the anonymous js. Probably improves logging too.
2021-05-25 18:46:41 -07:00
Dan Albert
e6e31fd234 Actually ASAP the ASAP packages.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1117
2021-05-25 14:23:00 -07:00
Brock Greman
d242079a74 Making Sweep flight types have appropriate aircraft 2021-05-25 13:20:47 -07:00
Dan Albert
48f26cb181 Fix Bandar Abbas airfield data.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1114
2021-05-25 00:14:20 -07:00
bgreman
c37a5b2405 Prevent empty transfers.
Disables the 'create transfer order' button in the unit transfer dialog if no units are actually selected for transfer (including when the dialog is first loaded).
2021-05-24 20:43:33 -07:00
Dan Albert
d15bfaac76 Fix group targeting for multi-group TGOs.
AI flights were only getting a single Attack Group task against multi-
group TGOs (currently only a small group of SAMs like the SA-10, I
believe), so the AI would never attack the point defense SA-15 or AAA
groups.
2021-05-24 19:26:03 -07:00
Dan Albert
e94657875f Reselect same row after deleting waypoint. 2021-05-24 18:20:19 -07:00
Dan Albert
f2bd7300aa Improve precision of kneeboard lat/lon.
DMS with decimal seconds is what the hornet uses for PP targest. In the
future we'll want to make this aircraft specific (and potentially user
preference for jets like the A-10 that can handle both L/L and MGRS).
2021-05-24 18:06:47 -07:00
Dan Albert
c255aee3b9 Make default AEW&C missions ASAP. 2021-05-24 17:21:33 -07:00
Dan Albert
305d1f0523 Reset non-custom loadout when changing task. 2021-05-24 16:57:36 -07:00
Dan Albert
970f2c25dd Fix loadout reset when disabling custom loadouts. 2021-05-24 16:49:31 -07:00
Dan Albert
b7b3b35816 Make some waypoint types undraggable.
None of these (takeoff, landing, divert, bullseye, precise target
locations) can be usefully moved, so prevent it.
2021-05-24 16:45:21 -07:00
Dan Albert
e8f326ebce Update Skynet to 2.1.0. 2021-05-24 14:35:22 -07:00
Dan Albert
62b743025a Fix supply route clobbering, make immutable.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1083
2021-05-23 17:30:50 -07:00
Dan Albert
7934463a53 Add base capture cheat to the new UI.
We don't have a context menu yet, so just add it to the base menu.

https://github.com/dcs-liberation/dcs_liberation/issues/1108
2021-05-23 17:18:01 -07:00
Dan Albert
d15ef63182 Remove unused method of ControlPoint. 2021-05-23 17:17:59 -07:00
Dan Albert
c7edba5120 Add TGO-specific layers.
This also splits the main and debug controls because the main list was
getting too long.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1069
2021-05-23 15:36:22 -07:00
Dan Albert
188f871bc8 Remove errant whitespace. 2021-05-23 15:01:10 -07:00
Dan Albert
31eba975fd Note flight planner changes.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1106
2021-05-23 15:00:27 -07:00
Dan Albert
2ea0bccd25 Hide dead opfor objectives.
These are just clutter.
2021-05-23 14:44:24 -07:00
Dan Albert
fa321c7ddc Don't plan SEAD when only a search radar remains. 2021-05-23 13:15:42 -07:00
Dan Albert
1d7b0c9b17 Tweak F/A-18 BARCAP and CAS loadouts.
Add bags to each. CAS only gets one but that should be plenty for only
four Mavericks. CAP gets two.
2021-05-23 13:07:16 -07:00
Dan Albert
a4fbcd2d02 Update pydcs for loadout loading fix. 2021-05-23 13:07:16 -07:00
Dan Albert
d788b286aa Remove unused UI classes. 2021-05-23 13:03:18 -07:00
Dan Albert
eedb5c26a9 Ignore non-escorted regions when planning escorts.
We shouldn't consider the non-escorted parts of the flight path when
checking for threats to determine if escorts should be used or not,
since escorts can't help in those areas anyway. This was causing escorts
to be overly requested since the bullseye is now a part of the
"flight plan", but could have also triggered for divert waypoints, or
for aircraft taking off in a retreat from a threatened location.
2021-05-23 13:01:44 -07:00
Dan Albert
ddd6e7d18f Improve detection of functional radar SAMs.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1109
2021-05-23 13:01:44 -07:00
Dan Albert
eae0d6be94 Add threat zone drawing for the new map.
https://github.com/dcs-liberation/dcs_liberation/issues/1097
2021-05-23 12:25:15 -07:00
Dan Albert
5e68dbe1ca Correct the list of units with radars.
Probably.

https://github.com/dcs-liberation/dcs_liberation/issues/1109
2021-05-23 01:30:23 -07:00
Dan Albert
98e0be6be9 Revert "Correct radar detection."
We need this after all, but we do need to audit the list. Will follow
up with that fix.

https://github.com/dcs-liberation/dcs_liberation/issues/1109

This reverts commit f68935735d.
2021-05-23 01:10:29 -07:00
Dan Albert
7450a6b7eb Configure more loadout fallbacks. 2021-05-22 20:12:56 -07:00
Dan Albert
c3802e5a37 Make UI created packages ASAP by default. 2021-05-22 19:35:56 -07:00
Dan Albert
43cd9bce67 Fix cargo ship locations in Abu Dhabi.
Apparently these ships have a > 32ft draft and these ports are shallow.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1104
2021-05-22 19:17:53 -07:00
Dan Albert
2f6ab6d2b0 Update Hornet loadouts, add DEAD loadout. 2021-05-22 17:11:10 -07:00
Dan Albert
2df17c32cd Clean up aircraft selector. 2021-05-22 17:11:10 -07:00
Dan Albert
16fff8d87a Fall back to SEAD loadout for SEAD escort.
These usually do not need to differ.
2021-05-22 17:11:10 -07:00
Dan Albert
1087069277 Fix F-14 SEAD/DEAD loadouts.
The AI doesn't use mixed loadouts effectively (only the TALDs get used
and the flight returns home with a full bomb load):
https://forums.eagle.ru/topic/271941-ai-rtbs-after-firing-decoys-despite-full-load-of-bombs/

This makes the SEAD loadout TALDs only and the DEAD loadout bombs only.
2021-05-22 16:53:48 -07:00
Dan Albert
1b624e7e6f Remove center tank for F-16 loadouts. 2021-05-22 16:53:48 -07:00
Dan Albert
7223ae327a Add DEAD loadout for the F-16. 2021-05-22 16:45:06 -07:00
Dan Albert
bcdefda0db Make the F-15E CAS loadout less empty. 2021-05-22 16:45:06 -07:00
Dan Albert
fc56642631 Fix default mission length in the UI too. 2021-05-22 16:15:29 -07:00
Dan Albert
69299d395c Empty gun for AI missions that do not need a gun.
There is no "all but gun" RTB winchester option, so air to ground
missions with mixed weapon types will insist on using all of their
bullets after running out of missiles and bombs. Take away their bullets
so they don't strafe a Tor.

Exceptions are made for player flights and for airframes where the gun
is essential like the A-10 or warbirds.
2021-05-22 16:14:20 -07:00
Dan Albert
a789f58068 Allow SEAD escorts against broken SAMs and EWRs. 2021-05-22 15:37:31 -07:00
Dan Albert
f68935735d Correct radar detection.
This list includede units without radars and also missed units with
radars. Stop curating the list and just query the unit type.
2021-05-22 15:36:56 -07:00
Dan Albert
ba2157cc43 Tweak winchester behavior for SEAD escort.
They can't suppress air defenses after running out of TALDs or ARMs.
2021-05-22 15:09:10 -07:00
Dan Albert
57fe5c04ec Improve DEAD mission planning.
We don't need to include a SEAD flight in missions against EWRs or SAMs
that no longer have a radar.

Also plan DEAD missions against air defenses that have no radars.
Previously we would never finish killing launcher only sites (which
cannot defend any more, but are cheaper to return to working order than
a fully destroyed site) nor would we plan DEAD against IR SAMs or AAA.
2021-05-22 14:56:18 -07:00
Dan Albert
3a08944c99 Use the CAS DCS type for SEAD so F-14s can do it.
The CAS task type appears to be a superset of the SEAD task in every
way. Larger task variety as well as larger aircraft pool.
2021-05-22 14:27:30 -07:00
Dan Albert
b6154b273c Differentiate SEAD and SEAD escorts.
SEAD suppresses the package's target. SEAD escort protects the package
from any SAM threat along its flight path.
2021-05-22 14:24:13 -07:00
Dan Albert
e332bff362 Decrease error margin on TOT planning.
Everyone seems to do pretty okay generally, with the exception of
estimating ground ops time, which I've also increased (and is a
non-issue for runway/air start defaults).
2021-05-22 01:11:28 -07:00
Dan Albert
59e03434e4 Increase flight speeds to mach 0.85 or 85% of max.
Everyone seems a bit slow, generally. 0.85 is probably a better cruise
speed for supersonic jets and 85% of max is probably fine for subsonic.
2021-05-22 01:10:23 -07:00
Dan Albert
2ca0edf5fd Increase estimate for airfield ground ops.
5 minutes is pretty optimistic at most airfields.
2021-05-22 01:09:43 -07:00
Dan Albert
90dca9072e Change default mission duration to 60 minutes.
This seems like it works better for the number of missions we usually
frag, plus the fact that players will almost always choose an ASAP
package.
2021-05-22 01:04:10 -07:00
Dan Albert
c0ead4a484 Add icons for CPs. 2021-05-21 23:26:15 -07:00
Dan Albert
f8cb9e2bd3 Update Inherent Resolve to use non-random TGOs.
Also adds Ramat David to blue.
2021-05-21 19:13:32 -07:00
Dan Albert
df4dabf68f Add an LHA to Abu Dhabi. 2021-05-21 17:59:19 -07:00
Dan Albert
40720f9949 Fix convoy spawn point from Tabqa to Jirah.
The current spawn point is a disonnected road network according to the
map, so the convoy will never start moving because they have no route to
the destination.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1101
2021-05-21 17:54:01 -07:00
Dan Albert
7e7a1dce7b Fix icons for dead SAMs showing as damaged.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1099
2021-05-21 11:11:44 -07:00
Dan Albert
39b9a7f0ed Note DCS bugs that are common issues. 2021-05-21 00:24:12 -07:00
Dan Albert
43010779d4 Enable multirole fighter targeting for escorts.
pydcs didn't support this until now :(
2021-05-20 23:58:05 -07:00
Dan Albert
a1a4fc8c7c Update pydcs. 2021-05-20 23:57:52 -07:00
Dan Albert
621e4a513c Fix DEAD flights to use more than just missiles.
It doesn't seem like AI pilots are capable of using more than one weapon
effectively (see link below), but this at least makes DEAD flights work
when the DEAD flight is carrying only one type of weapon and some other
flight is performing SEAD.

https://forums.eagle.ru/topic/271941-ai-rtbs-after-firing-decoys-despite-full-load-of-bombs/
2021-05-20 23:51:39 -07:00
Dan Albert
6c821039b5 Add a spectator slot.
So I stop accidentally giving orders while testing AI behavior after
waiting 20 minutes for them to get to their objective.
2021-05-20 21:46:20 -07:00
Dan Albert
f80b948fb1 Fix loadout downgrading bugs.
If the pylon had no weapon this would raise, and if no replacement was
found we wouldn't clear the pylon.
2021-05-20 21:10:02 -07:00
Dan Albert
d4c27da892 Move the strike eagle down the CAP list.
Most of the pylons are not actually capable of carrying air to air
missiles.
2021-05-20 20:12:49 -07:00
Dan Albert
11bf0ca868 Replace JSOW fallbacks with walleyes.
The GBU-12 that was chosen doesn't fit on the hornet in the first place,
and we ought to replace glide bombs with glide bombs.
2021-05-20 19:58:26 -07:00
Dan Albert
664092c023 Remove non-CP FOBs. 2021-05-20 19:01:43 -07:00
Dan Albert
0cd2c4a90c Stop awarding income for FOB structures.
The CP already grants income.

Fixes: https://github.com/dcs-liberation/dcs_liberation/issues/685
2021-05-20 18:59:03 -07:00
Dan Albert
2f6c04a86d Add bullseye to the kneeboard.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/136
2021-05-20 18:35:32 -07:00
Dan Albert
a382e74a89 Set up bullseye early, create waypoints.
Setting this up as part of the game makes it possible for us to show in
the UI.

https://github.com/dcs-liberation/dcs_liberation/issues/136
2021-05-20 18:29:35 -07:00
Khopa
3c8c76f50d Unit support : La Combattante II class ship 2021-05-21 02:45:15 +02:00
Khopa
cbce379132 Unit support : T-155 Firtina 2021-05-21 02:29:31 +02:00
Dan Albert
e795e96bfb Don't show red support units on blue kneeboards.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1080
2021-05-20 17:24:39 -07:00
Khopa
e12e2c4b0b Unit support : Chieftain Mk3 2021-05-21 02:24:14 +02:00
Khopa
9a1b21a2fa Unit support : PT-76 2021-05-21 02:19:52 +02:00
Khopa
79708f9ba6 Unit support : VAB Mephisto 2021-05-21 02:06:35 +02:00
Dan Albert
102544877d Fix common cases of kneeboard overflow. 2021-05-20 16:57:35 -07:00
Dan Albert
1c32ae1227 Create default strike target implementation. 2021-05-20 16:50:01 -07:00
Dan Albert
55d7e444c7 Split support info into its own kneeboard page.
The first page is getting very crowded.
2021-05-20 16:25:06 -07:00
Khopa
9243fd499b pydcs update for DCS 2.7.1 2021-05-21 01:15:54 +02:00
Khopa
844dc48d65 Merge branch 'develop' of https://github.com/khopa/dcs_liberation into develop 2021-05-20 13:23:06 +02:00
Dan Albert
52d96b8518 Include steerpoint number in strike page.
https://github.com/dcs-liberation/dcs_liberation/issues/1001
2021-05-20 00:20:48 -07:00
Dan Albert
8274e68846 Add SEAD/DEAD target info kneeboard page.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/965
2021-05-20 00:13:49 -07:00
Dan Albert
f1adcd1836 Be permissive about presumably incorrect loadouts.
These might be broken loadouts, or might be broken pydcs data. In case
it's the latter, attempt to load the pylon. DCS will remove the weapon
if it's not compatible automatically.
2021-05-19 23:33:24 -07:00
Dan Albert
2a77f57aa4 Improve AI strike targeting.
We were setting up all the correct *target* waypoints but the AI doesn't
use the target waypoints; they use the targets property of the ingress
waypoint. This meant that the flight plan looked correct in the UI and
was correct for players but the tasks were set up incorrectly for the AI
because building TGOs are aggravatingly multiple TGOs with the same name
in the implementation.

Mission targets now enumerate their own strike targets so that this
mistake is harder to make in the future.

This won't be perfect, the AI is still not able to parallelize tasks and
since buildings aren't groups they can only attack one structure at a
time, but they'll now at least switch to the next target after hitting
the first one.

As a bonus, stop bombing the dead buildings.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/235
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/244
2021-05-19 23:33:15 -07:00
Dan Albert
04ebe4c68a Add the rest of the TGO icons. 2021-05-19 21:40:36 -07:00
Dan Albert
1c7e065c52 Add more icons. 2021-05-19 21:24:54 -07:00
Dan Albert
80f3857f44 Stop forcing open CP tooltips.
These obscure the TGOs around them. Easy enough to mouse over, and the
names aren't the important part.
2021-05-19 20:49:51 -07:00
Dan Albert
3b62831401 Replace icons for some TGOs.
Only covered about half of the types so far.

This also removes the clustering, since that doesn't really seem to be
needed with the newer icons.
2021-05-19 20:47:52 -07:00
Khopa
a047e1d063 Updated Operation Dynamo campaign 2021-05-19 14:03:24 +02:00
Khopa
45985e1684 Updated payloads for Ju-88A4 2021-05-19 13:24:11 +02:00
Dan Albert
7dac886375 Remove fixed todo from the map. 2021-05-18 23:55:00 -07:00
Dan Albert
af3b8a9902 Set up icons for TGOs.
These are just the old icons, but it's better than nothing.
2021-05-18 23:35:25 -07:00
Dan Albert
4e37666037 Clean up the category property of TGOs.
This really needs to be a proper type, but this is a start: create new
categories for the types of TGOs that are missing. This removes some
icon special cases.
2021-05-18 23:35:25 -07:00
Dan Albert
2769d32c81 Mark enemy transfers in the transfer menu.
https://github.com/dcs-liberation/dcs_liberation/issues/1069
2021-05-18 21:20:38 -07:00
Dan Albert
4b004320a4 Make the transfer button more obvious.
https://github.com/dcs-liberation/dcs_liberation/issues/1069
2021-05-18 21:17:38 -07:00
Dan Albert
edfc879b41 Reorder some CAS aircraft priorities.
Move the harrier up, and the F-14s and B-1 down.
2021-05-18 21:04:11 -07:00
Dan Albert
0879d1da0d Use the best aircraft rather than first found.
The priority list was guiding the purchase decision which largely meant
that this was working correctly, but there were suboptimal cases where
the list was being taken in FIFO order by purchased type. This fixes the
search to be locally optimal, although this does still mean that a worse
but closer aircraft will be chosen over a better but slightly farther
away aircraft. We'd need to have a quality vs distance rating to do
better.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/755
2021-05-18 21:01:26 -07:00
Dan Albert
c5159f8a87 Fix swapped list order for purchase priorities.
We were accidentally iterating over the faction list and checking it
against the priority list rather than the other way around, so the
faction's aircraft list was being used for purchase priority rather than
the actual priority list in the game.
2021-05-18 19:54:39 -07:00
Dan Albert
a0d9bf0f26 Add Ju-88 to the CAS, strike, and DEAD lists.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1008
2021-05-18 19:51:27 -07:00
Dan Albert
a3cce8ff72 Invert the random location warning.
Campaigns should no longer be using these. Remove the warning when a
location cannot be found, and emit a warning when they are used.
2021-05-18 19:36:20 -07:00
Dan Albert
242f00390d Fix purchasing past 0 budget for ground units too.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1032
2021-05-18 19:23:29 -07:00
Dan Albert
f4b64370bb Remove fallback for old non-convoy behavior. 2021-05-18 19:21:43 -07:00
MaHuJa
ae57e4da83 Black sea campaign, inverted: "Russia" now blue. 2021-05-18 19:17:32 -07:00
Dan Albert
cd391a360c Add support for AAA objectives.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/999
2021-05-18 17:29:42 -07:00
Schneefl0cke
dcbe12f1af Changed latest planned flight to desired game settings. 2021-05-18 11:54:56 -07:00
Dan Albert
5b61cfe922 Remove random objectives from Abu Dhabi. 2021-05-18 00:48:12 -07:00
Dan Albert
739406614d Add support for required variants of all TGOs.
Adds required variants of:

* SHORADS
* Armor groups
* Buildings
* Oil rigs
* Coastal defenses
* Missile sites
* Ships

This is prep work for removing random generation.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1076
2021-05-18 00:48:12 -07:00
Dan Albert
8076206a90 Add major/minor versioning of the campaign schema.
Many of the schema version increases are just to add new features that
don't render old campaigns obsolete. Convert the version number to a
major/minor format so we can detect the difference between changes that
render old campaigns obsolete (major versions) and new features that
will not work on older builds of Liberation (minor versions).
2021-05-18 00:12:55 -07:00
Dan Albert
f63d218aae Fix loadouts to work with clean pylons. 2021-05-17 21:28:12 -07:00
Dan Albert
f2e3ccd18c Add loadout names for every Liberation task type.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/867
2021-05-17 01:51:21 -07:00
Dan Albert
d41e69d770 Add DCS loadout selector.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/273.
2021-05-17 01:24:55 -07:00
Dan Albert
0e3bc1ce43 Loadout implementation cleanup.
Loadout selection no longer has two (disagreeing) implementations. What
the UI shows is now what the miz will have.

We now store the chosen layout in the Flight *always*, not just for
custom loadouts. This means that we do loadout lookups at the start of
each turn, but the data is cached in pydcs.

Era-specific loadout degradation is still done at generation (and
presentation) time. This is so that players can toggle that option and
have it affect the *current* turn, rather than the next one.
2021-05-17 01:23:00 -07:00
Dan Albert
6ca175345f Fix DEAD for many bombers, audit DEAD/SEAD lists.
Many of the aircraft that we use for DEAD are not actually capable of
the SEAD task in DCS, so they were being loaded as some other task type,
usually one that doesn't support Attack Group, which made them lose
their waypoint actions and do nothing.

This switches them to using CAS which supports a superset of the SEAD
capable aircraft.

I've also audited the SEAD/DEAD lists. The F-117 was removed because it
is not capable of Attack Group *at all*, and all the non-SEAD aircraft
that are capable of ground attack moved from SEAD to DEAD.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1086
2021-05-16 22:23:01 -07:00
Dan Albert
c063a638cd Disable double click zoom action.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1087
2021-05-16 21:59:48 -07:00
Dan Albert
2dfe1420bc Add "Show all" option for flight plans.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1085
2021-05-16 13:15:51 -07:00
Dan Albert
7dd379c5c3 Show active supply routes in the new UI. 2021-05-16 12:58:03 -07:00
Dan Albert
752eb6235d Remove errant factory from Abu Dhabi.
This wasn't supposed to be here, and it wasn't showing up until I fixed
the bug preventing FOBs from spawning factories.
2021-05-16 12:55:20 -07:00
Dan Albert
3f077727ae Group allied and enemy SAM controls. 2021-05-15 22:08:00 -07:00
Dan Albert
51d557524d Fix unit list for non-building TGOs.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1082
2021-05-15 22:05:14 -07:00
Dan Albert
5d9563304f Update commit boundaries after moving waypoints.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1084
2021-05-15 21:57:37 -07:00
Dan Albert
53cb68f82c Make waypoints draggable. 2021-05-15 16:21:03 -07:00
Dan Albert
95b107ffad Pop up CV move errors. 2021-05-15 14:22:19 -07:00
Dan Albert
06dedf51aa Improve CV waypointing UX. 2021-05-15 14:12:43 -07:00
Dan Albert
ed7c8c11d9 Make CV waypoint less janky.
The CP is now draggable when there is no destination. Otherwise the
destination is draggable.
2021-05-15 13:23:56 -07:00
Dan Albert
643e5954f3 Cluster TGOs less aggressively. 2021-05-15 12:41:04 -07:00
Dan Albert
c7cc9d2a65 Disable SAM threat fill, thicken outline.
As predicted, this only runs well on my machine.
2021-05-15 12:36:56 -07:00
Dan Albert
5050914d25 Make the new UI the default. 2021-05-15 03:38:29 -07:00
Dan Albert
31fa2d866f Allow setting CV routes in the new UI.
This is a pretty janky system until we get add context menu support. For
now the destination is set by dragging the CV marker and cleared by
right clicking the destination marker. Once we have a context menu a
context action will begin setting the destination the way it did in the
old UI, and the destination marker will be draggable.
2021-05-15 03:35:35 -07:00
Dan Albert
4a096cb728 Use load instead of setHtml so paths resolve.
Without this we can't resolve local paths to files.
2021-05-15 01:21:25 -07:00
Dan Albert
16b52f929c Bring CP markers to the front. 2021-05-15 00:21:22 -07:00
Dan Albert
8ec133830f Don't tell the UI about CP TGOs.
These are an implementation quirk, and passing them to the UI just means
that we put TGO pins on top of the CP, which makes the base menu
unopenable.

In the old UI we avoided this by not drawing anything that was
`for_airbase`, but now that we can zoom in further we're drawing base
defenses.
2021-05-15 00:10:05 -07:00
Dan Albert
c144799a11 Shade SAM threat ranges.
This runs a little bit worse but looks a lot better. Will flip back off
if this runs poorly on less powerful computers.
2021-05-14 23:58:37 -07:00
Dan Albert
e56511a05a Ack campaign version 4 for Abu Dhabi.
V4 is scenery objects, so not required.
2021-05-14 23:48:58 -07:00
Dan Albert
bdb959d986 Draw patrol commit ranges in the new map. 2021-05-14 23:41:55 -07:00
Dan Albert
dae9c368b7 Replace var with const/let in map.js. 2021-05-14 23:18:34 -07:00
Dan Albert
2a401a302d Add a topographic map layer. 2021-05-14 23:10:04 -07:00
Dan Albert
ff3b8e5270 Note scenery objective support in the changelog. 2021-05-14 17:19:49 -07:00
SnappyComebacks
bb1a066ff7 Let map objects be Strike targets.
This PR allows campaign creators to incorporate map objects (referred to as Scenery in the code) into their Liberation campaign.

Map objects are defined using white trigger zones created by right clicking on scenery and clicking `assign as...`.   Objective groups are defined by creating a blue TriggerZone surrounding the centers of the white trigger zones.  The type of objective is determined by the campaign creator, assigning the value of the first property of the blue TriggerZone with the objective type.

Map objects maintain their visually dead state by assigning a `Mission Start` `Scenery Object Dead` trigger to the trigger zone.  It is important for the Liberation generated TriggerZone to be as small as possible so that no other scenery is marked dead by DCS.

TriggerZones are hidden during gameplay (DCS behavior.  I don't know if it's possible to turn that off.)  TriggerZones are visible in the mission editor and mission planner however.  If a player is using an older plane, it is important for them to remember where the target is.

In the mission planner, the trigger zones' will be blue or red depending on which faction the map objects belong to.

Inherent Resolve campaign has been modified to integrate scenery objects.

### **Limitations:**
- Objective definitions (Any Blue TriggerZones) in campaign definition cannot overlap.
- Map object deaths in `state.json` is tracking integers.  You won't know what died until debriefing.
- No images for the various buildings.  In theory it can be done, but an unreasonable amount of work.
- Every blue trigger zone must have a unique name.  (If you let DCS auto increment the names this is not a concern.
- No output to screen when scenery object is dead.  You can see the building drawn as dead in the F10 map though.


### **Pictures:**

An objective:
![CampaignCreation](https://user-images.githubusercontent.com/74509817/117526797-c294af00-af84-11eb-9fb7-3940db64c5d8.png)

How the objective looks once in the mission planner/editor.  This objective belongs to the enemy faction:
![MissionPlanner](https://user-images.githubusercontent.com/74509817/117526819-ece66c80-af84-11eb-9db0-64000dedcf89.png)
2021-05-14 17:18:03 -07:00
Dan Albert
9a9872812f Generate factories at FOBs.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1021
2021-05-14 11:38:14 -07:00
Khopa
969e4a2d65 Merge remote-tracking branch 'khopa/develop' into develop 2021-05-14 16:16:11 +02:00
Khopa
77f0b87c54 Campaigns : Migrated campaign Golan Height to version 3 2021-05-14 15:31:46 +02:00
Dan Albert
eec56256e8 Add AEW&C aircraft to the faction aircraft list.
To avoid confusion, use only the aircraft list for the purchasable
aircraft. This fix also caught a faction's Tu-142 that was not actually
purchasable. Invalid aircraft in the faction aircraft list will now
raise an error.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1074
2021-05-13 21:44:07 -07:00
Dan Albert
99dc91dcb4 Fix game break when capturing factory.
We need to recompute the transit networks after a capture *before*
processing transfers. Otherwise units deployed that turn will not be
able to find their destination.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1070
2021-05-13 21:00:13 -07:00
Dan Albert
5adcfbd7bd Work around PySide2 bug in Property.
https://bugreports.qt.io/browse/PYSIDE-1426

For whatever reason this only shows up in packaged builds for us, and
also the recommended workaround of using a member property rather than a
decorated method does not work for us.

Until PySide2 5.15.3 (or later) is released, we need to use a named
signal for every property we expose.
2021-05-13 20:24:22 -07:00
Dan Albert
3c5f1f7c4b Log python version at startup. 2021-05-13 19:16:54 -07:00
Dan Albert
4415429661 Stop requiring user input for mkrelease.py. 2021-05-13 19:16:40 -07:00
Dan Albert
92c404fbb6 Persist DCS configuration across installs. 2021-05-13 18:55:48 -07:00
Khopa
956b9aaf95 Updated campaign "Russia Small" to campaign version 3 2021-05-13 19:30:10 +02:00
Khopa
c8348f1b44 Added campaign Operation Dynamo for The Channel map. 2021-05-13 18:59:53 +02:00
Dan Albert
d73ceb374c Add front lines to new map UI.
Only shows an approximate front line. Still need support for "actual".
2021-05-13 01:49:35 -07:00
Dan Albert
3e01953a3a Supply route styling, line weight rebalancing. 2021-05-13 01:23:15 -07:00
Dan Albert
1a65b1affb Connect CP/TGO package fragging dialogs. 2021-05-13 01:05:15 -07:00
Dan Albert
dd75078019 Connect TGO dialogs. 2021-05-13 00:56:35 -07:00
Dan Albert
1ab205cb46 Add tooltips for TGOs. 2021-05-13 00:44:47 -07:00
Dan Albert
eb26d54ac1 Show tooltips automatically based on zoom level. 2021-05-13 00:16:59 -07:00
Dan Albert
d884645f37 Cluster TGO markers. 2021-05-12 23:43:47 -07:00
Dan Albert
45f0c3c85f Add map scale widget. 2021-05-12 23:30:54 -07:00
Dan Albert
4e498e6932 Add waypoint info tooltip to the new map. 2021-05-12 23:03:53 -07:00
Dan Albert
d9d68cd37c Add a new Leaflet based map UI.
This is extremely WIP. It is not usable for play yet. Enable with
`--new-map`.
2021-05-12 21:52:23 -07:00
Schneefl0cke
56abd0bb7f Add option for setting desired mission length. 2021-05-11 03:13:15 -07:00
Dan Albert
747683e9e8 Allow other TGO types to be factories.
The `FactoryGroundObject` is just a special case of
`BuildingGroundObject` that we maybe don't actually need. For now it
provides some special case logic for the layout, but this allows any TGO
with the "factory" category to behave as a ground unit source.

Note that the "factory" random strike targets are *not* generated
anymore, so this doesn't affect campaign design currently.
2021-05-10 20:21:19 -07:00
Hornet2041
5b191d72a6 Add F-4E Phantom to the list of CAS/STRIKE capable planes. 2021-05-10 18:14:56 -07:00
Dan Albert
b7619630cf Add logged_duration context manager for profiling. 2021-05-08 18:06:55 -07:00
Dan Albert
de07f10e57 Remove fixed TODO. 2021-05-08 16:50:35 -07:00
Dan Albert
87e6080215 Fix "show actual front line location". 2021-05-08 16:49:05 -07:00
Dan Albert
e721a234e1 Clean up front line code.
The routes do not need be be recreated each time we create a
`FrontLine`. The front lines follow the convoy routes, which are static.
Add the convoy route data to the `ControlPoint` the way we do for
shipping lanes and have `FrontLine` load the data from there.
2021-05-08 16:46:02 -07:00
Dan Albert
67289bbba2 Fix now obvious reversal of convoy friendliness.
Convoy attack and shipping attacks were being planned against *only*
friendly targets. The renaming made the bug obvious.
2021-05-07 21:10:22 -07:00
Dan Albert
b0c24f6e51 Refactor front line code to make sides explicit.
A was intended to be the blue point and B was intended to be the red
point. Make this a part of the name so that's clear, and clean up
related code to keep that reliable.
2021-05-07 21:10:22 -07:00
Dan Albert
12f474ecbe Fix name of Al Minhad for latest DCS version. 2021-05-07 19:59:34 -07:00
Dan Albert
e2f20a7a65 Fix initialization order of turn 0.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/674
2021-05-07 19:58:39 -07:00
Dan Albert
58ffabe2d6 Fix aggressive objectives in Abu Dhabi.
Some of the objective locations for the starting front line are too
aggressive and put opfor at a disadvantage since blue ships might start
so close to their bases.
2021-05-07 19:37:25 -07:00
SnappyComebacks
426f06045e Updated pydcs version. 2021-05-07 19:13:45 -07:00
Dan Albert
2ca875192a Save budget for filling whole packages.
No sense filling airbases with cheap escorts if we'll never afford the
rest of the package. Filling the airbases with cheap escorts also makes
it impossible to buy the rest of the package when the faction eventually
does have the money since there's nowhere to park the needed aircraft.

https://github.com/dcs-liberation/dcs_liberation/issues/1058
2021-05-07 18:22:52 -07:00
Dan Albert
36b2f24de9 Skip planning for faction incompatible missions.
Required for improving purchasing as well, since we need to not halt
purchasing when a faction has no AEW&C aircraft.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/683
2021-05-07 18:22:52 -07:00
Dan Albert
2cf3b3be2b Fix bug causing overpurchase of aircraft.
After fulfilling a request we were not exiting the loop, so we'd fulfill
the request for the aircraft at _all_ the bases capable of operating it
until either the bases were full or the budget ran out. In factions like
Iraq 1991 this could cause the budget to be spent on tons of cheap
MiG-19s while never buying the more expensive Su-17s or Su-24s that they
need to actually complete a package.

https://github.com/dcs-liberation/dcs_liberation/issues/1058
2021-05-07 17:25:14 -07:00
Dan Albert
8320c6940b Fix map centering for CLI generated games. 2021-05-07 17:13:53 -07:00
Dan Albert
3c9d21e38d Fix CLI campaign generator.
Factions can be loaded from the user directory now so we need to know
where that is.
2021-05-07 17:13:03 -07:00
Dan Albert
0d7f00aef6 Fix lint error. 2021-05-07 16:58:42 -07:00
Dan Albert
1640763a7f Show parking status for enemy airfields. 2021-05-07 15:30:09 -07:00
Dan Albert
0ec5346574 Auto center the map on load.
Stops all PG campaigns from starting with their view pointed way up
north at the empty mountains.
2021-05-07 14:55:33 -07:00
Khopa
977845e2f4 Fixed links to github in the repo to account for the transfer to the new github organization. 2021-05-07 13:08:39 +02:00
Khopa
4bb2ab73c1 Changelog update 2021-05-07 13:03:15 +02:00
Khopa
af5584d244 Added a settings to control the amount smoke effects on frontlines. Default smoke spacing changed from 800 to 1600 (half the current amount) 2021-05-07 12:49:56 +02:00
Khopa
b289e41a0d Merge remote-tracking branch 'khopa/develop' into develop 2021-05-07 01:01:00 +02:00
Khopa
c0b4eef948 Updated readme for new organization repo link 2021-05-07 01:00:41 +02:00
Marcos Sigueros Fernández
b8e6c2fe78 Check for empty package before estimating TOT.
Fixes https://github.com/Khopa/dcs_liberation/issues/1014.
2021-05-03 11:01:41 -07:00
Dan Albert
b10e86e484 Add support for user faction directory.
This allows users to install custom factions to their home directory
rather than the Liberation install directory. Makes it easier to keep
mods across Liberation downloads, and easier for us devs to keep custom
factions without git always wanting us to add them.
2021-05-02 14:35:21 -07:00
Dan Albert
1c31cffe4b More Python 3.9 compat. 2021-05-02 14:33:24 -07:00
Dan Albert
b9822cd5d1 Python 3.9 compatibility.
This argument was removed and wasn't needed anyway since the file itself
is already opened UTF-8.
2021-05-02 14:31:08 -07:00
Dan Albert
ef1c70123c Remove duplicate SH-60s from factions. 2021-05-02 14:19:47 -07:00
Dan Albert
a0e5a707fb Merge pull request #1053 from Khopa/develop_2_5_x
Release 2.5.1.
2021-05-02 13:28:45 -07:00
Dan Albert
c245531d65 Update changelog for 2.5.1.
(cherry picked from commit 4555a4968d)
2021-05-02 13:18:21 -07:00
Dan Albert
4555a4968d Update changelog for 2.5.1. 2021-05-02 13:17:35 -07:00
Dan Albert
522495fd11 Update pyinstaller.
Need a version smart enough to handle pyproj.

Fixes https://github.com/Khopa/dcs_liberation/issues/1049.
2021-05-01 11:06:01 -07:00
Marcos Sigueros Fernández
b2a551dc63 FIX: Purchase from airfield or anywhere allows negative budget. 2021-04-29 00:57:48 -07:00
SnappyComebacks
840107c69e Move base EWRs into their own category.
Without this we're sometimes spawning base EWRs at points far outside the base perimeter.
2021-04-28 21:08:54 -07:00
SnappyComebacks
ae34e4749b Move base EWRs into their own category.
Without this we're sometimes spawning base EWRs at points far outside the base perimeter.
2021-04-28 21:07:22 -07:00
Dan Albert
2a06a1ffdf Add proof-of-concept target info kneeboard page.
This is extremely rough and just serves as an example of how to use the
map projection API.
2021-04-27 21:20:11 -07:00
Dan Albert
8a01209ded Add data for lat/lon conversions. 2021-04-27 21:20:11 -07:00
Dan Albert
2b8dfc9dbc Stop counting neutral base captures in status.
These had no effect but were being counted on the waiting for mission
results page. Cleaned up the implementation a bunch while I was here.

Fixes https://github.com/Khopa/dcs_liberation/issues/1037
2021-04-26 17:54:32 -07:00
Dan Albert
e9f25eb562 Remove unused file.
mypy is flagging problems with this in the github action but not locally
for whatever reason, but it's not used so just delete it.
2021-04-25 20:30:55 -07:00
Dan Albert
475c7fd6db Update black and mypy requirements. 2021-04-25 18:46:37 -07:00
HerrTom
fa5d64022d Condense budget and intel sections of the top panel.
Budget and Intel panels now house a single button instead of separate Details buttons. Makes the top bar more compact and can fit in a 1080p monitor now.
2021-04-25 18:38:45 -07:00
Dan Albert
5c0f6cf65e Flip default for factory feature flag.
This is feature complete, we have a handful of campaigns that work with
the new mode now and it will be the only option at some point.
2021-04-25 18:20:20 -07:00
Dan Albert
0f8d366e31 Reformat with the latest black. 2021-04-25 18:09:33 -07:00
Dan Albert
9e2e593825 Improve transport descriptions. 2021-04-25 17:38:05 -07:00
Dan Albert
028bfc11eb Fix convoys skipping intermediate stops. 2021-04-25 17:38:05 -07:00
Dan Albert
b6cf7a4534 Fix cancelling convoys and cargo ships.
Not sure when I broke this but all transports were being cancelled as if
they were airlifts.
2021-04-25 17:10:24 -07:00
Dan Albert
0779679b99 Connect networks to enable multi-mode transfers.
Removing the per-transit type supply routes allows us to find the best
route from A to B even if the unit needs to switch transit modes along
the way.

The "best" route is the one that will generate better gameplay. That is,
convoys are preferred to ships (use cases for GMT are rare in DCS), and
ships are preferred to airlift (reasons to attack cargo ships are also
rare). Avoiding airlift is also a good strategic choice generally since
it consumes aircraft that could be performing other missions.

The extreme weight against airlift in the pathfinding algorithm could
probably be scaled way down so that airlift would be given preference
over a very long trip, possibly only for urgent transfers.

Later when we add rail that will probably be given the most preference,
but possibly between road and shipping.

https://github.com/Khopa/dcs_liberation/issues/823
2021-04-25 17:02:18 -07:00
Dan Albert
a48ef69e41 Disband cargo ships after processing.
Shipping lanes that received a ship would never lose their ships when
transfers completed, so the line on the map was staying solid (and
probably targetable).
2021-04-25 14:43:25 -07:00
Dan Albert
64d7953e50 Fix crash when opening FOB menus. 2021-04-25 14:39:32 -07:00
Dan Albert
21c35b31d4 AI planning for anti shipping missions.
Same as for anti convoy, these are rarely planned due to ordering issue
between mission planning and procurement.

Fixes https://github.com/Khopa/dcs_liberation/issues/826
2021-04-25 14:37:49 -07:00
Dan Albert
2d64acf299 Add mission targeting for cargo ships.
https://github.com/Khopa/dcs_liberation/issues/826
2021-04-25 14:31:13 -07:00
Dan Albert
e80819fc06 Add campaign inversion support to Abu Dhabi. 2021-04-25 14:17:24 -07:00
Dan Albert
7e40d58d04 Add cargo ships to the sim, track kills.
Not targetable yet.

https://github.com/Khopa/dcs_liberation/issues/826
2021-04-25 14:12:59 -07:00
Dan Albert
ba8fafcc95 First pass at cargo ships.
The simple form of this works, but without the multi-mode routing it'll
only get used when the final destination is a port with a link to a port
with a factory.

These also aren't targetable or simulated yet.

https://github.com/Khopa/dcs_liberation/issues/826
2021-04-25 12:10:06 -07:00
Dan Albert
42694d2004 Update 3.0 compatible campaigns to latest version.
Adds shipping lanes to Battle of Abu Dhabi. The others are acking the
new requirement but don't have an viable shipping lanes so no changes
are needed.
2021-04-24 21:55:40 -07:00
Dan Albert
5e67ce0ab2 Add shipping lane support to campaign files.
These don't actually do anything yet, this is just the campaign support
and UI.

https://github.com/Khopa/dcs_liberation/issues/826
2021-04-24 21:33:21 -07:00
Dan Albert
97b73e1a01 Add a north-to-south PG campaign.
Going to use this as the test bed for shipping transfers.
2021-04-24 19:43:40 -07:00
Dan Albert
4239257000 Add UAE 2015 faction.
Same as 2005 but with the Patriot  and some additional utilit aircraft.
2021-04-24 18:09:53 -07:00
Dan Albert
6bd94761d0 Update pydcs. 2021-04-24 15:33:51 -07:00
Starfire13
bc54e57fd4 Update campaigns and the splash damage plugin. 2021-04-24 15:30:31 -07:00
Dan Albert
17751e52fd Clear transports when disbanding convoys.
Also changes when we clear the convoys. Because we plan when transfers
are added (to plan UI orders immediately) we were planning convoys when
delivering units, then clearing the convoys, then planning them again.

Aside from the wasted effort, when we cleared the convoys we forgot to
tell the transfer that they no longer had transport, so when replanning
they did not get a new convoy.
2021-04-24 15:18:45 -07:00
Dan Albert
6016ebd3b4 Get transports from the closest airfield.
https://github.com/Khopa/dcs_liberation/issues/825
2021-04-23 23:24:25 -07:00
Dan Albert
8a44fc19ee Limit range for transport helicopters.
https://github.com/Khopa/dcs_liberation/issues/825
2021-04-23 23:24:25 -07:00
Dan Albert
f69450e2ae Add auto-procurment for airlift assets.
https://github.com/Khopa/dcs_liberation/issues/825
2021-04-23 23:24:25 -07:00
Mustang-25
3161ccced3 Re-adds GAR-8 (now AIM-9B in DCS) Fallback dates (#1025) 2021-04-23 22:25:11 -07:00
Dan Albert
909aad22a6 Make landing stops for cargo missions.
Until pydcs supports the timeReFuAr property this will have a wait time
of zero minutes, but it does seem to work.

Updating to https://github.com/pydcs/dcs/pull/132 will make the wait
time work automatically.

https://github.com/Khopa/dcs_liberation/issues/825
2021-04-23 22:19:54 -07:00
Dan Albert
c8b4fd1690 Use any (and only) transport aircraft for airlift.
https://github.com/Khopa/dcs_liberation/issues/825
2021-04-23 22:06:38 -07:00
Dan Albert
dac2271084 Add transport aircraft to US factions. 2021-04-23 22:06:38 -07:00
Dan Albert
5320d20f71 Add C-130 to the CAS capable list, clean up. 2021-04-23 21:21:05 -07:00
Dan Albert
20d8cc2b47 Plan transports at the beginning of the turn.
https://github.com/Khopa/dcs_liberation/issues/823
2021-04-23 20:31:52 -07:00
Dan Albert
d3fdbdbca5 Fix convoys not spawning where they should. 2021-04-23 20:23:23 -07:00
Dan Albert
d80f7ebf3b Refactor transfers to support unfulfilled orders.
This gives a clean break between the transfer request and the type of
transport allocated to make way for transports that need to switch
types (to support driving to a port, then getting on a ship, to a train,
then back on the road, etc).

https://github.com/Khopa/dcs_liberation/issues/823
2021-04-23 20:10:29 -07:00
C. Perreau
d6c84e362f Merge pull request #1012 from SpaceEnthusiast/develop
Added and modified factions
2021-04-24 00:13:45 +02:00
Khopa
635eee9590 Fixed ai_flight_planner for maps lacking frontlines (such as battle of britain on The Channel map) 2021-04-24 00:11:53 +02:00
Khopa
6aba07c33b Fixed ai_flight_planner for maps lacking frontlines (such as battle of britain on The Channel map) 2021-04-24 00:10:57 +02:00
Khopa
f0558c4c1e Fixed ai_flight_planner for maps lacking frontlines (such as battle of britain on The Channel map) 2021-04-23 23:45:14 +02:00
Dan Albert
45913b0add Bump to 3.0.
DCS version numbers and Liberation version numbers are getting
confusing. Push us ahead before we're both on 2.7.
2021-04-23 01:11:46 -07:00
Dan Albert
c258409a8d Add AI planning for airlifts.
Downside to the current implementation is that whether or not transports
that were purchased last turn will be available for airlift this turn is
arbitrary. This is because transfers are created at the same time as
units are delivered, and units are delivered in an arbitrary order per
CP. If the helicopters are delivered before the ground units they'll
have access to the transports, otherwise they'll be refunded. This will
be fixed later when I rework the transfer requests to not require
immediate fulfillment.

https://github.com/Khopa/dcs_liberation/issues/825
2021-04-23 01:10:03 -07:00
Dan Albert
26cd2d3fef Add todo for transfer refactor. 2021-04-22 22:24:57 -07:00
Dan Albert
4069074f41 Move unit delivery out of an unrelated file.
Historically this inherited from Event but there was no reason for that.
That's gone now. Finish the separation and move the unit order tracking
class out of the combat results reaction class's file.
2021-04-22 22:15:45 -07:00
Dan Albert
182422249f Remove support for mizdata/random locations.
The introduction of factories broke every existing campaign so we don't
need to keep support for the ancient formats any more.
2021-04-22 22:12:52 -07:00
Dan Albert
29b70b3247 Track airlift cargo kills.
https://github.com/Khopa/dcs_liberation/issues/825
2021-04-22 19:22:41 -07:00
Dan Albert
637ca8fbca Stop projecting threat zones from front lines.
This is an interim improvement since we should probably be pushing the
BARCAPs into TARCAP roles when the front line is so close. This does
regress flight pathing for anything that should route around the front
(to avoid getting shot at by SHORADS and TARCAPs), but for now it's one
or the other and this is the one everyone's complaining about.

(cherry picked from commit e474748f4d)
2021-04-22 18:23:12 -07:00
Dan Albert
e474748f4d Stop projecting threat zones from front lines.
This is an interim improvement since we should probably be pushing the
BARCAPs into TARCAP roles when the front line is so close. This does
regress flight pathing for anything that should route around the front
(to avoid getting shot at by SHORADS and TARCAPs), but for now it's one
or the other and this is the one everyone's complaining about.
2021-04-22 18:20:26 -07:00
Dan Albert
e4e65df976 Generalize commit range display for all patrols.
Fixes https://github.com/Khopa/dcs_liberation/issues/890

(cherry picked from commit 132ba905c7)
2021-04-22 17:55:49 -07:00
Dan Albert
132ba905c7 Generalize commit range display for all patrols.
Fixes https://github.com/Khopa/dcs_liberation/issues/890
2021-04-22 17:55:14 -07:00
Dan Albert
29579a2aec Remove missed merge conflict marker. 2021-04-22 17:49:34 -07:00
Dan Albert
e32b43cffb Show BARCAP commit ranges by default.
BARCAP placement confuses a lot of people but this should make it more
clear.

(cherry picked from commit 208d1b82b5)
2021-04-22 17:46:29 -07:00
Dan Albert
208d1b82b5 Show BARCAP commit ranges by default.
BARCAP placement confuses a lot of people but this should make it more
clear.
2021-04-22 17:45:19 -07:00
Simon Clark
1fd7c95f1b Fix Russian carrier name; Kuznetov -> Kuznetsov. 2021-04-22 23:08:56 +01:00
Dan Albert
481f195725 Airlift support.
UI isn't finished. Bulk transfers where the player doesn't care what
aircraft get used work (though they're chosen with no thought at all),
but being able to plan your own airlift flight isn't here yet.

Cargo planes are not implemented yet.

No way to view the cargo of a flight (will come with the cargo flight
planning UI).

The airlift flight/package creation should probably be moved out of the
UI and into the game code.

AI doesn't use these yet.

https://github.com/Khopa/dcs_liberation/issues/825
2021-04-22 00:30:18 -07:00
SpaceEnthusiast
d6c1550a1d Added Australia with C130 Faction 2021-04-21 23:58:18 -04:00
SpaceEnthusiast
60b9ae0a70 Added Canada with C130 Faction 2021-04-21 23:43:31 -04:00
SpaceEnthusiast
bf71351e6d Canadian, Australian, and Spanish Hornets now use proper liveries 2021-04-21 22:45:44 -04:00
Dan Albert
8e361a8776 Fix missing index valid check in transfer menu.
Fixes https://github.com/Khopa/dcs_liberation/issues/1006
2021-04-21 17:14:45 -07:00
C. Perreau
de2e5f861b Merge pull request #1007 from Khopa/develop_2_5_x
Release 2.5.0
2021-04-22 00:08:42 +02:00
Khopa
e39fd53727 Fixed Lint issue 2021-04-21 22:55:08 +02:00
Khopa
b27a7fc71b Fixed Lint issue 2021-04-21 22:54:48 +02:00
Khopa
76efcca64b Fixed error with Ramat David frequency (typo) 2021-04-21 22:41:04 +02:00
Khopa
5861ce6146 Fixed error with Ramat David frequency (typo) 2021-04-21 22:38:08 +02:00
Khopa
35f49d9bc0 Fixed airfields frequency on Persian Gulf 2021-04-21 22:31:52 +02:00
Khopa
a0a55797a9 Fixed airfields frequency on Syria 2021-04-21 22:31:49 +02:00
Khopa
c732ed556f Fixed airfields frequency on Persian Gulf 2021-04-21 22:30:08 +02:00
Khopa
be1a75e520 Fixed airfields frequency on Syria 2021-04-21 22:14:18 +02:00
Khopa
696a429e9e Pydcs update to latest version 2021-04-21 13:20:06 +02:00
Khopa
c41d10c581 Pydcs update to latest version 2021-04-21 12:57:19 +02:00
Dan Albert
29cd55e795 Add WIP airlift UI.
Doesn't do anything yet.

https://github.com/Khopa/dcs_liberation/issues/825
2021-04-21 01:02:02 -07:00
Dan Albert
c2ebf61fd3 Disable cancel actions on enemy transfers.
Decided to leave these in the menu as intel for the player, but the
player can no longer cancel them.

Fixes https://github.com/Khopa/dcs_liberation/issues/995
2021-04-20 23:20:08 -07:00
Dan Albert
489b4d6acf Add AI convoy attack planning.
Like the comment says this rarely has any effect due to the ordering of
flight planning and convoy creation. Could be separated, but opfor will
still not be able to target any convoys that the player creates in the
UI on that turn because they planning is done before the player can use
the UI.

Multi-turn transfers will be targetable, however.
2021-04-20 22:56:53 -07:00
Dan Albert
6cffc47f3c Clean up convoy code. 2021-04-20 22:21:42 -07:00
Dan Albert
50d8e08a34 Redo convoy attack flight plans.
The previous flight plan only makes sense if the convoy will make it a
significant distance from its starting point. At road speeds over the
typical mission duration this is not true, so we can actually plan this
as if it was a strike mission near the origin point and that's close
enough.

There's some cleanup work to do here that I've added todos for.

Fixes https://github.com/Khopa/dcs_liberation/issues/996
2021-04-20 22:04:57 -07:00
Dan Albert
3f16c0378a Add BAI planning against supply routes.
This currently is only supported for player flights. I have no idea how
to create an AI flight plan that won't just get them killed. AI-only BAI
missions against supply routes will warn the player on mission creation.
2021-04-19 23:09:39 -07:00
Dan Albert
2b06d8a096 Convert front line segments to proper class.
Needed so we can add context menus to the lines.
2021-04-19 21:41:56 -07:00
Dan Albert
2a5b37b9ad Show convoys on the map. 2021-04-19 20:37:15 -07:00
Dan Albert
cabbd234af Fix incorrect docs. 2021-04-19 20:24:20 -07:00
Dan Albert
480039ca50 Add --cheats to the CLI mission generator.
Doesn't enable the red ATO display because that's a lot of clutter, but
enables the commonly needed for debugging things.
2021-04-19 17:22:31 -07:00
Dan Albert
81d5cddac9 Remove weird single-CP supply route edge case.
A CP with a factory would be able to supply itself, but was not in a
supply route if it was the only connected friendly CP. When the player
starts with only one base against an enemy base this meant that it was
in no supply route, causing it to not be a recruitment location or a
place to buy more than a reserve of vehicles automatically.
2021-04-19 17:15:41 -07:00
Dan Albert
bb3e83548c Remove support for old-style campaigns.
These won't work any more since they won't be able to define factories
or supply routes. They were made obsolete ages ago, so just remove them.
2021-04-19 01:54:16 -07:00
Dan Albert
a3ff58c42d Turn feature flags on for development mode. 2021-04-19 00:04:46 -07:00
Dan Albert
30bf4542f0 Special case turn 0 for recruitment.
We want there to be units on the front line on turn 1 regardless of
factory locations, so bypass the recruitment restrictions on turn 0.

https://github.com/Khopa/dcs_liberation/issues/986
2021-04-19 00:03:47 -07:00
Dan Albert
d11c9a4615 Use convoy spawn points defined by the campaign.
The start/end points of the waypoints that define the course of the
front line also define the spawn points for convoys. Use them.

https://github.com/Khopa/dcs_liberation/issues/824
2021-04-18 23:57:13 -07:00
Dan Albert
d4679e0029 Note the semi-completion of the factory feature.
https://github.com/Khopa/dcs_liberation/issues/986
2021-04-18 23:35:18 -07:00
Dan Albert
3c4d6eb8e4 Automate transfers for purchases in the UI.
Buying a unit places the order, but the unit will appear at the nearest
connected source and a transfer will be automatically created next turn.

https://github.com/Khopa/dcs_liberation/issues/986
2021-04-18 23:31:45 -07:00
Dan Albert
df98e1f8ac Fix campaign format version.
Updatd the comment but forgot to update the number...
2021-04-18 23:25:44 -07:00
Dan Albert
56fc2986e9 Automate transfers from factories.
The purchase system will seek out a source for its units when the
purchase is completed. If no source is available the order will be
refunded. Orders with no source available are prevented, so this only
happens when the source was cut off from the destination during the
turn.

There's still some funkiness going on with the first turn (but possibly
only when the first turn includes a cheat to capture) where the AI buys
a ton of units somewhere other than the front line. First turn behavior
should probably be different anyway, with the first turn allowing
purchases anywhere to avoid empty front lines while troops reinforce if
the front line isn't a factory.

https://github.com/Khopa/dcs_liberation/issues/986
2021-04-18 23:22:30 -07:00
Dan Albert
627f18c42b Require factories for purchasing ground units.
https://github.com/Khopa/dcs_liberation/issues/986
2021-04-18 23:22:30 -07:00
Dan Albert
67b341cbd6 Add feature flag for new ground unit recruitment.
This is going to render most campaigns unusable because they won't have
any places to spawn ground units, so flagging off for now.

https://github.com/Khopa/dcs_liberation/issues/986
2021-04-18 23:22:30 -07:00
Dan Albert
eff5b94db7 Add factory placement to the campaign files.
This also removes the "factory" type from the normal strike target
(money generating) generators to avoid confusion. Later only control
points with factories will be able to spawn ground units, at which point
these will no longer generate income.

https://github.com/Khopa/dcs_liberation/issues/986
2021-04-18 23:22:30 -07:00
Dan Albert
707323ca12 Update Inherent Resolve to latest camapign format.
* Moves front line endpoints to roads for convoys (not used yet).
* Adds EWR sites.
2021-04-18 23:22:30 -07:00
Dan Albert
cb2ba2f53a Update pydcs, move back to upstream. 2021-04-18 23:22:11 -07:00
Dan Albert
777cd310ef Clarify which game we're talking about. 2021-04-18 17:45:27 -07:00
Dan Albert
9a4ec5a899 Fix campaign description template. 2021-04-18 17:41:27 -07:00
Dan Albert
c92e4e06cc Move campaign format version to a stable location. 2021-04-18 17:37:55 -07:00
Dan Albert
39135f8c80 Add version field to campaign descriptor file.
This is used to provide a UI hint to guide players towards campaigns
that have been updated to work with the current version of the game.
All the campaigns we currently have were made for an unknown version of
the game, so they're all flagged as incompatible.

The version field is not the DCS Liberation version number because the
campaign format may change multiple times during development. Instead
the version number is a monotonically increasing integer that we
increment whenever a game change requires campaign updates.
2021-04-18 17:30:49 -07:00
Dan Albert
5e054cfc77 Disallow selling ground units.
Ground units should be transferred to a new location, not sold and
repurchased.

https://github.com/Khopa/dcs_liberation/issues/823
2021-04-18 16:32:02 -07:00
Dan Albert
3b72c13f9d Add ground unit transfers to the changelog.
Also documented the behavior on the wiki (link in the changelog).

This is currently fully functional for players, but since units can be
bought and sold at any base there's no real reason to use these yet.
Will follow up with making ground units only purchasable at bases with
factories (the UI will still allow the purchase directly at the base,
but it will automatically create the transfer order) so convoys end up
being used, and to make factories a more interesting strategic target.

https://github.com/Khopa/dcs_liberation/issues/824
2021-04-18 16:22:51 -07:00
Dan Albert
65ed110ab7 Track convoy kills.
https://github.com/Khopa/dcs_liberation/issues/824
2021-04-18 16:22:51 -07:00
Dan Albert
5dd7ea3060 Spawn convoys for transfers.
Destroying these units currently has no effect.

https://github.com/Khopa/dcs_liberation/issues/824
2021-04-18 16:22:51 -07:00
Dan Albert
bd9cbf5e3b Move transfers one CP per turn.
https://github.com/Khopa/dcs_liberation/issues/824
2021-04-18 16:22:51 -07:00
Dan Albert
65f6a4eddd Restrict transfers to connected bases.
https://github.com/Khopa/dcs_liberation/issues/824
2021-04-18 15:59:15 -07:00
Dan Albert
e9ff554f39 Basic implementation of road based transfers.
This adds the models and UIs for creating ground unit transfer orders.
Most of the feature is still missing:

* The AI doesn't do them.
* Transfers can move across the whole map in one turn.
* Transfers between disconnected bases are allowed.
* Transfers are not modeled in the simulation, so they can't be
  interdicted.

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

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

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

No campaigns take advantage of this yet.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* AEW&C aircraft can be bought.
* AEW&C missions can be planned at any control point and at front lines.
* AEW&C will return after 4H or Bingo.
2021-02-07 11:39:22 +00:00
Khopa
4a0ccc4c2f Fixed error detected by mypy 2021-02-07 11:39:21 +00:00
Khopa
c92b7240eb Increased number of launchers on Silkworms sites 2021-02-07 11:39:21 +00:00
Khopa
6a74c3faeb Added coastal defenses sites generator for Iran and China. 2021-02-07 11:39:21 +00:00
BenBenBeartrax
c5ae872787 waypointbuilder: low altitude AGL for helos 2021-02-07 11:39:21 +00:00
Dan Albert
2e9ab0a9d7 Move develop to 2.5. 2021-02-07 11:39:20 +00:00
Malakhit
08f67860be Merge pull request #854 from Khopa/develop_2_4_x
2.4.1 Release
2021-02-07 11:22:38 +00:00
Simon Clark
3fab1d92b7 Fixed some areas where the non-pretty name for a unit was displayed. 2021-02-07 11:15:25 +00:00
Simon Clark
6573157112 Changelog. 2021-02-07 10:49:34 +00:00
Simon Clark
e83841eb0b Fix syntax error with SH-60B payload. 2021-02-07 10:47:36 +00:00
Simon Clark
6a0e18c0e9 Change the logic for culling missile sites.
Missile sites now generate a 2.5km culling circle around themselves, rather than using the standard full culling zone size.

Fixes #850.
2021-02-06 22:20:02 +00:00
Simon Clark
b71b6473e3 Start 2.4.1, fix #852. 2021-02-06 19:56:17 +00:00
Simon Krüger
a004f62fe8 Initial implementation of AEW&C missions.
Still a work in progress (the missions don't actually perform their task, just orbit). Currently:

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

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

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

View File

@@ -9,6 +9,8 @@ assignees: ''
Before filing, please search the issue tracker to see if the issue has already been reported.
If reporting a DCS AI bug, check https://github.com/dcs-liberation/dcs_liberation#dcs-bugs.
**Describe the bug**
A clear and concise description of what the bug is.
@@ -28,7 +30,8 @@ We will usually need more information for debugging. Include as much of the foll
- DCS Liberation save file (the `.liberation` file you save from the DCS Liberation window). By default these are located in your DCS saved games directory (`%USERPROFILE%/Saved Games/DCS`).
- The generated mission file (the `.miz` file that you load in DCS to play the turn). By default these are located in your missions directory (`%USERPROFILE%/Saved Games/DCS/Missions`).
- A tacview track file, especially when demonstrating an issue with AI behavior. By default these are locaed in your Tacview tracks directory (`%USERPROFILE%/Documents/Tacview`).
- A tacview track file, especially when demonstrating an issue with AI behavior. By default these are located in your Tacview tracks directory (`%USERPROFILE%/Documents/Tacview`).
- The state.json file from the finished mission when the problem is related to results processing. By default these are located in your Liberation install directory.
**Version information (please complete the following information):**
- DCS Liberation [e.g. 2.3.1]:

View File

@@ -0,0 +1,28 @@
---
name: Campaign update submission
about: Submit an update to a campaign you maintain.
title: 'Update for <campaign name>'
labels: campaign-update-submission
assignees: ''
---
This form should only be used for submitted updated miz/json files for campaigns
distributed with Liberation. If you are _requesting_ an update to a campaign, see
https://github.com/dcs-liberation/dcs_liberation/wiki/Campaign-maintenance. If the
campaign has an owner, it will be updated before release. If it does not, you can
volunteer to own it.
If you are not the owner of the campaign listed on
https://github.com/dcs-liberation/dcs_liberation/wiki/Campaign-maintenance, please start
there.
Otherwise, delete everything above the line below and fill out the following form. Note:
GitHub does not accept .miz files. You can either rename the file to .miz.txt or add the
file to a .zip file.
---
* Campaign name:
* Files:
* Update summary (optional):

View File

@@ -9,6 +9,8 @@ assignees: ''
Before filing, please search the issue tracker to see if this feature has already been requested.
If requesting a DCS AI feature, check If reporting a DCS AI bug, check https://github.com/dcs-liberation/dcs_liberation#dcs-bugs.
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

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

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

View File

@@ -11,10 +11,10 @@ jobs:
with:
submodules: true
- name: Set up Python 3.8
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: 3.8
python-version: 3.9
- name: Install environment
run: |

View File

@@ -13,10 +13,10 @@ jobs:
with:
submodules: true
- name: Set up Python 3.8
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: 3.8
python-version: 3.9
- name: Install environment
run: |

1
.gitignore vendored
View File

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

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

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

View File

@@ -11,7 +11,7 @@ Note that you may need to remove the filter for open bugs if it's something we'v
## Making content for Liberation
You can create new campaigns : See [campaign creation wiki](https://github.com/Khopa/dcs_liberation/wiki/Custom-Campaigns).
You can create new campaigns : See [campaign creation wiki](https://github.com/dcs-liberation/dcs_liberation/wiki/Custom-Campaigns).
You can also improve existing campaigns.
You can then submit new campaigns on the "campaigns" channel on Discord, or by making a pull request if you are comfortable with git.

View File

@@ -1,15 +1,14 @@
![Logo](https://i.imgur.com/c2k18E1.png)
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg?logo=paypal)](https://www.paypal.com/paypalme/KhopaDCSL)
[![Patreon](https://img.shields.io/badge/patreon-become%20a%20patron-orange?logo=patreon)](https://patreon.com/khopa)
[![Download](https://img.shields.io/github/downloads/khopa/dcs_liberation/total?label=Download)](https://github.com/Khopa/dcs_liberation/releases)
[![Download](https://img.shields.io/github/downloads/dcs-liberation/dcs_liberation/total?label=Download)](https://github.com/dcs-liberation/dcs_liberation/releases)
[![Discord](https://img.shields.io/discord/595702951800995872?label=Discord&logo=discord)](https://discord.gg/bKrtrkJ)
[![GitHub pull requests](https://img.shields.io/github/issues-pr/khopa/dcs_liberation)](https://github.com/Khopa/dcs_liberation)
[![GitHub issues](https://img.shields.io/github/issues/khopa/dcs_liberation)](https://github.com/Khopa/dcs_liberation/issues)
![GitHub stars](https://img.shields.io/github/stars/khopa/dcs_liberation?style=social)
[![GitHub pull requests](https://img.shields.io/github/issues-pr/dcs-liberation/dcs_liberation)](https://github.com/dcs-liberation/dcs_liberation)
[![GitHub issues](https://img.shields.io/github/issues/dcs-liberation/dcs_liberation)](https://github.com/dcs-liberation/dcs_liberation/issues)
![GitHub stars](https://img.shields.io/github/stars/dcs-liberation/dcs_liberation?style=social)
## About DCS Liberation
DCS Liberation is a [DCS World](https://www.digitalcombatsimulator.com/en/products/world/) turn based single-player or co-op dynamic campaign.
@@ -19,21 +18,29 @@ It is an external program that generates full and complex DCS missions and manag
## Downloads
Latest release is available here : https://github.com/Khopa/dcs_liberation/releases
Latest release is available here : https://github.com/dcs-liberation/dcs_liberation/releases
To download preview builds of the next version of DCS Liberation, see https://github.com/Khopa/dcs_liberation/wiki/Preview-builds.
To download preview builds of the next version of DCS Liberation, see https://github.com/dcs-liberation/dcs_liberation/wiki/Preview-builds.
## DCS bugs
These DCS bugs prevent us from improving AI behavior. Please upvote them! (But please
_don't_ spam them with comments):
* [A2A and SEAD escorts don't escort](https://forums.eagle.ru/topic/251798-options-for-alternate-ai-escort-behavior/?tab=comments#comment-4668033)
* [DEAD can't use mixed loadouts effectively](https://forums.eagle.ru/topic/271941-ai-rtbs-after-firing-decoys-despite-full-load-of-bombs/)
## Bugs and feature requests
If you need to report a bug or want to suggest a new feature, you can do this on our [bug tracker](https://github.com/Khopa/dcs_liberation/issues). In either case, please use the search bar at the top of the page to see if it has already been reported. Note that you may need to remove the filter for open bugs if it's something we've recently fixed.
If you need to report a bug or want to suggest a new feature, you can do this on our [bug tracker](https://github.com/dcs-liberation/dcs_liberation/issues). In either case, please use the search bar at the top of the page to see if it has already been reported. Note that you may need to remove the filter for open bugs if it's something we've recently fixed.
## Roadmap
Our plans for future releases can be found on our [Projects page](https://github.com/Khopa/dcs_liberation/projects). Each planned release has a Project, and the page for that project has columns for to do, in progress, and done. Items in the Done column are in the [preview build](https://github.com/Khopa/dcs_liberation/wiki/Preview-builds) for that release. Items in the To do column are planned to be added to that release.
Our plans for future releases can be found on our [Projects page](https://github.com/dcs-liberation/dcs_liberation/projects). Each planned release has a Project, and the page for that project has columns for to do, in progress, and done. Items in the Done column are in the [preview build](https://github.com/dcs-liberation/dcs_liberation/wiki/Preview-builds) for that release. Items in the To do column are planned to be added to that release.
## Resources
Tutorials, contributors and developer's guides are available in the project's [Wiki](https://github.com/Khopa/dcs_liberation/wiki/)
Tutorials, contributors and developer's guides are available in the project's [Wiki](https://github.com/dcs-liberation/dcs_liberation/wiki/)
## Special Thanks

View File

@@ -1,3 +1,139 @@
# 3.0.0
Saves from 2.5 are not compatible with 3.0.
## Features/Improvements
* **[Campaign]** Ground units can now be transferred by road, airlift, and cargo ship. See https://github.com/dcs-liberation/dcs_liberation/wiki/Unit-Transfers for more information.
* **[Campaign]** Ground units can no longer be sold. To move units to a new location, transfer them.
* **[Campaign]** Ground units must now be recruited at a base with a factory and transferred to their destination. When buying units in the UI, the purchase will automatically be fulfilled at the closest factory, and a transfer will be created on the next turn.
* **[Campaign]** Non-control point FOBs will no longer spawn.
* **[Campaign]** Added squadrons and pilots. See https://github.com/dcs-liberation/dcs_liberation/wiki/Squadrons-and-pilots for more information.
* **[Campaign]** Capturing a base now depopulates all of its attached objectives with units: air defenses, EWRs, ships, armor groups, etc. Buildings are captured.
* **[Campaign]** Ammunition Depots determine how many ground units can be deployed on the frontline by a control point.
* **[Campaign AI]** AI now considers Ju-88s for CAS, strike, and DEAD missions.
* **[Campaign AI]** AI planned AEW&C missions will now be scheduled ASAP.
* **[Campaign AI]** AI now considers the range to the SAM's threat zone rather than the range to the SAM itself when determining target priorities.
* **[Campaign AI]** Auto purchase of ground units will now maintain unit composition instead of buying randomly. The unit composition is predefined.
* **[Campaign AI]** Auto purchase will aim to purchase enough ground units to support the frontline, plus 30% reserve units.
* **[Campaign AI]** Auto purchase will now adjust its air/ground balance to favor whichever is under-funded.
* **[Flight Planner]** Desired mission length is now configurable (defaults to 60 minutes). A BARCAP will be planned every 30 minutes. Other packages will simply have their takeoffs spread out or compressed such that the last flight will take off around the mission end time.
* **[Flight Planner]** Flight plans now include bullseye waypoints.
* **[Flight Planner]** Differentiated SEAD and SEAD escort. SEAD is tasked with suppressing the package target, SEAD escort is tasked with protecting the package from all SAMs along its route.
* **[Flight Planner]** Planned airspeed increased to 0.85 mach for supersonic airframes and 85% of max speed for subsonic.
* **[Flight Planner]** Taxi time estimation for airfields increased from 5 minutes to 8 minutes.
* **[Flight Planner]** Reduce expected error margin for flight plans from 10% to 5%.
* **[Flight Planner]** SEAD flights are scheduled one minute ahead of the package's TOT so that they can suppress the site ahead of the strike.
* **[Flight Planner]** Automatic ATO generation for the player's coalition can now be disabled in the settings.
* **[Payloads]** AI flights for most air to ground mission types (CAS excluded) will have their guns emptied to prevent strafing fully armed and operational battle stations. Gun-reliant airframes like A-10s and warbirds will keep their bullets.
* **[Kneeboard]** ATC table overflow alleviated by wrapping long airfield names and splitting ATC frequency and channel into separate rows.
* **[UI]** Overhauled the map implementation. Now uses satellite imagery instead of low res map images. Display options have moved from the toolbar to panels in the map.
* **[UI]** Campaigns generated for an older or newer version of the game will now be marked as incompatible. They can still be played, but bugs may be present.
* **[UI]** DCS loadouts are now selectable in the loadout setup menu.
* **[UI]** Added global aircraft inventory view under Air Wing dialog.
* **[UI]** Base menu now shows information about ground unit deployment limits.
* **[Modding]** Campaigns now choose locations for factories to spawn.
* **[Modding]** Campaigns now choose locations for ammunition depots to spawn.
* **[Modding]** Campaigns now use map structures as strike targets.
* **[Modding]** Campaigns may now set *any* objective type to be a required spawn rather than random chance. Support for random objective generation was removed.
* **[Modding]** Campaigns may now place AAA objectives.
* **[Modding]** Can now install custom factions to <DCS saved games>/Liberation/Factions instead of the Liberation install directory.
* **[Performance Settings]** Added a settings to lower the number of smoke effects generated on frontlines. Lowered default settings for frontline smoke generators, so less smoke should be generated by default.
* **[Configuration]** Liberation preferences (DCS install and save game location) are now saved to `%LOCALAPPDATA%/DCSLiberation` to prevent needing to reconfigure each new install.
* **[Skynet]** Updated to 2.1.0.
## Fixes
* **[Campaign AI]** Fix purchase of aircraft by priority (the faction's list was being used as the priority list rather than the game's).
* **[Campaign AI]** Fixed bug causing AI to over-purchase cheap aircraft.
* **[Campaign AI]** Auto planner will no longer attempt to plan missions for which the faction has no compatible aircraft.
* **[Campaign AI]** Stop purchasing aircraft after the first unaffordable package to attempt to complete more packages rather than filling airfields with cheap escorts that will never be used.
* **[Campaign]** Fixed bug where offshore strike locations were being used to spawn ship objectives.
* **[Campaign]** EWR sites are now purchasable.
* **[Flight Planner]** AI strike flight plans now include the correct target actions for building groups.
* **[Flight Planner]** AI BAI/DEAD/SEAD flights now have tasks to attack all groups at the target location, not just the primary group (for multi-group SAM sites).
* **[Flight Planner]** Fixed some contexts where damaged runways would be used. Destroying a carrier will no longer break the game.
# 2.5.1
## Features/Improvements
* **[UI]** Engagement ranges are now displayed by default.
* **[UI]** Engagement range display generalized to work for all patrolling flight plans (BARCAP, TARCAP, and CAS).
* **[Flight Planner]** Front lines no longer project threat zones to avoid pushing BARCAPs back so much. TARCAPs will be forcibly planned but strike packages will not route around front lines even if it is reasonable to do so.
## Fixes
* **[Campaigns]** EWRs associated with a base will now only be generated near the base.
* **[Flight Planner]** Fixed error when generating AEW&C flight plans in campaigns with no front lines.
# 2.5.0
Saves from 2.4 are not compatible with 2.5.
## Features/Improvements
* **[Engine]** DCS 2.7 Support
* **[UI]** Improved FOB menu, added a custom banner, and do not display aircraft recruitment menu
* **[Flight Planner]** Added AEW&C missions. (by siKruger)
* **[Kneeboard]** Added dark kneeboard option (by GvonH)
* **[Campaigns]** Multiple EWR sites may now be generated, and EWR sites may be generated outside bases (by SnappyComebacks)
* **[Mission Generation]** Cloudy and rainy (but not thunderstorm) weather will use the cloud presets from DCS 2.7.
* **[Plugins]** Added LotATC export plugin (by drsoran)
* **[Plugins]** Added Splash Damage Plugin (by Wheelijoe)
* **[Loadouts]** Replaced Litening with ATFLIR for all default F/A-18C loadouts.
## Fixes
* **[Flight Planner]** Front lines now project threat zones, so TARCAP/escorts will not be pruned for flights near the front. Packages may also route around the front line when practical.
* **[Flight Planner]** Fixed error when planning BAI at SAMs with dead subgroups.
* **[Flight Planner]** Mig-19 was not allowed for CAS roles fixed
* **[Flight Planner]** Increased size of navigation planning area to avoid plannign failures with distant waypoints.
* **[Flight Planner]** Fixed UI refresh when unchecking the "default loadout" box in the loadout editor.
* **[Objective names]** Fixed typos in objective name : ARMADILLLO -> ARMADILLO (by SnappyComebacks)
* **[Payloads]** F-86 Sabre was missing a custom payload
* **[Payloads]** Added GAR-8 period restrictions (by Mustang-25)
* **[Campaign]** Date now progresses.
* **[Campaign]** Added game over message when a coalition runs out of functioning airbases.
* **[Mission Generation]** Fixed "invalid face handle" error in kneeboard generation that occurred on some machines.
## Regressions
* **[Mod Support]** Stopped support for 2.5.5 Rafale Mode, and removed factions that were using it
* **[Mod Support]** Su-57 mod support might be out of date
# 2.4.3
## Features/Improvements
* **[New Game Wizard]** Added the possibility to setup custom start date
## Fixes
* **[Mods]** Updated C-130J mod data to version 6.4
* **[Mods]** Updated F-22A mod to latest version
# 2.4.2
## Features/Improvements
* **[Factions]** Introduction dates and fallback weapons added for US, Russian, UK, and French weapons. Huge thanks to @TheCandianVendingMachine for the massive amount of data entry!
* **[Campaigns]** Added 1995 start dates.
## Fixes
* **[Economy]** Pending ground unit purchases will also be transferred when a connected base is captured.
* **[UI]** Fixed rounding of budget in recruitment menu.
# 2.4.1
## Fixes
* **[Units]** Fixed syntax error with the SH-60B payload file.
* **[Culling]** Missile sites generate reasonably sized non-cull zones rather than 100km ones.
* **[UI]** Budget display is also now rounded to 2 decimal places.
* **[UI]** Fixed some areas where the old, non-pretty name was displayed to users.
# 2.4.0
Saves from 2.3 are not compatible with 2.4.

0
game/data/__init__.py Normal file
View File

View File

@@ -2,20 +2,21 @@ from dcs.vehicles import AirDefence
AAA_UNITS = [
AirDefence.SPAAA_Gepard,
AirDefence.SPAAA_ZSU_23_4_Shilka,
AirDefence.AAA_Vulcan_M163,
AirDefence.AAA_ZU_23_Closed,
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
AirDefence.SPAAA_Vulcan_M163,
AirDefence.AAA_ZU_23_Closed_Emplacement,
AirDefence.AAA_ZU_23_Emplacement,
AirDefence.AAA_ZU_23_on_Ural_375,
AirDefence.AAA_ZU_23_Insurgent_Closed,
AirDefence.AAA_ZU_23_Insurgent_on_Ural_375,
AirDefence.AAA_ZU_23_Insurgent,
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,
AirDefence.AAA_Flak_38_20mm,
AirDefence.AAA_8_8cm_Flak_36,
AirDefence.AAA_8_8cm_Flak_37,
AirDefence.AAA_Flak_Vierling_38,
AirDefence.AAA_Kdo_G_40,
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_Bofors_40mm,
AirDefence.AAA_S_60_57mm,
]

40
game/data/alic.py Normal file
View File

@@ -0,0 +1,40 @@
from dcs.unit import Unit
from dcs.vehicles import AirDefence
class AlicCodes:
CODES = {
AirDefence.EWR_1L13.id: 101,
AirDefence.EWR_55G6.id: 102,
AirDefence.SAM_SA_10_S_300_Grumble_Clam_Shell_SR.id: 103,
AirDefence.SAM_SA_10_S_300_Grumble_Big_Bird_SR.id: 104,
AirDefence.SAM_SA_11_Buk_Gadfly_Snow_Drift_SR.id: 107,
AirDefence.SAM_SA_6_Kub_Straight_Flush_STR.id: 108,
AirDefence.MCC_SR_Sborka_Dog_Ear_SR.id: 109,
AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR.id: 110,
AirDefence.SAM_SA_11_Buk_Gadfly_Fire_Dome_TEL.id: 115,
AirDefence.SAM_SA_8_Osa_Gecko_TEL.id: 117,
AirDefence.SAM_SA_13_Strela_10M3_Gopher_TEL.id: 118,
AirDefence.SAM_SA_15_Tor_Gauntlet.id: 119,
AirDefence.SAM_SA_19_Tunguska_Grison.id: 120,
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish.id: 121,
AirDefence.SAM_P19_Flat_Face_SR__SA_2_3.id: 122,
AirDefence.SAM_SA_3_S_125_Low_Blow_TR.id: 123,
AirDefence.SAM_Rapier_Blindfire_TR.id: 124,
AirDefence.SAM_Rapier_LN.id: 125,
AirDefence.SAM_SA_2_S_75_Fan_Song_TR.id: 126,
AirDefence.HQ_7_Self_Propelled_LN.id: 127,
AirDefence.HQ_7_Self_Propelled_STR.id: 128,
AirDefence.SAM_Roland_ADS.id: 201,
AirDefence.SAM_Patriot_STR.id: 202,
AirDefence.SAM_Hawk_SR__AN_MPQ_50.id: 203,
AirDefence.SAM_Hawk_TR__AN_MPQ_46.id: 204,
AirDefence.SAM_Roland_EWR.id: 205,
AirDefence.SAM_Hawk_CWAR_AN_MPQ_55.id: 206,
AirDefence.SPAAA_Gepard.id: 207,
AirDefence.SPAAA_Vulcan_M163.id: 208,
}
@classmethod
def code_for(cls, unit: Unit) -> int:
return cls.CODES[unit.type]

View File

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

View File

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

View File

@@ -1,7 +1,20 @@
from dataclasses import dataclass
from datetime import timedelta
from dcs.task import Reconnaissance
from game.utils import Distance, feet, nautical_miles
from game.data.groundunitclass import GroundUnitClass
@dataclass
class GroundUnitProcurementRatios:
ratios: dict[GroundUnitClass, float]
def for_unit_class(self, unit_class: GroundUnitClass) -> float:
try:
return self.ratios[unit_class] / sum(self.ratios.values())
except KeyError:
return 0.0
@dataclass(frozen=True)
@@ -50,6 +63,8 @@ class Doctrine:
sweep_distance: Distance
ground_unit_procurement_ratios: GroundUnitProcurementRatios
MODERN_DOCTRINE = Doctrine(
cap=True,
@@ -76,6 +91,17 @@ MODERN_DOCTRINE = Doctrine(
cap_engagement_range=nautical_miles(50),
cas_duration=timedelta(minutes=30),
sweep_distance=nautical_miles(60),
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
{
GroundUnitClass.Tank: 3,
GroundUnitClass.Atgm: 2,
GroundUnitClass.Apc: 2,
GroundUnitClass.Ifv: 3,
GroundUnitClass.Artillery: 1,
GroundUnitClass.Shorads: 2,
GroundUnitClass.Recon: 1,
}
),
)
COLDWAR_DOCTRINE = Doctrine(
@@ -103,6 +129,17 @@ COLDWAR_DOCTRINE = Doctrine(
cap_engagement_range=nautical_miles(35),
cas_duration=timedelta(minutes=30),
sweep_distance=nautical_miles(40),
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
{
GroundUnitClass.Tank: 4,
GroundUnitClass.Atgm: 2,
GroundUnitClass.Apc: 3,
GroundUnitClass.Ifv: 2,
GroundUnitClass.Artillery: 1,
GroundUnitClass.Shorads: 2,
GroundUnitClass.Recon: 1,
}
),
)
WWII_DOCTRINE = Doctrine(
@@ -130,4 +167,14 @@ WWII_DOCTRINE = Doctrine(
cap_engagement_range=nautical_miles(20),
cas_duration=timedelta(minutes=30),
sweep_distance=nautical_miles(10),
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
{
GroundUnitClass.Tank: 3,
GroundUnitClass.Atgm: 3,
GroundUnitClass.Apc: 3,
GroundUnitClass.Artillery: 1,
GroundUnitClass.Shorads: 3,
GroundUnitClass.Recon: 1,
}
),
)

View File

@@ -0,0 +1,239 @@
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
class GroundUnitClass(Enum):
Tank = (
"Tank",
(
Armor.MBT_T_55,
Armor.MBT_T_72B,
Armor.MBT_T_72B3,
Armor.MBT_T_80U,
Armor.MBT_T_90,
Armor.MBT_Leopard_2A4,
Armor.MBT_Leopard_2A4_Trs,
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,6 +1,6 @@
from dcs.ships import (
CGN_1144_2_Pyotr_Velikiy,
CG_1164_Moskva,
Battlecruiser_1144_2_Pyotr_Velikiy,
Cruiser_1164_Moskva,
CVN_70_Carl_Vinson,
CVN_71_Theodore_Roosevelt,
CVN_72_Abraham_Lincoln,
@@ -8,67 +8,101 @@ from dcs.ships import (
CVN_74_John_C__Stennis,
CV_1143_5_Admiral_Kuznetsov,
CV_1143_5_Admiral_Kuznetsov_2017,
FFG_11540_Neustrashimy,
FFL_1124_4_Grisha,
FF_1135M_Rezky,
FSG_1241_1MP_Molniya,
Frigate_11540_Neustrashimy,
Corvette_1124_4_Grisha,
Frigate_1135M_Rezky,
Corvette_1241_1_Molniya,
LHA_1_Tarawa,
Oliver_Hazzard_Perry_class,
Ticonderoga_class,
FFG_Oliver_Hazzard_Perry,
CG_Ticonderoga,
Type_052B_Destroyer,
Type_052C_Destroyer,
Type_054A_Frigate,
USS_Arleigh_Burke_IIa,
DDG_Arleigh_Burke_IIa,
)
from dcs.vehicles import AirDefence
UNITS_WITH_RADAR = [
TELARS = {
AirDefence.SAM_SA_19_Tunguska_Grison,
AirDefence.SAM_SA_11_Buk_Gadfly_Fire_Dome_TEL,
AirDefence.SAM_SA_8_Osa_Gecko_TEL,
AirDefence.SAM_SA_15_Tor_Gauntlet,
AirDefence.SAM_Roland_ADS,
}
# Radars
AirDefence.SAM_SA_15_Tor_9A331,
AirDefence.SAM_SA_11_Buk_CC_9S470M1,
AirDefence.SAM_Patriot_AMG_AN_MRC_137,
AirDefence.SAM_Patriot_ECS_AN_MSQ_104,
AirDefence.SPAAA_Gepard,
AirDefence.AAA_Vulcan_M163,
AirDefence.SPAAA_ZSU_23_4_Shilka,
AirDefence.EWR_1L13,
AirDefence.SAM_SA_6_Kub_STR_9S91,
AirDefence.SAM_SA_10_S_300PS_TR_30N6,
AirDefence.SAM_SA_10_S_300PS_SR_5N66M,
AirDefence.EWR_55G6,
AirDefence.SAM_SA_10_S_300PS_SR_64H6E,
AirDefence.SAM_SA_11_Buk_SR_9S18M1,
AirDefence.CP_9S80M1_Sborka,
AirDefence.SAM_Hawk_TR_AN_MPQ_46,
AirDefence.SAM_Hawk_SR_AN_MPQ_50,
AirDefence.SAM_Patriot_STR_AN_MPQ_53,
AirDefence.SAM_Hawk_CWAR_AN_MPQ_55,
AirDefence.SAM_SR_P_19,
AirDefence.SAM_Roland_EWR,
AirDefence.SAM_SA_3_S_125_TR_SNR,
AirDefence.SAM_SA_2_TR_SNR_75_Fan_Song,
TRACK_RADARS = {
AirDefence.SAM_SA_6_Kub_Straight_Flush_STR,
AirDefence.SAM_SA_3_S_125_Low_Blow_TR,
AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR,
AirDefence.SAM_Hawk_TR__AN_MPQ_46,
AirDefence.SAM_Patriot_STR,
AirDefence.SAM_SA_2_S_75_Fan_Song_TR,
AirDefence.SAM_Rapier_Blindfire_TR,
AirDefence.HQ_7_Self_Propelled_STR,
}
LAUNCHER_TRACKER_PAIRS = {
AirDefence.SAM_SA_6_Kub_Gainful_TEL: AirDefence.SAM_SA_6_Kub_Straight_Flush_STR,
AirDefence.SAM_SA_3_S_125_Goa_LN: AirDefence.SAM_SA_3_S_125_Low_Blow_TR,
AirDefence.SAM_SA_10_S_300_Grumble_TEL_D: AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR,
AirDefence.SAM_SA_10_S_300_Grumble_TEL_C: AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR,
AirDefence.SAM_Hawk_LN_M192: AirDefence.SAM_Hawk_TR__AN_MPQ_46,
AirDefence.SAM_Patriot_LN: AirDefence.SAM_Patriot_STR,
AirDefence.SAM_SA_2_S_75_Guideline_LN: AirDefence.SAM_SA_2_S_75_Fan_Song_TR,
AirDefence.SAM_Rapier_LN: AirDefence.SAM_Rapier_Blindfire_TR,
AirDefence.HQ_7_Self_Propelled_LN: AirDefence.HQ_7_Self_Propelled_STR,
}
UNITS_WITH_RADAR = {
# Radars
AirDefence.SAM_SA_19_Tunguska_Grison,
AirDefence.SAM_SA_11_Buk_Gadfly_Fire_Dome_TEL,
AirDefence.SAM_SA_8_Osa_Gecko_TEL,
AirDefence.SAM_SA_15_Tor_Gauntlet,
AirDefence.SPAAA_Gepard,
AirDefence.SPAAA_Vulcan_M163,
AirDefence.SAM_Roland_ADS,
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
AirDefence.EWR_1L13,
AirDefence.SAM_SA_6_Kub_Straight_Flush_STR,
AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR,
AirDefence.SAM_SA_10_S_300_Grumble_Clam_Shell_SR,
AirDefence.EWR_55G6,
AirDefence.SAM_SA_10_S_300_Grumble_Big_Bird_SR,
AirDefence.SAM_SA_11_Buk_Gadfly_Snow_Drift_SR,
AirDefence.MCC_SR_Sborka_Dog_Ear_SR,
AirDefence.SAM_Hawk_TR__AN_MPQ_46,
AirDefence.SAM_Hawk_SR__AN_MPQ_50,
AirDefence.SAM_Patriot_STR,
AirDefence.SAM_Hawk_CWAR_AN_MPQ_55,
AirDefence.SAM_P19_Flat_Face_SR__SA_2_3,
AirDefence.SAM_Roland_EWR,
AirDefence.SAM_SA_3_S_125_Low_Blow_TR,
AirDefence.SAM_SA_2_S_75_Fan_Song_TR,
AirDefence.SAM_Rapier_Blindfire_TR,
AirDefence.HQ_7_Self_Propelled_LN,
AirDefence.HQ_7_Self_Propelled_STR,
AirDefence.EWR_FuMG_401_Freya_LZ,
AirDefence.EWR_FuSe_65_Würzburg_Riese,
# Ships
CVN_70_Carl_Vinson,
Oliver_Hazzard_Perry_class,
Ticonderoga_class,
FFL_1124_4_Grisha,
FFG_Oliver_Hazzard_Perry,
CG_Ticonderoga,
Corvette_1124_4_Grisha,
CV_1143_5_Admiral_Kuznetsov,
FSG_1241_1MP_Molniya,
CG_1164_Moskva,
FFG_11540_Neustrashimy,
CGN_1144_2_Pyotr_Velikiy,
FF_1135M_Rezky,
Corvette_1241_1_Molniya,
Cruiser_1164_Moskva,
Frigate_11540_Neustrashimy,
Battlecruiser_1144_2_Pyotr_Velikiy,
Frigate_1135M_Rezky,
CV_1143_5_Admiral_Kuznetsov_2017,
CVN_74_John_C__Stennis,
CVN_71_Theodore_Roosevelt,
CVN_72_Abraham_Lincoln,
CVN_73_George_Washington,
USS_Arleigh_Burke_IIa,
DDG_Arleigh_Burke_IIa,
LHA_1_Tarawa,
Type_052B_Destroyer,
Type_054A_Frigate,
Type_052C_Destroyer
]
Type_052C_Destroyer,
}

File diff suppressed because it is too large Load Diff

1148
game/db.py

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,16 @@ from dcs.unittype import FlyingType, UnitType
from game import db
from game.theater import Airfield, ControlPoint
from game.unitmap import Building, FrontLineUnit, GroundObjectUnit, UnitMap
from game.transfers import CargoShip
from game.unitmap import (
AirliftUnits,
Building,
ConvoyUnit,
FrontLineUnit,
GroundObjectUnit,
UnitMap,
FlyingUnit,
)
from gen.flights.flight import Flight
if TYPE_CHECKING:
@@ -33,24 +42,24 @@ DEBRIEFING_LOG_EXTENSION = "log"
@dataclass(frozen=True)
class AirLosses:
player: List[Flight]
enemy: List[Flight]
player: List[FlyingUnit]
enemy: List[FlyingUnit]
@property
def losses(self) -> Iterator[Flight]:
def losses(self) -> Iterator[FlyingUnit]:
return itertools.chain(self.player, self.enemy)
def by_type(self, player: bool) -> Dict[Type[FlyingType], int]:
losses_by_type: Dict[Type[FlyingType], int] = defaultdict(int)
losses = self.player if player else self.enemy
for loss in losses:
losses_by_type[loss.unit_type] += 1
losses_by_type[loss.flight.unit_type] += 1
return losses_by_type
def surviving_flight_members(self, flight: Flight) -> int:
losses = 0
for loss in self.losses:
if loss == flight:
if loss.flight == flight:
losses += 1
return flight.count - losses
@@ -60,6 +69,15 @@ class GroundLosses:
player_front_line: List[FrontLineUnit] = field(default_factory=list)
enemy_front_line: List[FrontLineUnit] = field(default_factory=list)
player_convoy: List[ConvoyUnit] = field(default_factory=list)
enemy_convoy: List[ConvoyUnit] = field(default_factory=list)
player_cargo_ships: List[CargoShip] = field(default_factory=list)
enemy_cargo_ships: List[CargoShip] = field(default_factory=list)
player_airlifts: List[AirliftUnits] = field(default_factory=list)
enemy_airlifts: List[AirliftUnits] = field(default_factory=list)
player_ground_objects: List[GroundObjectUnit] = field(default_factory=list)
enemy_ground_objects: List[GroundObjectUnit] = field(default_factory=list)
@@ -70,6 +88,12 @@ class GroundLosses:
enemy_airfields: List[Airfield] = field(default_factory=list)
@dataclass(frozen=True)
class BaseCaptureEvent:
control_point: ControlPoint
captured_by_player: bool
@dataclass(frozen=True)
class StateData:
#: True if the mission ended. If False, the mission exited abnormally.
@@ -94,16 +118,21 @@ class StateData:
killed_aircraft=data["killed_aircrafts"],
# Airfields emit a new "dead" event every time a bomb is dropped on
# them when they've already dead. Dedup.
killed_ground_units=list(set(data["killed_ground_units"])),
#
# Also normalize dead map objects (which are ints) to strings. The unit map
# only stores strings.
killed_ground_units=list({str(u) for u in data["killed_ground_units"]}),
destroyed_statics=data["destroyed_objects_positions"],
base_capture_events=data["base_capture_events"]
base_capture_events=data["base_capture_events"],
)
class Debriefing:
def __init__(self, state_data: Dict[str, Any], game: Game,
unit_map: UnitMap) -> None:
def __init__(
self, state_data: Dict[str, Any], game: Game, unit_map: UnitMap
) -> None:
self.state_data = StateData.from_json(state_data)
self.game = game
self.unit_map = unit_map
self.player_country = game.player_country
@@ -113,12 +142,28 @@ class Debriefing:
self.air_losses = self.dead_aircraft()
self.ground_losses = self.dead_ground_units()
self.base_captures = self.base_capture_events()
@property
def front_line_losses(self) -> Iterator[FrontLineUnit]:
yield from self.ground_losses.player_front_line
yield from self.ground_losses.enemy_front_line
@property
def convoy_losses(self) -> Iterator[ConvoyUnit]:
yield from self.ground_losses.player_convoy
yield from self.ground_losses.enemy_convoy
@property
def cargo_ship_losses(self) -> Iterator[CargoShip]:
yield from self.ground_losses.player_cargo_ships
yield from self.ground_losses.enemy_cargo_ships
@property
def airlift_losses(self) -> Iterator[AirliftUnits]:
yield from self.ground_losses.player_airlifts
yield from self.ground_losses.enemy_airlifts
@property
def ground_object_losses(self) -> Iterator[GroundObjectUnit]:
yield from self.ground_losses.player_ground_objects
@@ -135,12 +180,9 @@ class Debriefing:
yield from self.ground_losses.enemy_airfields
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[Type[UnitType], int]:
losses_by_type: Dict[Type[UnitType], int] = defaultdict(int)
if player:
losses = self.ground_losses.player_front_line
@@ -150,6 +192,38 @@ class Debriefing:
losses_by_type[loss.unit_type] += 1
return losses_by_type
def convoy_losses_by_type(self, player: bool) -> Dict[Type[UnitType], int]:
losses_by_type: Dict[Type[UnitType], int] = defaultdict(int)
if player:
losses = self.ground_losses.player_convoy
else:
losses = self.ground_losses.enemy_convoy
for loss in losses:
losses_by_type[loss.unit_type] += 1
return losses_by_type
def cargo_ship_losses_by_type(self, player: bool) -> Dict[Type[UnitType], int]:
losses_by_type: Dict[Type[UnitType], int] = defaultdict(int)
if player:
ships = self.ground_losses.player_cargo_ships
else:
ships = self.ground_losses.enemy_cargo_ships
for ship in ships:
for unit_type, count in ship.units.items():
losses_by_type[unit_type] += count
return losses_by_type
def airlift_losses_by_type(self, player: bool) -> Dict[Type[UnitType], int]:
losses_by_type: Dict[Type[UnitType], int] = defaultdict(int)
if player:
losses = self.ground_losses.player_airlifts
else:
losses = self.ground_losses.enemy_airlifts
for loss in losses:
for unit_type in loss.cargo:
losses_by_type[unit_type] += 1
return losses_by_type
def building_losses_by_type(self, player: bool) -> Dict[str, int]:
losses_by_type: Dict[str, int] = defaultdict(int)
if player:
@@ -167,14 +241,14 @@ class Debriefing:
player_losses = []
enemy_losses = []
for unit_name in self.state_data.killed_aircraft:
flight = self.unit_map.flight(unit_name)
if flight is None:
aircraft = self.unit_map.flight(unit_name)
if aircraft is None:
logging.error(f"Could not find Flight matching {unit_name}")
continue
if flight.departure.captured:
player_losses.append(flight)
if aircraft.flight.departure.captured:
player_losses.append(aircraft)
else:
enemy_losses.append(flight)
enemy_losses.append(aircraft)
return AirLosses(player_losses, enemy_losses)
def dead_ground_units(self) -> GroundLosses:
@@ -188,6 +262,22 @@ class Debriefing:
losses.enemy_front_line.append(front_line_unit)
continue
convoy_unit = self.unit_map.convoy_unit(unit_name)
if convoy_unit is not None:
if convoy_unit.convoy.player_owned:
losses.player_convoy.append(convoy_unit)
else:
losses.enemy_convoy.append(convoy_unit)
continue
cargo_ship = self.unit_map.cargo_ship(unit_name)
if cargo_ship is not None:
if cargo_ship.player_owned:
losses.player_cargo_ships.append(cargo_ship)
else:
losses.enemy_cargo_ships.append(cargo_ship)
continue
ground_object_unit = self.unit_map.ground_object_unit(unit_name)
if ground_object_unit is not None:
if ground_object_unit.ground_object.control_point.captured:
@@ -221,28 +311,60 @@ class Debriefing:
# deaths, so we expect to see quite a few unclaimed dead ground
# units. We should start tracking those and covert this to a
# warning.
logging.debug(f"Death of untracked ground unit {unit_name} will "
"have no effect. This may be normal behavior.")
logging.debug(
f"Death of untracked ground unit {unit_name} will "
"have no effect. This may be normal behavior."
)
for unit_name in self.state_data.killed_aircraft:
airlift_unit = self.unit_map.airlift_unit(unit_name)
if airlift_unit is not None:
if airlift_unit.transfer.player:
losses.player_airlifts.append(airlift_unit)
else:
losses.enemy_airlifts.append(airlift_unit)
continue
return losses
@property
def base_capture_events(self):
def base_capture_events(self) -> List[BaseCaptureEvent]:
"""Keeps only the last instance of a base capture event for each base ID."""
reversed_captures = list(reversed(self.state_data.base_capture_events))
last_base_cap_indexes = []
for idx, base in enumerate(i.split("||")[0] for i in reversed_captures):
if base not in [x[1] for x in last_base_cap_indexes]:
last_base_cap_indexes.append((idx, base))
return [reversed_captures[idx[0]] for idx in last_base_cap_indexes]
blue_coalition_id = 2
seen = set()
captures = []
for capture in reversed(self.state_data.base_capture_events):
cp_id_str, new_owner_id_str, _name = capture.split("||")
cp_id = int(cp_id_str)
# Only the most recent capture event matters.
if cp_id in seen:
continue
seen.add(cp_id)
try:
control_point = self.game.theater.find_control_point_by_id(cp_id)
except KeyError:
# Captured base is not a part of the campaign. This happens when neutral
# bases are near the conflict. Nothing to do.
continue
captured_by_player = int(new_owner_id_str) == blue_coalition_id
if control_point.is_friendly(to_player=captured_by_player):
# Base is currently friendly to the new owner. Was captured and
# recaptured in the same mission. Nothing to do.
continue
captures.append(BaseCaptureEvent(control_point, captured_by_player))
return captures
class PollDebriefingFileThread(threading.Thread):
"""Thread class with a stop() method. The thread itself has to check
regularly for the stopped() condition."""
def __init__(self, callback: Callable[[Debriefing], None],
game: Game, unit_map: UnitMap) -> None:
def __init__(
self, callback: Callable[[Debriefing], None], game: Game, unit_map: UnitMap
) -> None:
super().__init__()
self._stop_event = threading.Event()
self.callback = callback
@@ -261,7 +383,10 @@ class PollDebriefingFileThread(threading.Thread):
else:
last_modified = 0
while not self.stopped():
if os.path.isfile("state.json") and os.path.getmtime("state.json") > last_modified:
if (
os.path.isfile("state.json")
and os.path.getmtime("state.json") > last_modified
):
with open("state.json", "r") as json_file:
json_data = json.load(json_file)
debriefing = Debriefing(json_data, self.game, self.unit_map)
@@ -270,8 +395,9 @@ class PollDebriefingFileThread(threading.Thread):
time.sleep(5)
def wait_for_debriefing(callback: Callable[[Debriefing], None],
game: Game, unit_map) -> PollDebriefingFileThread:
def wait_for_debriefing(
callback: Callable[[Debriefing], None], game: Game, unit_map
) -> PollDebriefingFileThread:
thread = PollDebriefingFileThread(callback, game, unit_map)
thread.start()
return thread

View File

@@ -1,12 +1,11 @@
from __future__ import annotations
import logging
import math
from typing import Dict, Iterator, List, TYPE_CHECKING, Tuple, Type
from typing import List, TYPE_CHECKING, Type
from dcs.mapping import Point
from dcs.task import Task
from dcs.unittype import UnitType
from dcs.unittype import VehicleType
from game import persistency
from game.debriefing import AirLosses, Debriefing
@@ -15,7 +14,6 @@ from game.operation.operation import Operation
from game.theater import ControlPoint
from gen import AirTaskingOrder
from gen.ground_forces.combat_stance import CombatStance
from ..db import PRICES
from ..unitmap import UnitMap
if TYPE_CHECKING:
@@ -37,7 +35,15 @@ class Event:
to_cp = None # type: ControlPoint
difficulty = 1 # type: int
def __init__(self, game, from_cp: ControlPoint, target_cp: ControlPoint, location: Point, attacker_name: str, defender_name: str):
def __init__(
self,
game,
from_cp: ControlPoint,
target_cp: ControlPoint,
location: Point,
attacker_name: str,
defender_name: str,
):
self.game = game
self.from_cp = from_cp
self.to_cp = target_cp
@@ -57,12 +63,14 @@ class Event:
Operation.prepare(self.game)
unit_map = Operation.generate()
Operation.current_mission.save(
persistency.mission_path_for("liberation_nextturn.miz"))
persistency.mission_path_for("liberation_nextturn.miz")
)
return unit_map
@staticmethod
def _transfer_aircraft(ato: AirTaskingOrder, losses: AirLosses,
for_player: bool) -> None:
def _transfer_aircraft(
ato: AirTaskingOrder, losses: AirLosses, for_player: bool
) -> None:
for package in ato.packages:
for flight in package.flights:
# No need to transfer to the same location.
@@ -77,13 +85,16 @@ class Event:
if flight.arrival.captured != for_player:
logging.info(
f"Not transferring {flight} because {flight.arrival} "
"was captured")
"was captured"
)
continue
transfer_count = losses.surviving_flight_members(flight)
if transfer_count < 0:
logging.error(f"{flight} had {flight.count} aircraft but "
f"{transfer_count} losses were recorded.")
logging.error(
f"{flight} had {flight.count} aircraft but "
f"{transfer_count} losses were recorded."
)
continue
aircraft = flight.unit_type
@@ -91,7 +102,8 @@ class Event:
if available < transfer_count:
logging.error(
f"Found killed {aircraft} from {flight.departure} but "
f"that airbase has only {available} available.")
f"that airbase has only {available} available."
)
continue
flight.departure.base.aircraft[aircraft] -= transfer_count
@@ -101,26 +113,50 @@ class Event:
flight.arrival.base.aircraft[aircraft] += transfer_count
def complete_aircraft_transfers(self, debriefing: Debriefing) -> None:
self._transfer_aircraft(self.game.blue_ato, debriefing.air_losses,
for_player=True)
self._transfer_aircraft(self.game.red_ato, debriefing.air_losses,
for_player=False)
self._transfer_aircraft(
self.game.blue_ato, debriefing.air_losses, for_player=True
)
self._transfer_aircraft(
self.game.red_ato, debriefing.air_losses, for_player=False
)
@staticmethod
def commit_air_losses(debriefing: Debriefing) -> None:
def commit_air_losses(self, debriefing: Debriefing) -> None:
for loss in debriefing.air_losses.losses:
aircraft = loss.unit_type
cp = loss.departure
if (
not loss.pilot.player
or not self.game.settings.invulnerable_player_pilots
):
loss.pilot.kill()
aircraft = loss.flight.unit_type
cp = loss.flight.departure
available = cp.base.total_units_of_type(aircraft)
if available <= 0:
logging.error(
f"Found killed {aircraft} from {cp} but that airbase has "
"none available.")
"none available."
)
continue
logging.info(f"{aircraft} destroyed from {cp}")
cp.base.aircraft[aircraft] -= 1
@staticmethod
def _commit_pilot_experience(ato: AirTaskingOrder) -> None:
for package in ato.packages:
for flight in package.flights:
for idx, pilot in enumerate(flight.roster.pilots):
if pilot is None:
logging.error(
f"Cannot award experience to pilot #{idx} of {flight} "
"because no pilot is assigned"
)
continue
pilot.record.missions_flown += 1
def commit_pilot_experience(self) -> None:
self._commit_pilot_experience(self.game.blue_ato)
self._commit_pilot_experience(self.game.red_ato)
@staticmethod
def commit_front_line_losses(debriefing: Debriefing) -> None:
for loss in debriefing.front_line_losses:
@@ -130,12 +166,54 @@ class Event:
if available <= 0:
logging.error(
f"Found killed {unit_type} from {control_point} but that "
"airbase has none available.")
"airbase has none available."
)
continue
logging.info(f"{unit_type} destroyed from {control_point}")
control_point.base.armor[unit_type] -= 1
@staticmethod
def commit_convoy_losses(debriefing: Debriefing) -> None:
for loss in debriefing.convoy_losses:
unit_type = loss.unit_type
convoy = loss.convoy
available = loss.convoy.units.get(unit_type, 0)
convoy_name = f"convoy from {convoy.origin} to {convoy.destination}"
if available <= 0:
logging.error(
f"Found killed {unit_type} in {convoy_name} but that convoy has "
"none available."
)
continue
logging.info(f"{unit_type} destroyed in {convoy_name}")
convoy.kill_unit(unit_type)
@staticmethod
def commit_cargo_ship_losses(debriefing: Debriefing) -> None:
for ship in debriefing.cargo_ship_losses:
logging.info(
f"All units destroyed in cargo ship from {ship.origin} to "
f"{ship.destination}."
)
ship.kill_all()
@staticmethod
def commit_airlift_losses(debriefing: Debriefing) -> None:
for loss in debriefing.airlift_losses:
transfer = loss.transfer
airlift_name = f"airlift from {transfer.origin} to {transfer.destination}"
for unit_type in loss.cargo:
try:
transfer.kill_unit(unit_type)
logging.info(f"{unit_type} destroyed in {airlift_name}")
except KeyError:
logging.exception(
f"Found killed {unit_type} in {airlift_name} but that airlift "
"has none available."
)
@staticmethod
def commit_ground_object_losses(debriefing: Debriefing) -> None:
for loss in debriefing.ground_object_losses:
@@ -149,63 +227,55 @@ class Event:
def commit_building_losses(self, debriefing: Debriefing) -> None:
for loss in debriefing.building_losses:
loss.ground_object.kill()
self.game.informations.append(Information(
"Building destroyed",
f"{loss.ground_object.dcs_identifier} has been destroyed at "
f"location {loss.ground_object.obj_name}", self.game.turn
))
self.game.informations.append(
Information(
"Building destroyed",
f"{loss.ground_object.dcs_identifier} has been destroyed at "
f"location {loss.ground_object.obj_name}",
self.game.turn,
)
)
@staticmethod
def commit_damaged_runways(debriefing: Debriefing) -> None:
for damaged_runway in debriefing.damaged_runways:
damaged_runway.damage_runway()
def commit_captures(self, debriefing: Debriefing) -> None:
for captured in debriefing.base_captures:
try:
if captured.captured_by_player:
info = Information(
f"{captured.control_point} captured!",
f"We took control of {captured.control_point}.",
self.game.turn,
)
else:
info = Information(
f"{captured.control_point} lost!",
f"The enemy took control of {captured.control_point}.",
self.game.turn,
)
self.game.informations.append(info)
captured.control_point.capture(self.game, captured.captured_by_player)
logging.info(f"Will run redeploy for {captured.control_point}")
self.redeploy_units(captured.control_point)
except Exception:
logging.exception(f"Could not process base capture {captured}")
def commit(self, debriefing: Debriefing):
logging.info("Committing mission results")
self.commit_air_losses(debriefing)
self.commit_pilot_experience()
self.commit_front_line_losses(debriefing)
self.commit_convoy_losses(debriefing)
self.commit_airlift_losses(debriefing)
self.commit_ground_object_losses(debriefing)
self.commit_building_losses(debriefing)
self.commit_damaged_runways(debriefing)
# ------------------------------
# Captured bases
#if self.game.player_country in db.BLUEFOR_FACTIONS:
coalition = 2 # Value in DCS mission event for BLUE
#else:
# coalition = 1 # Value in DCS mission event for RED
for captured in debriefing.base_capture_events:
try:
id = int(captured.split("||")[0])
new_owner_coalition = int(captured.split("||")[1])
captured_cps = []
for cp in self.game.theater.controlpoints:
if cp.id == id:
if cp.captured and new_owner_coalition != coalition:
for_player = False
info = Information(cp.name + " lost !", "The ennemy took control of " + cp.name + "\nShame on us !", self.game.turn)
self.game.informations.append(info)
captured_cps.append(cp)
elif not(cp.captured) and new_owner_coalition == coalition:
for_player = True
info = Information(cp.name + " captured !", "We took control of " + cp.name + "! Great job !", self.game.turn)
self.game.informations.append(info)
captured_cps.append(cp)
else:
continue
cp.capture(self.game, for_player)
for cp in captured_cps:
logging.info("Will run redeploy for " + cp.name)
self.redeploy_units(cp)
except Exception:
logging.exception(f"Could not process base capture {captured}")
self.commit_captures(debriefing)
self.complete_aircraft_transfers(debriefing)
# Destroyed units carcass
@@ -218,7 +288,12 @@ class Event:
for cp in self.game.theater.player_points():
enemy_cps = [e for e in cp.connected_points if not e.captured]
for enemy_cp in enemy_cps:
print("Compute frontline progression for : " + cp.name + " to " + enemy_cp.name)
print(
"Compute frontline progression for : "
+ cp.name
+ " to "
+ enemy_cp.name
)
delta = 0.0
player_won = True
@@ -234,7 +309,11 @@ class Event:
ratio = (1.0 + enemy_casualties) / (1.0 + ally_casualties)
player_aggresive = cp.stances[enemy_cp.id] in [CombatStance.AGGRESSIVE, CombatStance.ELIMINATION, CombatStance.BREAKTHROUGH]
player_aggresive = cp.stances[enemy_cp.id] in [
CombatStance.AGGRESSIVE,
CombatStance.ELIMINATION,
CombatStance.BREAKTHROUGH,
]
if ally_units_alive == 0:
player_won = False
@@ -259,11 +338,17 @@ class Event:
delta = DEFEAT_INFLUENCE
elif ally_casualties > enemy_casualties:
if ally_units_alive > 2*enemy_units_alive and player_aggresive:
if (
ally_units_alive > 2 * enemy_units_alive
and player_aggresive
):
# Even with casualties if the enemy is overwhelmed, they are going to lose ground
player_won = True
delta = MINOR_DEFEAT_INFLUENCE
elif ally_units_alive > 3*enemy_units_alive and player_aggresive:
elif (
ally_units_alive > 3 * enemy_units_alive
and player_aggresive
):
player_won = True
delta = STRONG_DEFEAT_INFLUENCE
else:
@@ -275,7 +360,10 @@ class Event:
delta = STRONG_DEFEAT_INFLUENCE
# No progress with defensive strategies
if player_won and cp.stances[enemy_cp.id] in [CombatStance.DEFENSIVE, CombatStance.AMBUSH]:
if player_won and cp.stances[enemy_cp.id] in [
CombatStance.DEFENSIVE,
CombatStance.AMBUSH,
]:
print("Defensive stance, progress is limited")
delta = MINOR_DEFEAT_INFLUENCE
@@ -283,117 +371,89 @@ class Event:
print(cp.name + " won ! factor > " + str(delta))
cp.base.affect_strength(delta)
enemy_cp.base.affect_strength(-delta)
info = Information("Frontline Report",
"Our ground forces from " + cp.name + " are making progress toward " + enemy_cp.name,
self.game.turn)
info = Information(
"Frontline Report",
"Our ground forces from "
+ cp.name
+ " are making progress toward "
+ enemy_cp.name,
self.game.turn,
)
self.game.informations.append(info)
else:
print(cp.name + " lost ! factor > " + str(delta))
enemy_cp.base.affect_strength(delta)
cp.base.affect_strength(-delta)
info = Information("Frontline Report",
"Our ground forces from " + cp.name + " are losing ground against the enemy forces from " + enemy_cp.name,
self.game.turn)
info = Information(
"Frontline Report",
"Our ground forces from "
+ cp.name
+ " are losing ground against the enemy forces from "
+ enemy_cp.name,
self.game.turn,
)
self.game.informations.append(info)
def redeploy_units(self, cp):
""""
def redeploy_units(self, cp: ControlPoint) -> None:
""" "
Auto redeploy units to newly captured base
"""
ally_connected_cps = [ocp for ocp in cp.connected_points if cp.captured == ocp.captured]
enemy_connected_cps = [ocp for ocp in cp.connected_points if cp.captured != ocp.captured]
ally_connected_cps = [
ocp for ocp in cp.connected_points if cp.captured == ocp.captured
]
enemy_connected_cps = [
ocp for ocp in cp.connected_points if cp.captured != ocp.captured
]
# If the newly captured cp does not have enemy connected cp,
# then it is not necessary to redeploy frontline units there.
if len(enemy_connected_cps) == 0:
return
# From each ally cp, send reinforcements
for ally_cp in ally_connected_cps:
self.redeploy_between(cp, ally_cp)
def redeploy_between(self, destination: ControlPoint, source: ControlPoint) -> None:
total_units_redeployed = 0
moved_units = {}
if source.has_active_frontline or not destination.captured:
# If there are still active front lines to defend at the
# transferring CP we should not transfer all units.
#
# Opfor also does not transfer all of their units.
# TODO: Balance the CPs rather than moving half from everywhere.
move_factor = 0.5
else:
# From each ally cp, send reinforcements
for ally_cp in ally_connected_cps:
total_units_redeployed = 0
own_enemy_cp = [ocp for ocp in ally_cp.connected_points if ally_cp.captured != ocp.captured]
# Otherwise we can move everything.
move_factor = 1
moved_units = {}
for frontline_unit, count in source.base.armor.items():
moved_units[frontline_unit] = int(count * move_factor)
total_units_redeployed = total_units_redeployed + int(count * move_factor)
# If the connected base, does not have any more enemy cp connected.
# Or if it is not the opponent redeploying forces there (enemy AI will never redeploy all their forces at once)
if len(own_enemy_cp) > 0 or not cp.captured:
for frontline_unit, count in ally_cp.base.armor.items():
moved_units[frontline_unit] = int(count/2)
total_units_redeployed = total_units_redeployed + int(count/2)
else: # So if the old base, does not have any more enemy cp connected, or if it is an enemy base
for frontline_unit, count in ally_cp.base.armor.items():
moved_units[frontline_unit] = count
total_units_redeployed = total_units_redeployed + count
destination.base.commision_units(moved_units)
source.base.commit_losses(moved_units)
cp.base.commision_units(moved_units)
ally_cp.base.commit_losses(moved_units)
if total_units_redeployed > 0:
info = Information("Units redeployed", "", self.game.turn)
info.text = str(total_units_redeployed) + " units have been redeployed from " + ally_cp.name + " to " + cp.name
self.game.informations.append(info)
logging.info(info.text)
class UnitsDeliveryEvent:
def __init__(self, control_point: ControlPoint) -> None:
self.to_cp = control_point
self.units: Dict[Type[UnitType], int] = {}
def __str__(self) -> str:
return "Pending delivery to {}".format(self.to_cp)
def order(self, units: Dict[Type[UnitType], int]) -> None:
for k, v in units.items():
self.units[k] = self.units.get(k, 0) + v
def sell(self, units: Dict[Type[UnitType], int]) -> None:
for k, v in units.items():
self.units[k] = self.units.get(k, 0) - v
def consume_each_order(self) -> Iterator[Tuple[Type[UnitType], int]]:
while self.units:
yield self.units.popitem()
def refund_all(self, game: Game) -> None:
for unit_type, count in self.consume_each_order():
try:
price = PRICES[unit_type]
except KeyError:
logging.error(f"Could not refund {unit_type.id}, price unknown")
# Also transfer pending deliveries.
for unit_type, count in source.pending_unit_deliveries.units.items():
if not issubclass(unit_type, VehicleType):
continue
if count <= 0:
# Don't transfer *sales*...
continue
move_count = int(count * move_factor)
source.pending_unit_deliveries.sell({unit_type: move_count})
destination.pending_unit_deliveries.order({unit_type: move_count})
total_units_redeployed += move_count
logging.info(
f"Refunding {count} {unit_type.id} at {self.to_cp.name}")
game.adjust_budget(price * count, player=self.to_cp.captured)
def available_next_turn(self, unit_type: Type[UnitType]) -> int:
pending_units = self.units.get(unit_type)
if pending_units is None:
pending_units = 0
current_units = self.to_cp.base.total_units_of_type(unit_type)
return pending_units + current_units
def process(self, game: Game) -> None:
bought_units: Dict[Type[UnitType], int] = {}
sold_units: Dict[Type[UnitType], int] = {}
for unit_type, count in self.units.items():
coalition = "Ally" if self.to_cp.captured else "Enemy"
aircraft = unit_type.id
name = self.to_cp.name
if count >= 0:
bought_units[unit_type] = count
game.message(
f"{coalition} reinforcements: {aircraft} x {count} at {name}")
else:
sold_units[unit_type] = -count
game.message(
f"{coalition} sold: {aircraft} x {-count} at {name}")
self.to_cp.base.commision_units(bought_units)
self.to_cp.base.commit_losses(sold_units)
self.units = {}
bought_units = {}
sold_units = {}
if total_units_redeployed > 0:
text = (
f"{total_units_redeployed} units have been redeployed from "
f"{source.name} to {destination.name}"
)
info = Information("Units redeployed", text, self.game.turn)
self.game.informations.append(info)
logging.info(text)

View File

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

View File

@@ -1,4 +1,5 @@
from __future__ import annotations
from game.data.groundunitclass import GroundUnitClass
import logging
from dataclasses import dataclass, field
@@ -10,13 +11,26 @@ from dcs.planes import plane_map
from dcs.unittype import FlyingType, ShipType, VehicleType, UnitType
from dcs.vehicles import Armor, Unarmed, Infantry, Artillery, AirDefence
from game.data.building_data import WW2_ALLIES_BUILDINGS, DEFAULT_AVAILABLE_BUILDINGS, WW2_GERMANY_BUILDINGS, WW2_FREE
from game.data.doctrine import Doctrine, MODERN_DOCTRINE, COLDWAR_DOCTRINE, WWII_DOCTRINE
from game.data.building_data import (
WW2_ALLIES_BUILDINGS,
DEFAULT_AVAILABLE_BUILDINGS,
WW2_GERMANY_BUILDINGS,
WW2_FREE,
)
from game.data.doctrine import (
Doctrine,
MODERN_DOCTRINE,
COLDWAR_DOCTRINE,
WWII_DOCTRINE,
)
from pydcs_extensions.mod_units import MODDED_VEHICLES, MODDED_AIRPLANES
@dataclass
class Faction:
#: List of locales to use when generating random names. If not set, Faker will
#: choose the default locale.
locales: Optional[List[str]]
# Country used by this faction
country: str = field(default="")
@@ -60,6 +74,9 @@ class Faction:
# Possible Missile site generators for this faction
missiles: List[str] = field(default_factory=list)
# Possible costal site generators for this faction
coastal_defenses: List[str] = field(default_factory=list)
# Required mods or asset packs
requirements: Dict[str, str] = field(default_factory=dict)
@@ -90,6 +107,9 @@ class Faction:
# How many missiles group should we try to generate per CP on startup for this faction
missiles_group_count: int = field(default=1)
# How many coastal group should we try to generate per CP on startup for this faction
coastal_group_count: int = field(default=1)
# Whether this faction has JTAC access
has_jtac: bool = field(default=False)
@@ -103,8 +123,7 @@ class Faction:
building_set: List[str] = field(default_factory=list)
# List of default livery overrides
liveries_overrides: Dict[Type[UnitType], List[str]] = field(
default_factory=dict)
liveries_overrides: Dict[Type[UnitType], List[str]] = field(default_factory=dict)
#: Set to True if the faction should force the "Unrestricted satnav" option
#: for the mission. This option enables GPS for capable aircraft regardless
@@ -115,14 +134,27 @@ class Faction:
#: both will use it.
unrestricted_satnav: bool = False
def has_access_to_unittype(self, unitclass: GroundUnitClass) -> bool:
has_access = False
for vehicle in unitclass.unit_list:
if vehicle in self.frontline_units:
return True
if vehicle in self.artillery_units:
return True
return has_access
@classmethod
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
faction = Faction()
faction = Faction(locales=json.get("locales"))
faction.country = json.get("country", "/")
if faction.country not in [c.name for c in country_dict.values()]:
raise AssertionError("Faction's country (\"{}\") is not a valid DCS country ID".format(faction.country))
raise AssertionError(
'Faction\'s country ("{}") is not a valid DCS country ID'.format(
faction.country
)
)
faction.name = json.get("name", "")
if not faction.name:
@@ -135,14 +167,12 @@ class Faction:
faction.awacs = load_all_aircraft(json.get("awacs", []))
faction.tankers = load_all_aircraft(json.get("tankers", []))
faction.frontline_units = load_all_vehicles(
json.get("frontline_units", []))
faction.artillery_units = load_all_vehicles(
json.get("artillery_units", []))
faction.infantry_units = load_all_vehicles(
json.get("infantry_units", []))
faction.logistics_units = load_all_vehicles(
json.get("logistics_units", []))
faction.aircrafts = list(set(faction.aircrafts + faction.awacs))
faction.frontline_units = load_all_vehicles(json.get("frontline_units", []))
faction.artillery_units = load_all_vehicles(json.get("artillery_units", []))
faction.infantry_units = load_all_vehicles(json.get("infantry_units", []))
faction.logistics_units = load_all_vehicles(json.get("logistics_units", []))
faction.ewrs = json.get("ewrs", [])
@@ -153,16 +183,14 @@ class Faction:
faction.air_defenses.extend(json.get("shorads", []))
faction.missiles = json.get("missiles", [])
faction.coastal_defenses = json.get("coastal_defenses", [])
faction.requirements = json.get("requirements", {})
faction.carrier_names = json.get("carrier_names", [])
faction.helicopter_carrier_names = json.get(
"helicopter_carrier_names", [])
faction.helicopter_carrier_names = json.get("helicopter_carrier_names", [])
faction.navy_generators = json.get("navy_generators", [])
faction.aircraft_carrier = load_all_ships(
json.get("aircraft_carrier", []))
faction.helicopter_carrier = load_all_ships(
json.get("helicopter_carrier", []))
faction.aircraft_carrier = load_all_ships(json.get("aircraft_carrier", []))
faction.helicopter_carrier = load_all_ships(json.get("helicopter_carrier", []))
faction.destroyers = load_all_ships(json.get("destroyers", []))
faction.cruisers = load_all_ships(json.get("cruisers", []))
faction.has_jtac = json.get("has_jtac", False)
@@ -173,6 +201,7 @@ class Faction:
faction.jtac_unit = None
faction.navy_group_count = int(json.get("navy_group_count", 1))
faction.missiles_group_count = int(json.get("missiles_group_count", 0))
faction.coastal_group_count = int(json.get("coastal_group_count", 0))
# Load doctrine
doctrine = json.get("doctrine", "modern")
@@ -212,13 +241,18 @@ class Faction:
@property
def units(self) -> List[Type[UnitType]]:
return (self.infantry_units + self.aircrafts + self.awacs +
self.artillery_units + self.frontline_units +
self.tankers + self.logistics_units)
return (
self.infantry_units
+ self.aircrafts
+ self.awacs
+ self.artillery_units
+ self.frontline_units
+ self.tankers
+ self.logistics_units
)
def unit_loader(
unit: str, class_repository: List[Any]) -> Optional[Type[UnitType]]:
def unit_loader(unit: str, class_repository: List[Any]) -> Optional[Type[UnitType]]:
"""
Find unit by name
:param unit: Unit name as string
@@ -242,9 +276,10 @@ def unit_loader(
def load_aircraft(name: str) -> Optional[Type[FlyingType]]:
return cast(Optional[FlyingType], unit_loader(
name, [dcs.planes, dcs.helicopters, MODDED_AIRPLANES]
))
return cast(
Optional[FlyingType],
unit_loader(name, [dcs.planes, dcs.helicopters, MODDED_AIRPLANES]),
)
def load_all_aircraft(data) -> List[Type[FlyingType]]:
@@ -257,9 +292,12 @@ def load_all_aircraft(data) -> List[Type[FlyingType]]:
def load_vehicle(name: str) -> Optional[Type[VehicleType]]:
return cast(Optional[FlyingType], unit_loader(
name, [Infantry, Unarmed, Armor, AirDefence, Artillery, MODDED_VEHICLES]
))
return cast(
Optional[FlyingType],
unit_loader(
name, [Infantry, Unarmed, Armor, AirDefence, Artillery, MODDED_VEHICLES]
),
)
def load_all_vehicles(data) -> List[Type[VehicleType]]:

View File

@@ -2,8 +2,9 @@ from __future__ import annotations
import json
import logging
from pathlib import Path
from typing import Dict, Iterator, Optional, Type
from typing import Dict, Iterator, List, Optional, Type
from game import persistency
from game.factions.faction import Faction
FACTION_DIRECTORY = Path("./resources/factions/")
@@ -23,15 +24,22 @@ class FactionLoader:
if self._factions is None:
self._factions = self.load_factions()
@staticmethod
def find_faction_files_in(path: Path) -> List[Path]:
return [f for f in path.glob("*.json") if f.is_file()]
@classmethod
def load_factions(cls: Type[FactionLoader]) -> Dict[str, Faction]:
files = [f for f in FACTION_DIRECTORY.glob("*.json") if f.is_file()]
user_faction_path = Path(persistency.base_path()) / "Liberation/Factions"
files = cls.find_faction_files_in(
FACTION_DIRECTORY
) + cls.find_faction_files_in(user_faction_path)
factions = {}
for f in files:
try:
with f.open("r", encoding="utf-8") as fdata:
data = json.load(fdata, encoding="utf-8")
data = json.load(fdata)
factions[data["name"]] = Faction.from_json(data)
logging.info("Loaded faction : " + str(f))
except Exception:

View File

@@ -4,17 +4,19 @@ import random
import sys
from datetime import date, datetime, timedelta
from enum import Enum
from typing import Any, Dict, List
from typing import Any, Dict, List, Iterator
from dcs.action import Coalition
from dcs.mapping import Point
from dcs.task import CAP, CAS, PinpointStrike
from dcs.vehicles import AirDefence
from faker import Faker
from game import db
from game.inventory import GlobalAircraftInventory
from game.models.game_stats import GameStats
from game.plugins import LuaPluginManager
from game.theater.theatergroundobject import MissileSiteGroundObject
from gen.ato import AirTaskingOrder
from gen.conflictgen import Conflict
from gen.flights.ai_flight_planner import CoalitionMissionPlanner
@@ -23,17 +25,21 @@ from gen.flights.flight import FlightType
from gen.ground_forces.ai_ground_planner import GroundPlanner
from . import persistency
from .debriefing import Debriefing
from .event.event import Event, UnitsDeliveryEvent
from .event.event import Event
from .event.frontlineattack import FrontlineAttackEvent
from .factions.faction import Faction
from .income import Income
from .infos.information import Information
from .navmesh import NavMesh
from .procurement import ProcurementAi
from .settings import Settings
from .theater import ConflictTheater, ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import MissileSiteGroundObject
from .procurement import AircraftProcurementRequest, ProcurementAi
from .profiling import logged_duration
from .settings import Settings, AutoAtoBehavior
from .squadrons import Pilot, AirWing
from .theater import ConflictTheater
from .theater.bullseye import Bullseye
from .theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
from .threatzones import ThreatZones
from .transfers import PendingTransfers
from .unitmap import UnitMap
from .weather import Conditions, TimeOfDay
@@ -78,10 +84,16 @@ class TurnState(Enum):
class Game:
def __init__(self, player_name: str, enemy_name: str,
theater: ConflictTheater, start_date: datetime,
settings: Settings, player_budget: float,
enemy_budget: float) -> None:
def __init__(
self,
player_name: str,
enemy_name: str,
theater: ConflictTheater,
start_date: datetime,
settings: Settings,
player_budget: float,
enemy_budget: float,
) -> None:
self.settings = settings
self.events: List[Event] = []
self.theater = theater
@@ -89,14 +101,18 @@ class Game:
self.player_country = db.FACTIONS[player_name].country
self.enemy_name = enemy_name
self.enemy_country = db.FACTIONS[enemy_name].country
self.turn = 0
# pass_turn() will be called when initialization is complete which will
# increment this to turn 0 before it reaches the player.
self.turn = -1
# NB: This is the *start* date. It is never updated.
self.date = date(start_date.year, start_date.month, start_date.day)
self.game_stats = GameStats()
self.game_stats.update(self)
self.ground_planners: Dict[int, GroundPlanner] = {}
self.informations = []
self.informations.append(Information("Game Start", "-" * 40, 0))
self.__culling_points: List[Point] = []
# Culling Zones are for areas around points of interest that contain things we may not wish to cull.
self.__culling_zones: List[Point] = []
self.__destroyed_units: List[str] = []
self.savepath = ""
self.budget = player_budget
@@ -106,27 +122,31 @@ class Game:
self.conditions = self.generate_conditions()
self.blue_transit_network = TransitNetwork()
self.red_transit_network = TransitNetwork()
self.blue_procurement_requests: List[AircraftProcurementRequest] = []
self.red_procurement_requests: List[AircraftProcurementRequest] = []
self.blue_ato = AirTaskingOrder()
self.red_ato = AirTaskingOrder()
self.aircraft_inventory = GlobalAircraftInventory(
self.theater.controlpoints
)
self.blue_bullseye = Bullseye(Point(0, 0))
self.red_bullseye = Bullseye(Point(0, 0))
self.aircraft_inventory = GlobalAircraftInventory(self.theater.controlpoints)
self.transfers = PendingTransfers(self)
self.sanitize_sides()
self.on_load()
self.blue_faker = Faker(self.player_faction.locales)
self.red_faker = Faker(self.enemy_faction.locales)
# Turn 0 procurement. We don't actually have any missions to plan, but
# the planner will tell us what it would like to plan so we can use that
# to drive purchase decisions.
blue_planner = CoalitionMissionPlanner(self, is_player=True)
blue_planner.plan_missions()
self.blue_air_wing = AirWing(self, player=True)
self.red_air_wing = AirWing(self, player=False)
red_planner = CoalitionMissionPlanner(self, is_player=False)
red_planner.plan_missions()
self.plan_procurement(blue_planner, red_planner)
self.on_load(game_still_initializing=True)
def __getstate__(self) -> Dict[str, Any]:
state = self.__dict__.copy()
@@ -136,6 +156,8 @@ class Game:
del state["red_threat_zone"]
del state["blue_navmesh"]
del state["red_navmesh"]
del state["blue_faker"]
del state["red_faker"]
return state
def __setstate__(self, state: Dict[str, Any]) -> None:
@@ -143,9 +165,27 @@ class Game:
# Regenerate any state that was not persisted.
self.on_load()
def ato_for(self, player: bool) -> AirTaskingOrder:
if player:
return self.blue_ato
return self.red_ato
def procurement_requests_for(
self, player: bool
) -> List[AircraftProcurementRequest]:
if player:
return self.blue_procurement_requests
return self.red_procurement_requests
def transit_network_for(self, player: bool) -> TransitNetwork:
if player:
return self.blue_transit_network
return self.red_transit_network
def generate_conditions(self) -> Conditions:
return Conditions.generate(self.theater, self.date,
self.current_turn_time_of_day, self.settings)
return Conditions.generate(
self.theater, self.current_day, self.current_turn_time_of_day, self.settings
)
def sanitize_sides(self):
"""
@@ -173,6 +213,26 @@ class Game:
return self.player_faction
return self.enemy_faction
def faker_for(self, player: bool) -> Faker:
if player:
return self.blue_faker
return self.red_faker
def air_wing_for(self, player: bool) -> AirWing:
if player:
return self.blue_air_wing
return self.red_air_wing
def country_for(self, player: bool) -> str:
if player:
return self.player_country
return self.enemy_country
def bullseye_for(self, player: bool) -> Bullseye:
if player:
return self.blue_bullseye
return self.red_bullseye
def _roll(self, prob, mult):
if self.settings.version == "dev":
# always generate all events for dev
@@ -181,13 +241,24 @@ class Game:
return random.randint(1, 100) <= prob * mult
def _generate_player_event(self, event_class, player_cp, enemy_cp):
self.events.append(event_class(self, player_cp, enemy_cp, enemy_cp.position, self.player_name, self.enemy_name))
self.events.append(
event_class(
self,
player_cp,
enemy_cp,
enemy_cp.position,
self.player_name,
self.enemy_name,
)
)
def _generate_events(self):
for front_line in self.theater.conflicts(True):
self._generate_player_event(FrontlineAttackEvent,
front_line.control_point_a,
front_line.control_point_b)
for front_line in self.theater.conflicts():
self._generate_player_event(
FrontlineAttackEvent,
front_line.blue_cp,
front_line.red_cp,
)
def adjust_budget(self, amount: float, player: bool) -> None:
if player:
@@ -205,7 +276,7 @@ class Game:
self.enemy_budget += Income(self, player=False).total
def initiate_event(self, event: Event) -> UnitMap:
#assert event in self.events
# assert event in self.events
logging.info("Generating {} (regular)".format(event))
return event.generate()
@@ -220,29 +291,48 @@ class Game:
def is_player_attack(self, event):
if isinstance(event, Event):
return event and event.attacker_name and event.attacker_name == self.player_name
return (
event
and event.attacker_name
and event.attacker_name == self.player_name
)
else:
raise RuntimeError(f"{event} was passed when an Event type was expected")
def on_load(self) -> None:
def on_load(self, game_still_initializing: bool = False) -> None:
LuaPluginManager.load_settings(self.settings)
ObjectiveDistanceCache.set_theater(self.theater)
self.compute_conflicts_position()
self.compute_threat_zones()
if not game_still_initializing:
self.compute_threat_zones()
self.blue_faker = Faker(self.faction_for(player=True).locales)
self.red_faker = Faker(self.faction_for(player=False).locales)
def pass_turn(self, no_action: bool = False) -> None:
logging.info("Pass turn")
self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0))
def reset_ato(self) -> None:
self.blue_ato.clear()
self.red_ato.clear()
def finish_turn(self, skipped: bool = False) -> None:
self.informations.append(
Information("End of turn #" + str(self.turn), "-" * 40, 0)
)
self.turn += 1
# Need to recompute before transfers and deliveries to account for captures.
# This happens in in initialize_turn as well, because cheating doesn't advance a
# turn but can capture bases so we need to recompute there as well.
self.compute_transit_networks()
# Must happen *before* unit deliveries are handled, or else new units will spawn
# one hop ahead. ControlPoint.process_turn handles unit deliveries.
self.transfers.perform_transfers()
# Needs to happen *before* planning transfers so we don't cancel the
self.reset_ato()
for control_point in self.theater.controlpoints:
control_point.process_turn(self)
self.process_enemy_income()
self.process_player_income()
if not no_action and self.turn > 1:
if not skipped and self.turn > 1:
for cp in self.theater.player_points():
cp.base.affect_strength(+PLAYER_BASE_STRENGTH_RECOVERY)
else:
@@ -252,47 +342,88 @@ class Game:
self.conditions = self.generate_conditions()
self.process_enemy_income()
self.process_player_income()
def begin_turn_0(self) -> None:
self.turn = 0
self.initialize_turn()
def pass_turn(self, no_action: bool = False) -> None:
logging.info("Pass turn")
with logged_duration("Turn finalization"):
self.finish_turn(no_action)
with logged_duration("Turn initialization"):
self.initialize_turn()
# Autosave progress
persistency.autosave(self)
def check_win_loss(self):
captured_states = {i.captured for i in self.theater.controlpoints}
if True not in captured_states:
player_airbases = {
cp for cp in self.theater.player_points() if cp.runway_is_operational()
}
if not player_airbases:
return TurnState.LOSS
if False not in captured_states:
enemy_airbases = {
cp for cp in self.theater.enemy_points() if cp.runway_is_operational()
}
if not enemy_airbases:
return TurnState.WIN
return TurnState.CONTINUE
def set_bullseye(self) -> None:
player_cp, enemy_cp = self.theater.closest_opposing_control_points()
self.blue_bullseye = Bullseye(enemy_cp.position)
self.red_bullseye = Bullseye(player_cp.position)
def initialize_turn(self) -> None:
self.events = []
self._generate_events()
self.set_bullseye()
# Update statistics
self.game_stats.update(self)
self.blue_air_wing.reset()
self.red_air_wing.reset()
self.aircraft_inventory.reset()
for cp in self.theater.controlpoints:
self.aircraft_inventory.set_from_control_point(cp)
# Check for win or loss condition
turn_state = self.check_win_loss()
if turn_state in (TurnState.LOSS,TurnState.WIN):
if turn_state in (TurnState.LOSS, TurnState.WIN):
return self.process_win_loss(turn_state)
# Plan flights & combat for next turn
self.compute_conflicts_position()
self.compute_threat_zones()
with logged_duration("Computing conflict positions"):
self.compute_conflicts_position()
with logged_duration("Threat zone computation"):
self.compute_threat_zones()
with logged_duration("Transit network identification"):
self.compute_transit_networks()
self.ground_planners = {}
self.blue_ato.clear()
self.red_ato.clear()
blue_planner = CoalitionMissionPlanner(self, is_player=True)
blue_planner.plan_missions()
self.blue_procurement_requests.clear()
self.red_procurement_requests.clear()
red_planner = CoalitionMissionPlanner(self, is_player=False)
red_planner.plan_missions()
with logged_duration("Procurement of airlift assets"):
self.transfers.order_airlift_assets()
with logged_duration("Transport planning"):
self.transfers.plan_transports()
with logged_duration("Blue mission planning"):
if self.settings.auto_ato_behavior is not AutoAtoBehavior.Disabled:
blue_planner = CoalitionMissionPlanner(self, is_player=True)
blue_planner.plan_missions()
with logged_duration("Red mission planning"):
red_planner = CoalitionMissionPlanner(self, is_player=False)
red_planner.plan_missions()
for cp in self.theater.controlpoints:
if cp.has_frontline:
@@ -300,16 +431,15 @@ class Game:
gplanner.plan_groundwar()
self.ground_planners[cp.id] = gplanner
self.plan_procurement(blue_planner, red_planner)
self.plan_procurement()
def plan_procurement(self, blue_planner: CoalitionMissionPlanner,
red_planner: CoalitionMissionPlanner) -> None:
def plan_procurement(self) -> None:
# The first turn needs to buy a *lot* of aircraft to fill CAPs, so it
# gets much more of the budget that turn. Otherwise budget (after
# repairs) is split evenly between air and ground. For the default
# starting budget of 2000 this gives 600 to ground forces and 1400 to
# aircraft.
ground_portion = 0.3 if self.turn == 0 else 0.5
# aircraft. After that the budget will be spend proportionally based on how much is already invested
self.budget = ProcurementAi(
self,
for_player=True,
@@ -317,8 +447,7 @@ class Game:
manage_runways=self.settings.automate_runway_repair,
manage_front_line=self.settings.automate_front_line_reinforcements,
manage_aircraft=self.settings.automate_aircraft_reinforcements,
front_line_budget_share=ground_portion
).spend_budget(self.budget, blue_planner.procurement_requests)
).spend_budget(self.budget)
self.enemy_budget = ProcurementAi(
self,
@@ -327,8 +456,7 @@ class Game:
manage_runways=True,
manage_front_line=True,
manage_aircraft=True,
front_line_budget_share=ground_portion
).spend_budget(self.enemy_budget, red_planner.procurement_requests)
).spend_budget(self.enemy_budget)
def message(self, text: str) -> None:
self.informations.append(Information(text, turn=self.turn))
@@ -355,13 +483,22 @@ class Game:
self.current_group_id += 1
return self.current_group_id
def compute_transit_networks(self) -> None:
self.blue_transit_network = self.compute_transit_network_for(player=True)
self.red_transit_network = self.compute_transit_network_for(player=False)
def compute_transit_network_for(self, player: bool) -> TransitNetwork:
return TransitNetworkBuilder(self.theater, player).build()
def compute_threat_zones(self) -> None:
self.blue_threat_zone = ThreatZones.for_faction(self, player=True)
self.red_threat_zone = ThreatZones.for_faction(self, player=False)
self.blue_navmesh = NavMesh.from_threat_zones(self.red_threat_zone,
self.theater)
self.red_navmesh = NavMesh.from_threat_zones(self.blue_threat_zone,
self.theater)
self.blue_navmesh = NavMesh.from_threat_zones(
self.red_threat_zone, self.theater
)
self.red_navmesh = NavMesh.from_threat_zones(
self.blue_threat_zone, self.theater
)
def threat_zone_for(self, player: bool) -> ThreatZones:
if player:
@@ -378,30 +515,23 @@ class Game:
Compute the current conflict center position(s), mainly used for culling calculation
:return: List of points of interests
"""
points = []
zones = []
# By default, use the existing frontline conflict position
for front_line in self.theater.conflicts():
position = Conflict.frontline_position(front_line.control_point_a,
front_line.control_point_b,
self.theater)
points.append(position[0])
points.append(front_line.control_point_a.position)
points.append(front_line.control_point_b.position)
position = Conflict.frontline_position(front_line, self.theater)
zones.append(position[0])
zones.append(front_line.blue_cp.position)
zones.append(front_line.red_cp.position)
for cp in self.theater.controlpoints:
# Don't cull missile sites - their range is long enough to make them
# easily culled despite being a threat.
for tgo in cp.ground_objects:
if isinstance(tgo, MissileSiteGroundObject):
points.append(cp.position)
# If do_not_cull_carrier is enabled, add carriers as culling point
if self.settings.perf_do_not_cull_carrier:
if cp.is_carrier or cp.is_lha:
points.append(cp.position)
zones.append(cp.position)
# If there is no conflict take the center point between the two nearest opposing bases
if len(points) == 0:
if len(zones) == 0:
cpoint = None
min_distance = sys.maxsize
for cp in self.theater.player_points():
@@ -409,17 +539,19 @@ class Game:
d = cp.position.distance_to_point(cp2.position)
if d < min_distance:
min_distance = d
cpoint = Point((cp.position.x + cp2.position.x) / 2, (cp.position.y + cp2.position.y) / 2)
points.append(cp.position)
points.append(cp2.position)
cpoint = Point(
(cp.position.x + cp2.position.x) / 2,
(cp.position.y + cp2.position.y) / 2,
)
zones.append(cp.position)
zones.append(cp2.position)
break
if cpoint is not None:
break
if cpoint is not None:
points.append(cpoint)
zones.append(cpoint)
packages = itertools.chain(self.blue_ato.packages,
self.red_ato.packages)
packages = itertools.chain(self.blue_ato.packages, self.red_ato.packages)
for package in packages:
if package.primary_task is FlightType.BARCAP:
# BARCAPs will be planned at most locations on smaller theaters,
@@ -428,14 +560,14 @@ class Game:
# are only interesting if there are enemies in the area, and if
# there are they won't be culled because of the enemy's mission.
continue
points.append(package.target.position)
zones.append(package.target.position)
# Else 0,0, since we need a default value
# (in this case this means the whole map is owned by the same player, so it is not an issue)
if len(points) == 0:
points.append(Point(0, 0))
if len(zones) == 0:
zones.append(Point(0, 0))
self.__culling_points = points
self.__culling_zones = zones
def add_destroyed_units(self, data):
pos = Point(data["x"], data["z"])
@@ -451,20 +583,19 @@ class Game:
:param pos: Position you are tryng to spawn stuff at
:return: True if units can not be added at given position
"""
if self.settings.perf_culling == False:
if not self.settings.perf_culling:
return False
else:
for c in self.__culling_points:
if c.distance_to_point(pos) < self.settings.perf_culling_distance * 1000:
return False
return True
for z in self.__culling_zones:
if z.distance_to_point(pos) < self.settings.perf_culling_distance * 1000:
return False
return True
def get_culling_points(self):
def get_culling_zones(self):
"""
Check culling points
:return: List of culling points
:return: List of culling zones
"""
return self.__culling_points
return self.__culling_zones
# 1 = red, 2 = blue
def get_player_coalition_id(self):
@@ -487,6 +618,10 @@ class Game:
def process_win_loss(self, turn_state: TurnState):
if turn_state is TurnState.WIN:
return self.message("Congratulations, you are victorious! Start a new campaign to continue.")
return self.message(
"Congratulations, you are victorious! Start a new campaign to continue."
)
elif turn_state is TurnState.LOSS:
return self.message("Game Over, you lose. Start a new campaign to continue.")
return self.message(
"Game Over, you lose. Start a new campaign to continue."
)

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +0,0 @@
from game.theater import ControlPoint
class FrontlineData:
"""
This Data structure will store information about an existing frontline
"""
def __init__(self, from_cp:ControlPoint, to_cp: ControlPoint):
self.to_cp = to_cp
self.from_cp = from_cp
self.enemy_units_position = []
self.blue_units_position = []

View File

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

View File

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

View File

@@ -1,10 +1,9 @@
from __future__ import annotations
from game.theater.theatergroundobject import TheaterGroundObject
import logging
import os
from pathlib import Path
from typing import TYPE_CHECKING, Iterable, List, Optional, Set
from typing import Iterable, List, Set, TYPE_CHECKING
from dcs import Mission
from dcs.action import DoScript, DoScriptFile
@@ -14,7 +13,9 @@ from dcs.lua.parse import loads
from dcs.mapping import Point
from dcs.translation import String
from dcs.triggers import TriggerStart
from game.plugins import LuaPluginManager
from game.theater.theatergroundobject import TheaterGroundObject
from gen import Conflict, FlightType, VisualGenerator
from gen.aircraft import AIRCRAFT_DATA, AircraftConflictGenerator, FlightData
from gen.airfields import AIRFIELD_DATA
@@ -22,6 +23,8 @@ from gen.airsupportgen import AirSupport, AirSupportConflictGenerator
from gen.armor import GroundConflictGenerator, JtacInfo
from gen.beacons import load_beacons_for_terrain
from gen.briefinggen import BriefingGenerator, MissionInfoGenerator
from gen.cargoshipgen import CargoShipGenerator
from gen.convoygen import ConvoyGenerator
from gen.environmentgen import EnvironmentGenerator
from gen.forcedoptionsgen import ForcedOptionsGenerator
from gen.groundobjectsgen import GroundObjectsGenerator
@@ -30,9 +33,8 @@ from gen.naming import namegen
from gen.radios import RadioFrequency, RadioRegistry
from gen.tacan import TacanRegistry
from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator
from .. import db
from ..theater import Airfield
from ..theater import Airfield, FrontLine
from ..unitmap import UnitMap
if TYPE_CHECKING:
@@ -41,18 +43,14 @@ if TYPE_CHECKING:
class Operation:
"""Static class for managing the final Mission generation"""
current_mission = None # type: Mission
airgen = None # type: AircraftConflictGenerator
triggersgen = None # type: TriggersGenerator
airsupportgen = None # type: AirSupportConflictGenerator
visualgen = None # type: VisualGenerator
groundobjectgen = None # type: GroundObjectsGenerator
briefinggen = None # type: BriefingGenerator
forcedoptionsgen = None # type: ForcedOptionsGenerator
radio_registry: Optional[RadioRegistry] = None
tacan_registry: Optional[TacanRegistry] = None
game = None # type: Game
environment_settings = None
current_mission: Mission
airgen: AircraftConflictGenerator
airsupportgen: AirSupportConflictGenerator
groundobjectgen: GroundObjectsGenerator
radio_registry: RadioRegistry
tacan_registry: TacanRegistry
game: Game
trigger_radius = TRIGGER_RADIUS_MEDIUM
is_quick = None
player_awacs_enabled = True
@@ -78,13 +76,12 @@ class Operation:
for frontline in cls.game.theater.conflicts():
yield Conflict(
cls.game.theater,
frontline.control_point_a,
frontline.control_point_b,
frontline,
cls.game.player_name,
cls.game.enemy_name,
cls.game.player_country,
cls.game.enemy_country,
frontline.position
frontline.position,
)
@classmethod
@@ -93,17 +90,16 @@ class Operation:
player_cp, enemy_cp = cls.game.theater.closest_opposing_control_points()
mid_point = player_cp.position.point_from_heading(
player_cp.position.heading_between_point(enemy_cp.position),
player_cp.position.distance_to_point(enemy_cp.position) / 2
player_cp.position.distance_to_point(enemy_cp.position) / 2,
)
return Conflict(
cls.game.theater,
player_cp,
enemy_cp,
FrontLine(player_cp, enemy_cp),
cls.game.player_name,
cls.game.enemy_name,
cls.game.player_country,
cls.game.enemy_country,
mid_point
mid_point,
)
@classmethod
@@ -112,15 +108,21 @@ class Operation:
@classmethod
def _setup_mission_coalitions(cls):
cls.current_mission.coalition["blue"] = Coalition("blue")
cls.current_mission.coalition["red"] = Coalition("red")
cls.current_mission.coalition["blue"] = Coalition(
"blue", bullseye=cls.game.blue_bullseye.to_pydcs()
)
cls.current_mission.coalition["red"] = Coalition(
"red", bullseye=cls.game.red_bullseye.to_pydcs()
)
p_country = cls.game.player_country
e_country = cls.game.enemy_country
cls.current_mission.coalition["blue"].add_country(
country_dict[db.country_id_from_name(p_country)]())
country_dict[db.country_id_from_name(p_country)]()
)
cls.current_mission.coalition["red"].add_country(
country_dict[db.country_id_from_name(e_country)]())
country_dict[db.country_id_from_name(e_country)]()
)
@classmethod
def inject_lua_trigger(cls, contents: str, comment: str) -> None:
@@ -133,12 +135,11 @@ class Operation:
cls.plugin_scripts.append(mnemonic)
@classmethod
def inject_plugin_script(cls, plugin_mnemonic: str, script: str,
script_mnemonic: str) -> None:
def inject_plugin_script(
cls, plugin_mnemonic: str, script: str, script_mnemonic: str
) -> None:
if script_mnemonic in cls.plugin_scripts:
logging.debug(
f"Skipping already loaded {script} for {plugin_mnemonic}"
)
logging.debug(f"Skipping already loaded {script} for {plugin_mnemonic}")
else:
cls.plugin_scripts.append(script_mnemonic)
@@ -146,15 +147,12 @@ class Operation:
script_path = Path(plugin_path, script)
if not script_path.exists():
logging.error(
f"Cannot find {script_path} for plugin {plugin_mnemonic}"
)
logging.error(f"Cannot find {script_path} for plugin {plugin_mnemonic}")
return
trigger = TriggerStart(comment=f"Load {script_mnemonic}")
filename = script_path.resolve()
fileref = cls.current_mission.map_resource.add_resource_file(
filename)
fileref = cls.current_mission.map_resource.add_resource_file(filename)
trigger.add_action(DoScriptFile(fileref))
cls.current_mission.triggerrules.triggers.append(trigger)
@@ -166,25 +164,27 @@ class Operation:
jtacs: List[JtacInfo],
airgen: AircraftConflictGenerator,
):
"""Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings)
"""
"""Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings)"""
gens: List[MissionInfoGenerator] = [
KneeboardGenerator(cls.current_mission, cls.game),
BriefingGenerator(cls.current_mission, cls.game)
BriefingGenerator(cls.current_mission, cls.game),
]
for gen in gens:
for dynamic_runway in groundobjectgen.runways.values():
gen.add_dynamic_runway(dynamic_runway)
for tanker in airsupportgen.air_support.tankers:
gen.add_tanker(tanker)
if tanker.blue:
gen.add_tanker(tanker)
if cls.player_awacs_enabled:
for awacs in airsupportgen.air_support.awacs:
gen.add_awacs(awacs)
for aewc in airsupportgen.air_support.awacs:
if aewc.blue:
gen.add_awacs(aewc)
for jtac in jtacs:
gen.add_jtac(jtac)
if jtac.blue:
gen.add_jtac(jtac)
for flight in airgen.flights:
gen.add_flight(flight)
@@ -208,8 +208,9 @@ class Operation:
cls.radio_registry.reserve(frequency)
@classmethod
def assign_channels_to_flights(cls, flights: List[FlightData],
air_support: AirSupport) -> None:
def assign_channels_to_flights(
cls, flights: List[FlightData], air_support: AirSupport
) -> None:
"""Assigns preset radio channels for client flights."""
for flight in flights:
if not flight.client_units:
@@ -217,8 +218,7 @@ class Operation:
cls.assign_channels_to_flight(flight, air_support)
@staticmethod
def assign_channels_to_flight(flight: FlightData,
air_support: AirSupport) -> None:
def assign_channels_to_flight(flight: FlightData, air_support: AirSupport) -> None:
"""Assigns preset radio channels for a client flight."""
airframe = flight.aircraft_type
@@ -234,7 +234,9 @@ class Operation:
)
@classmethod
def _create_tacan_registry(cls, unique_map_frequencies: Set[RadioFrequency]) -> None:
def _create_tacan_registry(
cls, unique_map_frequencies: Set[RadioFrequency]
) -> None:
"""
Dedup beacon/radio frequencies, since some maps have some frequencies
used multiple times.
@@ -246,13 +248,14 @@ class Operation:
unique_map_frequencies.add(beacon.frequency)
if beacon.is_tacan:
if beacon.channel is None:
logging.error(
f"TACAN beacon has no channel: {beacon.callsign}")
logging.error(f"TACAN beacon has no channel: {beacon.callsign}")
else:
cls.tacan_registry.reserve(beacon.tacan_channel)
@classmethod
def _create_radio_registry(cls, unique_map_frequencies: Set[RadioFrequency]) -> None:
def _create_radio_registry(
cls, unique_map_frequencies: Set[RadioFrequency]
) -> None:
cls.radio_registry = RadioRegistry()
for data in AIRFIELD_DATA.values():
if data.theater == cls.game.theater.terrain.name and data.atc:
@@ -270,7 +273,7 @@ class Operation:
cls.game,
cls.radio_registry,
cls.tacan_registry,
cls.unit_map
cls.unit_map,
)
cls.groundobjectgen.generate()
@@ -284,10 +287,13 @@ class Operation:
continue
pos = Point(d["x"], d["z"])
if utype is not None and not cls.game.position_culled(pos) and cls.game.settings.perf_destroyed_units:
if (
utype is not None
and not cls.game.position_culled(pos)
and cls.game.settings.perf_destroyed_units
):
cls.current_mission.static_group(
country=cls.current_mission.country(
cls.game.player_country),
country=cls.current_mission.country(cls.game.player_country),
name="",
_type=utype,
hidden=True,
@@ -302,13 +308,14 @@ class Operation:
cls.create_unit_map()
cls.create_radio_registries()
# Set mission time and weather conditions.
EnvironmentGenerator(cls.current_mission,
cls.game.conditions).generate()
EnvironmentGenerator(cls.current_mission, cls.game.conditions).generate()
cls._generate_ground_units()
cls._generate_transports()
cls._generate_destroyed_units()
cls._generate_air_units()
cls.assign_channels_to_flights(cls.airgen.flights,
cls.airsupportgen.air_support)
cls.assign_channels_to_flights(
cls.airgen.flights, cls.airsupportgen.air_support
)
cls._generate_ground_conflicts()
# Triggers
@@ -317,14 +324,11 @@ class Operation:
# Setup combined arms parameters
cls.current_mission.groundControl.pilot_can_control_vehicles = cls.ca_slots > 0
if cls.game.player_country in [country.name for country in cls.current_mission.coalition["blue"].countries.values()]:
cls.current_mission.groundControl.blue_tactical_commander = cls.ca_slots
else:
cls.current_mission.groundControl.red_tactical_commander = cls.ca_slots
cls.current_mission.groundControl.blue_tactical_commander = cls.ca_slots
cls.current_mission.groundControl.blue_observer = 1
# Options
forcedoptionsgen = ForcedOptionsGenerator(
cls.current_mission, cls.game)
forcedoptionsgen = ForcedOptionsGenerator(cls.current_mission, cls.game)
forcedoptionsgen.generate()
# Generate Visuals Smoke Effects
@@ -341,13 +345,11 @@ class Operation:
plugin.inject_scripts(cls)
plugin.inject_configuration(cls)
cls.assign_channels_to_flights(cls.airgen.flights,
cls.airsupportgen.air_support)
cls.assign_channels_to_flights(
cls.airgen.flights, cls.airsupportgen.air_support
)
cls.notify_info_generators(
cls.groundobjectgen,
cls.airsupportgen,
cls.jtacs,
cls.airgen
cls.groundobjectgen, cls.airsupportgen, cls.jtacs, cls.airgen
)
cls.reset_naming_ids()
return cls.unit_map
@@ -359,66 +361,88 @@ class Operation:
# Air Support (Tanker & Awacs)
assert cls.radio_registry and cls.tacan_registry
cls.airsupportgen = AirSupportConflictGenerator(
cls.current_mission, cls.air_conflict(), cls.game, cls.radio_registry,
cls.tacan_registry)
cls.current_mission,
cls.air_conflict(),
cls.game,
cls.radio_registry,
cls.tacan_registry,
)
cls.airsupportgen.generate()
# Generate Aircraft Activity on the map
cls.airgen = AircraftConflictGenerator(
cls.current_mission, cls.game.settings, cls.game,
cls.radio_registry, cls.unit_map)
cls.current_mission,
cls.game.settings,
cls.game,
cls.radio_registry,
cls.unit_map,
air_support=cls.airsupportgen.air_support,
)
cls.airgen.clear_parking_slots()
cls.airgen.generate_flights(
cls.current_mission.country(cls.game.player_country),
cls.game.blue_ato,
cls.groundobjectgen.runways
cls.groundobjectgen.runways,
)
cls.airgen.generate_flights(
cls.current_mission.country(cls.game.enemy_country),
cls.game.red_ato,
cls.groundobjectgen.runways
cls.groundobjectgen.runways,
)
cls.airgen.spawn_unused_aircraft(
cls.current_mission.country(cls.game.player_country),
cls.current_mission.country(cls.game.enemy_country))
cls.current_mission.country(cls.game.enemy_country),
)
@classmethod
def _generate_ground_conflicts(cls) -> None:
"""For each frontline in the Operation, generate the ground conflicts and JTACs"""
for front_line in cls.game.theater.conflicts(True):
player_cp = front_line.control_point_a
enemy_cp = front_line.control_point_b
cls.jtacs = []
for front_line in cls.game.theater.conflicts():
player_cp = front_line.blue_cp
enemy_cp = front_line.red_cp
conflict = Conflict.frontline_cas_conflict(
cls.game.player_name,
cls.game.enemy_name,
cls.current_mission.country(cls.game.player_country),
cls.current_mission.country(cls.game.enemy_country),
player_cp,
enemy_cp,
cls.game.theater
front_line,
cls.game.theater,
)
# Generate frontline ops
player_gp = cls.game.ground_planners[player_cp.id].units_per_cp[enemy_cp.id]
enemy_gp = cls.game.ground_planners[enemy_cp.id].units_per_cp[player_cp.id]
ground_conflict_gen = GroundConflictGenerator(
cls.current_mission,
conflict, cls.game,
player_gp, enemy_gp,
conflict,
cls.game,
player_gp,
enemy_gp,
player_cp.stances[enemy_cp.id],
cls.unit_map
cls.unit_map,
)
ground_conflict_gen.generate()
cls.jtacs.extend(ground_conflict_gen.jtacs)
@classmethod
def _generate_transports(cls) -> None:
"""Generates convoys for unit transfers by road."""
ConvoyGenerator(cls.current_mission, cls.game, cls.unit_map).generate()
CargoShipGenerator(cls.current_mission, cls.game, cls.unit_map).generate()
@classmethod
def reset_naming_ids(cls):
namegen.reset_numbers()
@classmethod
def generate_lua(cls, airgen: AircraftConflictGenerator,
airsupportgen: AirSupportConflictGenerator,
jtacs: List[JtacInfo]) -> None:
def generate_lua(
cls,
airgen: AircraftConflictGenerator,
airsupportgen: AirSupportConflictGenerator,
jtacs: List[JtacInfo],
) -> None:
# TODO: Refactor this
luaData = {
"AircraftCarriers": {},
@@ -426,39 +450,43 @@ class Operation:
"AWACs": {},
"JTACs": {},
"TargetPoints": {},
"RedAA": {},
"BlueAA": {},
} # type: ignore
for tanker in airsupportgen.air_support.tankers:
luaData["Tankers"][tanker.callsign] = {
"dcsGroupName": tanker.dcsGroupName,
"dcsGroupName": tanker.group_name,
"callsign": tanker.callsign,
"variant": tanker.variant,
"radio": tanker.freq.mhz,
"tacan": str(tanker.tacan.number) + tanker.tacan.band.name
"tacan": str(tanker.tacan.number) + tanker.tacan.band.name,
}
if airsupportgen.air_support.awacs:
for awacs in airsupportgen.air_support.awacs:
luaData["AWACs"][awacs.callsign] = {
"dcsGroupName": awacs.dcsGroupName,
"dcsGroupName": awacs.group_name,
"callsign": awacs.callsign,
"radio": awacs.freq.mhz
"radio": awacs.freq.mhz,
}
for jtac in jtacs:
luaData["JTACs"][jtac.callsign] = {
"dcsGroupName": jtac.dcsGroupName,
"dcsGroupName": jtac.group_name,
"callsign": jtac.callsign,
"zone": jtac.region,
"dcsUnit": jtac.unit_name,
"laserCode": jtac.code
"laserCode": jtac.code,
}
for flight in airgen.flights:
if flight.friendly and flight.flight_type in [FlightType.ANTISHIP,
FlightType.DEAD,
FlightType.SEAD,
FlightType.STRIKE]:
if flight.friendly and flight.flight_type in [
FlightType.ANTISHIP,
FlightType.DEAD,
FlightType.SEAD,
FlightType.STRIKE,
]:
flightType = str(flight.flight_type)
flightTarget = flight.package.target
if flightTarget:
@@ -466,23 +494,47 @@ class Operation:
flightTargetType = None
if isinstance(flightTarget, TheaterGroundObject):
flightTargetName = flightTarget.obj_name
flightTargetType = flightType + \
f" TGT ({flightTarget.category})"
elif hasattr(flightTarget, 'name'):
flightTargetType = (
flightType + f" TGT ({flightTarget.category})"
)
elif hasattr(flightTarget, "name"):
flightTargetName = flightTarget.name
flightTargetType = flightType + " TGT (Airbase)"
luaData["TargetPoints"][flightTargetName] = {
"name": flightTargetName,
"type": flightTargetType,
"position": {"x": flightTarget.position.x,
"y": flightTarget.position.y}
"position": {
"x": flightTarget.position.x,
"y": flightTarget.position.y,
},
}
for cp in cls.game.theater.controlpoints:
for ground_object in cp.ground_objects:
if ground_object.might_have_aa and not ground_object.is_dead:
for g in ground_object.groups:
threat_range = ground_object.threat_range(g)
if not threat_range:
continue
faction = "BlueAA" if cp.captured else "RedAA"
luaData[faction][g.name] = {
"name": ground_object.name,
"range": threat_range.meters,
"position": {
"x": ground_object.position.x,
"y": ground_object.position.y,
},
}
# set a LUA table with data from Liberation that we want to set
# at the moment it contains Liberation's install path, and an overridable definition for the JTACAutoLase function
# later, we'll add data about the units and points having been generated, in order to facilitate the configuration of the plugin lua scripts
state_location = "[[" + os.path.abspath(".") + "]]"
lua = """
lua = (
"""
-- setting configuration table
env.info("DCSLiberation|: setting configuration table")
@@ -490,9 +542,12 @@ class Operation:
dcsLiberation = {}
-- the base location for state.json; if non-existent, it'll be replaced with LIBERATION_EXPORT_DIR, TEMP, or DCS working directory
dcsLiberation.installPath=""" + state_location + """
dcsLiberation.installPath="""
+ state_location
+ """
"""
)
# Process the tankers
lua += """
@@ -566,7 +621,33 @@ class Operation:
-- list the aircraft carriers generated by Liberation
-- dcsLiberation.Carriers = {}
-- later, we'll add more data to the table
-- list the Red AA generated by Liberation
dcsLiberation.RedAA = {
"""
for key in luaData["RedAA"]:
data = luaData["RedAA"][key]
name = data["name"]
radius = data["range"]
positionX = data["position"]["x"]
positionY = data["position"]["y"]
lua += f" {{dcsGroupName='{key}', name='{name}', range='{radius}', positionX='{positionX}', positionY='{positionY}' }}, \n"
lua += "}"
lua += """
-- list the Blue AA generated by Liberation
dcsLiberation.BlueAA = {
"""
for key in luaData["BlueAA"]:
data = luaData["BlueAA"][key]
name = data["name"]
radius = data["range"]
positionX = data["position"]["x"]
positionY = data["position"]["y"]
lua += f" {{dcsGroupName='{key}', name='{name}', range='{radius}', positionX='{positionX}', positionY='{positionY}' }}, \n"
lua += "}"
lua += """
"""

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,22 +3,24 @@ from __future__ import annotations
import math
import random
from dataclasses import dataclass
from typing import Iterator, List, Optional, TYPE_CHECKING, Type
from typing import Iterator, List, Optional, TYPE_CHECKING, Tuple, Type
from dcs.unittype import FlyingType, VehicleType
from game import db
from game.data.groundunitclass import GroundUnitClass
from game.factions.faction import Faction
from game.theater import ControlPoint, MissionTarget
from game.utils import Distance
from gen.flights.ai_flight_planner_db import aircraft_for_task
from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.flights.flight import FlightType
from gen.ground_forces.ai_ground_planner_db import TYPE_SHORAD
if TYPE_CHECKING:
from game import Game
FRONTLINE_RESERVES_FACTOR = 1.3
@dataclass(frozen=True)
class AircraftProcurementRequest:
@@ -35,28 +37,51 @@ class AircraftProcurementRequest:
class ProcurementAi:
def __init__(self, game: Game, for_player: bool, faction: Faction,
manage_runways: bool, manage_front_line: bool,
manage_aircraft: bool, front_line_budget_share: float) -> None:
if front_line_budget_share > 1.0:
raise ValueError
def __init__(
self,
game: Game,
for_player: bool,
faction: Faction,
manage_runways: bool,
manage_front_line: bool,
manage_aircraft: bool,
) -> None:
self.game = game
self.is_player = for_player
self.air_wing = game.air_wing_for(for_player)
self.faction = faction
self.manage_runways = manage_runways
self.manage_front_line = manage_front_line
self.manage_aircraft = manage_aircraft
self.front_line_budget_share = front_line_budget_share
self.threat_zones = self.game.threat_zone_for(not self.is_player)
def spend_budget(
self, budget: float,
aircraft_requests: List[AircraftProcurementRequest]) -> float:
def calculate_ground_unit_budget_share(self) -> float:
armor_investment = 0
aircraft_investment = 0
for cp in self.owned_points:
cp_ground_units = cp.allocated_ground_units(self.game.transfers)
armor_investment += cp_ground_units.total_value
cp_aircraft = cp.allocated_aircraft(self.game)
aircraft_investment += cp_aircraft.total_value
total_investment = aircraft_investment + armor_investment
if total_investment == 0:
# Turn 0 or all units were destroyed. Either way, split 30/70.
return 0.3
# the more planes we have, the more ground units we want and vice versa
ground_unit_share = aircraft_investment / total_investment
if ground_unit_share > 1.0:
raise ValueError
return ground_unit_share
def spend_budget(self, budget: float) -> float:
if self.manage_runways:
budget = self.repair_runways(budget)
if self.manage_front_line:
armor_budget = math.ceil(budget * self.front_line_budget_share)
armor_budget = budget * self.calculate_ground_unit_budget_share()
budget -= armor_budget
budget += self.reinforce_front_line(armor_budget)
@@ -65,20 +90,20 @@ class ProcurementAi:
if not self.is_player:
budget += self.sell_incomplete_squadrons()
if self.manage_aircraft:
budget = self.purchase_aircraft(budget, aircraft_requests)
budget = self.purchase_aircraft(budget)
return budget
def sell_incomplete_squadrons(self) -> float:
# Selling incomplete squadrons gives us more money to spend on the next
# turn. This serves as a short term fix for
# https://github.com/Khopa/dcs_liberation/issues/41.
# https://github.com/dcs-liberation/dcs_liberation/issues/41.
#
# Only incomplete squadrons which are unlikely to get used will be sold
# rather than all unused aircraft because the unused aircraft are what
# make OCA strikes worthwhile.
#
# This option is only used by the AI since players cannot cancel sales
# (https://github.com/Khopa/dcs_liberation/issues/365).
# (https://github.com/dcs-liberation/dcs_liberation/issues/365).
total = 0.0
for cp in self.game.theater.control_points_for(self.is_player):
inventory = self.game.aircraft_inventory.for_control_point(cp)
@@ -100,31 +125,22 @@ class ProcurementAi:
budget -= db.RUNWAY_REPAIR_COST
if self.is_player:
self.game.message(
"OPFOR has begun repairing the runway at "
f"{control_point}"
"OPFOR has begun repairing the runway at " f"{control_point}"
)
else:
self.game.message(
"We have begun repairing the runway at "
f"{control_point}"
"We have begun repairing the runway at " f"{control_point}"
)
return budget
def random_affordable_ground_unit(
self, budget: float,
cp: ControlPoint) -> Optional[Type[VehicleType]]:
affordable_units = [u for u in self.faction.frontline_units + self.faction.artillery_units if
db.PRICES[u] <= budget]
total_number_aa = cp.base.total_frontline_aa + cp.pending_frontline_aa_deliveries_count
total_non_aa = cp.base.total_armor + cp.pending_deliveries_count - total_number_aa
max_aa = math.ceil(total_non_aa/8)
# Limit the number of AA units the AI will buy
if not total_number_aa < max_aa:
for unit in [u for u in affordable_units if u in TYPE_SHORAD]:
affordable_units.remove(unit)
def affordable_ground_unit_of_class(
self, budget: float, unit_class: GroundUnitClass
) -> Optional[Type[VehicleType]]:
faction_units = set(self.faction.frontline_units) | set(
self.faction.artillery_units
)
of_class = set(unit_class.unit_list) & faction_units
affordable_units = [u for u in of_class if db.PRICES[u] <= budget]
if not affordable_units:
return None
return random.choice(affordable_units)
@@ -133,13 +149,15 @@ class ProcurementAi:
if not self.faction.frontline_units and not self.faction.artillery_units:
return budget
# TODO: Attempt to transfer from reserves.
while budget > 0:
candidates = self.front_line_candidates()
if not candidates:
cp = self.ground_reinforcement_candidate()
if cp is None:
break
cp = random.choice(candidates)
unit = self.random_affordable_ground_unit(budget, cp)
most_needed_type = self.most_needed_unit_class(cp)
unit = self.affordable_ground_unit_of_class(budget, most_needed_type)
if unit is None:
# Can't afford any more units.
break
@@ -149,48 +167,99 @@ class ProcurementAi:
return budget
def _affordable_aircraft_of_types(
self, types: List[Type[FlyingType]], airbase: ControlPoint,
number: int, max_price: float) -> Optional[Type[FlyingType]]:
def most_needed_unit_class(self, cp: ControlPoint) -> GroundUnitClass:
worst_balanced: Optional[GroundUnitClass] = None
worst_fulfillment = math.inf
for unit_class in GroundUnitClass:
if not self.faction.has_access_to_unittype(unit_class):
continue
current_ratio = self.cost_ratio_of_ground_unit(cp, unit_class)
desired_ratio = (
self.faction.doctrine.ground_unit_procurement_ratios.for_unit_class(
unit_class
)
)
if not desired_ratio:
continue
if current_ratio >= desired_ratio:
continue
fulfillment = current_ratio / desired_ratio
if fulfillment < worst_fulfillment:
worst_fulfillment = fulfillment
worst_balanced = unit_class
if worst_balanced is None:
return GroundUnitClass.Tank
return worst_balanced
def _affordable_aircraft_for_task(
self,
task: FlightType,
airbase: ControlPoint,
number: int,
max_price: float,
) -> Optional[Type[FlyingType]]:
best_choice: Optional[Type[FlyingType]] = None
for unit in [u for u in self.faction.aircrafts if u in types]:
for unit in aircraft_for_task(task):
if unit not in self.faction.aircrafts:
continue
if db.PRICES[unit] * number > max_price:
continue
if not airbase.can_operate(unit):
continue
# Affordable and compatible. To keep some variety, skip with a 50/50
# chance. Might be a good idea to have the chance to skip based on
# the price compared to the rest of the choices.
for squadron in self.air_wing.squadrons_for(unit):
if task in squadron.auto_assignable_mission_types:
break
else:
continue
# Affordable, compatible, and we have a squadron capable of the task. To
# keep some variety, skip with a 50/50 chance. Might be a good idea to have
# the chance to skip based on the price compared to the rest of the choices.
best_choice = unit
if random.choice([True, False]):
break
return best_choice
def affordable_aircraft_for(
self, request: AircraftProcurementRequest,
airbase: ControlPoint, budget: float) -> Optional[Type[FlyingType]]:
return self._affordable_aircraft_of_types(
aircraft_for_task(request.task_capability),
airbase, request.number, budget)
self, request: AircraftProcurementRequest, airbase: ControlPoint, budget: float
) -> Optional[Type[FlyingType]]:
return self._affordable_aircraft_for_task(
request.task_capability, airbase, request.number, budget
)
def purchase_aircraft(
self, budget: float,
aircraft_requests: List[AircraftProcurementRequest]) -> float:
for request in aircraft_requests:
for airbase in self.best_airbases_for(request):
unit = self.affordable_aircraft_for(request, airbase, budget)
if unit is None:
# Can't afford any aircraft capable of performing the
# required mission that can operate from this airbase. We
# might be able to afford aircraft at other airbases though,
# in the case where the airbase we attempted to use is only
# able to operate expensive aircraft.
continue
def fulfill_aircraft_request(
self, request: AircraftProcurementRequest, budget: float
) -> Tuple[float, bool]:
for airbase in self.best_airbases_for(request):
unit = self.affordable_aircraft_for(request, airbase, budget)
if unit is None:
# Can't afford any aircraft capable of performing the
# required mission that can operate from this airbase. We
# might be able to afford aircraft at other airbases though,
# in the case where the airbase we attempted to use is only
# able to operate expensive aircraft.
continue
budget -= db.PRICES[unit] * request.number
airbase.pending_unit_deliveries.order({unit: request.number})
budget -= db.PRICES[unit] * request.number
airbase.pending_unit_deliveries.order({unit: request.number})
return budget, True
return budget, False
def purchase_aircraft(self, budget: float) -> float:
for request in self.game.procurement_requests_for(self.is_player):
if not list(self.best_airbases_for(request)):
# No airbases in range of this request. Skip it.
continue
budget, fulfilled = self.fulfill_aircraft_request(request, budget)
if not fulfilled:
# The request was not fulfilled because we could not afford any suitable
# aircraft. Rather than continuing, which could proceed to buy tons of
# cheap escorts that will never allow us to plan a strike package, stop
# buying so we can save the budget until a turn where we *can* afford to
# fill the package.
break
return budget
@property
@@ -201,17 +270,13 @@ class ProcurementAi:
return self.game.theater.enemy_points()
def best_airbases_for(
self,
request: AircraftProcurementRequest) -> Iterator[ControlPoint]:
distance_cache = ObjectiveDistanceCache.get_closest_airfields(
request.near
)
self, request: AircraftProcurementRequest
) -> Iterator[ControlPoint]:
distance_cache = ObjectiveDistanceCache.get_closest_airfields(request.near)
threatened = []
for cp in distance_cache.airfields_within(request.range):
for cp in distance_cache.operational_airfields_within(request.range):
if not cp.is_friendly(self.is_player):
continue
if not cp.runway_is_operational():
continue
if cp.unclaimed_parking(self.game) < request.number:
continue
if self.threat_zones.threatened(cp.position):
@@ -219,37 +284,69 @@ class ProcurementAi:
yield cp
yield from threatened
def front_line_candidates(self) -> List[ControlPoint]:
candidates = []
def ground_reinforcement_candidate(self) -> Optional[ControlPoint]:
worst_supply = math.inf
understaffed: Optional[ControlPoint] = None
# Prefer to buy front line units at active front lines that are not
# already overloaded.
for cp in self.owned_points:
if cp.expected_ground_units_next_turn.total >= 30:
if not cp.has_active_frontline:
continue
if not cp.has_ground_unit_source(self.game):
# No source of ground units, so can't buy anything.
continue
purchase_target = cp.frontline_unit_count_limit * FRONTLINE_RESERVES_FACTOR
allocated = cp.allocated_ground_units(self.game.transfers)
if allocated.total >= purchase_target:
# Control point is already sufficiently defended.
continue
for connected in cp.connected_points:
if not connected.is_friendly(to_player=self.is_player):
candidates.append(cp)
if allocated.total < worst_supply:
worst_supply = allocated.total
understaffed = cp
if not candidates:
# Otherwise buy reserves, but don't exceed 10 reserve units per CP.
# These units do not exist in the world until the CP becomes
# connected to an active front line, at which point all these units
# will suddenly appear at the gates of the newly captured CP.
#
# To avoid sudden overwhelming numbers of units we avoid buying
# many.
#
# Also, do not bother buying units at bases that will never connect
# to a front line.
for cp in self.owned_points:
if not cp.can_deploy_ground_units:
continue
if cp.expected_ground_units_next_turn.total >= 10:
continue
if cp.is_global:
continue
candidates.append(cp)
if understaffed is not None:
return understaffed
return candidates
# Otherwise buy reserves, but don't exceed the amount defined in the settings.
# These units do not exist in the world until the CP becomes
# connected to an active front line, at which point all these units
# will suddenly appear at the gates of the newly captured CP.
#
# To avoid sudden overwhelming numbers of units we avoid buying
# many.
#
# Also, do not bother buying units at bases that will never connect
# to a front line.
for cp in self.owned_points:
if cp.is_global:
continue
if not cp.can_recruit_ground_units(self.game):
continue
allocated = cp.allocated_ground_units(self.game.transfers)
if allocated.total >= self.game.settings.reserves_procurement_target:
continue
if allocated.total < worst_supply:
worst_supply = allocated.total
understaffed = cp
return understaffed
def cost_ratio_of_ground_unit(
self, control_point: ControlPoint, unit_class: GroundUnitClass
) -> float:
allocations = control_point.allocated_ground_units(self.game.transfers)
class_cost = 0
total_cost = 0
for unit_type, count in allocations.all.items():
cost = db.PRICES[unit_type] * count
total_cost += cost
if unit_type in unit_class:
class_cost += cost
if not total_cost:
return 0
return class_cost / total_cost

35
game/profiling.py Normal file
View File

@@ -0,0 +1,35 @@
from __future__ import annotations
import logging
import timeit
from collections import defaultdict
from contextlib import contextmanager
from datetime import timedelta
from typing import Iterator
@contextmanager
def logged_duration(event: str) -> Iterator[None]:
start = timeit.default_timer()
yield
end = timeit.default_timer()
logging.debug("%s took %s", event, timedelta(seconds=end - start))
class MultiEventTracer:
def __init__(self) -> None:
self.events: dict[str, timedelta] = defaultdict(timedelta)
def __enter__(self) -> MultiEventTracer:
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
for event, duration in self.events.items():
logging.debug("%s took %s", event, duration)
@contextmanager
def trace(self, event: str) -> Iterator[None]:
start = timeit.default_timer()
yield
end = timeit.default_timer()
self.events[event] += timedelta(seconds=end - start)

89
game/scenery_group.py Normal file
View File

@@ -0,0 +1,89 @@
from __future__ import annotations
from game.theater.theatergroundobject import NAME_BY_CATEGORY
from dcs.triggers import TriggerZone
from typing import Iterable, List
class SceneryGroupError(RuntimeError):
"""Error for when there are insufficient conditions to create a SceneryGroup."""
pass
class SceneryGroup:
"""Store information about a scenery objective."""
def __init__(
self, zone_def: TriggerZone, zones: Iterable[TriggerZone], category: str
) -> None:
self.zone_def = zone_def
self.zones = zones
self.position = zone_def.position
self.category = category
@staticmethod
def from_trigger_zones(trigger_zones: Iterable[TriggerZone]) -> List[SceneryGroup]:
"""Define scenery objectives based on their encompassing blue/red circle."""
zone_definitions = []
white_zones = []
scenery_groups = []
# Aggregate trigger zones into different groups based on color.
for zone in trigger_zones:
if SceneryGroup.is_blue(zone):
zone_definitions.append(zone)
if SceneryGroup.is_white(zone):
white_zones.append(zone)
# For each objective definition.
for zone_def in zone_definitions:
zone_def_radius = zone_def.radius
zone_def_position = zone_def.position
zone_def_name = zone_def.name
if len(zone_def.properties) == 0:
raise SceneryGroupError(
"Undefined SceneryGroup category in TriggerZone: " + zone_def_name
)
# Arbitrary campaign design requirement: First property must define the category.
zone_def_category = zone_def.properties[1].get("value").lower()
valid_white_zones = []
for zone in list(white_zones):
if zone.position.distance_to_point(zone_def_position) < zone_def_radius:
valid_white_zones.append(zone)
white_zones.remove(zone)
if len(valid_white_zones) > 0 and zone_def_category in NAME_BY_CATEGORY:
scenery_groups.append(
SceneryGroup(zone_def, valid_white_zones, zone_def_category)
)
elif len(valid_white_zones) == 0:
raise SceneryGroupError(
"No white triggerzones found in: " + zone_def_name
)
elif zone_def_category not in NAME_BY_CATEGORY:
raise SceneryGroupError(
"Incorrect TriggerZone category definition for: "
+ zone_def_name
+ " in campaign definition. TriggerZone category: "
+ zone_def_category
)
return scenery_groups
@staticmethod
def is_blue(zone: TriggerZone) -> bool:
# Blue in RGB is [0 Red], [0 Green], [1 Blue]. Ignore the fourth position: Transparency.
return zone.color[1] == 0 and zone.color[2] == 0 and zone.color[3] == 1
@staticmethod
def is_white(zone: TriggerZone) -> bool:
# White in RGB is [1 Red], [1 Green], [1 Blue]. Ignore the fourth position: Transparency.
return zone.color[1] == 1 and zone.color[2] == 1 and zone.color[3] == 1

View File

@@ -1,9 +1,19 @@
from dataclasses import dataclass, field
from datetime import timedelta
from enum import Enum, unique
from typing import Dict, Optional
from dcs.forcedoptions import ForcedOptions
@unique
class AutoAtoBehavior(Enum):
Disabled = "Disabled"
Never = "Never assign player pilots"
Default = "No preference"
Prefer = "Prefer player pilots"
@dataclass
class Settings:
@@ -25,19 +35,29 @@ class Settings:
default_start_type: str = "Cold"
# Mission specific
desired_player_mission_duration: timedelta = timedelta(minutes=60)
# Campaign management
automate_runway_repair: bool = False
automate_front_line_reinforcements: bool = False
automate_aircraft_reinforcements: bool = False
restrict_weapons_by_date: bool = False
disable_legacy_aewc: bool = True
generate_dark_kneeboard: bool = False
invulnerable_player_pilots: bool = True
auto_ato_behavior: AutoAtoBehavior = AutoAtoBehavior.Default
auto_ato_player_missions_asap: bool = True
# Performance oriented
perf_red_alert_state: bool = True
perf_smoke_gen: bool = True
perf_smoke_spacing = 1600
perf_artillery: bool = True
perf_moving_units: bool = True
perf_infantry: bool = True
perf_destroyed_units: bool = True
reserves_procurement_target: int = 10
# Performance culling
perf_culling: bool = False
@@ -58,8 +78,7 @@ class Settings:
def plugin_settings_key(identifier: str) -> str:
return f"plugins.{identifier}"
def initialize_plugin_option(self, identifier: str,
default_value: bool) -> None:
def initialize_plugin_option(self, identifier: str, default_value: bool) -> None:
try:
self.plugin_option(identifier)
except KeyError:

373
game/squadrons.py Normal file
View File

@@ -0,0 +1,373 @@
from __future__ import annotations
import itertools
import logging
import random
from collections import defaultdict
from dataclasses import dataclass, field
from enum import unique, Enum
from pathlib import Path
from typing import (
Type,
Tuple,
TYPE_CHECKING,
Optional,
Iterator,
Sequence,
)
import yaml
from dcs.unittype import FlyingType
from faker import Faker
from game.db import flying_type_from_name
from game.settings import AutoAtoBehavior
if TYPE_CHECKING:
from game import Game
from gen.flights.flight import FlightType
@dataclass
class PilotRecord:
missions_flown: int = field(default=0)
@unique
class PilotStatus(Enum):
Active = "Active"
OnLeave = "On leave"
Dead = "Dead"
@dataclass
class Pilot:
name: str
player: bool = field(default=False)
status: PilotStatus = field(default=PilotStatus.Active)
record: PilotRecord = field(default_factory=PilotRecord)
@property
def alive(self) -> bool:
return self.status is not PilotStatus.Dead
@property
def on_leave(self) -> bool:
return self.status is PilotStatus.OnLeave
def send_on_leave(self) -> None:
if self.status is not PilotStatus.Active:
raise RuntimeError("Only active pilots may be sent on leave")
self.status = PilotStatus.OnLeave
def return_from_leave(self) -> None:
if self.status is not PilotStatus.OnLeave:
raise RuntimeError("Only pilots on leave may be returned from leave")
self.status = PilotStatus.Active
def kill(self) -> None:
self.status = PilotStatus.Dead
@classmethod
def random(cls, faker: Faker) -> Pilot:
return Pilot(faker.name())
@dataclass
class Squadron:
name: str
nickname: str
country: str
role: str
aircraft: Type[FlyingType]
livery: Optional[str]
mission_types: tuple[FlightType, ...]
pilots: list[Pilot]
available_pilots: list[Pilot] = field(init=False, hash=False, compare=False)
auto_assignable_mission_types: set[FlightType] = field(
init=False, hash=False, compare=False
)
# We need a reference to the Game so that we can access the Faker without needing to
# persist it to the save game, or having to reconstruct it (it's not cheap) each
# time we create or load a squadron.
game: Game = field(hash=False, compare=False)
player: bool
def __post_init__(self) -> None:
self.available_pilots = list(self.active_pilots)
self.auto_assignable_mission_types = set(self.mission_types)
def __str__(self) -> str:
return f'{self.name} "{self.nickname}"'
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:
self.enlist_new_pilots(1)
return self.available_pilots.pop()
# For opfor, so player/AI option is irrelevant.
if not self.player:
return self.available_pilots.pop()
preference = self.game.settings.auto_ato_behavior
# No preference, so the first pilot is fine.
if preference is AutoAtoBehavior.Default:
return self.available_pilots.pop()
prefer_players = preference is AutoAtoBehavior.Prefer
for pilot in self.available_pilots:
if pilot.player == prefer_players:
self.available_pilots.remove(pilot)
return pilot
# No pilot was found that matched the user's preference.
#
# 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.
#
# If they prefer players and we're out of players, just return an AI pilot.
if not prefer_players:
self.enlist_new_pilots(1)
return self.available_pilots.pop()
def claim_pilot(self, pilot: Pilot) -> None:
if pilot not in self.available_pilots:
raise ValueError(
f"Cannot assign {pilot} to {self} because they are not available"
)
self.available_pilots.remove(pilot)
def return_pilot(self, pilot: Pilot) -> None:
self.available_pilots.append(pilot)
def return_pilots(self, pilots: Sequence[Pilot]) -> None:
# Return in reverse so that returning two pilots and then getting two more
# results in the same ordering. This happens commonly when resetting rosters in
# the UI, when we clear the roster because the UI is updating, then end up
# repopulating the same size flight from the same squadron.
self.available_pilots.extend(reversed(pilots))
def enlist_new_pilots(self, count: int) -> None:
new_pilots = [Pilot(self.faker.name()) for _ in range(count)]
self.pilots.extend(new_pilots)
self.available_pilots.extend(new_pilots)
def return_all_pilots(self) -> None:
self.available_pilots = list(self.active_pilots)
@property
def faker(self) -> Faker:
return self.game.faker_for(self.player)
def _pilots_with_status(self, status: PilotStatus) -> list[Pilot]:
return [p for p in self.pilots if p.status == status]
def _pilots_without_status(self, status: PilotStatus) -> list[Pilot]:
return [p for p in self.pilots if p.status != status]
@property
def active_pilots(self) -> list[Pilot]:
return self._pilots_with_status(PilotStatus.Active)
@property
def pilots_on_leave(self) -> list[Pilot]:
return self._pilots_with_status(PilotStatus.OnLeave)
@property
def number_of_pilots_including_dead(self) -> int:
return len(self.pilots)
@property
def number_of_living_pilots(self) -> int:
return len(self._pilots_without_status(PilotStatus.Dead))
def pilot_at_index(self, index: int) -> Pilot:
return self.pilots[index]
@classmethod
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.flight import FlightType
with path.open() as squadron_file:
data = yaml.safe_load(squadron_file)
unit_type = flying_type_from_name(data["aircraft"])
if unit_type is None:
raise KeyError(f"Could not find any aircraft with the ID {unit_type}")
pilots = [Pilot(n, player=False) for n in data.get("pilots", [])]
pilots.extend([Pilot(n, player=True) for n in data.get("players", [])])
mission_types = [FlightType.from_name(n) for n in data["mission_types"]]
tasks = tasks_for_aircraft(unit_type)
for mission_type in list(mission_types):
if mission_type not in tasks:
logging.error(
f"Squadron has mission type {mission_type} but {unit_type} is not "
f"capable of that task: {path}"
)
mission_types.remove(mission_type)
return Squadron(
name=data["name"],
nickname=data["nickname"],
country=data["country"],
role=data["role"],
aircraft=unit_type,
livery=data.get("livery"),
mission_types=tuple(mission_types),
pilots=pilots,
game=game,
player=player,
)
def __setstate__(self, state) -> None:
# TODO: Remove save compat.
if "auto_assignable_mission_types" not in state:
state["auto_assignable_mission_types"] = set(state["mission_types"])
self.__dict__.update(state)
class SquadronLoader:
def __init__(self, game: Game, player: bool) -> None:
self.game = game
self.player = player
@staticmethod
def squadron_directories() -> Iterator[Path]:
from game import persistency
yield Path(persistency.base_path()) / "Liberation/Squadrons"
yield Path("resources/squadrons")
def load(self) -> dict[Type[FlyingType], list[Squadron]]:
squadrons: dict[Type[FlyingType], list[Squadron]] = defaultdict(list)
country = self.game.country_for(self.player)
faction = self.game.faction_for(self.player)
any_country = country.startswith("Combined Joint Task Forces ")
for directory in self.squadron_directories():
for path, squadron in self.load_squadrons_from(directory):
if not any_country and squadron.country != country:
logging.debug(
"Not using squadron for non-matching country (is "
f"{squadron.country}, need {country}: {path}"
)
continue
if squadron.aircraft not in faction.aircrafts:
logging.debug(
f"Not using squadron because {faction.name} cannot use "
f"{squadron.aircraft}: {path}"
)
continue
logging.debug(
f"Found {squadron.name} {squadron.aircraft} {squadron.role} "
f"compatible with {faction.name}"
)
squadrons[squadron.aircraft].append(squadron)
# Convert away from defaultdict because defaultdict doesn't unpickle so we don't
# want it in the save state.
return dict(squadrons)
def load_squadrons_from(self, directory: Path) -> Iterator[Tuple[Path, Squadron]]:
logging.debug(f"Looking for factions in {directory}")
# First directory level is the aircraft type so that historical squadrons that
# have flown multiple airframes can be defined as many times as needed. The main
# load() method is responsible for filtering out squadrons that aren't
# compatible with the faction.
for squadron_path in directory.glob("*/*.yaml"):
try:
yield squadron_path, Squadron.from_yaml(
squadron_path, self.game, self.player
)
except Exception as ex:
raise RuntimeError(
f"Failed to load squadron defined by {squadron_path}"
) from ex
class AirWing:
def __init__(self, game: Game, player: bool) -> None:
from gen.flights.ai_flight_planner_db import tasks_for_aircraft
self.game = game
self.player = player
self.squadrons = SquadronLoader(game, player).load()
count = itertools.count(1)
for aircraft in game.faction_for(player).aircrafts:
if aircraft in self.squadrons:
continue
self.squadrons[aircraft] = [
Squadron(
name=f"Squadron {next(count):03}",
nickname=self.random_nickname(),
country=game.country_for(player),
role="Flying Squadron",
aircraft=aircraft,
livery=None,
mission_types=tuple(tasks_for_aircraft(aircraft)),
pilots=[],
game=game,
player=player,
)
]
def squadrons_for(self, aircraft: Type[FlyingType]) -> Sequence[Squadron]:
return self.squadrons[aircraft]
def squadrons_for_task(self, task: FlightType) -> Iterator[Squadron]:
for squadron in self.iter_squadrons():
if task in squadron.mission_types:
yield squadron
def squadron_for(self, aircraft: Type[FlyingType]) -> Squadron:
return self.squadrons_for(aircraft)[0]
def iter_squadrons(self) -> Iterator[Squadron]:
return itertools.chain.from_iterable(self.squadrons.values())
def squadron_at_index(self, index: int) -> Squadron:
return list(self.iter_squadrons())[index]
def reset(self) -> None:
for squadron in self.iter_squadrons():
squadron.return_all_pilots()
@property
def size(self) -> int:
return sum(len(s) for s in self.squadrons.values())
@staticmethod
def _make_random_nickname() -> str:
from gen.naming import ANIMALS
animal = random.choice(ANIMALS)
adjective = random.choice(
(
None,
"Red",
"Blue",
"Green",
"Golden",
"Black",
"Fighting",
"Flying",
)
)
if adjective is None:
return animal.title()
return f"{adjective} {animal}".title()
def random_nickname(self) -> str:
while True:
nickname = self._make_random_nickname()
for squadron in self.iter_squadrons():
if squadron.nickname == nickname:
break
else:
return nickname

View File

@@ -1,5 +1,6 @@
from .base import *
from .conflicttheater import *
from .controlpoint import *
from .frontline import FrontLine
from .missiontarget import MissionTarget
from .theatergroundobject import SamGroundObject

View File

@@ -4,13 +4,12 @@ import math
import typing
from typing import Dict, Type
from dcs.task import CAP, CAS, Embarking, PinpointStrike, Task
from dcs.task import AWACS, CAP, CAS, Embarking, PinpointStrike, Task, Transport
from dcs.unittype import FlyingType, UnitType, VehicleType
from dcs.vehicles import AirDefence, Armor
from game import db
from game.db import PRICES
from gen.ground_forces.ai_ground_planner_db import TYPE_SHORAD
STRENGTH_AA_ASSEMBLE_MIN = 0.2
PLANES_SCRAMBLE_MIN_BASE = 2
@@ -22,10 +21,10 @@ BASE_MIN_STRENGTH = 0
class Base:
def __init__(self):
self.aircraft: Dict[Type[FlyingType], int] = {}
self.armor: Dict[Type[VehicleType], int] = {}
# TODO: Appears unused.
self.aa: Dict[AirDefence, int] = {}
self.commision_points: Dict[Type, float] = {}
self.strength = 1
@@ -48,32 +47,48 @@ class Base:
logging.exception(f"No price found for {unit_type.id}")
return total
@property
def total_frontline_aa(self) -> int:
return sum([v for k, v in self.armor.items() if k in TYPE_SHORAD])
@property
def total_aa(self) -> int:
return sum(self.aa.values())
def total_units(self, task: Task) -> int:
return sum([c for t, c in itertools.chain(self.aircraft.items(), self.armor.items(), self.aa.items()) if t in db.UNIT_BY_TASK[task]])
return sum(
[
c
for t, c in itertools.chain(
self.aircraft.items(), self.armor.items(), self.aa.items()
)
if t in db.UNIT_BY_TASK[task]
]
)
def total_units_of_type(self, unit_type) -> int:
return sum([c for t, c in itertools.chain(self.aircraft.items(), self.armor.items(), self.aa.items()) if t == unit_type])
return sum(
[
c
for t, c in itertools.chain(
self.aircraft.items(), self.armor.items(), self.aa.items()
)
if t == unit_type
]
)
@property
def all_units(self):
return itertools.chain(self.aircraft.items(), self.armor.items(), self.aa.items())
return itertools.chain(
self.aircraft.items(), self.armor.items(), self.aa.items()
)
def _find_best_unit(self, available_units: Dict[UnitType, int],
for_type: Task, count: int) -> Dict[UnitType, int]:
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 = [
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] = {}
@@ -94,14 +109,18 @@ class Base:
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]:
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
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)
@@ -110,7 +129,9 @@ class Base:
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.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]):
@@ -122,7 +143,13 @@ class Base:
for_task = db.unit_task(unit_type)
target_dict = None
if for_task == CAS or for_task == CAP or for_task == Embarking:
if (
for_task == AWACS
or for_task == CAS
or for_task == CAP
or for_task == Embarking
or for_task == Transport
):
target_dict = self.aircraft
elif for_task == PinpointStrike:
target_dict = self.armor
@@ -149,7 +176,7 @@ class Base:
if unit_type not in target_array:
print("Base didn't find event type {}".format(unit_type))
continue
target_array[unit_type] = max(target_array[unit_type] - count, 0)
if target_array[unit_type] == 0:
del target_array[unit_type]
@@ -166,12 +193,20 @@ class Base:
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])
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)
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)
@@ -202,4 +237,8 @@ class Base:
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())
return self._find_best_unit(
self.aa,
AirDefence,
count and min(count, self.total_aa) or self.assemble_aa_count(),
)

26
game/theater/bullseye.py Normal file
View File

@@ -0,0 +1,26 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Dict, TYPE_CHECKING
from dcs import Point
from game.theater import LatLon
if TYPE_CHECKING:
from game.theater import ConflictTheater
@dataclass
class Bullseye:
position: Point
@classmethod
def from_pydcs(cls, bulls: Dict[str, float]) -> Bullseye:
return cls(Point(bulls["x"], bulls["y"]))
def to_pydcs(self) -> Dict[str, float]:
return {"x": self.position.x, "y": self.position.y}
def to_lat_lon(self, theater: ConflictTheater) -> LatLon:
return theater.point_to_ll(self.position)

8
game/theater/caucasus.py Normal file
View File

@@ -0,0 +1,8 @@
from game.theater.projections import TransverseMercator
PARAMETERS = TransverseMercator(
central_meridian=33,
false_easting=-99516.9999999732,
false_northing=-4998114.999999984,
scale_factor=0.9996,
)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

179
game/theater/frontline.py Normal file
View File

@@ -0,0 +1,179 @@
from __future__ import annotations
import logging
from dataclasses import dataclass
from typing import Iterator, List, Tuple
from dcs.mapping import Point
from gen.flights.flight import FlightType
from .controlpoint import (
ControlPoint,
MissionTarget,
)
from ..utils import pairwise
FRONTLINE_MIN_CP_DISTANCE = 5000
@dataclass
class FrontLineSegment:
"""
Describes a line segment of a FrontLine
"""
point_a: Point
point_b: Point
@property
def attack_heading(self) -> float:
"""The heading of the frontline segment from player to enemy control point"""
return self.point_a.heading_between_point(self.point_b)
@property
def attack_distance(self) -> float:
"""Length of the segment"""
return self.point_a.distance_to_point(self.point_b)
class FrontLine(MissionTarget):
"""Defines a front line location between two control points.
Front lines are the area where ground combat happens.
Overwrites the entirety of MissionTarget __init__ method to allow for
dynamic position calculation.
"""
def __init__(
self,
blue_point: ControlPoint,
red_point: ControlPoint,
) -> None:
self.blue_cp = blue_point
self.red_cp = red_point
try:
route = list(blue_point.convoy_route_to(red_point))
except KeyError:
# Some campaigns are air only and the mission generator currently relies on
# *some* "front line" being drawn between these two. In this case there will
# be no supply route to follow. Just create an arbitrary route between the
# two points.
route = [blue_point.position, red_point.position]
# Snap the beginning and end points to the CPs rather than the convoy waypoints,
# which are on roads.
route[0] = blue_point.position
route[-1] = red_point.position
self.segments: List[FrontLineSegment] = [
FrontLineSegment(a, b) for a, b in pairwise(route)
]
self.name = f"Front line {blue_point}/{red_point}"
def control_point_hostile_to(self, player: bool) -> ControlPoint:
if player:
return self.red_cp
return self.blue_cp
def is_friendly(self, to_player: bool) -> bool:
"""Returns True if the objective is in friendly territory."""
return False
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
yield from [
FlightType.CAS,
FlightType.AEWC,
# TODO: FlightType.TROOP_TRANSPORT
# TODO: FlightType.EVAC
]
yield from super().mission_types(for_player)
@property
def position(self):
"""
The position where the conflict should occur
according to the current strength of each control point.
"""
return self.point_from_a(self._position_distance)
@property
def points(self) -> Iterator[Point]:
yield self.segments[0].point_a
for segment in self.segments:
yield segment.point_b
@property
def control_points(self) -> Tuple[ControlPoint, ControlPoint]:
"""Returns a tuple of the two control points."""
return self.blue_cp, self.red_cp
@property
def attack_distance(self):
"""The total distance of all segments"""
return sum(i.attack_distance for i in self.segments)
@property
def attack_heading(self):
"""The heading of the active attack segment from player to enemy control point"""
return self.active_segment.attack_heading
@property
def active_segment(self) -> FrontLineSegment:
"""The FrontLine segment where there can be an active conflict"""
if self._position_distance <= self.segments[0].attack_distance:
return self.segments[0]
remaining_dist = self._position_distance
for segment in self.segments:
if remaining_dist <= segment.attack_distance:
return segment
else:
remaining_dist -= segment.attack_distance
logging.error(
"Frontline attack distance is greater than the sum of its segments"
)
return self.segments[0]
def point_from_a(self, distance: float) -> Point:
"""
Returns a point {distance} away from control_point_a along the frontline segments.
"""
if distance < self.segments[0].attack_distance:
return self.blue_cp.position.point_from_heading(
self.segments[0].attack_heading, distance
)
remaining_dist = distance
for segment in self.segments:
if remaining_dist < segment.attack_distance:
return segment.point_a.point_from_heading(
segment.attack_heading, remaining_dist
)
else:
remaining_dist -= segment.attack_distance
@property
def _position_distance(self) -> float:
"""
The distance from point "a" where the conflict should occur
according to the current strength of each control point
"""
total_strength = self.blue_cp.base.strength + self.red_cp.base.strength
if self.blue_cp.base.strength == 0:
return self._adjust_for_min_dist(0)
if self.red_cp.base.strength == 0:
return self._adjust_for_min_dist(self.attack_distance)
strength_pct = self.blue_cp.base.strength / total_strength
return self._adjust_for_min_dist(strength_pct * self.attack_distance)
def _adjust_for_min_dist(self, distance: float) -> float:
"""
Ensures the frontline conflict is never located within the minimum distance
constant of either end control point.
"""
if (distance > self.attack_distance / 2) and (
distance + FRONTLINE_MIN_CP_DISTANCE > self.attack_distance
):
distance = self.attack_distance - FRONTLINE_MIN_CP_DISTANCE
elif (distance < self.attack_distance / 2) and (
distance < FRONTLINE_MIN_CP_DISTANCE
):
distance = FRONTLINE_MIN_CP_DISTANCE
return distance

View File

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

34
game/theater/latlon.py Normal file
View File

@@ -0,0 +1,34 @@
from dataclasses import dataclass
from typing import List, Tuple
@dataclass(frozen=True)
class LatLon:
latitude: float
longitude: float
def as_list(self) -> List[float]:
return [self.latitude, self.longitude]
@staticmethod
def _components(dimension: float) -> Tuple[int, int, float]:
degrees = int(dimension)
minutes = int(dimension * 60 % 60)
seconds = dimension * 3600 % 60
return degrees, minutes, seconds
def _format_component(
self, dimension: float, hemispheres: Tuple[str, str], seconds_precision: int
) -> str:
hemisphere = hemispheres[0] if dimension >= 0 else hemispheres[1]
degrees, minutes, seconds = self._components(dimension)
return f"{degrees}°{minutes:02}'{seconds:02.{seconds_precision}f}\"{hemisphere}"
def format_dms(self, include_decimal_seconds: bool = False) -> str:
precision = 2 if include_decimal_seconds else 0
return " ".join(
[
self._format_component(self.latitude, ("N", "S"), precision),
self._format_component(self.longitude, ("E", "W"), precision),
]
)

View File

@@ -1,8 +1,9 @@
from __future__ import annotations
from typing import Iterator, TYPE_CHECKING
from typing import Iterator, TYPE_CHECKING, List, Union
from dcs.mapping import Point
from dcs.unit import Unit
if TYPE_CHECKING:
from gen.flights.flight import FlightType
@@ -29,15 +30,20 @@ class MissionTarget:
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType
if self.is_friendly(for_player):
yield FlightType.BARCAP
else:
yield from [
FlightType.ESCORT,
FlightType.TARCAP,
FlightType.SEAD,
FlightType.SEAD_ESCORT,
FlightType.SWEEP,
# TODO: FlightType.ELINT,
# TODO: FlightType.EWAR,
# TODO: FlightType.RECON,
]
@property
def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
return []

8
game/theater/nevada.py Normal file
View File

@@ -0,0 +1,8 @@
from game.theater.projections import TransverseMercator
PARAMETERS = TransverseMercator(
central_meridian=-117,
false_easting=-193996.80999964548,
false_northing=-4410028.063999966,
scale_factor=0.9996,
)

8
game/theater/normandy.py Normal file
View File

@@ -0,0 +1,8 @@
from game.theater.projections import TransverseMercator
PARAMETERS = TransverseMercator(
central_meridian=-3,
false_easting=-195526.00000000204,
false_northing=-5484812.999999951,
scale_factor=0.9996,
)

View File

@@ -0,0 +1,8 @@
from game.theater.projections import TransverseMercator
PARAMETERS = TransverseMercator(
central_meridian=57,
false_easting=75755.99999999645,
false_northing=-2894933.0000000377,
scale_factor=0.9996,
)

View File

@@ -0,0 +1,31 @@
from dataclasses import dataclass
from pyproj import CRS
@dataclass(frozen=True)
class TransverseMercator:
central_meridian: int
false_easting: float
false_northing: float
scale_factor: float
def to_crs(self) -> CRS:
return CRS.from_proj4(
" ".join(
[
"+proj=tmerc",
"+lat_0=0",
f"+lon_0={self.central_meridian}",
f"+k_0={self.scale_factor}",
f"+x_0={self.false_easting}",
f"+y_0={self.false_northing}",
"+towgs84=0,0,0,0,0,0,0",
"+units=m",
"+vunits=m",
"+ellps=WGS84",
"+no_defs",
"+axis=neu",
]
)
)

View File

@@ -5,7 +5,7 @@ import pickle
import random
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Dict, Iterable, List, Optional, Set
from typing import Any, Dict, Iterable, List, Set
from dcs.mapping import Point
from dcs.task import CAP, CAS, PinpointStrike
@@ -13,32 +13,34 @@ from dcs.vehicles import AirDefence
from game import Game, db
from game.factions.faction import Faction
from game.theater import Carrier, Lha, LocationType
from game.scenery_group import SceneryGroup
from game.theater import Carrier, Lha, PointWithHeading
from game.theater.theatergroundobject import (
BuildingGroundObject,
CarrierGroundObject,
EwrGroundObject,
FactoryGroundObject,
LhaGroundObject,
MissileSiteGroundObject,
SamGroundObject,
ShipGroundObject,
SceneryGroundObject,
VehicleGroupGroundObject,
CoastalSiteGroundObject,
)
from game.version import VERSION
from gen import namegen
from gen.coastal.coastal_group_generator import generate_coastal_group
from gen.defenses.armor_group_generator import generate_armor_group
from gen.fleet.ship_group_generator import (
generate_carrier_group,
generate_lha_group,
generate_ship_group,
)
from gen.locations.preset_location_finder import MizDataLocationFinder
from gen.missiles.missiles_group_generator import generate_missile_group
from gen.sam.airdefensegroupgenerator import AirDefenseRange
from gen.sam.sam_group_generator import (
generate_anti_air_group,
generate_ewr_group,
)
from gen.sam.ewr_group_generator import generate_ewr_group
from gen.sam.sam_group_generator import generate_anti_air_group
from . import (
ConflictTheater,
ControlPoint,
@@ -46,6 +48,7 @@ from . import (
Fob,
OffMapSpawn,
)
from ..profiling import logged_duration
from ..settings import Settings
GroundObjectTemplates = Dict[str, Dict[str, Any]]
@@ -76,9 +79,14 @@ class GeneratorSettings:
class GameGenerator:
def __init__(self, player: str, enemy: str, theater: ConflictTheater,
settings: Settings,
generator_settings: GeneratorSettings) -> None:
def __init__(
self,
player: str,
enemy: str,
theater: ConflictTheater,
settings: Settings,
generator_settings: GeneratorSettings,
) -> None:
self.player = player
self.enemy = enemy
self.theater = theater
@@ -86,21 +94,23 @@ class GameGenerator:
self.generator_settings = generator_settings
def generate(self) -> Game:
# Reset name generator
namegen.reset()
self.prepare_theater()
game = Game(
player_name=self.player,
enemy_name=self.enemy,
theater=self.theater,
start_date=self.generator_settings.start_date,
settings=self.settings,
player_budget=self.generator_settings.player_budget,
enemy_budget=self.generator_settings.enemy_budget
)
with logged_duration("TGO population"):
# Reset name generator
namegen.reset()
self.prepare_theater()
game = Game(
player_name=self.player,
enemy_name=self.enemy,
theater=self.theater,
start_date=self.generator_settings.start_date,
settings=self.settings,
player_budget=self.generator_settings.player_budget,
enemy_budget=self.generator_settings.enemy_budget,
)
GroundObjectGenerator(game, self.generator_settings).generate()
GroundObjectGenerator(game, self.generator_settings).generate()
game.settings.version = VERSION
game.begin_turn_0()
return game
def prepare_theater(self) -> None:
@@ -108,7 +118,7 @@ class GameGenerator:
# Auto-capture half the bases if midgame.
if self.generator_settings.midgame:
control_points = self.theater.controlpoints
for control_point in control_points[:len(control_points) // 2]:
for control_point in control_points[: len(control_points) // 2]:
control_point.captured = True
# Remove carrier and lha, invert situation if needed
@@ -135,156 +145,16 @@ class GameGenerator:
cp.captured = True
class LocationFinder:
def __init__(self, game: Game, control_point: ControlPoint) -> None:
self.game = game
self.control_point = control_point
self.miz_data = MizDataLocationFinder.compute_possible_locations(
game.theater.terrain.name, control_point.full_name)
def location_for(self, location_type: LocationType) -> Optional[Point]:
position = self.control_point.preset_locations.random_for(location_type)
if position is not None:
return position
logging.warning(f"No campaign location for %s at %s",
location_type.value, self.control_point)
position = self.random_from_miz_data(
location_type == LocationType.OffshoreStrikeTarget)
if position is not None:
return position
logging.debug(f"No mizdata location for %s at %s", location_type.value,
self.control_point)
position = self.random_position(location_type)
if position is not None:
return position
logging.error(f"Could not find position for %s at %s",
location_type.value, self.control_point)
return None
def random_from_miz_data(self, offshore: bool) -> Optional[Point]:
if offshore:
locations = self.miz_data.offshore_locations
else:
locations = self.miz_data.ashore_locations
if self.miz_data.offshore_locations:
preset = random.choice(locations)
locations.remove(preset)
return preset.position
return None
def random_position(self, location_type: LocationType) -> Optional[Point]:
# TODO: Flesh out preset locations so we never hit this case.
logging.warning("Falling back to random location for %s at %s",
location_type.value, self.control_point)
is_base_defense = location_type in {
LocationType.BaseAirDefense,
LocationType.Garrison,
LocationType.Shorad,
}
on_land = location_type not in {
LocationType.OffshoreStrikeTarget,
LocationType.Ship,
}
avoid_others = location_type not in {
LocationType.Garrison,
LocationType.MissileSite,
LocationType.Sam,
LocationType.Ship,
LocationType.Shorad,
}
if is_base_defense:
min_range = 400
max_range = 3200
elif location_type == LocationType.Ship:
min_range = 5000
max_range = 40000
elif location_type == LocationType.MissileSite:
min_range = 2500
max_range = 40000
else:
min_range = 10000
max_range = 40000
position = self._find_random_position(min_range, max_range,
on_land, is_base_defense,
avoid_others)
# Retry once, searching a bit further (On some big airbases, 3200 is too
# short (Ex : Incirlik)), but searching farther on every base would be
# problematic, as some base defense units would end up very far away
# from small airfields.
if position is None and is_base_defense:
position = self._find_random_position(3200, 4800,
on_land, is_base_defense,
avoid_others)
return position
def _find_random_position(self, min_range: int, max_range: int,
on_ground: bool, is_base_defense: bool,
avoid_others: bool) -> Optional[Point]:
"""
Find a valid ground object location
:param on_ground: Whether it should be on ground or on sea (True = on
ground)
:param min_range: Minimal range from point
:param max_range: Max range from point
:param is_base_defense: True if the location is for base defense.
:return:
"""
near = self.control_point.position
others = self.control_point.ground_objects
def is_valid(point: Optional[Point]) -> bool:
if point is None:
return False
if on_ground and not self.game.theater.is_on_land(point):
return False
elif not on_ground and not self.game.theater.is_in_sea(point):
return False
if avoid_others:
for other in others:
if other.position.distance_to_point(point) < 10000:
return False
if is_base_defense:
# If it's a base defense we don't care how close it is to other
# points.
return True
# Else verify that it's not too close to another control point.
for control_point in self.game.theater.controlpoints:
if control_point != self.control_point:
if control_point.position.distance_to_point(point) < 30000:
return False
for ground_obj in control_point.ground_objects:
if ground_obj.position.distance_to_point(point) < 10000:
return False
return True
for _ in range(300):
# Check if on land or sea
p = near.random_point_within(max_range, min_range)
if is_valid(p):
return p
return None
class ControlPointGroundObjectGenerator:
def __init__(self, game: Game, generator_settings: GeneratorSettings,
control_point: ControlPoint) -> None:
def __init__(
self,
game: Game,
generator_settings: GeneratorSettings,
control_point: ControlPoint,
) -> None:
self.game = game
self.generator_settings = generator_settings
self.control_point = control_point
self.location_finder = LocationFinder(game, control_point)
@property
def faction_name(self) -> str:
@@ -300,10 +170,7 @@ class ControlPointGroundObjectGenerator:
def generate(self) -> bool:
self.control_point.connected_objectives = []
if self.faction.navy_generators:
# Even airbases can generate navies if they are close enough to the
# water. This is not controlled by the control point definition, but
# rather by whether or not the generator can find a valid position
# for the ship.
# Even airbases can generate navies if they are close enough to the water.
self.generate_navy()
return True
@@ -317,19 +184,15 @@ class ControlPointGroundObjectGenerator:
if not self.control_point.captured and skip_enemy_navy:
return
for _ in range(self.faction.navy_group_count):
self.generate_ship()
def generate_ship(self) -> None:
point = self.location_finder.location_for(
LocationType.OffshoreStrikeTarget)
if point is None:
return
for position in self.control_point.preset_locations.ships:
self.generate_ship_at(position)
def generate_ship_at(self, position: PointWithHeading) -> None:
group_id = self.game.next_group_id()
g = ShipGroundObject(namegen.random_objective_name(), group_id, point,
self.control_point)
g = ShipGroundObject(
namegen.random_objective_name(), group_id, position, self.control_point
)
group = generate_ship_group(self.game, g, self.faction_name)
g.groups = []
@@ -352,13 +215,15 @@ class CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator):
if not carrier_names:
logging.info(
f"Skipping generation of {self.control_point.name} because "
f"{self.faction_name} has no carriers")
f"{self.faction_name} has no carriers"
)
return False
# Create ground object group
group_id = self.game.next_group_id()
g = CarrierGroundObject(namegen.random_objective_name(), group_id,
self.control_point)
g = CarrierGroundObject(
namegen.random_objective_name(), group_id, self.control_point
)
group = generate_carrier_group(self.faction_name, self.game, g)
g.groups = []
if group is not None:
@@ -377,13 +242,15 @@ class LhaGroundObjectGenerator(ControlPointGroundObjectGenerator):
if not lha_names:
logging.info(
f"Skipping generation of {self.control_point.name} because "
f"{self.faction_name} has no LHAs")
f"{self.faction_name} has no LHAs"
)
return False
# Create ground object group
group_id = self.game.next_group_id()
g = LhaGroundObject(namegen.random_objective_name(), group_id,
self.control_point)
g = LhaGroundObject(
namegen.random_objective_name(), group_id, self.control_point
)
group = generate_lha_group(self.faction_name, self.game, g)
g.groups = []
if group is not None:
@@ -393,144 +260,14 @@ class LhaGroundObjectGenerator(ControlPointGroundObjectGenerator):
return True
class BaseDefenseGenerator:
def __init__(self, game: Game, control_point: ControlPoint) -> None:
self.game = game
self.control_point = control_point
self.location_finder = LocationFinder(game, control_point)
@property
def faction_name(self) -> str:
if self.control_point.captured:
return self.game.player_name
else:
return self.game.enemy_name
@property
def faction(self) -> Faction:
return db.FACTIONS[self.faction_name]
def generate(self) -> None:
self.generate_ewr()
self.generate_garrison()
self.generate_base_defenses()
def generate_ewr(self) -> None:
position = self.location_finder.location_for(LocationType.Ewr)
if position is None:
return
group_id = self.game.next_group_id()
g = EwrGroundObject(namegen.random_objective_name(), group_id,
position, self.control_point)
group = generate_ewr_group(self.game, g, self.faction)
if group is None:
logging.error(f"Could not generate EWR at {self.control_point}")
return
g.groups = [group]
self.control_point.base_defenses.append(g)
def generate_base_defenses(self) -> None:
# First group has a 1/2 chance of being a SAM, 1/6 chance of SHORAD,
# and a 1/6 chance of a garrison.
#
# Further groups have a 1/3 chance of being SHORAD and 2/3 chance of
# being a garrison.
for i in range(random.randint(2, 5)):
if i == 0 and random.randint(0, 1) == 0:
self.generate_sam()
elif random.randint(0, 2) == 1:
self.generate_shorad()
else:
self.generate_garrison()
def generate_garrison(self) -> None:
position = self.location_finder.location_for(LocationType.Garrison)
if position is None:
return
group_id = self.game.next_group_id()
g = VehicleGroupGroundObject(namegen.random_objective_name(), group_id,
position, self.control_point,
for_airbase=True)
group = generate_armor_group(self.faction_name, self.game, g)
if group is None:
logging.error(
f"Could not generate garrison at {self.control_point}")
return
g.groups.append(group)
self.control_point.base_defenses.append(g)
def generate_sam(self) -> None:
position = self.location_finder.location_for(
LocationType.BaseAirDefense)
if position is None:
return
group_id = self.game.next_group_id()
g = SamGroundObject(namegen.random_objective_name(), group_id,
position, self.control_point, for_airbase=True)
groups = generate_anti_air_group(self.game, g, self.faction)
if not groups:
logging.error(f"Could not generate SAM at {self.control_point}")
return
g.groups = groups
self.control_point.base_defenses.append(g)
def generate_shorad(self) -> None:
position = self.location_finder.location_for(
LocationType.BaseAirDefense)
if position is None:
return
group_id = self.game.next_group_id()
g = SamGroundObject(namegen.random_objective_name(), group_id,
position, self.control_point, for_airbase=True)
groups = generate_anti_air_group(self.game, g, self.faction,
ranges=[{AirDefenseRange.Short}])
if not groups:
logging.error(
f"Could not generate SHORAD group at {self.control_point}")
return
g.groups = groups
self.control_point.base_defenses.append(g)
class FobDefenseGenerator(BaseDefenseGenerator):
def generate(self) -> None:
self.generate_garrison()
self.generate_fob_defenses()
def generate_fob_defenses(self):
# First group has a 1/2 chance of being a SHORAD,
# and a 1/2 chance of a garrison.
#
# Further groups have a 1/3 chance of being SHORAD and 2/3 chance of
# being a garrison.
for i in range(random.randint(2, 5)):
if i == 0 and random.randint(0, 1) == 0:
self.generate_shorad()
elif i == 0 and random.randint(0, 1) == 0:
self.generate_garrison()
elif random.randint(0, 2) == 1:
self.generate_shorad()
else:
self.generate_garrison()
class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
def __init__(self, game: Game, generator_settings: GeneratorSettings,
control_point: ControlPoint,
templates: GroundObjectTemplates) -> None:
def __init__(
self,
game: Game,
generator_settings: GeneratorSettings,
control_point: ControlPoint,
templates: GroundObjectTemplates,
) -> None:
super().__init__(game, generator_settings, control_point)
self.templates = templates
@@ -538,77 +275,93 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
if not super().generate():
return False
BaseDefenseGenerator(self.game, self.control_point).generate()
self.generate_ground_points()
if self.faction.missiles:
self.generate_missile_sites()
return True
def generate_ground_points(self) -> None:
"""Generate ground objects and AA sites for the control point."""
skip_sams = self.generate_required_aa()
self.generate_armor_groups()
self.generate_aa()
self.generate_ewrs()
self.generate_scenery_sites()
self.generate_strike_targets()
self.generate_offshore_strike_targets()
self.generate_factories()
self.generate_ammunition_depots()
if self.control_point.is_global:
if self.faction.missiles:
self.generate_missile_sites()
if self.faction.coastal_defenses:
self.generate_coastal_sites()
def generate_armor_groups(self) -> None:
for position in self.control_point.preset_locations.armor_groups:
self.generate_armor_at(position)
def generate_armor_at(self, position: PointWithHeading) -> None:
group_id = self.game.next_group_id()
g = VehicleGroupGroundObject(
namegen.random_objective_name(),
group_id,
position,
self.control_point,
)
group = generate_armor_group(self.faction_name, self.game, g)
if group is None:
logging.error(
"Could not generate armor group for %s at %s",
g.name,
self.control_point,
)
return
g.groups = [group]
self.control_point.connected_objectives.append(g)
# Always generate at least one AA point.
self.generate_aa_site()
# And between 2 and 7 other objectives.
amount = random.randrange(2, 7)
for i in range(amount):
# 1 in 4 additional objectives are AA.
if random.randint(0, 3) == 0:
if skip_sams > 0:
skip_sams -= 1
else:
self.generate_aa_site()
else:
self.generate_ground_point()
def generate_required_aa(self) -> int:
"""Generates the AA sites that are required by the campaign.
Returns:
The number of AA sites that were generated.
"""
def generate_aa(self) -> None:
presets = self.control_point.preset_locations
for position in presets.required_long_range_sams:
self.generate_aa_at(position, ranges=[
{AirDefenseRange.Long},
{AirDefenseRange.Medium},
{AirDefenseRange.Short},
])
for position in presets.required_medium_range_sams:
self.generate_aa_at(position, ranges=[
{AirDefenseRange.Medium},
{AirDefenseRange.Short},
])
return (len(presets.required_long_range_sams) +
len(presets.required_medium_range_sams))
for position in presets.long_range_sams:
self.generate_aa_at(
position,
ranges=[
{AirDefenseRange.Long},
{AirDefenseRange.Medium},
{AirDefenseRange.Short},
{AirDefenseRange.AAA},
],
)
for position in presets.medium_range_sams:
self.generate_aa_at(
position,
ranges=[
{AirDefenseRange.Medium},
{AirDefenseRange.Short},
{AirDefenseRange.AAA},
],
)
for position in presets.short_range_sams:
self.generate_aa_at(
position,
ranges=[{AirDefenseRange.Short}, {AirDefenseRange.AAA}],
)
for position in presets.aaa:
self.generate_aa_at(
position,
ranges=[{AirDefenseRange.AAA}],
)
def generate_ground_point(self) -> None:
try:
category = random.choice(self.faction.building_set)
except IndexError:
logging.exception("Faction has no buildings defined")
return
def generate_ewrs(self) -> None:
presets = self.control_point.preset_locations
for position in presets.ewrs:
self.generate_ewr_at(position)
def generate_strike_target_at(self, category: str, position: Point) -> None:
obj_name = namegen.random_objective_name()
template = random.choice(list(self.templates[category].values()))
if category == "oil":
location_type = LocationType.OffshoreStrikeTarget
else:
location_type = LocationType.StrikeTarget
# Pick from preset locations
point = self.location_finder.location_for(location_type)
if point is None:
return
object_id = 0
group_id = self.game.next_group_id()
@@ -618,49 +371,126 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
template_point = Point(unit["offset"].x, unit["offset"].y)
g = BuildingGroundObject(
obj_name, category, group_id, object_id, point + template_point,
unit["heading"], self.control_point, unit["type"])
obj_name,
category,
group_id,
object_id,
position + template_point,
unit["heading"],
self.control_point,
unit["type"],
)
self.control_point.connected_objectives.append(g)
def generate_aa_site(self) -> None:
position = self.location_finder.location_for(LocationType.Sam)
if position is None:
return
self.generate_aa_at(position, ranges=[
# Prefer to use proper SAMs, but fall back to SHORADs if needed.
{AirDefenseRange.Long, AirDefenseRange.Medium},
{AirDefenseRange.Short},
])
def generate_ammunition_depots(self) -> None:
for position in self.control_point.preset_locations.ammunition_depots:
self.generate_strike_target_at(category="ammo", position=position)
def generate_aa_at(
self, position: Point,
ranges: Iterable[Set[AirDefenseRange]]) -> None:
def generate_factories(self) -> None:
for position in self.control_point.preset_locations.factories:
self.generate_factory_at(position)
def generate_factory_at(self, point: PointWithHeading) -> None:
obj_name = namegen.random_objective_name()
group_id = self.game.next_group_id()
g = SamGroundObject(namegen.random_objective_name(), group_id,
position, self.control_point, for_airbase=False)
g = FactoryGroundObject(
obj_name,
group_id,
point,
point.heading,
self.control_point,
)
self.control_point.connected_objectives.append(g)
def generate_aa_at(
self, position: Point, ranges: Iterable[Set[AirDefenseRange]]
) -> None:
group_id = self.game.next_group_id()
g = SamGroundObject(
namegen.random_objective_name(),
group_id,
position,
self.control_point,
)
groups = generate_anti_air_group(self.game, g, self.faction, ranges)
if not groups:
logging.error("Could not generate air defense group for %s at %s",
g.name, self.control_point)
logging.error(
"Could not generate air defense group for %s at %s",
g.name,
self.control_point,
)
return
g.groups = groups
self.control_point.connected_objectives.append(g)
def generate_missile_sites(self) -> None:
for i in range(self.faction.missiles_group_count):
self.generate_missile_site()
def generate_missile_site(self) -> None:
position = self.location_finder.location_for(LocationType.MissileSite)
if position is None:
return
def generate_ewr_at(self, position: PointWithHeading) -> None:
group_id = self.game.next_group_id()
g = MissileSiteGroundObject(namegen.random_objective_name(), group_id,
position, self.control_point)
g = EwrGroundObject(
namegen.random_objective_name(),
group_id,
position,
self.control_point,
)
group = generate_ewr_group(self.game, g, self.faction)
if group is None:
logging.error(
"Could not generate ewr group for %s at %s",
g.name,
self.control_point,
)
return
g.groups = [group]
self.control_point.connected_objectives.append(g)
def generate_scenery_sites(self) -> None:
presets = self.control_point.preset_locations
for scenery_group in presets.scenery:
self.generate_tgo_for_scenery(scenery_group)
def generate_tgo_for_scenery(self, scenery: SceneryGroup) -> None:
obj_name = namegen.random_objective_name()
category = scenery.category
group_id = self.game.next_group_id()
object_id = 0
# Each nested trigger zone is a target/building/unit for an objective.
for zone in scenery.zones:
object_id += 1
local_position = zone.position
local_dcs_identifier = zone.name
g = SceneryGroundObject(
obj_name,
category,
group_id,
object_id,
local_position,
self.control_point,
local_dcs_identifier,
zone,
)
self.control_point.connected_objectives.append(g)
return
def generate_missile_sites(self) -> None:
for position in self.control_point.preset_locations.missile_sites:
self.generate_missile_site_at(position)
def generate_missile_site_at(self, position: PointWithHeading) -> None:
group_id = self.game.next_group_id()
g = MissileSiteGroundObject(
namegen.random_objective_name(), group_id, position, self.control_point
)
group = generate_missile_group(self.game, g, self.faction_name)
g.groups = []
if group is not None:
@@ -668,21 +498,66 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
self.control_point.connected_objectives.append(g)
return
def generate_coastal_sites(self) -> None:
for position in self.control_point.preset_locations.coastal_defenses:
self.generate_coastal_site_at(position)
def generate_coastal_site_at(self, position: PointWithHeading) -> None:
group_id = self.game.next_group_id()
g = CoastalSiteGroundObject(
namegen.random_objective_name(),
group_id,
position,
self.control_point,
position.heading,
)
group = generate_coastal_group(self.game, g, self.faction_name)
g.groups = []
if group is not None:
g.groups.append(group)
self.control_point.connected_objectives.append(g)
return
def generate_strike_targets(self) -> None:
building_set = list(set(self.faction.building_set) - {"oil"})
if not building_set:
logging.error("Faction has no buildings defined")
return
for position in self.control_point.preset_locations.strike_locations:
category = random.choice(building_set)
self.generate_strike_target_at(category, position)
def generate_offshore_strike_targets(self) -> None:
if "oil" not in self.faction.building_set:
logging.error("Faction does not support offshore strike targets")
return
for position in self.control_point.preset_locations.offshore_strike_locations:
self.generate_strike_target_at("oil", position)
class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
def generate(self) -> bool:
self.generate_fob()
FobDefenseGenerator(self.game, self.control_point).generate()
self.generate_required_aa()
self.generate_armor_groups()
self.generate_factories()
self.generate_ammunition_depots()
self.generate_aa()
self.generate_ewrs()
self.generate_scenery_sites()
self.generate_strike_targets()
self.generate_offshore_strike_targets()
if self.faction.missiles:
self.generate_missile_sites()
if self.faction.coastal_defenses:
self.generate_coastal_sites()
return True
def generate_fob(self) -> None:
try:
category = self.faction.building_set[self.faction.building_set.index('fob')]
except IndexError:
logging.exception("Faction has no fob buildings defined")
return
category = "fob"
obj_name = self.control_point.name
template = random.choice(list(self.templates[category].values()))
point = self.control_point.position
@@ -696,14 +571,21 @@ class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
template_point = Point(unit["offset"].x, unit["offset"].y)
g = BuildingGroundObject(
obj_name, category, group_id, object_id, point + template_point,
unit["heading"], self.control_point, unit["type"], airbase_group=True)
obj_name,
category,
group_id,
object_id,
point + template_point,
unit["heading"],
self.control_point,
unit["type"],
is_fob_structure=True,
)
self.control_point.connected_objectives.append(g)
class GroundObjectGenerator:
def __init__(self, game: Game,
generator_settings: GeneratorSettings) -> None:
def __init__(self, game: Game, generator_settings: GeneratorSettings) -> None:
self.game = game
self.generator_settings = generator_settings
with open("resources/groundobject_templates.p", "rb") as f:
@@ -721,19 +603,22 @@ class GroundObjectGenerator:
generator: ControlPointGroundObjectGenerator
if control_point.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP:
generator = CarrierGroundObjectGenerator(
self.game, self.generator_settings, control_point)
self.game, self.generator_settings, control_point
)
elif control_point.cptype == ControlPointType.LHA_GROUP:
generator = LhaGroundObjectGenerator(
self.game, self.generator_settings, control_point)
self.game, self.generator_settings, control_point
)
elif isinstance(control_point, OffMapSpawn):
generator = NoOpGroundObjectGenerator(
self.game, self.generator_settings, control_point)
self.game, self.generator_settings, control_point
)
elif isinstance(control_point, Fob):
generator = FobGroundObjectGenerator(
self.game, self.generator_settings, control_point,
self.templates)
self.game, self.generator_settings, control_point, self.templates
)
else:
generator = AirbaseGroundObjectGenerator(
self.game, self.generator_settings, control_point,
self.templates)
self.game, self.generator_settings, control_point, self.templates
)
return generator.generate()

8
game/theater/syria.py Normal file
View File

@@ -0,0 +1,8 @@
from game.theater.projections import TransverseMercator
PARAMETERS = TransverseMercator(
central_meridian=39,
false_easting=282801.00000003993,
false_northing=-3879865.9999999935,
scale_factor=0.9996,
)

View File

@@ -2,14 +2,20 @@ from __future__ import annotations
import itertools
import logging
from typing import Iterator, List, TYPE_CHECKING
from typing import Iterator, List, TYPE_CHECKING, Union
from dcs.mapping import Point
from dcs.triggers import TriggerZone
from dcs.unit import Unit
from dcs.unitgroup import Group
from dcs.unittype import VehicleType
from .. import db
from ..data.radar_db import UNITS_WITH_RADAR
from ..data.radar_db import (
TRACK_RADARS,
TELARS,
LAUNCHER_TRACKER_PAIRS,
)
from ..utils import Distance, meters
if TYPE_CHECKING:
@@ -19,76 +25,46 @@ if TYPE_CHECKING:
from .missiontarget import MissionTarget
NAME_BY_CATEGORY = {
"power": "Power plant",
"ammo": "Ammo depot",
"fuel": "Fuel depot",
"ewr": "Early Warning Radar",
"aa": "AA Defense Site",
"ware": "Warehouse",
"farp": "FARP",
"fob": "FOB",
"factory": "Factory",
"comms": "Comms. tower",
"oil": "Oil platform",
"derrick": "Derrick",
"ww2bunker": "Bunker",
"village": "Village",
"allycamp": "Camp",
"EWR":"EWR",
}
ABBREV_NAME = {
"power": "PLANT",
"ammo": "AMMO",
"fuel": "FUEL",
"aa": "AA",
"ware": "WARE",
"ammo": "Ammo depot",
"armor": "Armor group",
"coastal": "Coastal defense",
"comms": "Communications tower",
"derrick": "Derrick",
"factory": "Factory",
"farp": "FARP",
"fob": "FOB",
"factory": "FACTORY",
"comms": "COMMST",
"oil": "OILP",
"derrick": "DERK",
"ww2bunker": "BUNK",
"village": "VLG",
"allycamp": "CMP",
}
CATEGORY_MAP = {
# Special cases
"CARRIER": ["CARRIER"],
"LHA": ["LHA"],
"aa": ["AA"],
# Buildings
"power": ["Workshop A", "Electric power box", "Garage small A", "Farm B", "Repair workshop", "Garage B"],
"ware": ["Warehouse", "Hangar A"],
"fuel": ["Tank", "Tank 2", "Tank 3", "Fuel tank"],
"ammo": [".Ammunition depot", "Hangar B"],
"farp": ["FARP Tent", "FARP Ammo Dump Coating", "FARP Fuel Depot", "FARP Command Post", "FARP CP Blindage"],
"fob": ["Bunker 2", "Bunker 1", "Garage small B", ".Command Center", "Barracks 2"],
"factory": ["Tech combine", "Tech hangar A"],
"comms": ["TV tower", "Comms tower M"],
"oil": ["Oil platform"],
"derrick": ["Oil derrick", "Pump station", "Subsidiary structure 2"],
"ww2bunker": ["Siegfried Line", "Fire Control Bunker", "SK_C_28_naval_gun", "Concertina Wire", "Czech hedgehogs 1"],
"village": ["Small house 1B", "Small House 1A", "Small warehouse 1"],
"allycamp": [],
"fuel": "Fuel depot",
"missile": "Missile site",
"oil": "Oil platform",
"power": "Power plant",
"ship": "Ship",
"village": "Village",
"ware": "Warehouse",
"ww2bunker": "Bunker",
}
class TheaterGroundObject(MissionTarget):
def __init__(self, name: str, category: str, group_id: int, position: Point,
heading: int, control_point: ControlPoint, dcs_identifier: str,
airbase_group: bool, sea_object: bool) -> None:
def __init__(
self,
name: str,
category: str,
group_id: int,
position: Point,
heading: int,
control_point: ControlPoint,
dcs_identifier: str,
sea_object: bool,
) -> None:
super().__init__(name, position)
self.category = category
self.group_id = group_id
self.heading = heading
self.control_point = control_point
self.dcs_identifier = dcs_identifier
self.airbase_group = airbase_group
self.sea_object = sea_object
self.groups: List[Group] = []
@@ -103,6 +79,17 @@ class TheaterGroundObject(MissionTarget):
"""
return list(itertools.chain.from_iterable([g.units for g in self.groups]))
@property
def dead_units(self) -> List[Unit]:
"""
:return: all the dead units at this location
"""
return list(
itertools.chain.from_iterable(
[getattr(g, "units_losts", []) for g in self.groups]
)
)
@property
def group_name(self) -> str:
"""The name of the unit group."""
@@ -131,6 +118,7 @@ class TheaterGroundObject(MissionTarget):
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType
if self.is_friendly(for_player):
yield from [
# TODO: FlightType.LOGISTICS
@@ -152,12 +140,11 @@ class TheaterGroundObject(MissionTarget):
return False
@property
def has_radar(self) -> bool:
"""Returns True if the ground object contains a unit with radar."""
def has_live_radar_sam(self) -> bool:
"""Returns True if the ground object contains a unit with working radar SAM."""
for group in self.groups:
for unit in group.units:
if db.unit_type_from_name(unit.type) in UNITS_WITH_RADAR:
return True
if self.threat_range(group, radar_only=True):
return True
return False
def _max_range_of_type(self, group: Group, range_type: str) -> Distance:
@@ -178,24 +165,60 @@ class TheaterGroundObject(MissionTarget):
max_range = max(max_range, meters(unit_range))
return max_range
def max_detection_range(self) -> Distance:
return max(self.detection_range(g) for g in self.groups)
def detection_range(self, group: Group) -> Distance:
return self._max_range_of_type(group, "detection_range")
def threat_range(self, group: Group) -> Distance:
if not self.detection_range(group):
# For simple SAMs like shilkas, the unit has both a threat and
# detection range. For complex sites like SA-2s, the launcher has a
# threat range and the search/track radars have detection ranges. If
# the site has no detection range it has no radars and can't fire,
# so it's not actually a threat even if it still has launchers.
return meters(0)
def max_threat_range(self) -> Distance:
return max(self.threat_range(g) for g in self.groups)
def threat_range(self, group: Group, radar_only: bool = False) -> Distance:
return self._max_range_of_type(group, "threat_range")
@property
def is_factory(self) -> bool:
return self.category == "factory"
@property
def is_control_point(self) -> bool:
"""True if this TGO is the group for the control point itself (CVs and FOBs)."""
return False
@property
def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
return self.units
@property
def mark_locations(self) -> Iterator[Point]:
yield self.position
def clear(self) -> None:
self.groups = []
@property
def capturable(self) -> bool:
raise NotImplementedError
@property
def purchasable(self) -> bool:
raise NotImplementedError
class BuildingGroundObject(TheaterGroundObject):
def __init__(self, name: str, category: str, group_id: int, object_id: int,
position: Point, heading: int, control_point: ControlPoint,
dcs_identifier: str, airbase_group=False) -> None:
def __init__(
self,
name: str,
category: str,
group_id: int,
object_id: int,
position: Point,
heading: int,
control_point: ControlPoint,
dcs_identifier: str,
is_fob_structure=False,
) -> None:
super().__init__(
name=name,
category=category,
@@ -204,9 +227,9 @@ class BuildingGroundObject(TheaterGroundObject):
heading=heading,
control_point=control_point,
dcs_identifier=dcs_identifier,
airbase_group=airbase_group,
sea_object=False
sea_object=False,
)
self.is_fob_structure = is_fob_structure
self.object_id = object_id
# Other TGOs track deadness based on the number of alive units, but
# buildings don't have groups assigned to the TGO.
@@ -230,10 +253,95 @@ class BuildingGroundObject(TheaterGroundObject):
def kill(self) -> None:
self._dead = True
def iter_building_group(self) -> Iterator[TheaterGroundObject]:
for tgo in self.control_point.ground_objects:
if tgo.obj_name == self.obj_name and not tgo.is_dead:
yield tgo
@property
def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
return list(self.iter_building_group())
@property
def mark_locations(self) -> Iterator[Point]:
for building in self.iter_building_group():
yield building.position
@property
def is_control_point(self) -> bool:
return self.is_fob_structure
@property
def capturable(self) -> bool:
return True
@property
def purchasable(self) -> bool:
return False
class SceneryGroundObject(BuildingGroundObject):
def __init__(
self,
name: str,
category: str,
group_id: int,
object_id: int,
position: Point,
control_point: ControlPoint,
dcs_identifier: str,
zone: TriggerZone,
) -> None:
super().__init__(
name=name,
category=category,
group_id=group_id,
object_id=object_id,
position=position,
heading=0,
control_point=control_point,
dcs_identifier=dcs_identifier,
is_fob_structure=False,
)
self.zone = zone
try:
# In the default TriggerZone using "assign as..." in the DCS Mission Editor,
# property three has the scenery's object ID as its value.
self.map_object_id = self.zone.properties[3]["value"]
except (IndexError, KeyError):
logging.exception(
"Invalid TriggerZone for Scenery definition. The third property must "
"be the map object ID."
)
raise
class FactoryGroundObject(BuildingGroundObject):
def __init__(
self,
name: str,
group_id: int,
position: Point,
heading: int,
control_point: ControlPoint,
) -> None:
super().__init__(
name=name,
category="factory",
group_id=group_id,
object_id=0,
position=position,
heading=heading,
control_point=control_point,
dcs_identifier="Workshop A",
is_fob_structure=False,
)
class NavalGroundObject(TheaterGroundObject):
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType
if not self.is_friendly(for_player):
yield FlightType.ANTISHIP
yield from super().mission_types(for_player)
@@ -242,15 +350,24 @@ class NavalGroundObject(TheaterGroundObject):
def might_have_aa(self) -> bool:
return True
@property
def capturable(self) -> bool:
return False
@property
def purchasable(self) -> bool:
return False
class GenericCarrierGroundObject(NavalGroundObject):
pass
@property
def is_control_point(self) -> bool:
return True
# TODO: Why is this both a CP and a TGO?
class CarrierGroundObject(GenericCarrierGroundObject):
def __init__(self, name: str, group_id: int,
control_point: ControlPoint) -> None:
def __init__(self, name: str, group_id: int, control_point: ControlPoint) -> None:
super().__init__(
name=name,
category="CARRIER",
@@ -259,8 +376,7 @@ class CarrierGroundObject(GenericCarrierGroundObject):
heading=0,
control_point=control_point,
dcs_identifier="CARRIER",
airbase_group=True,
sea_object=True
sea_object=True,
)
@property
@@ -272,8 +388,7 @@ class CarrierGroundObject(GenericCarrierGroundObject):
# TODO: Why is this both a CP and a TGO?
class LhaGroundObject(GenericCarrierGroundObject):
def __init__(self, name: str, group_id: int,
control_point: ControlPoint) -> None:
def __init__(self, name: str, group_id: int, control_point: ControlPoint) -> None:
super().__init__(
name=name,
category="LHA",
@@ -282,8 +397,7 @@ class LhaGroundObject(GenericCarrierGroundObject):
heading=0,
control_point=control_point,
dcs_identifier="LHA",
airbase_group=True,
sea_object=True
sea_object=True,
)
@property
@@ -294,31 +408,69 @@ class LhaGroundObject(GenericCarrierGroundObject):
class MissileSiteGroundObject(TheaterGroundObject):
def __init__(self, name: str, group_id: int, position: Point,
control_point: ControlPoint) -> None:
def __init__(
self, name: str, group_id: int, position: Point, control_point: ControlPoint
) -> None:
super().__init__(
name=name,
category="aa",
category="missile",
group_id=group_id,
position=position,
heading=0,
control_point=control_point,
dcs_identifier="AA",
airbase_group=False,
sea_object=False
sea_object=False,
)
@property
def capturable(self) -> bool:
return False
class BaseDefenseGroundObject(TheaterGroundObject):
"""Base type for all base defenses."""
@property
def purchasable(self) -> bool:
return False
class CoastalSiteGroundObject(TheaterGroundObject):
def __init__(
self,
name: str,
group_id: int,
position: Point,
control_point: ControlPoint,
heading,
) -> None:
super().__init__(
name=name,
category="coastal",
group_id=group_id,
position=position,
heading=heading,
control_point=control_point,
dcs_identifier="AA",
sea_object=False,
)
@property
def capturable(self) -> bool:
return False
@property
def purchasable(self) -> bool:
return False
# TODO: Differentiate types.
# This type gets used both for AA sites (SAM, AAA, or SHORAD). These should each
# be split into their own types.
class SamGroundObject(BaseDefenseGroundObject):
def __init__(self, name: str, group_id: int, position: Point,
control_point: ControlPoint, for_airbase: bool) -> None:
class SamGroundObject(TheaterGroundObject):
def __init__(
self,
name: str,
group_id: int,
position: Point,
control_point: ControlPoint,
) -> None:
super().__init__(
name=name,
category="aa",
@@ -327,8 +479,7 @@ class SamGroundObject(BaseDefenseGroundObject):
heading=0,
control_point=control_point,
dcs_identifier="AA",
airbase_group=for_airbase,
sea_object=False
sea_object=False,
)
# Set by the SAM unit generator if the generated group is compatible
# with Skynet.
@@ -345,44 +496,102 @@ class SamGroundObject(BaseDefenseGroundObject):
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType
if not self.is_friendly(for_player):
yield FlightType.DEAD
yield FlightType.SEAD
yield from super().mission_types(for_player)
@property
def might_have_aa(self) -> bool:
return True
def threat_range(self, group: Group, radar_only: bool = False) -> Distance:
max_non_radar = meters(0)
live_trs = set()
max_telar_range = meters(0)
launchers = set()
for unit in group.units:
unit_type = db.unit_type_from_name(unit.type)
if unit_type is None or not issubclass(unit_type, VehicleType):
continue
if unit_type in TRACK_RADARS:
live_trs.add(unit_type)
elif unit_type in TELARS:
max_telar_range = max(
max_telar_range, meters(getattr(unit_type, "threat_range", 0))
)
elif unit_type in LAUNCHER_TRACKER_PAIRS:
launchers.add(unit_type)
else:
max_non_radar = max(
max_non_radar, meters(getattr(unit_type, "threat_range", 0))
)
max_tel_range = meters(0)
for launcher in launchers:
if LAUNCHER_TRACKER_PAIRS[launcher] in live_trs:
max_tel_range = max(
max_tel_range, meters(getattr(launcher, "threat_range"))
)
if radar_only:
return max(max_tel_range, max_telar_range)
else:
return max(max_tel_range, max_telar_range, max_non_radar)
class VehicleGroupGroundObject(BaseDefenseGroundObject):
def __init__(self, name: str, group_id: int, position: Point,
control_point: ControlPoint, for_airbase: bool) -> None:
@property
def capturable(self) -> bool:
return False
@property
def purchasable(self) -> bool:
return True
class VehicleGroupGroundObject(TheaterGroundObject):
def __init__(
self,
name: str,
group_id: int,
position: Point,
control_point: ControlPoint,
) -> None:
super().__init__(
name=name,
category="aa",
category="armor",
group_id=group_id,
position=position,
heading=0,
control_point=control_point,
dcs_identifier="AA",
airbase_group=for_airbase,
sea_object=False
sea_object=False,
)
@property
def capturable(self) -> bool:
return False
class EwrGroundObject(BaseDefenseGroundObject):
def __init__(self, name: str, group_id: int, position: Point,
control_point: ControlPoint) -> None:
@property
def purchasable(self) -> bool:
return True
class EwrGroundObject(TheaterGroundObject):
def __init__(
self,
name: str,
group_id: int,
position: Point,
control_point: ControlPoint,
) -> None:
super().__init__(
name=name,
category="EWR",
category="ewr",
group_id=group_id,
position=position,
heading=0,
control_point=control_point,
dcs_identifier="EWR",
airbase_group=True,
sea_object=False
sea_object=False,
)
@property
@@ -392,6 +601,7 @@ class EwrGroundObject(BaseDefenseGroundObject):
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType
if not self.is_friendly(for_player):
yield FlightType.DEAD
yield from super().mission_types(for_player)
@@ -400,20 +610,28 @@ class EwrGroundObject(BaseDefenseGroundObject):
def might_have_aa(self) -> bool:
return True
@property
def capturable(self) -> bool:
return False
@property
def purchasable(self) -> bool:
return True
class ShipGroundObject(NavalGroundObject):
def __init__(self, name: str, group_id: int, position: Point,
control_point: ControlPoint) -> None:
def __init__(
self, name: str, group_id: int, position: Point, control_point: ControlPoint
) -> None:
super().__init__(
name=name,
category="aa",
category="ship",
group_id=group_id,
position=position,
heading=0,
control_point=control_point,
dcs_identifier="AA",
airbase_group=False,
sea_object=True
sea_object=True,
)
@property

View File

@@ -0,0 +1,8 @@
from game.theater.projections import TransverseMercator
PARAMETERS = TransverseMercator(
central_meridian=3,
false_easting=99376.00000000288,
false_northing=-5636889.00000001,
scale_factor=0.9996,
)

View File

@@ -0,0 +1,196 @@
from __future__ import annotations
import heapq
import math
from collections import defaultdict
from dataclasses import dataclass, field
from enum import Enum, auto
from typing import Dict, Iterator, List, Optional, Set, Tuple
from game.theater import ConflictTheater
from game.theater.controlpoint import ControlPoint
class NoPathError(RuntimeError):
def __init__(self, origin: ControlPoint, destination: ControlPoint) -> None:
super().__init__(f"Could not reconstruct path to {destination} from {origin}")
@dataclass(frozen=True, order=True)
class FrontierNode:
cost: float
point: ControlPoint = field(compare=False)
class Frontier:
def __init__(self) -> None:
self.nodes: List[FrontierNode] = []
def push(self, poly: ControlPoint, cost: float) -> None:
heapq.heappush(self.nodes, FrontierNode(cost, poly))
def pop(self) -> Optional[FrontierNode]:
try:
return heapq.heappop(self.nodes)
except IndexError:
return None
def __bool__(self) -> bool:
return bool(self.nodes)
class TransitConnection(Enum):
Road = auto()
Shipping = auto()
Airlift = auto()
class TransitNetwork:
def __init__(self) -> None:
self.nodes: Dict[
ControlPoint, Dict[ControlPoint, TransitConnection]
] = defaultdict(dict)
def has_destinations(self, control_point: ControlPoint) -> bool:
return bool(self.nodes[control_point])
def has_link(self, a: ControlPoint, b: ControlPoint) -> bool:
return b in self.nodes[a]
def link_type(self, a: ControlPoint, b: ControlPoint) -> TransitConnection:
return self.nodes[a][b]
def link_with(
self, a: ControlPoint, b: ControlPoint, link_type: TransitConnection
) -> None:
self.nodes[a][b] = link_type
self.nodes[b][a] = link_type
def link_road(self, a: ControlPoint, b: ControlPoint) -> None:
self.link_with(a, b, TransitConnection.Road)
def link_shipping(self, a: ControlPoint, b: ControlPoint) -> None:
self.link_with(a, b, TransitConnection.Shipping)
def link_airport(self, a: ControlPoint, b: ControlPoint) -> None:
self.link_with(a, b, TransitConnection.Airlift)
def connections_from(self, control_point: ControlPoint) -> Iterator[ControlPoint]:
yield from self.nodes[control_point]
def cost(self, a: ControlPoint, b: ControlPoint) -> float:
return {
TransitConnection.Road: 1,
TransitConnection.Shipping: 3,
# Set arbitrarily high so that other methods are preferred, but still scaled
# by distance so that when we do need it we still pick the closest airfield.
# The units of distance are meters so there's no risk of these
TransitConnection.Airlift: a.position.distance_to_point(b.position),
}[self.link_type(a, b)]
def has_path_between(
self,
origin: ControlPoint,
destination: ControlPoint,
seen: Optional[set[ControlPoint]] = None,
) -> bool:
if seen is None:
seen = set()
seen.add(origin)
for connection in self.connections_from(origin):
if connection in seen:
continue
if connection == destination:
return True
if self.has_path_between(connection, destination, seen):
return True
return False
def shortest_path_between(
self, origin: ControlPoint, destination: ControlPoint
) -> list[ControlPoint]:
return self.shortest_path_with_cost(origin, destination)[0]
def shortest_path_with_cost(
self, origin: ControlPoint, destination: ControlPoint
) -> Tuple[List[ControlPoint], float]:
if origin not in self.nodes:
raise ValueError(f"{origin} is not in the transit network.")
if destination not in self.nodes:
raise ValueError(f"{destination} is not in the transit network.")
frontier = Frontier()
frontier.push(origin, 0)
came_from: Dict[ControlPoint, Optional[ControlPoint]] = {origin: None}
best_known: Dict[ControlPoint, float] = defaultdict(lambda: math.inf)
best_known[origin] = 0.0
while (node := frontier.pop()) is not None:
cost = node.cost
current = node.point
if cost > best_known[current]:
continue
for neighbor in self.connections_from(current):
new_cost = cost + self.cost(node.point, neighbor)
if new_cost < best_known[neighbor]:
best_known[neighbor] = new_cost
frontier.push(neighbor, new_cost)
came_from[neighbor] = current
# Reconstruct and reverse the path.
current = destination
path: List[ControlPoint] = []
while current != origin:
path.append(current)
previous = came_from.get(current)
if previous is None:
raise NoPathError(origin, destination)
current = previous
path.reverse()
return path, best_known[destination]
class TransitNetworkBuilder:
def __init__(self, theater: ConflictTheater, for_player: bool) -> None:
self.control_points = list(theater.control_points_for(for_player))
self.network = TransitNetwork()
self.airports: Set[ControlPoint] = {
cp
for cp in self.control_points
if cp.is_friendly(for_player) and cp.runway_is_operational()
}
def build(self) -> TransitNetwork:
seen = set()
for control_point in self.control_points:
if control_point not in seen:
seen.add(control_point)
self.add_transit_links(control_point)
return self.network
def add_transit_links(self, control_point: ControlPoint) -> None:
# Prefer road connections.
for road_connection in control_point.connected_points:
if road_connection.is_friendly_to(control_point):
self.network.link_road(control_point, road_connection)
# Use sea connections if there's no road or rail connection.
for sea_connection in control_point.shipping_lanes:
if self.network.has_link(control_point, sea_connection):
continue
if sea_connection.is_friendly_to(control_point):
self.network.link_shipping(control_point, sea_connection)
# And use airports as a last resort.
if control_point in self.airports:
for airport in self.airports:
if control_point == airport:
continue
if self.network.has_link(control_point, airport):
continue
if not airport.is_friendly_to(control_point):
continue
self.network.link_airport(control_point, airport)

View File

@@ -1,7 +1,7 @@
from __future__ import annotations
from functools import singledispatchmethod
from typing import Optional, TYPE_CHECKING, Union
from typing import Optional, TYPE_CHECKING, Union, Iterable
from dcs.mapping import Point as DcsPoint
from shapely.geometry import (
@@ -13,10 +13,10 @@ from shapely.geometry import (
from shapely.geometry.base import BaseGeometry
from shapely.ops import nearest_points, unary_union
from game.theater import ControlPoint
from game.theater import ControlPoint, MissionTarget
from game.utils import Distance, meters, nautical_miles
from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.flights.flight import Flight
from gen.flights.flight import Flight, FlightWaypoint
if TYPE_CHECKING:
from game import Game
@@ -26,16 +26,24 @@ ThreatPoly = Union[MultiPolygon, Polygon]
class ThreatZones:
def __init__(self, airbases: ThreatPoly, air_defenses: ThreatPoly) -> None:
def __init__(
self, airbases: ThreatPoly, air_defenses: ThreatPoly, radar_sam_threats
) -> None:
self.airbases = airbases
self.air_defenses = air_defenses
self.radar_sam_threats = radar_sam_threats
self.all = unary_union([airbases, air_defenses])
def closest_boundary(self, point: DcsPoint) -> DcsPoint:
boundary, _ = nearest_points(self.all.boundary,
self.dcs_to_shapely_point(point))
boundary, _ = nearest_points(
self.all.boundary, self.dcs_to_shapely_point(point)
)
return DcsPoint(boundary.x, boundary.y)
def distance_to_threat(self, point: DcsPoint) -> Distance:
boundary = self.closest_boundary(point)
return meters(boundary.distance_to_point(point))
@singledispatchmethod
def threatened(self, position) -> bool:
raise NotImplementedError
@@ -49,8 +57,9 @@ class ThreatZones:
return self.all.intersects(self.dcs_to_shapely_point(position))
def path_threatened(self, a: DcsPoint, b: DcsPoint) -> bool:
return self.threatened(LineString(
[self.dcs_to_shapely_point(a), self.dcs_to_shapely_point(b)]))
return self.threatened(
LineString([self.dcs_to_shapely_point(a), self.dcs_to_shapely_point(b)])
)
@singledispatchmethod
def threatened_by_aircraft(self, target) -> bool:
@@ -62,9 +71,16 @@ class ThreatZones:
@threatened_by_aircraft.register
def _threatened_by_aircraft_flight(self, flight: Flight) -> bool:
return self.threatened_by_aircraft(LineString((
self.dcs_to_shapely_point(p.position) for p in flight.points
)))
return self.threatened_by_aircraft(
LineString((self.dcs_to_shapely_point(p.position) for p in flight.points))
)
def waypoints_threatened_by_aircraft(
self, waypoints: Iterable[FlightWaypoint]
) -> bool:
return self.threatened_by_aircraft(
LineString((self.dcs_to_shapely_point(p.position) for p in waypoints))
)
@singledispatchmethod
def threatened_by_air_defense(self, target) -> bool:
@@ -76,27 +92,56 @@ class ThreatZones:
@threatened_by_air_defense.register
def _threatened_by_air_defense_flight(self, flight: Flight) -> bool:
return self.threatened_by_air_defense(LineString((
self.dcs_to_shapely_point(p.position) for p in flight.points
)))
return self.threatened_by_air_defense(
LineString((self.dcs_to_shapely_point(p.position) for p in flight.points))
)
@threatened_by_air_defense.register
def _threatened_by_air_defense_mission_target(self, target: MissionTarget) -> bool:
return self.threatened_by_air_defense(
self.dcs_to_shapely_point(target.position)
)
@singledispatchmethod
def threatened_by_radar_sam(self, target) -> bool:
raise NotImplementedError
@threatened_by_radar_sam.register
def _threatened_by_radar_sam_geom(self, position: BaseGeometry) -> bool:
return self.radar_sam_threats.intersects(position)
@threatened_by_radar_sam.register
def _threatened_by_radar_sam_flight(self, flight: Flight) -> bool:
return self.threatened_by_radar_sam(
LineString((self.dcs_to_shapely_point(p.position) for p in flight.points))
)
def waypoints_threatened_by_radar_sam(
self, waypoints: Iterable[FlightWaypoint]
) -> bool:
return self.threatened_by_radar_sam(
LineString((self.dcs_to_shapely_point(p.position) for p in waypoints))
)
@classmethod
def closest_enemy_airbase(cls, location: ControlPoint,
max_distance: Distance) -> Optional[ControlPoint]:
def closest_enemy_airbase(
cls, location: ControlPoint, max_distance: Distance
) -> Optional[ControlPoint]:
airfields = ObjectiveDistanceCache.get_closest_airfields(location)
for airfield in airfields.airfields_within(max_distance):
for airfield in airfields.all_airfields_within(max_distance):
if airfield.captured != location.captured:
return airfield
return None
@classmethod
def barcap_threat_range(cls, game: Game,
control_point: ControlPoint) -> Distance:
def barcap_threat_range(cls, game: Game, control_point: ControlPoint) -> Distance:
doctrine = game.faction_for(control_point.captured).doctrine
cap_threat_range = (doctrine.cap_max_distance_from_cp +
doctrine.cap_engagement_range)
opposing_airfield = cls.closest_enemy_airbase(control_point,
cap_threat_range * 2)
cap_threat_range = (
doctrine.cap_max_distance_from_cp + doctrine.cap_engagement_range
)
opposing_airfield = cls.closest_enemy_airbase(
control_point, cap_threat_range * 2
)
if opposing_airfield is None:
return cap_threat_range
@@ -127,16 +172,16 @@ class ThreatZones:
zone belongs to the player, it is the zone that will be avoided by
the enemy and vice versa.
"""
airbases = []
air_threats = []
air_defenses = []
radar_sam_threats = []
for control_point in game.theater.controlpoints:
if control_point.captured != player:
continue
if control_point.runway_is_operational():
point = ShapelyPoint(control_point.position.x,
control_point.position.y)
point = ShapelyPoint(control_point.position.x, control_point.position.y)
cap_threat_range = cls.barcap_threat_range(game, control_point)
airbases.append(point.buffer(cap_threat_range.meters))
air_threats.append(point.buffer(cap_threat_range.meters))
for tgo in control_point.ground_objects:
for group in tgo.groups:
@@ -147,12 +192,18 @@ class ThreatZones:
point = ShapelyPoint(tgo.position.x, tgo.position.y)
threat_zone = point.buffer(threat_range.meters)
air_defenses.append(threat_zone)
radar_threat_range = tgo.threat_range(group, radar_only=True)
if radar_threat_range > nautical_miles(3):
point = ShapelyPoint(tgo.position.x, tgo.position.y)
threat_zone = point.buffer(threat_range.meters)
radar_sam_threats.append(threat_zone)
return cls(
airbases=unary_union(airbases),
air_defenses=unary_union(air_defenses)
airbases=unary_union(air_threats),
air_defenses=unary_union(air_defenses),
radar_sam_threats=unary_union(radar_sam_threats),
)
@staticmethod
def dcs_to_shapely_point(point: DcsPoint) -> ShapelyPoint:
return ShapelyPoint(point.x, point.y)
return ShapelyPoint(point.x, point.y)

619
game/transfers.py Normal file
View File

@@ -0,0 +1,619 @@
from __future__ import annotations
import logging
import math
from collections import defaultdict
from dataclasses import dataclass, field
from functools import singledispatchmethod
from typing import (
Dict,
Generic,
Iterator,
List,
Optional,
TYPE_CHECKING,
Type,
TypeVar,
Sequence,
)
from dcs.mapping import Point
from dcs.unittype import FlyingType, VehicleType
from game.procurement import AircraftProcurementRequest
from game.squadrons import Squadron
from game.theater import ControlPoint, MissionTarget
from game.theater.transitnetwork import (
TransitConnection,
TransitNetwork,
)
from game.utils import meters, nautical_miles
from gen.ato import Package
from gen.flights.ai_flight_planner_db import TRANSPORT_CAPABLE
from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.flights.flight import Flight, FlightType
from gen.flights.flightplan import FlightPlanBuilder
from gen.naming import namegen
if TYPE_CHECKING:
from game import Game
from game.inventory import ControlPointAircraftInventory
class Transport:
def __init__(self, destination: ControlPoint):
self.destination = destination
def find_escape_route(self) -> Optional[ControlPoint]:
raise NotImplementedError
def description(self) -> str:
raise NotImplementedError
@dataclass
class TransferOrder:
"""The base type of all transfer orders.
A transfer order can transfer multiple units of multiple types.
"""
#: The location the units are transferring from.
origin: ControlPoint
#: The location the units are transferring to.
destination: ControlPoint
#: The current position of the group being transferred. Groups may make multiple
#: stops and can switch transport modes before reaching their destination.
position: ControlPoint = field(init=False)
#: True if the transfer order belongs to the player.
player: bool = field(init=False)
#: The units being transferred.
units: Dict[Type[VehicleType], int]
transport: Optional[Transport] = field(default=None)
def __post_init__(self) -> None:
self.position = self.origin
self.player = self.origin.is_friendly(to_player=True)
@property
def description(self) -> str:
if self.transport is None:
return "No transports available"
return self.transport.description()
def kill_all(self) -> None:
self.units.clear()
def kill_unit(self, unit_type: Type[VehicleType]) -> None:
if unit_type not in self.units or not self.units[unit_type]:
raise KeyError(f"{self.destination} has no {unit_type} remaining")
self.units[unit_type] -= 1
@property
def size(self) -> int:
return sum(c for c in self.units.values())
def iter_units(self) -> Iterator[Type[VehicleType]]:
for unit_type, count in self.units.items():
for _ in range(count):
yield unit_type
@property
def completed(self) -> bool:
return self.destination == self.position or not self.units
def disband_at(self, location: ControlPoint) -> None:
logging.info(f"Units halting at {location}.")
location.base.commision_units(self.units)
self.units.clear()
@property
def next_stop(self) -> ControlPoint:
if self.transport is None:
raise RuntimeError(
"TransferOrder.next_stop called with no transport assigned"
)
return self.transport.destination
def proceed(self) -> None:
if self.transport is None:
return
if not self.destination.is_friendly(self.player):
logging.info(f"Transfer destination {self.destination} was captured.")
if self.position.is_friendly(self.player):
self.disband_at(self.position)
elif (escape_route := self.transport.find_escape_route()) is not None:
self.disband_at(escape_route)
else:
logging.info(
f"No escape route available. Units were surrounded and destroyed "
"during transfer."
)
self.kill_all()
return
self.position = self.next_stop
self.transport = None
if self.completed:
self.disband_at(self.position)
class Airlift(Transport):
"""A transfer order that moves units by cargo planes and helicopters."""
def __init__(
self, transfer: TransferOrder, flight: Flight, next_stop: ControlPoint
) -> None:
super().__init__(next_stop)
self.transfer = transfer
self.flight = flight
@property
def units(self) -> Dict[Type[VehicleType], int]:
return self.transfer.units
@property
def player_owned(self) -> bool:
return self.transfer.player
def find_escape_route(self) -> Optional[ControlPoint]:
# TODO: Move units to closest base.
return None
def description(self) -> str:
return (
f"Being airlifted from {self.transfer.position} to {self.destination} by "
f"{self.flight}"
)
class AirliftPlanner:
#: Maximum range from for any link in the route of takeoff, pickup, dropoff, and RTB
#: for a helicopter to be considered for airlift. Total route length is not
#: considered because the helicopter can refuel at each stop. Cargo planes have no
#: maximum range.
HELO_MAX_RANGE = nautical_miles(100)
def __init__(
self, game: Game, transfer: TransferOrder, next_stop: ControlPoint
) -> None:
self.game = game
self.transfer = transfer
self.next_stop = next_stop
self.for_player = transfer.destination.captured
self.package = Package(target=next_stop, auto_asap=True)
def compatible_with_mission(
self, unit_type: Type[FlyingType], airfield: ControlPoint
) -> bool:
if not unit_type in TRANSPORT_CAPABLE:
return False
if not self.transfer.origin.can_operate(unit_type):
return False
if not self.next_stop.can_operate(unit_type):
return False
# Cargo planes have no maximum range.
if not unit_type.helicopter:
return True
# A helicopter that is transport capable and able to operate at both bases. Need
# to check that no leg of the journey exceeds the maximum range. This doesn't
# account for any routing around threats that might take place, but it's close
# enough.
home = airfield.position
pickup = self.transfer.position.position
drop_off = self.transfer.position.position
if meters(home.distance_to_point(pickup)) > self.HELO_MAX_RANGE:
return False
if meters(pickup.distance_to_point(drop_off)) > self.HELO_MAX_RANGE:
return False
if meters(drop_off.distance_to_point(home)) > self.HELO_MAX_RANGE:
return False
return True
def create_package_for_airlift(self) -> None:
distance_cache = ObjectiveDistanceCache.get_closest_airfields(
self.transfer.position
)
for cp in distance_cache.closest_airfields:
if cp.captured != self.for_player:
continue
inventory = self.game.aircraft_inventory.for_control_point(cp)
for unit_type, available in inventory.all_aircraft:
squadrons = [
s
for s in self.game.air_wing_for(self.for_player).squadrons_for(
unit_type
)
if FlightType.TRANSPORT in s.auto_assignable_mission_types
]
if not squadrons:
continue
squadron = squadrons[0]
if self.compatible_with_mission(unit_type, cp):
while available and self.transfer.transport is None:
flight_size = self.create_airlift_flight(squadron, inventory)
available -= flight_size
if self.package.flights:
self.game.ato_for(self.for_player).add_package(self.package)
def create_airlift_flight(
self, squadron: Squadron, inventory: ControlPointAircraftInventory
) -> int:
available = inventory.available(squadron.aircraft)
capacity_each = 1 if squadron.aircraft.helicopter else 2
required = math.ceil(self.transfer.size / capacity_each)
flight_size = min(required, available, squadron.aircraft.group_size_max)
capacity = flight_size * capacity_each
if capacity < self.transfer.size:
transfer = self.game.transfers.split_transfer(self.transfer, capacity)
else:
transfer = self.transfer
player = inventory.control_point.captured
flight = Flight(
self.package,
self.game.country_for(player),
squadron,
flight_size,
FlightType.TRANSPORT,
self.game.settings.default_start_type,
departure=inventory.control_point,
arrival=inventory.control_point,
divert=None,
cargo=transfer,
)
transport = Airlift(transfer, flight, self.next_stop)
transfer.transport = transport
self.package.add_flight(flight)
planner = FlightPlanBuilder(self.game, self.package, self.for_player)
planner.populate_flight_plan(flight)
self.game.aircraft_inventory.claim_for_flight(flight)
return flight_size
class MultiGroupTransport(MissionTarget, Transport):
def __init__(
self, name: str, origin: ControlPoint, destination: ControlPoint
) -> None:
MissionTarget.__init__(self, name, origin.position)
Transport.__init__(self, destination)
self.origin = origin
self.transfers: List[TransferOrder] = []
def is_friendly(self, to_player: bool) -> bool:
return self.origin.captured
def add_units(self, transfer: TransferOrder) -> None:
self.transfers.append(transfer)
transfer.transport = self
def remove_units(self, transfer: TransferOrder) -> None:
transfer.transport = None
self.transfers.remove(transfer)
def kill_unit(self, unit_type: Type[VehicleType]) -> None:
for transfer in self.transfers:
try:
transfer.kill_unit(unit_type)
return
except KeyError:
pass
raise KeyError
def kill_all(self) -> None:
for transfer in self.transfers:
transfer.kill_all()
def disband(self) -> None:
for transfer in list(self.transfers):
self.remove_units(transfer)
self.transfers.clear()
@property
def size(self) -> int:
return sum(sum(t.units.values()) for t in self.transfers)
@property
def units(self) -> Dict[Type[VehicleType], int]:
units: Dict[Type[VehicleType], int] = defaultdict(int)
for transfer in self.transfers:
for unit_type, count in transfer.units.items():
units[unit_type] += count
return units
@property
def player_owned(self) -> bool:
return self.origin.captured
def find_escape_route(self) -> Optional[ControlPoint]:
raise NotImplementedError
def description(self) -> str:
raise NotImplementedError
class Convoy(MultiGroupTransport):
def __init__(self, origin: ControlPoint, destination: ControlPoint) -> None:
super().__init__(namegen.next_convoy_name(), origin, destination)
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
if self.is_friendly(for_player):
return
yield FlightType.BAI
yield from super().mission_types(for_player)
@property
def route_start(self) -> Point:
return self.origin.convoy_origin_for(self.destination)
@property
def route_end(self) -> Point:
return self.destination.convoy_origin_for(self.origin)
def description(self) -> str:
return f"In a convoy from {self.origin} to {self.destination}"
def find_escape_route(self) -> Optional[ControlPoint]:
return None
class CargoShip(MultiGroupTransport):
def __init__(self, origin: ControlPoint, destination: ControlPoint) -> None:
super().__init__(namegen.next_cargo_ship_name(), origin, destination)
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
if self.is_friendly(for_player):
return
yield FlightType.ANTISHIP
yield from super().mission_types(for_player)
@property
def route(self) -> Sequence[Point]:
return self.origin.shipping_lanes[self.destination]
def description(self) -> str:
return f"On a ship from {self.origin} to {self.destination}"
def find_escape_route(self) -> Optional[ControlPoint]:
return None
TransportType = TypeVar("TransportType", bound=MultiGroupTransport)
class TransportMap(Generic[TransportType]):
def __init__(self) -> None:
# Dict of origin -> destination -> transport.
self.transports: Dict[
ControlPoint, Dict[ControlPoint, TransportType]
] = defaultdict(dict)
def create_transport(
self, origin: ControlPoint, destination: ControlPoint
) -> TransportType:
raise NotImplementedError
def transport_exists(self, origin: ControlPoint, destination: ControlPoint) -> bool:
return destination in self.transports[origin]
def find_transport(
self, origin: ControlPoint, destination: ControlPoint
) -> Optional[TransportType]:
return self.transports[origin].get(destination)
def find_or_create_transport(
self, origin: ControlPoint, destination: ControlPoint
) -> TransportType:
transport = self.find_transport(origin, destination)
if transport is None:
transport = self.create_transport(origin, destination)
self.transports[origin][destination] = transport
return transport
def departing_from(self, origin: ControlPoint) -> Iterator[TransportType]:
yield from self.transports[origin].values()
def travelling_to(self, destination: ControlPoint) -> Iterator[TransportType]:
for destination_dict in self.transports.values():
if destination in destination_dict:
yield destination_dict[destination]
def disband_transport(self, transport: TransportType) -> None:
transport.disband()
del self.transports[transport.origin][transport.destination]
def add(self, transfer: TransferOrder, next_stop: ControlPoint) -> None:
self.find_or_create_transport(transfer.position, next_stop).add_units(transfer)
def remove(self, transport: TransportType, transfer: TransferOrder) -> None:
transport.remove_units(transfer)
if not transport.transfers:
self.disband_transport(transport)
def disband_all(self) -> None:
for transport in list(self):
self.disband_transport(transport)
def __iter__(self) -> Iterator[TransportType]:
for destination_dict in self.transports.values():
yield from destination_dict.values()
class ConvoyMap(TransportMap):
def create_transport(
self, origin: ControlPoint, destination: ControlPoint
) -> Convoy:
return Convoy(origin, destination)
class CargoShipMap(TransportMap):
def create_transport(
self, origin: ControlPoint, destination: ControlPoint
) -> CargoShip:
return CargoShip(origin, destination)
class PendingTransfers:
def __init__(self, game: Game) -> None:
self.game = game
self.convoys = ConvoyMap()
self.cargo_ships = CargoShipMap()
self.pending_transfers: List[TransferOrder] = []
def __iter__(self) -> Iterator[TransferOrder]:
yield from self.pending_transfers
@property
def pending_transfer_count(self) -> int:
return len(self.pending_transfers)
def transfer_at_index(self, index: int) -> TransferOrder:
return self.pending_transfers[index]
def index_of_transfer(self, transfer: TransferOrder) -> int:
return self.pending_transfers.index(transfer)
def network_for(self, control_point: ControlPoint) -> TransitNetwork:
return self.game.transit_network_for(control_point.captured)
def arrange_transport(self, transfer: TransferOrder) -> None:
network = self.network_for(transfer.position)
path = network.shortest_path_between(transfer.position, transfer.destination)
next_stop = path[0]
if network.link_type(transfer.position, next_stop) == TransitConnection.Road:
self.convoys.add(transfer, next_stop)
elif (
network.link_type(transfer.position, next_stop)
== TransitConnection.Shipping
):
self.cargo_ships.add(transfer, next_stop)
else:
AirliftPlanner(self.game, transfer, next_stop).create_package_for_airlift()
def new_transfer(self, transfer: TransferOrder) -> None:
transfer.origin.base.commit_losses(transfer.units)
self.pending_transfers.append(transfer)
self.arrange_transport(transfer)
def split_transfer(self, transfer: TransferOrder, size: int) -> TransferOrder:
"""Creates a smaller transfer that is a subset of the original."""
if transfer.size <= size:
raise ValueError
units = {}
for unit_type, remaining in transfer.units.items():
take = min(remaining, size)
size -= take
transfer.units[unit_type] -= take
units[unit_type] = take
if not size:
break
new_transfer = TransferOrder(transfer.origin, transfer.destination, units)
self.pending_transfers.append(new_transfer)
return new_transfer
@singledispatchmethod
def cancel_transport(self, transport, transfer: TransferOrder) -> None:
pass
@cancel_transport.register
def _cancel_transport_air(
self, transport: Airlift, _transfer: TransferOrder
) -> None:
flight = transport.flight
flight.package.remove_flight(flight)
if not flight.package.flights:
self.game.ato_for(transport.player_owned).remove_package(flight.package)
self.game.aircraft_inventory.return_from_flight(flight)
flight.clear_roster()
@cancel_transport.register
def _cancel_transport_convoy(
self, transport: Convoy, transfer: TransferOrder
) -> None:
self.convoys.remove(transport, transfer)
@cancel_transport.register
def _cancel_transport_cargo_ship(
self, transport: CargoShip, transfer: TransferOrder
) -> None:
self.cargo_ships.remove(transport, transfer)
def cancel_transfer(self, transfer: TransferOrder) -> None:
if transfer.transport is not None:
self.cancel_transport(transfer.transport, transfer)
self.pending_transfers.remove(transfer)
transfer.origin.base.commision_units(transfer.units)
def perform_transfers(self) -> None:
incomplete = []
for transfer in self.pending_transfers:
transfer.proceed()
if not transfer.completed:
incomplete.append(transfer)
self.pending_transfers = incomplete
self.convoys.disband_all()
self.cargo_ships.disband_all()
def plan_transports(self) -> None:
for transfer in self.pending_transfers:
if transfer.transport is None:
self.arrange_transport(transfer)
def order_airlift_assets(self) -> None:
for control_point in self.game.theater.controlpoints:
self.order_airlift_assets_at(control_point)
@staticmethod
def desired_airlift_capacity(control_point: ControlPoint) -> int:
return 4 if control_point.has_factory else 0
def current_airlift_capacity(self, control_point: ControlPoint) -> int:
inventory = self.game.aircraft_inventory.for_control_point(control_point)
squadrons = self.game.air_wing_for(control_point.captured).squadrons_for_task(
FlightType.TRANSPORT
)
unit_types = {s.aircraft for s in squadrons}.intersection(TRANSPORT_CAPABLE)
return sum(
count
for unit_type, count in inventory.all_aircraft
if unit_type in unit_types
)
def order_airlift_assets_at(self, control_point: ControlPoint) -> None:
gap = self.desired_airlift_capacity(
control_point
) - self.current_airlift_capacity(control_point)
if gap <= 0:
return
if gap % 2:
# Always buy in pairs since we're not trying to fill odd squadrons. Purely
# aesthetic.
gap += 1
self.game.procurement_requests_for(player=control_point.captured).append(
AircraftProcurementRequest(
control_point, nautical_miles(200), FlightType.TRANSPORT, gap
)
)

161
game/unitdelivery.py Normal file
View File

@@ -0,0 +1,161 @@
from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass
from typing import Dict, Optional, TYPE_CHECKING, Type
from dcs.unittype import UnitType, VehicleType
from game.theater import ControlPoint
from .db import PRICES
from .theater.transitnetwork import (
NoPathError,
TransitNetwork,
)
from .transfers import TransferOrder
if TYPE_CHECKING:
from .game import Game
@dataclass(frozen=True)
class GroundUnitSource:
control_point: ControlPoint
class PendingUnitDeliveries:
def __init__(self, destination: ControlPoint) -> None:
self.destination = destination
# Maps unit type to order quantity.
self.units: Dict[Type[UnitType], int] = defaultdict(int)
def __str__(self) -> str:
return f"Pending delivery to {self.destination}"
def order(self, units: Dict[Type[UnitType], int]) -> None:
for k, v in units.items():
self.units[k] += v
def sell(self, units: Dict[Type[UnitType], int]) -> None:
for k, v in units.items():
self.units[k] -= v
def refund_all(self, game: Game) -> None:
self.refund(game, self.units)
self.units = defaultdict(int)
def refund(self, game: Game, units: Dict[Type[UnitType], int]) -> None:
for unit_type, count in units.items():
try:
price = PRICES[unit_type]
except KeyError:
logging.error(f"Could not refund {unit_type.id}, price unknown")
continue
logging.info(f"Refunding {count} {unit_type.id} at {self.destination.name}")
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)
if pending_units is None:
pending_units = 0
return pending_units
def available_next_turn(self, unit_type: Type[UnitType]) -> int:
current_units = self.destination.base.total_units_of_type(unit_type)
return self.pending_orders(unit_type) + current_units
def process(self, game: Game) -> None:
ground_unit_source = self.find_ground_unit_source(game)
if ground_unit_source is None:
game.message(
f"{self.destination.name} lost its source for ground unit "
"reinforcements. Refunding purchase price."
)
self.refund_all(game)
return
bought_units: Dict[Type[UnitType], int] = {}
units_needing_transfer: Dict[Type[VehicleType], int] = {}
sold_units: Dict[Type[UnitType], int] = {}
for unit_type, count in self.units.items():
coalition = "Ally" if self.destination.captured else "Enemy"
name = unit_type.id
if (
issubclass(unit_type, VehicleType)
and self.destination != ground_unit_source
):
source = ground_unit_source
d = units_needing_transfer
else:
source = self.destination
d = bought_units
if count >= 0:
d[unit_type] = count
game.message(
f"{coalition} reinforcements: {name} x {count} at {source}"
)
else:
sold_units[unit_type] = -count
game.message(f"{coalition} sold: {name} x {-count} at {source}")
self.units = defaultdict(int)
self.destination.base.commision_units(bought_units)
self.destination.base.commit_losses(sold_units)
if units_needing_transfer:
ground_unit_source.base.commision_units(units_needing_transfer)
self.create_transfer(game, ground_unit_source, units_needing_transfer)
def create_transfer(
self, game: Game, source: ControlPoint, units: Dict[Type[VehicleType], int]
) -> None:
game.transfers.new_transfer(TransferOrder(source, self.destination, units))
def find_ground_unit_source(self, game: Game) -> Optional[ControlPoint]:
# This is running *after* the turn counter has been incremented, so this is the
# reaction to turn 0. On turn zero we allow units to be recruited anywhere for
# delivery on turn 1 so that turn 1 always starts with units on the front line.
if game.turn == 1:
return self.destination
# Fast path if the destination is a valid source.
if self.destination.can_recruit_ground_units(game):
return self.destination
try:
return self.find_ground_unit_source_in_network(
game.transit_network_for(self.destination.captured), game
)
except NoPathError:
return None
def find_ground_unit_source_in_network(
self, network: TransitNetwork, game: Game
) -> Optional[ControlPoint]:
sources = []
for control_point in game.theater.control_points_for(self.destination.captured):
if control_point.can_recruit_ground_units(
game
) and network.has_path_between(self.destination, control_point):
sources.append(control_point)
if not sources:
return None
# Fast path to skip the distance calculation if we have only one option.
if len(sources) == 1:
return sources[0]
closest = sources[0]
_, cost = network.shortest_path_with_cost(self.destination, closest)
for source in sources:
_, new_cost = network.shortest_path_with_cost(self.destination, source)
if new_cost < cost:
closest = source
cost = new_cost
return closest

View File

@@ -1,4 +1,6 @@
"""Maps generated units back to their Liberation types."""
import itertools
import math
from dataclasses import dataclass
from typing import Dict, Optional, Type
@@ -7,11 +9,19 @@ from dcs.unitgroup import FlyingGroup, Group, VehicleGroup
from dcs.unittype import VehicleType
from game import db
from game.squadrons import Pilot
from game.theater import Airfield, ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import BuildingGroundObject
from game.theater.theatergroundobject import BuildingGroundObject, SceneryGroundObject
from game.transfers import CargoShip, Convoy, TransferOrder
from gen.flights.flight import Flight
@dataclass(frozen=True)
class FlyingUnit:
flight: Flight
pilot: Pilot
@dataclass(frozen=True)
class FrontLineUnit:
unit_type: Type[VehicleType]
@@ -25,6 +35,18 @@ class GroundObjectUnit:
unit: Unit
@dataclass(frozen=True)
class ConvoyUnit:
unit_type: Type[VehicleType]
convoy: Convoy
@dataclass(frozen=True)
class AirliftUnits:
cargo: tuple[Type[VehicleType], ...]
transfer: TransferOrder
@dataclass(frozen=True)
class Building:
ground_object: BuildingGroundObject
@@ -32,22 +54,29 @@ class Building:
class UnitMap:
def __init__(self) -> None:
self.aircraft: Dict[str, Flight] = {}
self.aircraft: Dict[str, FlyingUnit] = {}
self.airfields: Dict[str, Airfield] = {}
self.front_line_units: Dict[str, FrontLineUnit] = {}
self.ground_object_units: Dict[str, GroundObjectUnit] = {}
self.buildings: Dict[str, Building] = {}
self.convoys: Dict[str, ConvoyUnit] = {}
self.cargo_ships: Dict[str, CargoShip] = {}
self.airlifts: Dict[str, AirliftUnits] = {}
def add_aircraft(self, group: FlyingGroup, flight: Flight) -> None:
for unit in group.units:
for pilot, unit in zip(flight.roster.pilots, group.units):
# The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__.
name = str(unit.name)
if name in self.aircraft:
raise RuntimeError(f"Duplicate unit name: {name}")
self.aircraft[name] = flight
if pilot is None:
raise ValueError(f"{name} has no pilot assigned")
self.aircraft[name] = FlyingUnit(flight, pilot)
if flight.cargo is not None:
self.add_airlift_units(group, flight.cargo)
def flight(self, unit_name: str) -> Optional[Flight]:
def flight(self, unit_name: str) -> Optional[FlyingUnit]:
return self.aircraft.get(unit_name, None)
def add_airfield(self, airfield: Airfield) -> None:
@@ -70,15 +99,19 @@ class UnitMap:
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")
f"{name} is a {unit_type.__name__}, expected a VehicleType"
)
self.front_line_units[name] = FrontLineUnit(unit_type, origin)
def front_line_unit(self, name: str) -> Optional[FrontLineUnit]:
return self.front_line_units.get(name, None)
def add_ground_object_units(self, ground_object: TheaterGroundObject,
persistence_group: Group,
miz_group: Group) -> None:
def add_ground_object_units(
self,
ground_object: TheaterGroundObject,
persistence_group: Group,
miz_group: Group,
) -> None:
"""Adds a group associated with a TGO to the unit map.
Args:
@@ -103,13 +136,72 @@ class UnitMap:
if name in self.ground_object_units:
raise RuntimeError(f"Duplicate TGO unit: {name}")
self.ground_object_units[name] = GroundObjectUnit(
ground_object, persistence_group, persistent_unit)
ground_object, persistence_group, persistent_unit
)
def ground_object_unit(self, name: str) -> Optional[GroundObjectUnit]:
return self.ground_object_units.get(name, None)
def add_building(self, ground_object: BuildingGroundObject,
group: Group) -> None:
def add_convoy_units(self, group: Group, convoy: Convoy) -> None:
for unit in group.units:
# The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__.
name = str(unit.name)
if name in self.convoys:
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)
def convoy_unit(self, name: str) -> Optional[ConvoyUnit]:
return self.convoys.get(name, None)
def add_cargo_ship(self, group: Group, ship: CargoShip) -> None:
if len(group.units) > 1:
# Cargo ship "groups" are single units. Killing the one ship kills the whole
# transfer. If we ever want to add escorts or create multiple cargo ships in
# a convoy of ships that logic needs to change.
raise ValueError("Expected cargo ship to be a single unit group.")
unit = group.units[0]
# The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__.
name = str(unit.name)
if name in self.cargo_ships:
raise RuntimeError(f"Duplicate cargo ship: {name}")
self.cargo_ships[name] = ship
def cargo_ship(self, name: str) -> Optional[CargoShip]:
return self.cargo_ships.get(name, None)
def add_airlift_units(self, group: FlyingGroup, transfer: TransferOrder) -> None:
capacity_each = math.ceil(transfer.size / len(group.units))
for idx, transport in enumerate(group.units):
# Slice the units in groups based on the capacity of each unit. Cargo is
# assigned arbitrarily to units in the order of the group. The last unit in
# the group will receive a partial load if there is not enough cargo to fill
# every transport.
base_idx = idx * capacity_each
cargo = tuple(
itertools.islice(
transfer.iter_units(), base_idx, base_idx + capacity_each
)
)
# The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__.
name = str(transport.name)
if name in self.airlifts:
raise RuntimeError(f"Duplicate airlift unit: {name}")
self.airlifts[name] = AirliftUnits(cargo, transfer)
def airlift_unit(self, name: str) -> Optional[AirliftUnits]:
return self.airlifts.get(name, None)
def add_building(self, ground_object: BuildingGroundObject, group: Group) -> None:
# The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__.
# The name of the initiator in the DCS dead event will have " object"
@@ -119,8 +211,9 @@ class UnitMap:
raise RuntimeError(f"Duplicate TGO unit: {name}")
self.buildings[name] = Building(ground_object)
def add_fortification(self, ground_object: BuildingGroundObject,
group: VehicleGroup) -> None:
def add_fortification(
self, ground_object: BuildingGroundObject, group: VehicleGroup
) -> None:
if len(group.units) != 1:
raise ValueError("Fortification groups must have exactly one unit.")
unit = group.units[0]
@@ -131,5 +224,15 @@ class UnitMap:
raise RuntimeError(f"Duplicate TGO unit: {name}")
self.buildings[name] = Building(ground_object)
def add_scenery(self, ground_object: SceneryGroundObject) -> None:
name = str(ground_object.map_object_id)
if name in self.buildings:
raise RuntimeError(
f"Duplicate TGO unit: {name}. TriggerZone name: "
f"{ground_object.dcs_identifier}"
)
self.buildings[name] = Building(ground_object)
def building_or_fortification(self, name: str) -> Optional[Building]:
return self.buildings.get(name, None)

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
import itertools
import math
from dataclasses import dataclass
from typing import Union
@@ -178,3 +179,13 @@ def mach(value: float, altitude: Distance) -> Speed:
SPEED_OF_SOUND_AT_SEA_LEVEL = knots(661.5)
def pairwise(iterable):
"""
itertools recipe
s -> (s0,s1), (s1,s2), (s2, s3), ...
"""
a, b = itertools.tee(iterable)
next(b, None)
return zip(a, b)

View File

@@ -2,7 +2,7 @@ from pathlib import Path
def _build_version_string() -> str:
components = ["2.4"]
components = ["3.0"]
build_number_path = Path("resources/buildnumber")
if build_number_path.exists():
with build_number_path.open("r") as build_number_file:
@@ -16,3 +16,75 @@ def _build_version_string() -> str:
#: Current version of Liberation.
VERSION = _build_version_string()
#: The latest version of the campaign format. Increment this version whenever all
#: existing campaigns should be flagged as incompatible in the UI. We will still attempt
#: to load old campaigns, but this provides a warning to the user that the campaign may
#: not work correctly.
#:
#: There is no verification that the campaign author updated their campaign correctly
#: this is just a UI hint.
#:
#: Version history:
#:
#: Version 0
#: * Unknown compatibility.
#:
#: Version 1
#: * Compatible with Liberation 2.5.
#:
#: Version 2
#: * Front line endpoints now define convoy origin/destination waypoints. They should be
#: placed on or near roads.
#: * Factories (Workshop_A) define factory objectives. Only control points with
#: factories will be able to recruit ground units, so they should exist in sufficient
#: number and be protected by IADS.
#:
#: Version 3
#: * Bulker Handy Winds define shipping lanes. They should be placed in port areas that
#: are navigable by ships and have a route to another port area. DCS ships *will not*
#: avoid driving into islands, so ensure that their waypoints plot a navigable route.
#:
#: Version 4
#: * TriggerZones define map based building targets. White TriggerZones created by right
#: clicking an object and using "assign as..." define the buildings within an objective.
#: Blue circular TriggerZones created normally must surround groups of one or more
#: white TriggerZones to define an objective. If a white TriggerZone is not surrounded
#: by a blue circular TriggerZone, campaign creation will fail. Blue circular
#: TriggerZones must also have their first property's value field define the type of
#: objective (a valid value for a building TGO category, from `game.db.PRICES`).
#:
#: Version 4.1
#: * All objective types may now be set as required generation (similar to the required
#: IADS generation). This includes:
#: * SHORADS
#: * Armor groups
#: * Strike targets
#: * Offshore strike targets
#: * Ships
#: * Missile sites
#: * Coastal defenses
#:
#: See the unit lists in MizCampaignLoader in conflicttheater.py for unit types.
#:
#: Version 4.2
#: * Adds support for AAA objectives. Place with any of the following units (either red
#: or blue):
#: * AAA_8_8cm_Flak_18,
#: * SPAAA_Vulcan_M163,
#: * SPAAA_ZSU_23_4_Shilka_Gun_Dish,
#:
#: Version 5.0
#: * Ammunition Depots objective locations are now predetermined using the "Ammunition
# Depot" Warehouse object, and through trigger zone based scenery objects.
#: * The number of alive Ammunition Depot objective buildings connected to a control
#: point directly influences how many ground units can be supported on the front
#: line.
#: * The number of supported ground units at any control point is artificially
#: capped at 50, even if the number of alive Ammunition Depot objectives can
#: support more.
#:
#: Version 6.0
#: * Random objective generation no is longer supported. Fixed objective locations were
#: added in 4.1.
CAMPAIGN_FORMAT_VERSION = (6, 0)

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -8,6 +8,8 @@ example, the package to strike an enemy airfield may contain an escort flight,
a SEAD flight, and the strike aircraft themselves. CAP packages may contain only
the single CAP flight.
"""
from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass, field
@@ -65,6 +67,10 @@ class Package:
waypoints: Optional[PackageWaypoints] = field(default=None)
@property
def has_players(self) -> bool:
return any(flight.client_count for flight in self.flights)
@property
def formation_speed(self) -> Optional[Speed]:
"""The speed of the package when in formation.
@@ -96,7 +102,8 @@ class Package:
if tot is None:
logging.error(
f"{flight} requested escort at {waypoint} but that "
"waypoint has no TOT. It may not be escorted.")
"waypoint has no TOT. It may not be escorted."
)
continue
times.append(tot)
if times:
@@ -117,7 +124,8 @@ class Package:
logging.error(
f"{flight} dismissed escort at {waypoint} but that "
"waypoint has no TOT or departure time. It may not be "
"escorted.")
"escorted."
)
continue
times.append(tot)
if times:
@@ -170,9 +178,11 @@ class Package:
FlightType.OCA_RUNWAY,
FlightType.BAI,
FlightType.DEAD,
FlightType.TRANSPORT,
FlightType.SEAD,
FlightType.TARCAP,
FlightType.BARCAP,
FlightType.AEWC,
FlightType.SWEEP,
FlightType.ESCORT,
]

View File

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

47
gen/cargoshipgen.py Normal file
View File

@@ -0,0 +1,47 @@
from __future__ import annotations
import itertools
from typing import TYPE_CHECKING
from dcs import Mission
from dcs.ships import Bulker_Handy_Wind
from dcs.unitgroup import ShipGroup
from game.transfers import CargoShip
from game.unitmap import UnitMap
from game.utils import knots
if TYPE_CHECKING:
from game import Game
class CargoShipGenerator:
def __init__(self, mission: Mission, game: Game, unit_map: UnitMap) -> None:
self.mission = mission
self.game = game
self.unit_map = unit_map
self.count = itertools.count()
def generate(self) -> None:
# Reset the count to make generation deterministic.
for ship in self.game.transfers.cargo_ships:
self.generate_cargo_ship(ship)
def generate_cargo_ship(self, ship: CargoShip) -> ShipGroup:
country = self.mission.country(
self.game.player_country if ship.player_owned else self.game.enemy_country
)
waypoints = ship.route
group = self.mission.ship_group(
country,
ship.name,
Bulker_Handy_Wind,
position=waypoints[0],
group_size=1,
)
for waypoint in waypoints[1:]:
# 12 knots is very slow but it's also nearly the max allowed by DCS for this
# type of ship.
group.add_waypoint(waypoint, speed=knots(12).kph)
self.unit_map.add_cargo_ship(group, ship)
return group

View File

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

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

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

View File

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

92
gen/convoygen.py Normal file
View File

@@ -0,0 +1,92 @@
from __future__ import annotations
import itertools
from typing import Dict, TYPE_CHECKING, Type
from dcs import Mission
from dcs.mapping import Point
from dcs.point import PointAction
from dcs.unit import Vehicle
from dcs.unitgroup import VehicleGroup
from dcs.unittype import VehicleType
from game.transfers import Convoy
from game.unitmap import UnitMap
from game.utils import kph
if TYPE_CHECKING:
from game import Game
class ConvoyGenerator:
def __init__(self, mission: Mission, game: Game, unit_map: UnitMap) -> None:
self.mission = mission
self.game = game
self.unit_map = unit_map
self.count = itertools.count()
def generate(self) -> None:
# Reset the count to make generation deterministic.
for convoy in self.game.transfers.convoys:
self.generate_convoy(convoy)
def generate_convoy(self, convoy: Convoy) -> VehicleGroup:
group = self._create_mixed_unit_group(
convoy.name,
convoy.route_start,
convoy.units,
convoy.player_owned,
)
group.add_waypoint(
convoy.route_end,
speed=kph(40).kph,
move_formation=PointAction.OnRoad,
)
self.make_drivable(group)
self.unit_map.add_convoy_units(group, convoy)
return group
def _create_mixed_unit_group(
self,
name: str,
position: Point,
units: Dict[Type[VehicleType], int],
for_player: bool,
) -> VehicleGroup:
country = self.mission.country(
self.game.player_country if for_player else self.game.enemy_country
)
unit_types = list(units.items())
main_unit_type, main_unit_count = unit_types[0]
group = self.mission.vehicle_group(
country,
name,
main_unit_type,
position=position,
group_size=main_unit_count,
move_formation=PointAction.OnRoad,
)
unit_name_counter = itertools.count(main_unit_count + 1)
# pydcs spreads units out by 20 in the Y axis by default. Pick up where it left
# off.
y = itertools.count(position.y + main_unit_count * 20, 20)
for unit_type, count in unit_types[1:]:
for i in range(count):
v = self.mission.vehicle(
f"{name} Unit #{next(unit_name_counter)}", unit_type
)
v.position.x = position.x
v.position.y = next(y)
v.heading = 0
group.add_unit(v)
return group
@staticmethod
def make_drivable(group: VehicleGroup) -> None:
for v in group.units:
if isinstance(v, Vehicle):
v.player_can_drive = True

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,12 +17,17 @@ from typing import (
TYPE_CHECKING,
Tuple,
Type,
TypeVar,
Union,
)
from dcs.unittype import FlyingType
from game.factions.faction import Faction
from game.infos.information import Information
from game.procurement import AircraftProcurementRequest
from game.profiling import logged_duration, MultiEventTracer
from game.squadrons import AirWing, Squadron
from game.theater import (
Airfield,
ControlPoint,
@@ -39,8 +44,8 @@ from game.theater.theatergroundobject import (
NavalGroundObject,
VehicleGroupGroundObject,
)
from game.utils import Distance, nautical_miles
from gen import Conflict
from game.transfers import CargoShip, Convoy
from game.utils import Distance, nautical_miles, meters
from gen.ato import Package
from gen.flights.ai_flight_planner_db import aircraft_for_task
from gen.flights.closestairfields import (
@@ -108,24 +113,31 @@ class ProposedMission:
#: The proposed flights that are required for the mission.
flights: List[ProposedFlight]
asap: bool = field(default=False)
def __str__(self) -> str:
flights = ', '.join([str(f) for f in self.flights])
flights = ", ".join([str(f) for f in self.flights])
return f"{self.location.name}: {flights}"
class AircraftAllocator:
"""Finds suitable aircraft for proposed missions."""
def __init__(self, closest_airfields: ClosestAirfields,
global_inventory: GlobalAircraftInventory,
is_player: bool) -> None:
def __init__(
self,
air_wing: AirWing,
closest_airfields: ClosestAirfields,
global_inventory: GlobalAircraftInventory,
is_player: bool,
) -> None:
self.air_wing = air_wing
self.closest_airfields = closest_airfields
self.global_inventory = global_inventory
self.is_player = is_player
def find_aircraft_for_flight(
self, flight: ProposedFlight
) -> Optional[Tuple[ControlPoint, Type[FlyingType]]]:
def find_squadron_for_flight(
self, flight: ProposedFlight
) -> Optional[Tuple[ControlPoint, Squadron]]:
"""Finds aircraft suitable for the given mission.
Searches for aircraft capable of performing the given mission within the
@@ -144,45 +156,70 @@ class AircraftAllocator:
on subsequent calls. If the found aircraft are not used, the caller is
responsible for returning them to the inventory.
"""
return self.find_aircraft_of_type(
flight, aircraft_for_task(flight.task)
)
return self.find_aircraft_for_task(flight, flight.task)
def find_aircraft_of_type(
self, flight: ProposedFlight, types: List[Type[FlyingType]],
) -> Optional[Tuple[ControlPoint, Type[FlyingType]]]:
airfields_in_range = self.closest_airfields.airfields_within(
def find_aircraft_for_task(
self, flight: ProposedFlight, task: FlightType
) -> Optional[Tuple[ControlPoint, Squadron]]:
types = aircraft_for_task(task)
airfields_in_range = self.closest_airfields.operational_airfields_within(
flight.max_distance
)
# Prefer using squadrons with pilots first
best_understaffed: Optional[Tuple[ControlPoint, Squadron]] = None
for airfield in airfields_in_range:
if not airfield.is_friendly(self.is_player):
continue
inventory = self.global_inventory.for_control_point(airfield)
for aircraft, available in inventory.all_aircraft:
for aircraft in types:
if not airfield.can_operate(aircraft):
continue
if aircraft in types and available >= flight.num_aircraft:
inventory.remove_aircraft(aircraft, flight.num_aircraft)
return airfield, aircraft
if inventory.available(aircraft) < flight.num_aircraft:
continue
# Valid location with enough aircraft available. Find a squadron to fit
# the role.
for squadron in self.air_wing.squadrons_for(aircraft):
if task not in squadron.auto_assignable_mission_types:
continue
if len(squadron.available_pilots) >= flight.num_aircraft:
inventory.remove_aircraft(aircraft, flight.num_aircraft)
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:
"""Builds a Package for the flights it receives."""
def __init__(self, location: MissionTarget,
closest_airfields: ClosestAirfields,
global_inventory: GlobalAircraftInventory,
is_player: bool,
package_country: str,
start_type: str) -> None:
def __init__(
self,
location: MissionTarget,
closest_airfields: ClosestAirfields,
global_inventory: GlobalAircraftInventory,
air_wing: AirWing,
is_player: bool,
package_country: str,
start_type: str,
asap: bool,
) -> None:
self.closest_airfields = closest_airfields
self.is_player = is_player
self.package_country = package_country
self.package = Package(location)
self.allocator = AircraftAllocator(closest_airfields, global_inventory,
is_player)
self.package = Package(location, auto_asap=asap)
self.allocator = AircraftAllocator(
air_wing, closest_airfields, global_inventory, is_player
)
self.global_inventory = global_inventory
self.start_type = start_type
@@ -194,25 +231,36 @@ class PackageBuilder:
caller should return any previously planned flights to the inventory
using release_planned_aircraft.
"""
assignment = self.allocator.find_aircraft_for_flight(plan)
assignment = self.allocator.find_squadron_for_flight(plan)
if assignment is None:
return False
airfield, aircraft = assignment
airfield, squadron = assignment
if isinstance(airfield, OffMapSpawn):
start_type = "In Flight"
else:
start_type = self.start_type
flight = Flight(self.package, self.package_country, aircraft, plan.num_aircraft, plan.task,
start_type, departure=airfield, arrival=airfield,
divert=self.find_divert_field(aircraft, airfield))
flight = Flight(
self.package,
self.package_country,
squadron,
plan.num_aircraft,
plan.task,
start_type,
departure=airfield,
arrival=airfield,
divert=self.find_divert_field(squadron.aircraft, airfield),
)
self.package.add_flight(flight)
return True
def find_divert_field(self, aircraft: Type[FlyingType],
arrival: ControlPoint) -> Optional[ControlPoint]:
def find_divert_field(
self, aircraft: Type[FlyingType], arrival: ControlPoint
) -> Optional[ControlPoint]:
divert_limit = nautical_miles(150)
for airfield in self.closest_airfields.airfields_within(divert_limit):
for airfield in self.closest_airfields.operational_airfields_within(
divert_limit
):
if airfield.captured != self.is_player:
continue
if airfield == arrival:
@@ -233,9 +281,13 @@ class PackageBuilder:
flights = list(self.package.flights)
for flight in flights:
self.global_inventory.return_from_flight(flight)
flight.clear_roster()
self.package.remove_flight(flight)
MissionTargetType = TypeVar("MissionTargetType", bound=MissionTarget)
class ObjectiveFinder:
"""Identifies potential objectives for the mission planner."""
@@ -247,41 +299,53 @@ class ObjectiveFinder:
self.game = game
self.is_player = is_player
def enemy_sams(self) -> Iterator[TheaterGroundObject]:
def enemy_air_defenses(self) -> Iterator[tuple[TheaterGroundObject, Distance]]:
"""Iterates over all enemy SAM sites."""
# Control points might have the same ground object several times, for
# some reason.
found_targets: Set[str] = set()
doctrine = self.game.faction_for(self.is_player).doctrine
threat_zones = self.game.threat_zone_for(not self.is_player)
for cp in self.enemy_control_points():
for ground_object in cp.ground_objects:
is_ewr = isinstance(ground_object, EwrGroundObject)
is_sam = isinstance(ground_object, SamGroundObject)
if not is_ewr and not is_sam:
continue
if ground_object.is_dead:
continue
if ground_object.name in found_targets:
if isinstance(ground_object, EwrGroundObject):
if threat_zones.threatened_by_air_defense(ground_object):
# This is a very weak heuristic for determining whether the EWR
# is close enough to be worth targeting before a SAM that is
# covering it. Ingress distance corresponds to the beginning of
# the attack range and is sufficient for most standoff weapons,
# so treating the ingress distance as the threat distance sorts
# these EWRs such that they will be attacked before SAMs that do
# not threaten the ingress point, but after those that do.
target_range = doctrine.ingress_egress_distance
else:
# But if the EWR isn't covered then we should only be worrying
# about its detection range.
target_range = ground_object.max_detection_range()
elif isinstance(ground_object, SamGroundObject):
target_range = ground_object.max_threat_range()
else:
continue
if not ground_object.has_radar:
continue
yield ground_object, target_range
# TODO: Yield in order of most threatening.
# Need to sort in order of how close their defensive range comes
# to friendly assets. To do that we need to add effective range
# information to the database.
yield ground_object
found_targets.add(ground_object.name)
def threatening_sams(self) -> Iterator[MissionTarget]:
def threatening_air_defenses(self) -> Iterator[TheaterGroundObject]:
"""Iterates over enemy SAMs in threat range of friendly control points.
SAM sites are sorted by their closest proximity to any friendly control
point (airfield or fleet).
"""
return self._targets_by_range(self.enemy_sams())
target_ranges: list[tuple[TheaterGroundObject, Distance]] = []
for target, threat_range in self.enemy_air_defenses():
ranges: list[Distance] = []
for cp in self.friendly_control_points():
ranges.append(meters(target.distance_to(cp)) - threat_range)
target_ranges.append((target, min(ranges)))
target_ranges = sorted(target_ranges, key=operator.itemgetter(1))
for target, _range in target_ranges:
yield target
def enemy_vehicle_groups(self) -> Iterator[VehicleGroupGroundObject]:
"""Iterates over all enemy vehicle groups."""
@@ -323,9 +387,9 @@ class ObjectiveFinder:
return self._targets_by_range(self.enemy_ships())
def _targets_by_range(
self,
targets: Iterable[MissionTarget]) -> Iterator[MissionTarget]:
target_ranges: List[Tuple[MissionTarget, int]] = []
self, targets: Iterable[MissionTargetType]
) -> Iterator[MissionTargetType]:
target_ranges: List[Tuple[MissionTargetType, int]] = []
for target in targets:
ranges: List[int] = []
for cp in self.friendly_control_points():
@@ -371,7 +435,7 @@ class ObjectiveFinder:
is_building = isinstance(ground_object, BuildingGroundObject)
is_fob = isinstance(enemy_cp, Fob)
if is_building and is_fob and ground_object.airbase_group:
if is_building and is_fob and ground_object.is_control_point:
# This is the FOB structure itself. Can't be repaired or
# targeted by the player, so shouldn't be targetable by the
# AI.
@@ -392,13 +456,7 @@ class ObjectiveFinder:
def front_lines(self) -> Iterator[FrontLine]:
"""Iterates over all active front lines in the theater."""
for cp in self.friendly_control_points():
for connected in cp.connected_points:
if connected.is_friendly(self.is_player):
continue
if Conflict.has_frontline_between(cp, connected):
yield FrontLine(cp, connected, self.game.theater)
yield from self.game.theater.conflicts()
def vulnerable_control_points(self) -> Iterator[ControlPoint]:
"""Iterates over friendly CPs that are vulnerable to enemy CPs.
@@ -411,8 +469,10 @@ class ObjectiveFinder:
# Off-map spawn locations don't need protection.
continue
airfields_in_proximity = self.closest_airfields_to(cp)
airfields_in_threat_range = airfields_in_proximity.airfields_within(
self.AIRFIELD_THREAT_RANGE
airfields_in_threat_range = (
airfields_in_proximity.operational_airfields_within(
self.AIRFIELD_THREAT_RANGE
)
)
for airfield in airfields_in_threat_range:
if not airfield.is_friendly(self.is_player):
@@ -428,15 +488,49 @@ class ObjectiveFinder:
airfields.append(control_point)
return self._targets_by_range(airfields)
def convoys(self) -> Iterator[Convoy]:
for front_line in self.front_lines():
yield from self.game.transfers.convoys.travelling_to(
front_line.control_point_hostile_to(self.is_player)
)
def cargo_ships(self) -> Iterator[CargoShip]:
for front_line in self.front_lines():
yield from self.game.transfers.cargo_ships.travelling_to(
front_line.control_point_hostile_to(self.is_player)
)
def friendly_control_points(self) -> Iterator[ControlPoint]:
"""Iterates over all friendly control points."""
return (c for c in self.game.theater.controlpoints if
c.is_friendly(self.is_player))
return (
c for c in self.game.theater.controlpoints if c.is_friendly(self.is_player)
)
def farthest_friendly_control_point(self) -> ControlPoint:
"""Finds the friendly control point that is farthest from any threats."""
threat_zones = self.game.threat_zone_for(not self.is_player)
farthest = None
max_distance = meters(0)
for cp in self.friendly_control_points():
if isinstance(cp, OffMapSpawn):
continue
distance = threat_zones.distance_to_threat(cp.position)
if distance > max_distance:
farthest = cp
max_distance = distance
if farthest is None:
raise RuntimeError("Found no friendly control points. You probably lost.")
return farthest
def enemy_control_points(self) -> Iterator[ControlPoint]:
"""Iterates over all enemy control points."""
return (c for c in self.game.theater.controlpoints if
not c.is_friendly(self.is_player))
return (
c
for c in self.game.theater.controlpoints
if not c.is_friendly(self.is_player)
)
def all_possible_targets(self) -> Iterator[MissionTarget]:
"""Iterates over all possible mission targets in the theater.
@@ -487,6 +581,7 @@ class CoalitionMissionPlanner:
MAX_OCA_RANGE = nautical_miles(150)
MAX_SEAD_RANGE = nautical_miles(150)
MAX_STRIKE_RANGE = nautical_miles(150)
MAX_AWEC_RANGE = nautical_miles(200)
def __init__(self, game: Game, is_player: bool) -> None:
self.game = game
@@ -494,7 +589,25 @@ class CoalitionMissionPlanner:
self.objective_finder = ObjectiveFinder(self.game, self.is_player)
self.ato = self.game.blue_ato if is_player else self.game.red_ato
self.threat_zones = self.game.threat_zone_for(not self.is_player)
self.procurement_requests: List[AircraftProcurementRequest] = []
self.procurement_requests = self.game.procurement_requests_for(self.is_player)
self.faction = self.game.faction_for(self.is_player)
def air_wing_can_plan(self, mission_type: FlightType) -> bool:
"""Returns True if it is possible for the air wing to plan this mission type.
Not all mission types can be fulfilled by all air wings. Many factions do not
have AEW&C aircraft, so they will never be able to plan those missions. It's
also possible for the player to exclude mission types from their squadron
designs.
"""
all_compatible = aircraft_for_task(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]:
"""Identifies the most important missions to plan this turn.
@@ -506,27 +619,55 @@ class CoalitionMissionPlanner:
ensure that they can be planned again next turn even if all aircraft are
eliminated this turn.
"""
# Find farthest, friendly CP for AEWC.
yield ProposedMission(
self.objective_finder.farthest_friendly_control_point(),
[ProposedFlight(FlightType.AEWC, 1, self.MAX_AWEC_RANGE)],
# Supports all the early CAP flights, so should be in the air ASAP.
asap=True,
)
# Find friendly CPs within 100 nmi from an enemy airfield, plan CAP.
for cp in self.objective_finder.vulnerable_control_points():
# Plan three rounds of CAP to give ~90 minutes coverage. Spacing
# these out appropriately is done in stagger_missions.
yield ProposedMission(cp, [
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
])
yield ProposedMission(cp, [
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
])
yield ProposedMission(cp, [
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
])
# Plan CAP in such a way, that it is established during the whole desired mission length
for _ in range(
0,
int(self.game.settings.desired_player_mission_duration.total_seconds()),
int(self.faction.doctrine.cap_duration.total_seconds()),
):
yield ProposedMission(
cp,
[
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
],
)
# Find front lines, plan CAS.
for front_line in self.objective_finder.front_lines():
yield ProposedMission(front_line, [
ProposedFlight(FlightType.CAS, 2, self.MAX_CAS_RANGE),
ProposedFlight(FlightType.TARCAP, 2, self.MAX_CAP_RANGE,
EscortType.AirToAir),
])
yield ProposedMission(
front_line,
[
ProposedFlight(FlightType.CAS, 2, self.MAX_CAS_RANGE),
# This is *not* an escort because front lines don't create a threat
# zone. Generating threat zones from front lines causes the front
# line to push back BARCAPs as it gets closer to the base. While
# front lines do have the same problem of potentially pulling
# BARCAPs off bases to engage a front line TARCAP, that's probably
# the one time where we do want that.
#
# TODO: Use intercepts and extra TARCAPs to cover bases near fronts.
# We don't have intercept missions yet so this isn't something we
# can do today, but we should probably return to having the front
# line project a threat zone (so that strike missions will route
# around it) and instead *not plan* a BARCAP at bases near the
# front, since there isn't a place to put a barrier. Instead, the
# aircraft that would have been a BARCAP could be used as additional
# interceptors and TARCAPs which will defend the base but won't be
# trying to avoid front line contacts.
ProposedFlight(FlightType.TARCAP, 2, self.MAX_CAP_RANGE),
],
)
def propose_missions(self) -> Iterator[ProposedMission]:
"""Identifies and iterates over potential mission in priority order."""
@@ -536,31 +677,109 @@ class CoalitionMissionPlanner:
# or objects, plan DEAD.
# Find enemy SAM sites with ranges that extend to within 50 nmi of
# friendly CPs, front, lines, or objects, plan DEAD.
for sam in self.objective_finder.threatening_sams():
yield ProposedMission(sam, [
ProposedFlight(FlightType.DEAD, 2, self.MAX_SEAD_RANGE),
# TODO: Max escort range.
ProposedFlight(FlightType.ESCORT, 2, self.MAX_SEAD_RANGE,
EscortType.AirToAir),
])
for sam in self.objective_finder.threatening_air_defenses():
flights = [ProposedFlight(FlightType.DEAD, 2, self.MAX_SEAD_RANGE)]
# Only include SEAD against SAMs that still have emitters. No need to
# suppress an EWR, and SEAD isn't useful against a SAM that no longer has a
# working track radar.
#
# For SAMs without track radars and EWRs, we still want a SEAD escort if
# needed.
#
# Note that there is a quirk here: we should potentially be included a SEAD
# escort *and* SEAD when the target is a radar SAM but the flight path is
# also threatened by SAMs. We don't want to include a SEAD escort if the
# package is *only* threatened by the target though. Could be improved, but
# needs a decent refactor to the escort planning to do so.
if sam.has_live_radar_sam:
flights.append(ProposedFlight(FlightType.SEAD, 2, self.MAX_SEAD_RANGE))
else:
flights.append(
ProposedFlight(
FlightType.SEAD_ESCORT, 2, self.MAX_SEAD_RANGE, EscortType.Sead
)
)
# TODO: Max escort range.
flights.append(
ProposedFlight(
FlightType.ESCORT, 2, self.MAX_SEAD_RANGE, EscortType.AirToAir
)
)
yield ProposedMission(sam, flights)
# These will only rarely get planned. When a convoy is travelling multiple legs,
# they're targetable after the first leg. The reason for this is that
# procurement happens *after* mission planning so that the missions that could
# not be filled will guide the procurement process. Procurement is the stage
# that convoys are created (because they're created to move ground units that
# were just purchased), so we haven't created any yet. Any incomplete transfers
# from the previous turn (multi-leg journeys) will still be present though so
# they can be targeted.
#
# Even after this is fixed, the player's convoys that were created through the
# UI will never be targeted on the first turn of their journey because the AI
# stops planning after the start of the turn. We could potentially fix this by
# moving opfor mission planning until the takeoff button is pushed.
for convoy in self.objective_finder.convoys():
yield ProposedMission(
convoy,
[
ProposedFlight(FlightType.BAI, 2, self.MAX_BAI_RANGE),
# TODO: Max escort range.
ProposedFlight(
FlightType.ESCORT, 2, self.MAX_BAI_RANGE, EscortType.AirToAir
),
ProposedFlight(
FlightType.SEAD_ESCORT, 2, self.MAX_BAI_RANGE, EscortType.Sead
),
],
)
for ship in self.objective_finder.cargo_ships():
yield ProposedMission(
ship,
[
ProposedFlight(FlightType.ANTISHIP, 2, self.MAX_ANTISHIP_RANGE),
# TODO: Max escort range.
ProposedFlight(
FlightType.ESCORT, 2, self.MAX_BAI_RANGE, EscortType.AirToAir
),
ProposedFlight(
FlightType.SEAD_ESCORT, 2, self.MAX_BAI_RANGE, EscortType.Sead
),
],
)
for group in self.objective_finder.threatening_ships():
yield ProposedMission(group, [
ProposedFlight(FlightType.ANTISHIP, 2, self.MAX_ANTISHIP_RANGE),
# TODO: Max escort range.
ProposedFlight(FlightType.ESCORT, 2, self.MAX_ANTISHIP_RANGE,
EscortType.AirToAir),
])
yield ProposedMission(
group,
[
ProposedFlight(FlightType.ANTISHIP, 2, self.MAX_ANTISHIP_RANGE),
# TODO: Max escort range.
ProposedFlight(
FlightType.ESCORT,
2,
self.MAX_ANTISHIP_RANGE,
EscortType.AirToAir,
),
],
)
for group in self.objective_finder.threatening_vehicle_groups():
yield ProposedMission(group, [
ProposedFlight(FlightType.BAI, 2, self.MAX_BAI_RANGE),
# TODO: Max escort range.
ProposedFlight(FlightType.ESCORT, 2, self.MAX_BAI_RANGE,
EscortType.AirToAir),
ProposedFlight(FlightType.SEAD, 2, self.MAX_OCA_RANGE,
EscortType.Sead),
])
yield ProposedMission(
group,
[
ProposedFlight(FlightType.BAI, 2, self.MAX_BAI_RANGE),
# TODO: Max escort range.
ProposedFlight(
FlightType.ESCORT, 2, self.MAX_BAI_RANGE, EscortType.AirToAir
),
ProposedFlight(
FlightType.SEAD_ESCORT, 2, self.MAX_OCA_RANGE, EscortType.Sead
),
],
)
for target in self.objective_finder.oca_targets(min_aircraft=20):
flights = [
@@ -569,54 +788,77 @@ class CoalitionMissionPlanner:
if self.game.settings.default_start_type == "Cold":
# Only schedule if the default start type is Cold. If the player
# has set anything else there are no targets to hit.
flights.append(ProposedFlight(FlightType.OCA_AIRCRAFT, 2,
self.MAX_OCA_RANGE))
flights.extend([
# TODO: Max escort range.
ProposedFlight(FlightType.ESCORT, 2, self.MAX_OCA_RANGE,
EscortType.AirToAir),
ProposedFlight(FlightType.SEAD, 2, self.MAX_OCA_RANGE,
EscortType.Sead),
])
flights.append(
ProposedFlight(FlightType.OCA_AIRCRAFT, 2, self.MAX_OCA_RANGE)
)
flights.extend(
[
# TODO: Max escort range.
ProposedFlight(
FlightType.ESCORT, 2, self.MAX_OCA_RANGE, EscortType.AirToAir
),
ProposedFlight(
FlightType.SEAD_ESCORT, 2, self.MAX_OCA_RANGE, EscortType.Sead
),
]
)
yield ProposedMission(target, flights)
# Plan strike missions.
for target in self.objective_finder.strike_targets():
yield ProposedMission(target, [
ProposedFlight(FlightType.STRIKE, 2, self.MAX_STRIKE_RANGE),
# TODO: Max escort range.
ProposedFlight(FlightType.ESCORT, 2, self.MAX_STRIKE_RANGE,
EscortType.AirToAir),
ProposedFlight(FlightType.SEAD, 2, self.MAX_STRIKE_RANGE,
EscortType.Sead),
])
yield ProposedMission(
target,
[
ProposedFlight(FlightType.STRIKE, 2, self.MAX_STRIKE_RANGE),
# TODO: Max escort range.
ProposedFlight(
FlightType.ESCORT, 2, self.MAX_STRIKE_RANGE, EscortType.AirToAir
),
ProposedFlight(
FlightType.SEAD_ESCORT,
2,
self.MAX_STRIKE_RANGE,
EscortType.Sead,
),
],
)
def plan_missions(self) -> None:
"""Identifies and plans mission for the turn."""
for proposed_mission in self.propose_missions():
self.plan_mission(proposed_mission)
player = "Blue" if self.is_player else "Red"
with logged_duration(f"{player} mission identification and fulfillment"):
with MultiEventTracer() as tracer:
for proposed_mission in self.propose_missions():
self.plan_mission(proposed_mission, tracer)
for critical_mission in self.critical_missions():
self.plan_mission(critical_mission, reserves=True)
with logged_duration(f"{player} reserve mission planning"):
with MultiEventTracer() as tracer:
for critical_mission in self.critical_missions():
self.plan_mission(critical_mission, tracer, reserves=True)
self.stagger_missions()
with logged_duration(f"{player} mission scheduling"):
self.stagger_missions()
for cp in self.objective_finder.friendly_control_points():
inventory = self.game.aircraft_inventory.for_control_point(cp)
for aircraft, available in inventory.all_aircraft:
self.message("Unused aircraft",
f"{available} {aircraft.id} from {cp}")
self.message("Unused aircraft", f"{available} {aircraft.id} from {cp}")
def plan_flight(self, mission: ProposedMission, flight: ProposedFlight,
builder: PackageBuilder, missing_types: Set[FlightType],
for_reserves: bool) -> None:
def plan_flight(
self,
mission: ProposedMission,
flight: ProposedFlight,
builder: PackageBuilder,
missing_types: Set[FlightType],
for_reserves: bool,
) -> None:
if not builder.plan_flight(flight):
missing_types.add(flight.task)
purchase_order = AircraftProcurementRequest(
near=mission.location,
range=flight.max_distance,
task_capability=flight.task,
number=flight.num_aircraft
number=flight.num_aircraft,
)
if for_reserves:
# Reserves are planned for critical missions, so prioritize
@@ -626,50 +868,53 @@ class CoalitionMissionPlanner:
self.procurement_requests.append(purchase_order)
def scrub_mission_missing_aircraft(
self, mission: ProposedMission, builder: PackageBuilder,
missing_types: Set[FlightType],
not_attempted: Iterable[ProposedFlight],
reserves: bool) -> None:
self,
mission: ProposedMission,
builder: PackageBuilder,
missing_types: Set[FlightType],
not_attempted: Iterable[ProposedFlight],
reserves: bool,
) -> None:
# Try to plan the rest of the mission just so we can count the missing
# types to buy.
for flight in not_attempted:
self.plan_flight(mission, flight, builder, missing_types, reserves)
missing_types_str = ", ".join(
sorted([t.name for t in missing_types]))
missing_types_str = ", ".join(sorted([t.name for t in missing_types]))
builder.release_planned_aircraft()
desc = "reserve aircraft" if reserves else "aircraft"
self.message(
"Insufficient aircraft",
f"Not enough {desc} in range for {mission.location.name} "
f"capable of: {missing_types_str}")
f"capable of: {missing_types_str}",
)
def check_needed_escorts(
self, builder: PackageBuilder) -> Dict[EscortType, bool]:
def check_needed_escorts(self, builder: PackageBuilder) -> Dict[EscortType, bool]:
threats = defaultdict(bool)
for flight in builder.package.flights:
if self.threat_zones.threatened_by_aircraft(flight):
if self.threat_zones.waypoints_threatened_by_aircraft(
flight.flight_plan.escorted_waypoints()
):
threats[EscortType.AirToAir] = True
if self.threat_zones.threatened_by_air_defense(flight):
if self.threat_zones.waypoints_threatened_by_radar_sam(
list(flight.flight_plan.escorted_waypoints())
):
threats[EscortType.Sead] = True
return threats
def plan_mission(self, mission: ProposedMission,
reserves: bool = False) -> None:
def plan_mission(
self, mission: ProposedMission, tracer: MultiEventTracer, reserves: bool = False
) -> None:
"""Allocates aircraft for a proposed mission and adds it to the ATO."""
if self.is_player:
package_country = self.game.player_country
else:
package_country = self.game.enemy_country
builder = PackageBuilder(
mission.location,
self.objective_finder.closest_airfields_to(mission.location),
self.game.aircraft_inventory,
self.game.air_wing_for(self.is_player),
self.is_player,
package_country,
self.game.settings.default_start_type
self.game.country_for(self.is_player),
self.game.settings.default_start_type,
mission.asap,
)
# Attempt to plan all the main elements of the mission first. Escorts
@@ -678,17 +923,31 @@ class CoalitionMissionPlanner:
missing_types: Set[FlightType] = set()
escorts = []
for proposed_flight in mission.flights:
if not self.air_wing_can_plan(proposed_flight.task):
# This air wing can never plan this mission type because they do not
# have compatible aircraft or squadrons. Skip fulfillment so that we
# don't place the purchase request.
continue
if proposed_flight.escort_type is not None:
# Escorts are planned after the primary elements of the package.
# If the package does not need escorts they may be pruned.
escorts.append(proposed_flight)
continue
self.plan_flight(mission, proposed_flight, builder, missing_types,
reserves)
with tracer.trace("Flight planning"):
self.plan_flight(
mission, proposed_flight, builder, missing_types, reserves
)
if missing_types:
self.scrub_mission_missing_aircraft(mission, builder, missing_types,
escorts, reserves)
self.scrub_mission_missing_aircraft(
mission, builder, missing_types, escorts, reserves
)
return
if not builder.package.flights:
# The non-escort part of this mission is unplannable by this faction. Scrub
# the mission and do not attempt planning escorts because there's no reason
# to buy them because this mission will never be planned.
return
# Create flight plans for the main flights of the package so we can
@@ -697,10 +956,12 @@ class CoalitionMissionPlanner:
# flights that will rendezvous with their package will be affected by
# the other flights in the package. Escorts will not be able to
# contribute to this.
flight_plan_builder = FlightPlanBuilder(self.game, builder.package,
self.is_player)
flight_plan_builder = FlightPlanBuilder(
self.game, builder.package, self.is_player
)
for flight in builder.package.flights:
flight_plan_builder.populate_flight_plan(flight)
with tracer.trace("Flight plan population"):
flight_plan_builder.populate_flight_plan(flight)
needed_escorts = self.check_needed_escorts(builder)
for escort in escorts:
@@ -708,14 +969,15 @@ class CoalitionMissionPlanner:
# impossible.
assert escort.escort_type is not None
if needed_escorts[escort.escort_type]:
self.plan_flight(mission, escort, builder, missing_types,
reserves)
with tracer.trace("Flight planning"):
self.plan_flight(mission, escort, builder, missing_types, reserves)
# Check again for unavailable aircraft. If the escort was required and
# none were found, scrub the mission.
if missing_types:
self.scrub_mission_missing_aircraft(mission, builder, missing_types,
escorts, reserves)
self.scrub_mission_missing_aircraft(
mission, builder, missing_types, escorts, reserves
)
return
if reserves:
@@ -728,33 +990,41 @@ class CoalitionMissionPlanner:
# Add flight plans for escorts.
for flight in package.flights:
if not flight.flight_plan.waypoints:
flight_plan_builder.populate_flight_plan(flight)
with tracer.trace("Flight plan population"):
flight_plan_builder.populate_flight_plan(flight)
if package.has_players and self.game.settings.auto_ato_player_missions_asap:
package.auto_asap = True
package.set_tot_asap()
self.ato.add_package(package)
def stagger_missions(self) -> None:
def start_time_generator(count: int, earliest: int, latest: int,
margin: int) -> Iterator[timedelta]:
def start_time_generator(
count: int, earliest: int, latest: int, margin: int
) -> Iterator[timedelta]:
interval = (latest - earliest) // count
for time in range(earliest, latest, interval):
error = random.randint(-margin, margin)
yield timedelta(minutes=max(0, time + error))
yield timedelta(seconds=max(0, time + error))
dca_types = {
FlightType.BARCAP,
FlightType.TARCAP,
}
previous_cap_end_time: Dict[MissionTarget, timedelta] = defaultdict(
timedelta
)
non_dca_packages = [p for p in self.ato.packages if
p.primary_task not in dca_types]
previous_cap_end_time: Dict[MissionTarget, timedelta] = defaultdict(timedelta)
non_dca_packages = [
p for p in self.ato.packages if p.primary_task not in dca_types
]
start_time = start_time_generator(
count=len(non_dca_packages),
earliest=5,
latest=90,
margin=5
earliest=5 * 60,
latest=int(
self.game.settings.desired_player_mission_duration.total_seconds()
),
margin=5 * 60,
)
for package in self.ato.packages:
tot = TotEstimator(package).earliest_tot()
@@ -771,10 +1041,11 @@ class CoalitionMissionPlanner:
departure_time = package.mission_departure_time
# Should be impossible for CAPs
if departure_time is None:
logging.error(
f"Could not determine mission end time for {package}")
logging.error(f"Could not determine mission end time for {package}")
continue
previous_cap_end_time[package.target] = departure_time
elif package.auto_asap:
package.set_tot_asap()
else:
# But other packages should be spread out a bit. Note that take
# times are delayed, but all aircraft will become active at
@@ -791,8 +1062,6 @@ class CoalitionMissionPlanner:
message to the info panel.
"""
if self.is_player:
self.game.informations.append(
Information(title, text, self.game.turn)
)
self.game.informations.append(Information(title, text, self.game.turn))
else:
logging.info(f"{title}: {text}")

View File

@@ -5,15 +5,19 @@ from dcs.helicopters import (
AH_1W,
AH_64A,
AH_64D,
CH_47D,
CH_53E,
Ka_50,
Mi_24V,
Mi_26,
Mi_28N,
Mi_8MT,
OH_58D,
SA342L,
SA342M,
SH_60B,
UH_1H,
SH_60B
UH_60A,
)
from dcs.planes import (
AJS37,
@@ -22,11 +26,17 @@ from dcs.planes import (
A_10C,
A_10C_2,
A_20G,
A_50,
An_26B,
B_17G,
B_1B,
B_52H,
Bf_109K_4,
C_101CC,
C_130,
C_17A,
E_2C,
E_3A,
FA_18C_hornet,
FW_190A8,
FW_190D9,
@@ -40,9 +50,12 @@ from dcs.planes import (
F_4E,
F_5E_3,
F_86F_Sabre,
IL_76MD,
I_16,
JF_17,
J_11A,
Ju_88A4,
KJ_2000,
L_39ZA,
MQ_9_Reaper,
M_2000C,
@@ -54,7 +67,6 @@ from dcs.planes import (
MiG_27K,
MiG_29A,
MiG_29G,
MiG_29K,
MiG_29S,
MiG_31,
Mirage_2000_5,
@@ -83,18 +95,18 @@ from dcs.planes import (
Tu_22M3,
Tu_95MS,
WingLoong_I,
I_16
I_16,
Yak_40,
)
from dcs.unittype import FlyingType
from gen.flights.flight import FlightType
from pydcs_extensions.a4ec.a4ec import A_4E_C
from pydcs_extensions.f22a.f22a import F_22A
from pydcs_extensions.mb339.mb339 import MB_339PAN
from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_M, Rafale_B
from pydcs_extensions.su57.su57 import Su_57
from pydcs_extensions.jas39.jas39 import JAS39Gripen, JAS39Gripen_AG
from pydcs_extensions.hercules.hercules import Hercules
from pydcs_extensions.mb339.mb339 import MB_339PAN
from pydcs_extensions.su57.su57 import Su_57
# All aircraft lists are in priority order. Aircraft higher in the list will be
# preferred over those lower in the list.
@@ -111,26 +123,25 @@ CAP_CAPABLE = [
F_14B,
F_14A_135_GR,
MiG_25PD,
Rafale_M,
Su_33,
Su_30,
Su_27,
J_11A,
F_15C,
MiG_29S,
MiG_29K,
MiG_29G,
MiG_29A,
F_16C_50,
FA_18C_hornet,
F_15E,
F_16A,
F_4E,
JAS39Gripen,
JF_17,
MiG_23MLD,
MiG_21Bis,
Mirage_2000_5,
M_2000C,
F_15E,
F_5E_3,
MiG_19P,
A_4E_C,
@@ -156,30 +167,32 @@ CAP_CAPABLE = [
CAS_CAPABLE = [
A_10C_2,
A_10C,
B_1B,
F_14B,
F_14A_135_GR,
Hercules,
Su_25TM,
Su_25T,
Su_25,
F_15E,
F_16C_50,
FA_18C_hornet,
Rafale_A_S,
Rafale_B,
Tornado_GR4,
Tornado_IDS,
JAS39Gripen_AG,
JF_17,
AV8BNA,
A_10A,
B_1B,
A_4E_C,
F_14B,
F_14A_135_GR,
AJS37,
Su_24MR,
Su_24M,
Su_17M4,
AV8BNA,
F_4E,
S_3B,
Su_34,
Su_30,
MiG_19P,
MiG_29S,
MiG_27K,
MiG_29A,
@@ -202,6 +215,7 @@ CAS_CAPABLE = [
MB_339PAN,
L_39ZA,
A_20G,
Ju_88A4,
P_47D_40,
P_47D_30bl1,
P_47D_30,
@@ -219,7 +233,7 @@ CAS_CAPABLE = [
]
# Aircraft used for SEAD tasks
# Aircraft used for SEAD and SEAD Escort tasks. Must be capable of the CAS DCS task.
SEAD_CAPABLE = [
JF_17,
F_16C_50,
@@ -227,10 +241,11 @@ SEAD_CAPABLE = [
Tornado_IDS,
Su_25T,
Su_25TM,
Rafale_A_S,
Rafale_B,
F_4E,
A_4E_C,
F_14B,
F_14A_135_GR,
JAS39Gripen_AG,
AV8BNA,
Su_24M,
Su_17M4,
@@ -238,9 +253,21 @@ SEAD_CAPABLE = [
Su_30,
MiG_27K,
Tornado_GR4,
F_117A,
B_17G,
]
# Aircraft used for DEAD tasks. Must be capable of the CAS DCS task.
DEAD_CAPABLE = [
AJS37,
F_14B,
F_14A_135_GR,
JAS39Gripen_AG,
B_1B,
B_52H,
Tu_160,
Tu_95MS,
A_20G,
Ju_88A4,
P_47D_40,
P_47D_30bl1,
P_47D_30,
@@ -251,18 +278,6 @@ SEAD_CAPABLE = [
Bf_109K_4,
FW_190D9,
FW_190A8,
]
# Aircraft used for DEAD tasks
DEAD_CAPABLE = [
AJS37,
F_14B,
F_14A_135_GR,
B_1B,
B_52H,
Tu_160,
Tu_95MS,
] + SEAD_CAPABLE
@@ -276,14 +291,13 @@ STRIKE_CAPABLE = [
Tu_22M3,
F_15E,
AJS37,
Rafale_A_S,
Rafale_B,
Tornado_GR4,
F_16C_50,
FA_18C_hornet,
F_16A,
F_14B,
F_14A_135_GR,
JAS39Gripen_AG,
Tornado_IDS,
Su_17M4,
Su_24MR,
@@ -296,10 +310,10 @@ STRIKE_CAPABLE = [
Su_30,
Su_27,
MiG_29S,
MiG_29K,
MiG_29G,
MiG_29A,
JF_17,
F_4E,
A_10C_2,
A_10C,
AV8BNA,
@@ -316,6 +330,7 @@ STRIKE_CAPABLE = [
L_39ZA,
B_17G,
A_20G,
Ju_88A4,
P_47D_40,
P_47D_30bl1,
P_47D_30,
@@ -333,8 +348,7 @@ ANTISHIP_CAPABLE = [
AJS37,
Tu_22M3,
FA_18C_hornet,
Rafale_A_S,
Rafale_B,
JAS39Gripen_AG,
Su_24M,
Su_17M4,
JF_17,
@@ -362,20 +376,34 @@ RUNWAY_ATTACK_CAPABLE = [
# For any aircraft that isn't necessarily directly involved in strike
# missions in a direct combat sense, but can transport objects and infantry.
TRANSPORT_CAPABLE = [
C_17A,
Hercules,
Mi_8MT,
C_130,
IL_76MD,
An_26B,
Yak_40,
CH_53E,
CH_47D,
SH_60B,
UH_60A,
UH_1H,
Mi_8MT,
Mi_8MT,
Mi_26,
]
DRONES = [
MQ_9_Reaper,
RQ_1A_Predator,
WingLoong_I
DRONES = [MQ_9_Reaper, RQ_1A_Predator, WingLoong_I]
AEWC_CAPABLE = [
E_3A,
E_2C,
A_50,
KJ_2000,
]
def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
cap_missions = (FlightType.BARCAP, FlightType.TARCAP)
cap_missions = (FlightType.BARCAP, FlightType.TARCAP, FlightType.SWEEP)
if task in cap_missions:
return CAP_CAPABLE
elif task == FlightType.ANTISHIP:
@@ -386,6 +414,8 @@ def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
return CAS_CAPABLE
elif task == FlightType.SEAD:
return SEAD_CAPABLE
elif task == FlightType.SEAD_ESCORT:
return SEAD_CAPABLE
elif task == FlightType.DEAD:
return DEAD_CAPABLE
elif task == FlightType.OCA_AIRCRAFT:
@@ -396,6 +426,18 @@ def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
return STRIKE_CAPABLE
elif task == FlightType.ESCORT:
return CAP_CAPABLE
elif task == FlightType.AEWC:
return AEWC_CAPABLE
elif task == FlightType.TRANSPORT:
return TRANSPORT_CAPABLE
else:
logging.error(f"Unplannable flight type: {task}")
return []
def tasks_for_aircraft(aircraft: Type[FlyingType]) -> list[FlightType]:
tasks = []
for task in FlightType:
if aircraft in aircraft_for_task(task):
tasks.append(task)
return tasks

View File

@@ -12,12 +12,13 @@ if TYPE_CHECKING:
class ClosestAirfields:
"""Precalculates which control points are closes to the given target."""
def __init__(self, target: MissionTarget,
all_control_points: List[ControlPoint]) -> None:
def __init__(
self, target: MissionTarget, all_control_points: List[ControlPoint]
) -> None:
self.target = target
# This cache is configured once on load, so it's important that it is
# complete and deterministic to avoid different behaviors across loads.
# E.g. https://github.com/Khopa/dcs_liberation/issues/819
# E.g. https://github.com/dcs-liberation/dcs_liberation/issues/819
self.closest_airfields: List[ControlPoint] = sorted(
all_control_points, key=lambda c: self.target.distance_to(c)
)
@@ -26,17 +27,35 @@ class ClosestAirfields:
def operational_airfields(self) -> Iterator[ControlPoint]:
return (c for c in self.closest_airfields if c.runway_is_operational())
def airfields_within(self, distance: Distance) -> Iterator[ControlPoint]:
def _airfields_within(
self, distance: Distance, operational: bool
) -> Iterator[ControlPoint]:
airfields = (
self.operational_airfields if operational else self.closest_airfields
)
for cp in airfields:
if cp.distance_to(self.target) < distance.meters:
yield cp
else:
break
def operational_airfields_within(
self, distance: Distance
) -> Iterator[ControlPoint]:
"""Iterates over all airfields within the given range of the target.
Note that this iterates over *all* airfields, not just friendly
airfields.
"""
for cp in self.closest_airfields:
if cp.distance_to(self.target) < distance.meters:
yield cp
else:
break
return self._airfields_within(distance, operational=True)
def all_airfields_within(self, distance: Distance) -> Iterator[ControlPoint]:
"""Iterates over all airfields within the given range of the target.
Note that this iterates over *all* airfields, not just friendly
airfields.
"""
return self._airfields_within(distance, operational=False)
class ObjectiveDistanceCache:
@@ -52,9 +71,7 @@ class ObjectiveDistanceCache:
@classmethod
def get_closest_airfields(cls, location: MissionTarget) -> ClosestAirfields:
if cls.theater is None:
raise RuntimeError(
"Call ObjectiveDistanceCache.set_theater before using"
)
raise RuntimeError("Call ObjectiveDistanceCache.set_theater before using")
if location.name not in cls.closest_airfields:
cls.closest_airfields[location.name] = ClosestAirfields(

View File

@@ -1,25 +1,58 @@
from __future__ import annotations
from collections import defaultdict
from dataclasses import dataclass
from datetime import timedelta
from enum import Enum
from typing import Dict, List, Optional, TYPE_CHECKING, Type
from typing import List, Optional, TYPE_CHECKING, Type, Union
from dcs.mapping import Point
from dcs.point import MovingPoint, PointAction
from dcs.unit import Unit
from dcs.unittype import FlyingType
from game import db
from game.data.weapons import Weapon
from game.squadrons import Pilot, Squadron
from game.theater.controlpoint import ControlPoint, MissionTarget
from game.utils import Distance, meters
from gen.flights.loadouts import Loadout
if TYPE_CHECKING:
from game.transfers import TransferOrder
from gen.ato import Package
from gen.flights.flightplan import FlightPlan
class FlightType(Enum):
"""Enumeration of mission types.
The value of each enumeration is the name that will be shown in the UI.
These values are persisted to the save game as well since they are a part of
each flight and thus a part of the ATO, so changing these values will break
save compat.
When adding new mission types to this list, you will also need to update:
* flightplan.py: Add waypoint population in generate_flight_plan. Add a new flight
plan type if necessary, though most are a subclass of StrikeFlightPlan.
* aircraft.py: Add a configuration method and call it in setup_flight_group. This is
responsible for configuring waypoint 0 actions like setting ROE, threat reaction,
and mission abort parameters (winchester, bingo, etc).
* Implementations of MissionTarget.mission_types: A mission type can only be planned
against compatible targets. The mission_types method of each target class defines
which missions may target it.
* ai_flight_planner_db.py: Add the new mission type to aircraft_for_task that
returns the list of compatible aircraft in order of preference.
You may also need to update:
* flight.py: Add a new waypoint type if necessary. Most mission types will need
these, as aircraft.py uses the ingress point type to specialize AI tasks, and non-
strike-like missions will need more specialized control.
* ai_flight_planner.py: Use the new mission type in propose_missions so the AI will
plan the new mission type.
"""
TARCAP = "TARCAP"
BARCAP = "BARCAP"
CAS = "CAS"
@@ -33,28 +66,52 @@ class FlightType(Enum):
SWEEP = "Fighter sweep"
OCA_RUNWAY = "OCA/Runway"
OCA_AIRCRAFT = "OCA/Aircraft"
AEWC = "AEW&C"
TRANSPORT = "Transport"
SEAD_ESCORT = "SEAD Escort"
def __str__(self) -> str:
return self.value
@classmethod
def from_name(cls, name: str) -> FlightType:
for entry in cls:
if name == entry.value:
return entry
raise KeyError(f"No FlightType with name {name}")
class FlightWaypointType(Enum):
TAKEOFF = 0 # Take off point
ASCEND_POINT = 1 # Ascension point after take off
PATROL = 2 # Patrol point
PATROL_TRACK = 3 # Patrol race track
NAV = 4 # Nav point
INGRESS_STRIKE = 5 # Ingress strike (For generator, means that this should have bombing on next TARGET_POINT points)
INGRESS_SEAD = 6 # Ingress sead (For generator, means that this should attack groups on TARGET_GROUP_LOC points)
INGRESS_CAS = 7 # Ingress cas (should start CAS task)
CAS = 8 # Should do CAS there
EGRESS = 9 # Should stop attack
DESCENT_POINT = 10 # Should start descending to pattern alt
LANDING_POINT = 11 # Should land there
TARGET_POINT = 12 # A target building or static object, position
TARGET_GROUP_LOC = 13 # A target group approximate location
TARGET_SHIP = 14 # A target ship known location
CUSTOM = 15 # User waypoint (no specific behaviour)
"""Enumeration of waypoint types.
The value of the enum has no meaning but should remain stable to prevent breaking
save game compatibility.
When adding a new waypoint type, you will also need to update:
* waypointbuilder.py: Add a builder to simplify construction of the new waypoint
type unless the new waypoint type will be a parameter to an existing builder
method (such as how escort ingress waypoints work).
* aircraft.py: Associate AI actions with the new waypoint type by subclassing
PydcsWaypointBuilder and using it in PydcsWaypointBuilder.for_waypoint.
"""
TAKEOFF = 0 # Take off point
ASCEND_POINT = 1 # Ascension point after take off
PATROL = 2 # Patrol point
PATROL_TRACK = 3 # Patrol race track
NAV = 4 # Nav point
INGRESS_STRIKE = 5 # Ingress strike (For generator, means that this should have bombing on next TARGET_POINT points)
INGRESS_SEAD = 6 # Ingress sead (For generator, means that this should attack groups on TARGET_GROUP_LOC points)
INGRESS_CAS = 7 # Ingress cas (should start CAS task)
CAS = 8 # Should do CAS there
EGRESS = 9 # Should stop attack
DESCENT_POINT = 10 # Should start descending to pattern alt
LANDING_POINT = 11 # Should land there
TARGET_POINT = 12 # A target building or static object, position
TARGET_GROUP_LOC = 13 # A target group approximate location
TARGET_SHIP = 14 # Unused.
CUSTOM = 15 # User waypoint (no specific behaviour)
JOIN = 16
SPLIT = 17
LOITER = 18
@@ -65,12 +122,19 @@ class FlightWaypointType(Enum):
DIVERT = 23
INGRESS_OCA_RUNWAY = 24
INGRESS_OCA_AIRCRAFT = 25
PICKUP = 26
DROP_OFF = 27
BULLSEYE = 28
class FlightWaypoint:
def __init__(self, waypoint_type: FlightWaypointType, x: float, y: float,
alt: Distance = meters(0)) -> None:
def __init__(
self,
waypoint_type: FlightWaypointType,
x: float,
y: float,
alt: Distance = meters(0),
) -> None:
"""Creates a flight waypoint.
Args:
@@ -90,7 +154,7 @@ class FlightWaypoint:
# Only used in the waypoint list in the flight edit page. No sense
# having three names. A short and long form is enough.
self.description = ""
self.targets: List[MissionTarget] = []
self.targets: List[Union[MissionTarget, Unit]] = []
self.obj_name = ""
self.pretty_name = ""
self.only_for_player = False
@@ -108,10 +172,13 @@ class FlightWaypoint:
return Point(self.x, self.y)
@classmethod
def from_pydcs(cls, point: MovingPoint,
from_cp: ControlPoint) -> "FlightWaypoint":
waypoint = FlightWaypoint(FlightWaypointType.NAV, point.position.x,
point.position.y, meters(point.alt))
def from_pydcs(cls, point: MovingPoint, from_cp: ControlPoint) -> "FlightWaypoint":
waypoint = FlightWaypoint(
FlightWaypointType.NAV,
point.position.x,
point.position.y,
meters(point.alt),
)
waypoint.alt_type = point.alt_type
# Other actions exist... but none of them *should* be the first
# waypoint for a flight.
@@ -134,39 +201,107 @@ class FlightWaypoint:
return waypoint
class Flight:
class FlightRoster:
def __init__(self, squadron: Squadron, initial_size: int = 0) -> None:
self.squadron = squadron
self.pilots: list[Optional[Pilot]] = []
self.resize(initial_size)
def __init__(self, package: Package, country: str, unit_type: Type[FlyingType],
count: int, flight_type: FlightType, start_type: str,
departure: ControlPoint, arrival: ControlPoint,
divert: Optional[ControlPoint],
custom_name: Optional[str] = None) -> None:
@property
def max_size(self) -> int:
return len(self.pilots)
@property
def player_count(self) -> int:
return len([p for p in self.pilots if p is not None and p.player])
@property
def missing_pilots(self) -> int:
return len([p for p in self.pilots if p is None])
def resize(self, new_size: int) -> None:
if self.max_size > new_size:
self.squadron.return_pilots(
[p for p in self.pilots[new_size:] if p is not None]
)
self.pilots = self.pilots[:new_size]
return
self.pilots.extend(
[
self.squadron.claim_available_pilot()
for _ in range(new_size - self.max_size)
]
)
def set_pilot(self, index: int, pilot: Optional[Pilot]) -> None:
if pilot is not None:
self.squadron.claim_pilot(pilot)
if (current_pilot := self.pilots[index]) is not None:
self.squadron.return_pilot(current_pilot)
self.pilots[index] = pilot
def clear(self) -> None:
self.squadron.return_pilots([p for p in self.pilots if p is not None])
class Flight:
def __init__(
self,
package: Package,
country: str,
squadron: Squadron,
count: int,
flight_type: FlightType,
start_type: str,
departure: ControlPoint,
arrival: ControlPoint,
divert: Optional[ControlPoint],
custom_name: Optional[str] = None,
cargo: Optional[TransferOrder] = None,
roster: Optional[FlightRoster] = None,
) -> None:
self.package = package
self.country = country
self.unit_type = unit_type
self.count = count
self.squadron = squadron
if roster is None:
self.roster = FlightRoster(self.squadron, initial_size=count)
else:
self.roster = roster
self.departure = departure
self.arrival = arrival
self.divert = divert
self.flight_type = flight_type
# TODO: Replace with FlightPlan.
self.targets: List[MissionTarget] = []
self.loadout: Dict[int, Optional[Weapon]] = {}
self.loadout = Loadout.default_for(self)
self.start_type = start_type
self.use_custom_loadout = False
self.client_count = 0
self.custom_name = custom_name
# Only used by transport missions.
self.cargo = cargo
# Will be replaced with a more appropriate FlightPlan by
# FlightPlanBuilder, but an empty flight plan the flight begins with an
# empty flight plan.
from gen.flights.flightplan import CustomFlightPlan
self.flight_plan: FlightPlan = CustomFlightPlan(
package=package,
flight=self,
custom_waypoints=[]
package=package, flight=self, custom_waypoints=[]
)
@property
def count(self) -> int:
return self.roster.max_size
@property
def client_count(self) -> int:
return self.roster.player_count
@property
def unit_type(self) -> Type[FlyingType]:
return self.squadron.aircraft
@property
def from_cp(self) -> ControlPoint:
return self.departure
@@ -175,6 +310,19 @@ class Flight:
def points(self) -> List[FlightWaypoint]:
return self.flight_plan.waypoints[1:]
def resize(self, new_size: int) -> None:
self.roster.resize(new_size)
def set_pilot(self, index: int, pilot: Optional[Pilot]) -> None:
self.roster.set_pilot(index, pilot)
@property
def missing_pilots(self) -> int:
return self.roster.missing_pilots
def clear_roster(self) -> None:
self.roster.clear()
def __repr__(self):
name = db.unit_type_name(self.unit_type)
if self.custom_name:
@@ -182,7 +330,7 @@ class Flight:
return f"[{self.flight_type}] {self.count} x {name}"
def __str__(self):
name = db.unit_get_expanded_info(self.country, self.unit_type, 'name')
name = db.unit_get_expanded_info(self.country, self.unit_type, "name")
if self.custom_name:
return f"{self.custom_name} {self.count} x {name}"
return f"[{self.flight_type}] {self.count} x {name}"

File diff suppressed because it is too large Load Diff

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