mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Compare commits
157 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de2e5f861b | ||
|
|
b27a7fc71b | ||
|
|
5861ce6146 | ||
|
|
c732ed556f | ||
|
|
be1a75e520 | ||
|
|
c41d10c581 | ||
|
|
157a59e3c4 | ||
|
|
d24c65c3aa | ||
|
|
d4d441ff9b | ||
|
|
f43fb1223f | ||
|
|
3db275414d | ||
|
|
6e0ff6c805 | ||
|
|
9c359efbff | ||
|
|
c5cc1ea8e8 | ||
|
|
afb6a33131 | ||
|
|
539a11f54d | ||
|
|
9324e549e6 | ||
|
|
c8f6b6df87 | ||
|
|
38f632097e | ||
|
|
e63743f537 | ||
|
|
ce13295cf0 | ||
|
|
23c02a3510 | ||
|
|
01ea7b9ee1 | ||
|
|
6fed1284a1 | ||
|
|
5574d849bd | ||
|
|
c2ce3a6992 | ||
|
|
b61d15fdf4 | ||
|
|
ad5cc83fb3 | ||
|
|
2f53edd775 | ||
|
|
923459c88b | ||
|
|
1192d26448 | ||
|
|
2d5e827417 | ||
|
|
a30d9276b8 | ||
|
|
b963c2272f | ||
|
|
221cb8709b | ||
|
|
648857fc44 | ||
|
|
8091051bb4 | ||
|
|
1e468cd3e0 | ||
|
|
15d2a5bb2b | ||
|
|
5c76229ee5 | ||
|
|
0cd088122e | ||
|
|
b6f3467a89 | ||
|
|
52ce1a5959 | ||
|
|
7ce05762f5 | ||
|
|
cce736bc16 | ||
|
|
2a1127e637 | ||
|
|
8ca68b3d7a | ||
|
|
0f76d893b8 | ||
|
|
ab746b5195 | ||
|
|
828c87df39 | ||
|
|
888aeb621d | ||
|
|
ac2fddf87e | ||
|
|
614304cc81 | ||
|
|
f363d66aac | ||
|
|
1706c42695 | ||
|
|
2b44b2fc0b | ||
|
|
25986aa15c | ||
|
|
264eb01afc | ||
|
|
6db3c3f9f1 | ||
|
|
714992bdcb | ||
|
|
ca7a86b6d7 | ||
|
|
49e729e9ec | ||
|
|
7f0a690c7b | ||
|
|
d07afc603b | ||
|
|
5bd4c00257 | ||
|
|
13272aa280 | ||
|
|
260358c5fb | ||
|
|
0f07b2c095 | ||
|
|
b2fafc22dd | ||
|
|
6200ec8e0e | ||
|
|
831516c5f5 | ||
|
|
fb49d9e6ae | ||
|
|
e660726828 | ||
|
|
4519f47b19 | ||
|
|
47174bbb4d | ||
|
|
de18ce3e7b | ||
|
|
b5934633fa | ||
|
|
f314c08216 | ||
|
|
6704cded2d | ||
|
|
f11918fc41 | ||
|
|
58d5aa9944 | ||
|
|
60af2ad0a4 | ||
|
|
4ec8994c38 | ||
|
|
49d6cece50 | ||
|
|
03251d5bd5 | ||
|
|
a6e03184bc | ||
|
|
54f2a6f1b5 | ||
|
|
7eed7ae6ba | ||
|
|
b3d642fdf5 | ||
|
|
35dd9427a5 | ||
|
|
bf1087df3c | ||
|
|
523ef08697 | ||
|
|
c2310453d8 | ||
|
|
b4a6a5dc26 | ||
|
|
20ceb440fa | ||
|
|
4ee9c66524 | ||
|
|
9d04abd3af | ||
|
|
a562345876 | ||
|
|
2e7daceb3c | ||
|
|
a9aba3484a | ||
|
|
1b9bbb8eb6 | ||
|
|
25c44723a9 | ||
|
|
688a20c312 | ||
|
|
4c1b34461e | ||
|
|
c6939e7194 | ||
|
|
cd9432e395 | ||
|
|
7d5244a5bc | ||
|
|
1aa5d4f7de | ||
|
|
ad74204fe4 | ||
|
|
5cb1a47ed3 | ||
|
|
fff22d3cd1 | ||
|
|
665cd7b996 | ||
|
|
61173196d2 | ||
|
|
03e98ba562 | ||
|
|
6195290adf | ||
|
|
4f1b0055e1 | ||
|
|
dd9fe87ff4 | ||
|
|
5bda4abfce | ||
|
|
24f98aede5 | ||
|
|
f8ae1e9076 | ||
|
|
16b0dcad71 | ||
|
|
9c1265d50d | ||
|
|
8c0e781c94 | ||
|
|
a47bef1f13 | ||
|
|
053663bd76 | ||
|
|
2ffe3bf722 | ||
|
|
f7889b785d | ||
|
|
27829a024a | ||
|
|
a0fda2552f | ||
|
|
65c185ebd2 | ||
|
|
5792eb354c | ||
|
|
dce7d91511 | ||
|
|
3d1afa74d4 | ||
|
|
d8c94f5ece | ||
|
|
61f1e11a48 | ||
|
|
98249b1aca | ||
|
|
8e51b7fc1d | ||
|
|
71914b8a8b | ||
|
|
7a077a0d21 | ||
|
|
d23e4665e7 | ||
|
|
df12b54856 | ||
|
|
ae12053d74 | ||
|
|
0e7695f2d6 | ||
|
|
38cee87ee9 | ||
|
|
c64a6083e2 | ||
|
|
e0501e46e3 | ||
|
|
4a0ccc4c2f | ||
|
|
c92b7240eb | ||
|
|
6a74c3faeb | ||
|
|
c5ae872787 | ||
|
|
2e9ab0a9d7 | ||
|
|
a004f62fe8 | ||
|
|
07ce20fd29 | ||
|
|
df74f7c6c7 | ||
|
|
2a6e2d470d | ||
|
|
643dd65113 | ||
|
|
cee0532b85 |
2
.git-blame-ignore-revs
Normal file
2
.git-blame-ignore-revs
Normal file
@@ -0,0 +1,2 @@
|
||||
# Black
|
||||
a47bef1f1336fd264d0b175f4421758339a30acb
|
||||
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -28,7 +28,8 @@ We will usually need more information for debugging. Include as much of the foll
|
||||
|
||||
- DCS Liberation save file (the `.liberation` file you save from the DCS Liberation window). By default these are located in your DCS saved games directory (`%USERPROFILE%/Saved Games/DCS`).
|
||||
- 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]:
|
||||
|
||||
13
.github/workflows/black.yml
vendored
Normal file
13
.github/workflows/black.yml
vendored
Normal 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"
|
||||
6
.pre-commit-config.yaml
Normal file
6
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 20.8b1
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3
|
||||
46
changelog.md
46
changelog.md
@@ -1,3 +1,49 @@
|
||||
# 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
|
||||
|
||||
@@ -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.SPAAA_ZU_23_2_Mounted_Ural_375,
|
||||
AirDefence.AAA_ZU_23_Closed_Emplacement_Insurgent,
|
||||
AirDefence.SPAAA_ZU_23_2_Insurgent_Mounted_Ural_375,
|
||||
AirDefence.AAA_ZU_23_Insurgent,
|
||||
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_40mm_Bofors,
|
||||
AirDefence.AAA_S_60_57mm,
|
||||
]
|
||||
|
||||
@@ -1,17 +1,70 @@
|
||||
import inspect
|
||||
import dcs
|
||||
|
||||
DEFAULT_AVAILABLE_BUILDINGS = ['fuel', 'ammo', 'comms', 'oil', 'ware', 'farp', 'fob', 'power', 'factory', 'derrick']
|
||||
DEFAULT_AVAILABLE_BUILDINGS = [
|
||||
"fuel",
|
||||
"ammo",
|
||||
"comms",
|
||||
"oil",
|
||||
"ware",
|
||||
"farp",
|
||||
"fob",
|
||||
"power",
|
||||
"factory",
|
||||
"derrick",
|
||||
]
|
||||
|
||||
WW2_FREE = ['fuel', 'factory', 'ware', 'fob']
|
||||
WW2_GERMANY_BUILDINGS = ['fuel', 'factory', 'ww2bunker', 'ww2bunker', 'ww2bunker', 'allycamp', 'allycamp', 'fob']
|
||||
WW2_ALLIES_BUILDINGS = ['fuel', 'factory', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'fob']
|
||||
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",
|
||||
]
|
||||
|
||||
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)
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
|
||||
]
|
||||
]
|
||||
|
||||
@@ -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,65 @@ 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 = [
|
||||
|
||||
# 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.SAM_SA_15_Tor_Gauntlet,
|
||||
AirDefence.SAM_SA_11_Buk_Gadfly_C2,
|
||||
AirDefence.SAM_Patriot_CR__AMG_AN_MRC_137,
|
||||
AirDefence.SAM_Patriot_ECS,
|
||||
AirDefence.SPAAA_Gepard,
|
||||
AirDefence.AAA_Vulcan_M163,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka,
|
||||
AirDefence.SPAAA_Vulcan_M163,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
|
||||
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.SAM_SA_6_Kub_Long_Track_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_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_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_SR_P_19,
|
||||
AirDefence.SAM_P19_Flat_Face_SR__SA_2_3,
|
||||
AirDefence.SAM_Roland_EWR,
|
||||
AirDefence.SAM_SA_3_S_125_TR_SNR,
|
||||
AirDefence.SAM_SA_2_TR_SNR_75_Fan_Song,
|
||||
AirDefence.SAM_SA_3_S_125_Low_Blow_TR,
|
||||
AirDefence.SAM_SA_2_S_75_Fan_Song_TR,
|
||||
AirDefence.HQ_7_Self_Propelled_STR,
|
||||
|
||||
# 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,
|
||||
]
|
||||
|
||||
1310
game/data/weapons.py
1310
game/data/weapons.py
File diff suppressed because it is too large
Load Diff
841
game/db.py
841
game/db.py
File diff suppressed because it is too large
Load Diff
@@ -96,13 +96,14 @@ class StateData:
|
||||
# them when they've already dead. Dedup.
|
||||
killed_ground_units=list(set(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.unit_map = unit_map
|
||||
|
||||
@@ -135,12 +136,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
|
||||
@@ -221,8 +219,10 @@ 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."
|
||||
)
|
||||
|
||||
return losses
|
||||
|
||||
@@ -234,15 +234,16 @@ class Debriefing:
|
||||
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]
|
||||
return [reversed_captures[idx[0]] for idx in last_base_cap_indexes]
|
||||
|
||||
|
||||
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 +262,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 +274,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
|
||||
|
||||
@@ -37,7 +37,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 +65,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 +87,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 +104,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,10 +115,12 @@ 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:
|
||||
@@ -115,7 +131,8 @@ class Event:
|
||||
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}")
|
||||
@@ -130,7 +147,8 @@ 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}")
|
||||
@@ -149,11 +167,14 @@ 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:
|
||||
@@ -171,9 +192,9 @@ class Event:
|
||||
|
||||
# ------------------------------
|
||||
# Captured bases
|
||||
#if self.game.player_country in db.BLUEFOR_FACTIONS:
|
||||
coalition = 2 # Value in DCS mission event for BLUE
|
||||
#else:
|
||||
# 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:
|
||||
@@ -187,12 +208,22 @@ class Event:
|
||||
|
||||
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)
|
||||
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:
|
||||
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)
|
||||
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:
|
||||
@@ -218,7 +249,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 +270,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 +299,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 +321,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,28 +332,40 @@ 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: 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.
|
||||
@@ -315,8 +376,7 @@ class Event:
|
||||
for ally_cp in ally_connected_cps:
|
||||
self.redeploy_between(cp, ally_cp)
|
||||
|
||||
def redeploy_between(self, destination: ControlPoint,
|
||||
source: ControlPoint) -> None:
|
||||
def redeploy_between(self, destination: ControlPoint, source: ControlPoint) -> None:
|
||||
total_units_redeployed = 0
|
||||
moved_units = {}
|
||||
|
||||
@@ -333,8 +393,7 @@ class Event:
|
||||
|
||||
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)
|
||||
total_units_redeployed = total_units_redeployed + int(count * move_factor)
|
||||
|
||||
destination.base.commision_units(moved_units)
|
||||
source.base.commit_losses(moved_units)
|
||||
@@ -362,7 +421,6 @@ class Event:
|
||||
|
||||
|
||||
class UnitsDeliveryEvent:
|
||||
|
||||
def __init__(self, control_point: ControlPoint) -> None:
|
||||
self.to_cp = control_point
|
||||
self.units: Dict[Type[UnitType], int] = {}
|
||||
@@ -390,8 +448,7 @@ class UnitsDeliveryEvent:
|
||||
logging.error(f"Could not refund {unit_type.id}, price unknown")
|
||||
continue
|
||||
|
||||
logging.info(
|
||||
f"Refunding {count} {unit_type.id} at {self.to_cp.name}")
|
||||
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:
|
||||
@@ -409,13 +466,13 @@ class UnitsDeliveryEvent:
|
||||
aircraft = unit_type.id
|
||||
name = self.to_cp.name
|
||||
if count >= 0:
|
||||
bought_units[unit_type] = count
|
||||
bought_units[unit_type] = count
|
||||
game.message(
|
||||
f"{coalition} reinforcements: {aircraft} x {count} at {name}")
|
||||
f"{coalition} reinforcements: {aircraft} x {count} at {name}"
|
||||
)
|
||||
else:
|
||||
sold_units[unit_type] = -count
|
||||
game.message(
|
||||
f"{coalition} sold: {aircraft} x {-count} at {name}")
|
||||
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 = {}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -10,8 +10,18 @@ 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
|
||||
|
||||
|
||||
@@ -60,6 +70,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 +103,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 +119,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
|
||||
@@ -122,7 +137,11 @@ class Faction:
|
||||
|
||||
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 +154,10 @@ 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.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 +168,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 +186,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 +226,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 +261,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 +277,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]]:
|
||||
|
||||
118
game/game.py
118
game/game.py
@@ -78,10 +78,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
|
||||
@@ -90,6 +96,7 @@ class Game:
|
||||
self.enemy_name = enemy_name
|
||||
self.enemy_country = db.FACTIONS[enemy_name].country
|
||||
self.turn = 0
|
||||
# NB: This is the *start* date. It is never updated.
|
||||
self.date = date(start_date.year, start_date.month, start_date.day)
|
||||
self.game_stats = GameStats()
|
||||
self.game_stats.update(self)
|
||||
@@ -112,9 +119,7 @@ class Game:
|
||||
self.blue_ato = AirTaskingOrder()
|
||||
self.red_ato = AirTaskingOrder()
|
||||
|
||||
self.aircraft_inventory = GlobalAircraftInventory(
|
||||
self.theater.controlpoints
|
||||
)
|
||||
self.aircraft_inventory = GlobalAircraftInventory(self.theater.controlpoints)
|
||||
|
||||
self.sanitize_sides()
|
||||
|
||||
@@ -147,8 +152,9 @@ class Game:
|
||||
self.on_load()
|
||||
|
||||
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):
|
||||
"""
|
||||
@@ -184,13 +190,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)
|
||||
self._generate_player_event(
|
||||
FrontlineAttackEvent,
|
||||
front_line.control_point_a,
|
||||
front_line.control_point_b,
|
||||
)
|
||||
|
||||
def adjust_budget(self, amount: float, player: bool) -> None:
|
||||
if player:
|
||||
@@ -208,7 +225,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()
|
||||
|
||||
@@ -223,7 +240,11 @@ 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")
|
||||
|
||||
@@ -235,7 +256,9 @@ class Game:
|
||||
|
||||
def pass_turn(self, no_action: bool = False) -> None:
|
||||
logging.info("Pass turn")
|
||||
self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0))
|
||||
self.informations.append(
|
||||
Information("End of turn #" + str(self.turn), "-" * 40, 0)
|
||||
)
|
||||
self.turn += 1
|
||||
|
||||
for control_point in self.theater.controlpoints:
|
||||
@@ -261,11 +284,18 @@ class Game:
|
||||
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 initialize_turn(self) -> None:
|
||||
@@ -281,7 +311,7 @@ class Game:
|
||||
|
||||
# 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
|
||||
@@ -305,8 +335,11 @@ class Game:
|
||||
|
||||
self.plan_procurement(blue_planner, red_planner)
|
||||
|
||||
def plan_procurement(self, blue_planner: CoalitionMissionPlanner,
|
||||
red_planner: CoalitionMissionPlanner) -> None:
|
||||
def plan_procurement(
|
||||
self,
|
||||
blue_planner: CoalitionMissionPlanner,
|
||||
red_planner: CoalitionMissionPlanner,
|
||||
) -> None:
|
||||
# The first turn needs to buy a *lot* of aircraft to fill CAPs, so it
|
||||
# gets much more of the budget that turn. Otherwise budget (after
|
||||
# repairs) is split evenly between air and ground. For the default
|
||||
@@ -320,7 +353,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
|
||||
front_line_budget_share=ground_portion,
|
||||
).spend_budget(self.budget, blue_planner.procurement_requests)
|
||||
|
||||
self.enemy_budget = ProcurementAi(
|
||||
@@ -330,7 +363,7 @@ class Game:
|
||||
manage_runways=True,
|
||||
manage_front_line=True,
|
||||
manage_aircraft=True,
|
||||
front_line_budget_share=ground_portion
|
||||
front_line_budget_share=ground_portion,
|
||||
).spend_budget(self.enemy_budget, red_planner.procurement_requests)
|
||||
|
||||
def message(self, text: str) -> None:
|
||||
@@ -361,10 +394,12 @@ class Game:
|
||||
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:
|
||||
@@ -386,9 +421,9 @@ class Game:
|
||||
|
||||
# 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)
|
||||
position = Conflict.frontline_position(
|
||||
front_line.control_point_a, front_line.control_point_b, self.theater
|
||||
)
|
||||
zones.append(position[0])
|
||||
zones.append(front_line.control_point_a.position)
|
||||
zones.append(front_line.control_point_b.position)
|
||||
@@ -413,7 +448,10 @@ 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)
|
||||
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
|
||||
@@ -422,8 +460,7 @@ class Game:
|
||||
if cpoint is not None:
|
||||
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,
|
||||
@@ -460,7 +497,10 @@ class Game:
|
||||
return False
|
||||
else:
|
||||
for z in self.__culling_zones:
|
||||
if z.distance_to_point(pos) < self.settings.perf_culling_distance * 1000:
|
||||
if (
|
||||
z.distance_to_point(pos)
|
||||
< self.settings.perf_culling_distance * 1000
|
||||
):
|
||||
return False
|
||||
for p in self.__culling_points:
|
||||
if p.distance_to_point(pos) < 2500:
|
||||
@@ -502,6 +542,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."
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ class FrontlineData:
|
||||
This Data structure will store information about an existing frontline
|
||||
"""
|
||||
|
||||
def __init__(self, from_cp:ControlPoint, to_cp: ControlPoint):
|
||||
def __init__(self, from_cp: ControlPoint, to_cp: ControlPoint):
|
||||
self.to_cp = to_cp
|
||||
self.from_cp = from_cp
|
||||
self.enemy_units_position = []
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -41,6 +41,7 @@ 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
|
||||
@@ -84,7 +85,7 @@ class Operation:
|
||||
cls.game.enemy_name,
|
||||
cls.game.player_country,
|
||||
cls.game.enemy_country,
|
||||
frontline.position
|
||||
frontline.position,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -93,7 +94,7 @@ 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,
|
||||
@@ -103,7 +104,7 @@ class Operation:
|
||||
cls.game.enemy_name,
|
||||
cls.game.player_country,
|
||||
cls.game.enemy_country,
|
||||
mid_point
|
||||
mid_point,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -118,9 +119,11 @@ class Operation:
|
||||
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 +136,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 +148,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,11 +165,11 @@ 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():
|
||||
@@ -179,9 +178,8 @@ class Operation:
|
||||
for tanker in airsupportgen.air_support.tankers:
|
||||
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:
|
||||
gen.add_awacs(aewc)
|
||||
|
||||
for jtac in jtacs:
|
||||
gen.add_jtac(jtac)
|
||||
@@ -208,8 +206,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 +216,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 +232,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 +246,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 +271,7 @@ class Operation:
|
||||
cls.game,
|
||||
cls.radio_registry,
|
||||
cls.tacan_registry,
|
||||
cls.unit_map
|
||||
cls.unit_map,
|
||||
)
|
||||
cls.groundobjectgen.generate()
|
||||
|
||||
@@ -284,10 +285,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 +306,13 @@ 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_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 +321,16 @@ 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()]:
|
||||
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
|
||||
|
||||
# Options
|
||||
forcedoptionsgen = ForcedOptionsGenerator(
|
||||
cls.current_mission, cls.game)
|
||||
forcedoptionsgen = ForcedOptionsGenerator(cls.current_mission, cls.game)
|
||||
forcedoptionsgen.generate()
|
||||
|
||||
# Generate Visuals Smoke Effects
|
||||
@@ -341,13 +347,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,29 +363,40 @@ 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:
|
||||
@@ -396,17 +411,19 @@ class Operation:
|
||||
cls.current_mission.country(cls.game.enemy_country),
|
||||
player_cp,
|
||||
enemy_cp,
|
||||
cls.game.theater
|
||||
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)
|
||||
@@ -416,9 +433,12 @@ class Operation:
|
||||
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,6 +446,8 @@ class Operation:
|
||||
"AWACs": {},
|
||||
"JTACs": {},
|
||||
"TargetPoints": {},
|
||||
"RedAA": {},
|
||||
"BlueAA": {},
|
||||
} # type: ignore
|
||||
|
||||
for tanker in airsupportgen.air_support.tankers:
|
||||
@@ -434,7 +456,7 @@ class Operation:
|
||||
"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:
|
||||
@@ -442,7 +464,7 @@ class Operation:
|
||||
luaData["AWACs"][awacs.callsign] = {
|
||||
"dcsGroupName": awacs.dcsGroupName,
|
||||
"callsign": awacs.callsign,
|
||||
"radio": awacs.freq.mhz
|
||||
"radio": awacs.freq.mhz,
|
||||
}
|
||||
|
||||
for jtac in jtacs:
|
||||
@@ -451,14 +473,16 @@ class Operation:
|
||||
"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 +490,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 +538,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 +617,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 += """
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@@ -67,4 +67,3 @@ def autosave(game) -> bool:
|
||||
except Exception:
|
||||
logging.exception("Could not save game")
|
||||
return False
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
15
game/point_with_heading.py
Normal file
15
game/point_with_heading.py
Normal 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
|
||||
@@ -35,9 +35,16 @@ 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:
|
||||
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
|
||||
|
||||
@@ -51,8 +58,8 @@ class ProcurementAi:
|
||||
self.threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||
|
||||
def spend_budget(
|
||||
self, budget: float,
|
||||
aircraft_requests: List[AircraftProcurementRequest]) -> float:
|
||||
self, budget: float, aircraft_requests: List[AircraftProcurementRequest]
|
||||
) -> float:
|
||||
if self.manage_runways:
|
||||
budget = self.repair_runways(budget)
|
||||
if self.manage_front_line:
|
||||
@@ -100,25 +107,30 @@ 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]
|
||||
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)
|
||||
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:
|
||||
@@ -150,8 +162,12 @@ class ProcurementAi:
|
||||
return budget
|
||||
|
||||
def _affordable_aircraft_of_types(
|
||||
self, types: List[Type[FlyingType]], airbase: ControlPoint,
|
||||
number: int, max_price: float) -> Optional[Type[FlyingType]]:
|
||||
self,
|
||||
types: List[Type[FlyingType]],
|
||||
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]:
|
||||
if db.PRICES[unit] * number > max_price:
|
||||
@@ -168,15 +184,15 @@ class ProcurementAi:
|
||||
return best_choice
|
||||
|
||||
def affordable_aircraft_for(
|
||||
self, request: AircraftProcurementRequest,
|
||||
airbase: ControlPoint, budget: float) -> Optional[Type[FlyingType]]:
|
||||
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)
|
||||
aircraft_for_task(request.task_capability), airbase, request.number, budget
|
||||
)
|
||||
|
||||
def purchase_aircraft(
|
||||
self, budget: float,
|
||||
aircraft_requests: List[AircraftProcurementRequest]) -> float:
|
||||
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)
|
||||
@@ -201,11 +217,9 @@ 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):
|
||||
if not cp.is_friendly(self.is_player):
|
||||
|
||||
@@ -30,6 +30,8 @@ class Settings:
|
||||
automate_front_line_reinforcements: bool = False
|
||||
automate_aircraft_reinforcements: bool = False
|
||||
restrict_weapons_by_date: bool = False
|
||||
disable_legacy_aewc: bool = False
|
||||
generate_dark_kneeboard: bool = False
|
||||
|
||||
# Performance oriented
|
||||
perf_red_alert_state: bool = True
|
||||
@@ -58,8 +60,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:
|
||||
|
||||
@@ -4,7 +4,7 @@ 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
|
||||
from dcs.unittype import FlyingType, UnitType, VehicleType
|
||||
from dcs.vehicles import AirDefence, Armor
|
||||
|
||||
@@ -22,7 +22,6 @@ BASE_MIN_STRENGTH = 0
|
||||
|
||||
|
||||
class Base:
|
||||
|
||||
def __init__(self):
|
||||
self.aircraft: Dict[Type[FlyingType], int] = {}
|
||||
self.armor: Dict[Type[VehicleType], int] = {}
|
||||
@@ -57,23 +56,43 @@ class Base:
|
||||
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 +113,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 +133,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 +147,12 @@ 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
|
||||
):
|
||||
target_dict = self.aircraft
|
||||
elif for_task == PinpointStrike:
|
||||
target_dict = self.armor
|
||||
@@ -149,7 +179,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 +196,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 +240,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(),
|
||||
)
|
||||
|
||||
@@ -23,7 +23,7 @@ from dcs.planes import F_15C
|
||||
from dcs.ships import (
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
USS_Arleigh_Burke_IIa,
|
||||
DDG_Arleigh_Burke_IIa,
|
||||
)
|
||||
from dcs.statics import Fortification
|
||||
from dcs.terrain import (
|
||||
@@ -55,6 +55,7 @@ from .controlpoint import (
|
||||
Fob,
|
||||
)
|
||||
from .landmap import Landmap, load_landmap, poly_contains
|
||||
from ..point_with_heading import PointWithHeading
|
||||
from ..utils import Distance, meters, nautical_miles
|
||||
|
||||
Numeric = Union[int, float]
|
||||
@@ -71,6 +72,7 @@ IMPORTANCE_HIGH = 1.4
|
||||
|
||||
FRONTLINE_MIN_CP_DISTANCE = 5000
|
||||
|
||||
|
||||
def pairwise(iterable):
|
||||
"""
|
||||
itertools recipe
|
||||
@@ -91,30 +93,33 @@ class MizCampaignLoader:
|
||||
LHA_UNIT_TYPE = LHA_1_Tarawa.id
|
||||
FRONT_LINE_UNIT_TYPE = Armor.APC_M113.id
|
||||
|
||||
FOB_UNIT_TYPE = Unarmed.CP_SKP_11_ATC_Mobile_Command_Post.id
|
||||
FOB_UNIT_TYPE = Unarmed.Truck_SKP_11_Mobile_ATC.id
|
||||
FARP_HELIPAD = "SINGLE_HELIPAD"
|
||||
|
||||
EWR_UNIT_TYPE = AirDefence.EWR_55G6.id
|
||||
SAM_UNIT_TYPE = AirDefence.SAM_SA_10_S_300PS_SR_64H6E.id
|
||||
GARRISON_UNIT_TYPE = AirDefence.SAM_SA_19_Tunguska_2S6.id
|
||||
SAM_UNIT_TYPE = AirDefence.SAM_SA_10_S_300_Grumble_Big_Bird_SR.id
|
||||
GARRISON_UNIT_TYPE = AirDefence.SAM_SA_19_Tunguska_Grison.id
|
||||
OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id
|
||||
SHIP_UNIT_TYPE = USS_Arleigh_Burke_IIa.id
|
||||
MISSILE_SITE_UNIT_TYPE = MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M.id
|
||||
COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.SS_N_2_Silkworm.id
|
||||
SHIP_UNIT_TYPE = DDG_Arleigh_Burke_IIa.id
|
||||
MISSILE_SITE_UNIT_TYPE = MissilesSS.SSM_SS_1C_Scud_B.id
|
||||
COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.AShM_SS_N_2_Silkworm.id
|
||||
|
||||
# Multiple options for the required SAMs so campaign designers can more
|
||||
# accurately see the coverage of their IADS for the expected type.
|
||||
REQUIRED_LONG_RANGE_SAM_UNIT_TYPES = {
|
||||
AirDefence.SAM_Patriot_LN_M901.id,
|
||||
AirDefence.SAM_SA_10_S_300PS_LN_5P85C.id,
|
||||
AirDefence.SAM_SA_10_S_300PS_LN_5P85D.id,
|
||||
AirDefence.SAM_Patriot_LN.id,
|
||||
AirDefence.SAM_SA_10_S_300_Grumble_TEL_C.id,
|
||||
AirDefence.SAM_SA_10_S_300_Grumble_TEL_D.id,
|
||||
}
|
||||
|
||||
REQUIRED_MEDIUM_RANGE_SAM_UNIT_TYPES = {
|
||||
AirDefence.SAM_Hawk_LN_M192.id,
|
||||
AirDefence.SAM_SA_2_LN_SM_90.id,
|
||||
AirDefence.SAM_SA_3_S_125_LN_5P73.id,
|
||||
AirDefence.SAM_SA_2_S_75_Guideline_LN.id,
|
||||
AirDefence.SAM_SA_3_S_125_Goa_LN.id,
|
||||
}
|
||||
|
||||
REQUIRED_EWR_UNIT_TYPE = AirDefence.EWR_1L13.id
|
||||
|
||||
BASE_DEFENSE_RADIUS = nautical_miles(2)
|
||||
|
||||
def __init__(self, miz: Path, theater: ConflictTheater) -> None:
|
||||
@@ -156,7 +161,8 @@ class MizCampaignLoader:
|
||||
|
||||
def country(self, blue: bool) -> Country:
|
||||
country = self.mission.country(
|
||||
self.BLUE_COUNTRY.name if blue else self.RED_COUNTRY.name)
|
||||
self.BLUE_COUNTRY.name if blue else self.RED_COUNTRY.name
|
||||
)
|
||||
# Should be guaranteed because we initialized them.
|
||||
assert country
|
||||
return country
|
||||
@@ -183,7 +189,7 @@ class MizCampaignLoader:
|
||||
for group in self.country(blue).ship_group:
|
||||
if group.units[0].type == self.LHA_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
|
||||
def fobs(self, blue: bool) -> Iterator[VehicleGroup]:
|
||||
for group in self.country(blue).vehicle_group:
|
||||
if group.units[0].type == self.FOB_UNIT_TYPE:
|
||||
@@ -243,6 +249,18 @@ class MizCampaignLoader:
|
||||
if group.units[0].type in self.REQUIRED_MEDIUM_RANGE_SAM_UNIT_TYPES:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def required_ewrs(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.red.vehicle_group:
|
||||
if group.units[0].type in self.REQUIRED_EWR_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def helipads(self) -> Iterator[StaticGroup]:
|
||||
for group in self.blue.static_group:
|
||||
if group.units[0].type == self.FARP_HELIPAD:
|
||||
yield group
|
||||
|
||||
@cached_property
|
||||
def control_points(self) -> Dict[int, ControlPoint]:
|
||||
control_points = {}
|
||||
@@ -253,22 +271,23 @@ class MizCampaignLoader:
|
||||
|
||||
for blue in (False, True):
|
||||
for group in self.off_map_spawns(blue):
|
||||
control_point = OffMapSpawn(next(self.control_point_id),
|
||||
str(group.name), group.position)
|
||||
control_point = OffMapSpawn(
|
||||
next(self.control_point_id), str(group.name), group.position
|
||||
)
|
||||
control_point.captured = blue
|
||||
control_point.captured_invert = group.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
for group in self.carriers(blue):
|
||||
# TODO: Name the carrier.
|
||||
control_point = Carrier(
|
||||
"carrier", group.position, next(self.control_point_id))
|
||||
"carrier", group.position, next(self.control_point_id)
|
||||
)
|
||||
control_point.captured = blue
|
||||
control_point.captured_invert = group.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
for group in self.lhas(blue):
|
||||
# TODO: Name the LHA.
|
||||
control_point = Lha(
|
||||
"lha", group.position, next(self.control_point_id))
|
||||
# TODO: Name the LHA.db
|
||||
control_point = Lha("lha", group.position, next(self.control_point_id))
|
||||
control_point.captured = blue
|
||||
control_point.captured_invert = group.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
@@ -297,24 +316,24 @@ class MizCampaignLoader:
|
||||
# final waypoint at the destination CP. Intermediate waypoints
|
||||
# define the curve of the front line.
|
||||
waypoints = [p.position for p in group.points]
|
||||
origin = self.theater.closest_control_point(waypoints[0])
|
||||
origin = self.theater.closest_control_point(waypoints[0])
|
||||
if origin is None:
|
||||
raise RuntimeError(
|
||||
f"No control point near the first waypoint of {group.name}")
|
||||
f"No control point near the first waypoint of {group.name}"
|
||||
)
|
||||
destination = self.theater.closest_control_point(waypoints[-1])
|
||||
if destination is None:
|
||||
raise RuntimeError(
|
||||
f"No control point near the final waypoint of {group.name}")
|
||||
f"No control point near the final waypoint of {group.name}"
|
||||
)
|
||||
|
||||
# Snap the begin and end points to the control points.
|
||||
waypoints[0] = origin.position
|
||||
waypoints[-1] = destination.position
|
||||
front_line_id = f"{origin.id}|{destination.id}"
|
||||
front_lines[front_line_id] = ComplexFrontLine(origin, waypoints)
|
||||
self.control_points[origin.id].connect(
|
||||
self.control_points[destination.id])
|
||||
self.control_points[destination.id].connect(
|
||||
self.control_points[origin.id])
|
||||
self.control_points[origin.id].connect(self.control_points[destination.id])
|
||||
self.control_points[destination.id].connect(self.control_points[origin.id])
|
||||
return front_lines
|
||||
|
||||
def objective_info(self, group: Group) -> Tuple[ControlPoint, Distance]:
|
||||
@@ -326,49 +345,80 @@ class MizCampaignLoader:
|
||||
for group in self.garrisons:
|
||||
closest, distance = self.objective_info(group)
|
||||
if distance < self.BASE_DEFENSE_RADIUS:
|
||||
closest.preset_locations.base_garrisons.append(group.position)
|
||||
closest.preset_locations.base_garrisons.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
else:
|
||||
logging.warning(
|
||||
f"Found garrison unit too far from base: {group.name}")
|
||||
logging.warning(f"Found garrison unit too far from base: {group.name}")
|
||||
|
||||
for group in self.sams:
|
||||
closest, distance = self.objective_info(group)
|
||||
if distance < self.BASE_DEFENSE_RADIUS:
|
||||
closest.preset_locations.base_air_defense.append(group.position)
|
||||
closest.preset_locations.base_air_defense.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
else:
|
||||
closest.preset_locations.strike_locations.append(group.position)
|
||||
closest.preset_locations.strike_locations.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.ewrs:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.ewrs.append(group.position)
|
||||
if distance < self.BASE_DEFENSE_RADIUS:
|
||||
closest.preset_locations.ewrs.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
else:
|
||||
closest.preset_locations.ewrs.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.offshore_strike_targets:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.offshore_strike_locations.append(
|
||||
group.position)
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.ships:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.ships.append(group.position)
|
||||
closest.preset_locations.ships.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.missile_sites:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.missile_sites.append(group.position)
|
||||
closest.preset_locations.missile_sites.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.coastal_defenses:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.coastal_defenses.append(group.position)
|
||||
closest.preset_locations.coastal_defenses.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.required_long_range_sams:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.required_long_range_sams.append(
|
||||
group.position
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.required_medium_range_sams:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.required_medium_range_sams.append(
|
||||
group.position
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.required_ewrs:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.required_ewrs.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.helipads:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.helipads.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
def populate_theater(self) -> None:
|
||||
@@ -423,8 +473,9 @@ class ConflictTheater:
|
||||
logging.warning("Replacing existing frontline data")
|
||||
self._frontline_data = data
|
||||
|
||||
def add_controlpoint(self, point: ControlPoint,
|
||||
connected_to: Optional[List[ControlPoint]] = None):
|
||||
def add_controlpoint(
|
||||
self, point: ControlPoint, connected_to: Optional[List[ControlPoint]] = None
|
||||
):
|
||||
if connected_to is None:
|
||||
connected_to = []
|
||||
for connected_point in connected_to:
|
||||
@@ -486,8 +537,8 @@ class ConflictTheater:
|
||||
for inclusion_zone in self.landmap.inclusion_zones:
|
||||
nearest_pair = ops.nearest_points(point, inclusion_zone)
|
||||
nearest_points.append(nearest_pair[1])
|
||||
min_distance = point.distance(nearest_points[0]) # type: geometry.Point
|
||||
nearest_point = nearest_points[0] # type: geometry.Point
|
||||
min_distance = point.distance(nearest_points[0]) # type: geometry.Point
|
||||
nearest_point = nearest_points[0] # type: geometry.Point
|
||||
for pt in nearest_points[1:]:
|
||||
distance = point.distance(pt)
|
||||
if distance < min_distance:
|
||||
@@ -498,7 +549,7 @@ class ConflictTheater:
|
||||
nearest_point = Point(nearest_point.x, nearest_point.y)
|
||||
new_point = point.point_from_heading(
|
||||
point.heading_between_point(nearest_point),
|
||||
point.distance_to_point(nearest_point) + extend_dist
|
||||
point.distance_to_point(nearest_point) + extend_dist,
|
||||
)
|
||||
return new_point
|
||||
|
||||
@@ -512,7 +563,9 @@ class ConflictTheater:
|
||||
|
||||
def conflicts(self, from_player=True) -> Iterator[FrontLine]:
|
||||
for cp in [x for x in self.controlpoints if x.captured == from_player]:
|
||||
for connected_point in [x for x in cp.connected_points if x.captured != from_player]:
|
||||
for connected_point in [
|
||||
x for x in cp.connected_points if x.captured != from_player
|
||||
]:
|
||||
yield FrontLine(cp, connected_point, self)
|
||||
|
||||
def enemy_points(self) -> List[ControlPoint]:
|
||||
@@ -547,7 +600,7 @@ class ConflictTheater:
|
||||
closest = conflict
|
||||
closest_distance = distance
|
||||
return closest
|
||||
|
||||
|
||||
def closest_opposing_control_points(self) -> Tuple[ControlPoint, ControlPoint]:
|
||||
"""
|
||||
Returns a tuple of the two nearest opposing ControlPoints in theater.
|
||||
@@ -567,17 +620,22 @@ class ConflictTheater:
|
||||
distances[cp.id] = dist
|
||||
closest_cp_id = min(distances, key=distances.get) # type: ignore
|
||||
|
||||
all_cp_min_distances[(control_point.id, closest_cp_id)] = distances[closest_cp_id]
|
||||
all_cp_min_distances[(control_point.id, closest_cp_id)] = distances[
|
||||
closest_cp_id
|
||||
]
|
||||
closest_opposing_cps = [
|
||||
self.find_control_point_by_id(i)
|
||||
for i
|
||||
in min(all_cp_min_distances, key=all_cp_min_distances.get) # type: ignore
|
||||
] # type: List[ControlPoint]
|
||||
for i in min(
|
||||
all_cp_min_distances, key=all_cp_min_distances.get
|
||||
) # type: ignore
|
||||
] # type: List[ControlPoint]
|
||||
assert len(closest_opposing_cps) == 2
|
||||
if closest_opposing_cps[0].captured:
|
||||
return cast(Tuple[ControlPoint, ControlPoint], tuple(closest_opposing_cps))
|
||||
else:
|
||||
return cast(Tuple[ControlPoint, ControlPoint], tuple(reversed(closest_opposing_cps)))
|
||||
return cast(
|
||||
Tuple[ControlPoint, ControlPoint], tuple(reversed(closest_opposing_cps))
|
||||
)
|
||||
|
||||
def find_control_point_by_id(self, id: int) -> ControlPoint:
|
||||
for i in self.controlpoints:
|
||||
@@ -613,7 +671,7 @@ class ConflictTheater:
|
||||
cp.captured_invert = False
|
||||
|
||||
return cp
|
||||
|
||||
|
||||
@staticmethod
|
||||
def from_json(directory: Path, data: Dict[str, Any]) -> ConflictTheater:
|
||||
theaters = {
|
||||
@@ -649,7 +707,7 @@ class ConflictTheater:
|
||||
cps[l[1]].connect(cps[l[0]])
|
||||
|
||||
return t
|
||||
|
||||
|
||||
|
||||
class CaucasusTheater(ConflictTheater):
|
||||
terrain = caucasus.Caucasus()
|
||||
@@ -672,9 +730,8 @@ class PersianGulfTheater(ConflictTheater):
|
||||
terrain = persiangulf.PersianGulf()
|
||||
overview_image = "persiangulf.gif"
|
||||
reference_points = (
|
||||
ReferencePoint(persiangulf.Jiroft_Airport.position,
|
||||
Point(1692, 1343)),
|
||||
ReferencePoint(persiangulf.Liwa_Airbase.position, Point(358, 3238)),
|
||||
ReferencePoint(persiangulf.Jiroft.position, Point(1692, 1343)),
|
||||
ReferencePoint(persiangulf.Liwa_AFB.position, Point(358, 3238)),
|
||||
)
|
||||
landmap = load_landmap("resources\\gulflandmap.p")
|
||||
daytime_map = {
|
||||
@@ -722,7 +779,7 @@ class TheChannelTheater(ConflictTheater):
|
||||
overview_image = "thechannel.gif"
|
||||
reference_points = (
|
||||
ReferencePoint(thechannel.Abbeville_Drucat.position, Point(2005, 2390)),
|
||||
ReferencePoint(thechannel.Detling.position, Point(706, 382))
|
||||
ReferencePoint(thechannel.Detling.position, Point(706, 382)),
|
||||
)
|
||||
landmap = load_landmap("resources\\channellandmap.p")
|
||||
daytime_map = {
|
||||
@@ -791,7 +848,7 @@ class FrontLine(MissionTarget):
|
||||
self,
|
||||
control_point_a: ControlPoint,
|
||||
control_point_b: ControlPoint,
|
||||
theater: ConflictTheater
|
||||
theater: ConflictTheater,
|
||||
) -> None:
|
||||
self.control_point_a = control_point_a
|
||||
self.control_point_b = control_point_b
|
||||
@@ -807,6 +864,7 @@ class FrontLine(MissionTarget):
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
yield from [
|
||||
FlightType.CAS,
|
||||
FlightType.AEWC,
|
||||
# TODO: FlightType.TROOP_TRANSPORT
|
||||
# TODO: FlightType.EVAC
|
||||
]
|
||||
@@ -935,10 +993,9 @@ class FrontLine(MissionTarget):
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def load_json_frontlines(
|
||||
theater: ConflictTheater
|
||||
theater: ConflictTheater,
|
||||
) -> Optional[Dict[str, ComplexFrontLine]]:
|
||||
"""Load complex frontlines from json"""
|
||||
try:
|
||||
|
||||
@@ -4,7 +4,6 @@ import heapq
|
||||
import itertools
|
||||
import logging
|
||||
import random
|
||||
import re
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
@@ -28,6 +27,7 @@ from gen.ground_forces.combat_stance import CombatStance
|
||||
from gen.runways import RunwayAssigner, RunwayData
|
||||
from .base import Base
|
||||
from .missiontarget import MissionTarget
|
||||
from game.point_with_heading import PointWithHeading
|
||||
from .theatergroundobject import (
|
||||
BaseDefenseGroundObject,
|
||||
EwrGroundObject,
|
||||
@@ -77,38 +77,41 @@ class PresetLocations:
|
||||
"""Defines the preset locations loaded from the campaign mission file."""
|
||||
|
||||
#: Locations used for spawning ground defenses for bases.
|
||||
base_garrisons: List[Point] = field(default_factory=list)
|
||||
base_garrisons: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
#: Locations used for spawning air defenses for bases. Used by SAMs, AAA,
|
||||
#: and SHORADs.
|
||||
base_air_defense: List[Point] = field(default_factory=list)
|
||||
base_air_defense: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
#: Locations used by EWRs.
|
||||
ewrs: List[Point] = field(default_factory=list)
|
||||
ewrs: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
#: Locations used by non-carrier ships. Carriers and LHAs are not random.
|
||||
ships: List[Point] = field(default_factory=list)
|
||||
ships: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
#: Locations used by coastal defenses.
|
||||
coastal_defenses: List[Point] = field(default_factory=list)
|
||||
coastal_defenses: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
#: Locations used by ground based strike objectives.
|
||||
strike_locations: List[Point] = field(default_factory=list)
|
||||
strike_locations: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
#: Locations used by offshore strike objectives.
|
||||
offshore_strike_locations: List[Point] = field(default_factory=list)
|
||||
offshore_strike_locations: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
#: Locations used by missile sites like scuds and V-2s.
|
||||
missile_sites: List[Point] = field(default_factory=list)
|
||||
missile_sites: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
#: Locations of long range SAMs which should always be spawned.
|
||||
required_long_range_sams: List[Point] = field(default_factory=list)
|
||||
required_long_range_sams: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
#: Locations of medium range SAMs which should always be spawned.
|
||||
required_medium_range_sams: List[Point] = field(default_factory=list)
|
||||
required_medium_range_sams: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
#: Locations of EWRs which should always be spawned.
|
||||
required_ewrs: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
@staticmethod
|
||||
def _random_from(points: List[Point]) -> Optional[Point]:
|
||||
def _random_from(points: List[PointWithHeading]) -> Optional[PointWithHeading]:
|
||||
"""Finds, removes, and returns a random position from the given list."""
|
||||
if not points:
|
||||
return None
|
||||
@@ -116,7 +119,7 @@ class PresetLocations:
|
||||
points.remove(point)
|
||||
return point
|
||||
|
||||
def random_for(self, location_type: LocationType) -> Optional[Point]:
|
||||
def random_for(self, location_type: LocationType) -> Optional[PointWithHeading]:
|
||||
"""Returns a position suitable for the given location type.
|
||||
|
||||
The location, if found, will be claimed by the caller and not available
|
||||
@@ -231,10 +234,17 @@ class ControlPoint(MissionTarget, ABC):
|
||||
# TODO: Only airbases have IDs.
|
||||
# TODO: has_frontline is only reasonable for airbases.
|
||||
# TODO: cptype is obsolete.
|
||||
def __init__(self, cp_id: int, name: str, position: Point,
|
||||
at: db.StartingPosition, size: int,
|
||||
importance: float, has_frontline=True,
|
||||
cptype=ControlPointType.AIRBASE):
|
||||
def __init__(
|
||||
self,
|
||||
cp_id: int,
|
||||
name: str,
|
||||
position: Point,
|
||||
at: db.StartingPosition,
|
||||
size: int,
|
||||
importance: float,
|
||||
has_frontline=True,
|
||||
cptype=ControlPointType.AIRBASE,
|
||||
):
|
||||
super().__init__(name, position)
|
||||
# TODO: Should be Airbase specific.
|
||||
self.id = cp_id
|
||||
@@ -243,6 +253,7 @@ class ControlPoint(MissionTarget, ABC):
|
||||
self.connected_objectives: List[TheaterGroundObject] = []
|
||||
self.base_defenses: List[BaseDefenseGroundObject] = []
|
||||
self.preset_locations = PresetLocations()
|
||||
self.helipads: List[PointWithHeading] = []
|
||||
|
||||
# TODO: Should be Airbase specific.
|
||||
self.size = size
|
||||
@@ -257,17 +268,17 @@ class ControlPoint(MissionTarget, ABC):
|
||||
# TODO: Should be Airbase specific.
|
||||
self.stances: Dict[int, CombatStance] = {}
|
||||
from ..event import UnitsDeliveryEvent
|
||||
|
||||
self.pending_unit_deliveries = UnitsDeliveryEvent(self)
|
||||
|
||||
self.target_position: Optional[Point] = None
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{__class__}: {self.name}>"
|
||||
|
||||
@property
|
||||
def ground_objects(self) -> List[TheaterGroundObject]:
|
||||
return list(
|
||||
itertools.chain(self.connected_objectives, self.base_defenses))
|
||||
return list(itertools.chain(self.connected_objectives, self.base_defenses))
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
@@ -342,15 +353,18 @@ class ControlPoint(MissionTarget, ABC):
|
||||
Get the carrier group name if the airbase is a carrier
|
||||
:return: Carrier group name
|
||||
"""
|
||||
if self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP,
|
||||
ControlPointType.LHA_GROUP]:
|
||||
if self.cptype in [
|
||||
ControlPointType.AIRCRAFT_CARRIER_GROUP,
|
||||
ControlPointType.LHA_GROUP,
|
||||
]:
|
||||
for g in self.ground_objects:
|
||||
if g.dcs_identifier == "CARRIER":
|
||||
for group in g.groups:
|
||||
for u in group.units:
|
||||
if db.unit_type_from_name(u.type) in [
|
||||
CVN_74_John_C__Stennis,
|
||||
CV_1143_5_Admiral_Kuznetsov]:
|
||||
CVN_74_John_C__Stennis,
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
]:
|
||||
return group.name
|
||||
elif g.dcs_identifier == "LHA":
|
||||
for group in g.groups:
|
||||
@@ -376,20 +390,19 @@ class ControlPoint(MissionTarget, ABC):
|
||||
# TODO: Should be Airbase specific.
|
||||
def clear_base_defenses(self) -> None:
|
||||
for base_defense in self.base_defenses:
|
||||
p = PointWithHeading.from_point(base_defense.position, base_defense.heading)
|
||||
if isinstance(base_defense, EwrGroundObject):
|
||||
self.preset_locations.ewrs.append(base_defense.position)
|
||||
self.preset_locations.ewrs.append(p)
|
||||
elif isinstance(base_defense, SamGroundObject):
|
||||
self.preset_locations.base_air_defense.append(
|
||||
base_defense.position)
|
||||
self.preset_locations.base_air_defense.append(p)
|
||||
elif isinstance(base_defense, VehicleGroupGroundObject):
|
||||
self.preset_locations.base_garrisons.append(
|
||||
base_defense.position)
|
||||
self.preset_locations.base_garrisons.append(p)
|
||||
else:
|
||||
logging.error(
|
||||
"Could not determine preset location type for "
|
||||
f"{base_defense}. Assuming garrison type.")
|
||||
self.preset_locations.base_garrisons.append(
|
||||
base_defense.position)
|
||||
f"{base_defense}. Assuming garrison type."
|
||||
)
|
||||
self.preset_locations.base_garrisons.append(p)
|
||||
self.base_defenses = []
|
||||
|
||||
def capture_equipment(self, game: Game) -> None:
|
||||
@@ -398,15 +411,18 @@ class ControlPoint(MissionTarget, ABC):
|
||||
game.adjust_budget(total, player=not self.captured)
|
||||
game.message(
|
||||
f"{self.name} is not connected to any friendly points. Ground "
|
||||
f"vehicles have been captured and sold for ${total}M.")
|
||||
f"vehicles have been captured and sold for ${total}M."
|
||||
)
|
||||
|
||||
def retreat_ground_units(self, game: Game):
|
||||
# When there are multiple valid destinations, deliver units to whichever
|
||||
# base is least defended first. The closest approximation of unit
|
||||
# strength we have is price
|
||||
destinations = [GroundUnitDestination(cp)
|
||||
for cp in self.connected_points
|
||||
if cp.captured == self.captured]
|
||||
destinations = [
|
||||
GroundUnitDestination(cp)
|
||||
for cp in self.connected_points
|
||||
if cp.captured == self.captured
|
||||
]
|
||||
if not destinations:
|
||||
self.capture_equipment(game)
|
||||
return
|
||||
@@ -419,8 +435,9 @@ class ControlPoint(MissionTarget, ABC):
|
||||
destination.control_point.base.commision_units({unit_type: 1})
|
||||
destination = heapq.heappushpop(destinations, destination)
|
||||
|
||||
def capture_aircraft(self, game: Game, airframe: Type[FlyingType],
|
||||
count: int) -> None:
|
||||
def capture_aircraft(
|
||||
self, game: Game, airframe: Type[FlyingType], count: int
|
||||
) -> None:
|
||||
try:
|
||||
value = PRICES[airframe] * count
|
||||
except KeyError:
|
||||
@@ -431,11 +448,12 @@ class ControlPoint(MissionTarget, ABC):
|
||||
game.message(
|
||||
f"No valid retreat destination in range of {self.name} for "
|
||||
f"{airframe.id}. {count} aircraft have been captured and sold for "
|
||||
f"${value}M.")
|
||||
f"${value}M."
|
||||
)
|
||||
|
||||
def aircraft_retreat_destination(
|
||||
self, game: Game,
|
||||
airframe: Type[FlyingType]) -> Optional[ControlPoint]:
|
||||
self, game: Game, airframe: Type[FlyingType]
|
||||
) -> Optional[ControlPoint]:
|
||||
closest = ObjectiveDistanceCache.get_closest_airfields(self)
|
||||
# TODO: Should be airframe dependent.
|
||||
max_retreat_distance = nautical_miles(200)
|
||||
@@ -451,8 +469,9 @@ class ControlPoint(MissionTarget, ABC):
|
||||
return airbase
|
||||
return None
|
||||
|
||||
def _retreat_air_units(self, game: Game, airframe: Type[FlyingType],
|
||||
count: int) -> None:
|
||||
def _retreat_air_units(
|
||||
self, game: Game, airframe: Type[FlyingType], count: int
|
||||
) -> None:
|
||||
while count:
|
||||
logging.debug(f"Retreating {count} {airframe.id} from {self.name}")
|
||||
destination = self.aircraft_retreat_destination(game, airframe)
|
||||
@@ -485,6 +504,7 @@ class ControlPoint(MissionTarget, ABC):
|
||||
|
||||
self.clear_base_defenses()
|
||||
from .start_generator import BaseDefenseGenerator
|
||||
|
||||
BaseDefenseGenerator(game, self).generate()
|
||||
|
||||
@abstractmethod
|
||||
@@ -514,16 +534,19 @@ class ControlPoint(MissionTarget, ABC):
|
||||
if issubclass(unit_bought, FlyingType):
|
||||
on_order += self.pending_unit_deliveries.units[unit_bought]
|
||||
|
||||
return PendingOccupancy(self.base.total_aircraft, on_order,
|
||||
self.aircraft_transferring(game))
|
||||
return PendingOccupancy(
|
||||
self.base.total_aircraft, on_order, self.aircraft_transferring(game)
|
||||
)
|
||||
|
||||
def unclaimed_parking(self, game: Game) -> int:
|
||||
return (self.total_aircraft_parking -
|
||||
self.expected_aircraft_next_turn(game).total)
|
||||
return (
|
||||
self.total_aircraft_parking - self.expected_aircraft_next_turn(game).total
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def active_runway(self, conditions: Conditions,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
|
||||
def active_runway(
|
||||
self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData]
|
||||
) -> RunwayData:
|
||||
...
|
||||
|
||||
@property
|
||||
@@ -574,7 +597,13 @@ class ControlPoint(MissionTarget, ABC):
|
||||
Get number of pending frontline aa units
|
||||
"""
|
||||
if self.pending_unit_deliveries:
|
||||
return sum([v for k,v in self.pending_unit_deliveries.units.items() if k in TYPE_SHORAD])
|
||||
return sum(
|
||||
[
|
||||
v
|
||||
for k, v in self.pending_unit_deliveries.units.items()
|
||||
if k in TYPE_SHORAD
|
||||
]
|
||||
)
|
||||
else:
|
||||
return 0
|
||||
|
||||
@@ -598,26 +627,45 @@ class ControlPoint(MissionTarget, ABC):
|
||||
continue
|
||||
on_order += self.pending_unit_deliveries.units[unit_bought]
|
||||
|
||||
return PendingOccupancy(self.base.total_armor, on_order,
|
||||
# Ground unit transfers not yet implemented.
|
||||
transferring=0)
|
||||
return PendingOccupancy(
|
||||
self.base.total_armor,
|
||||
on_order,
|
||||
# Ground unit transfers not yet implemented.
|
||||
transferring=0,
|
||||
)
|
||||
|
||||
@property
|
||||
def income_per_turn(self) -> int:
|
||||
return 0
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
if self.is_friendly(for_player):
|
||||
yield from [
|
||||
FlightType.AEWC,
|
||||
]
|
||||
yield from super().mission_types(for_player)
|
||||
|
||||
@property
|
||||
def has_active_frontline(self) -> bool:
|
||||
return any(
|
||||
not c.is_friendly(self.captured) for c in self.connected_points)
|
||||
return any(not c.is_friendly(self.captured) for c in self.connected_points)
|
||||
|
||||
|
||||
class Airfield(ControlPoint):
|
||||
|
||||
def __init__(self, airport: Airport, size: int,
|
||||
importance: float, has_frontline=True):
|
||||
super().__init__(airport.id, airport.name, airport.position, airport,
|
||||
size, importance, has_frontline,
|
||||
cptype=ControlPointType.AIRBASE)
|
||||
def __init__(
|
||||
self, airport: Airport, size: int, importance: float, has_frontline=True
|
||||
):
|
||||
super().__init__(
|
||||
airport.id,
|
||||
airport.name,
|
||||
airport.position,
|
||||
airport,
|
||||
size,
|
||||
importance,
|
||||
has_frontline,
|
||||
cptype=ControlPointType.AIRBASE,
|
||||
)
|
||||
self.airport = airport
|
||||
self._runway_status = RunwayStatus()
|
||||
|
||||
@@ -631,6 +679,7 @@ class Airfield(ControlPoint):
|
||||
|
||||
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.INTERCEPTION
|
||||
@@ -661,8 +710,9 @@ class Airfield(ControlPoint):
|
||||
def damage_runway(self) -> None:
|
||||
self.runway_status.damage()
|
||||
|
||||
def active_runway(self, conditions: Conditions,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
|
||||
def active_runway(
|
||||
self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData]
|
||||
) -> RunwayData:
|
||||
assigner = RunwayAssigner(conditions)
|
||||
return assigner.get_preferred_runway(self.airport)
|
||||
|
||||
@@ -680,13 +730,13 @@ class Airfield(ControlPoint):
|
||||
|
||||
|
||||
class NavalControlPoint(ControlPoint, ABC):
|
||||
|
||||
@property
|
||||
def is_fleet(self) -> bool:
|
||||
return True
|
||||
|
||||
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.INTERCEPTION
|
||||
@@ -710,14 +760,17 @@ class NavalControlPoint(ControlPoint, ABC):
|
||||
for group in g.groups:
|
||||
for u in group.units:
|
||||
if db.unit_type_from_name(u.type) in [
|
||||
CVN_74_John_C__Stennis, LHA_1_Tarawa,
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
Type_071_Amphibious_Transport_Dock]:
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
Type_071_Amphibious_Transport_Dock,
|
||||
]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def active_runway(self, conditions: Conditions,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
|
||||
def active_runway(
|
||||
self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData]
|
||||
) -> RunwayData:
|
||||
# TODO: Assign TACAN and ICLS earlier so we don't need this.
|
||||
fallback = RunwayData(self.full_name, runway_heading=0, runway_name="")
|
||||
return dynamic_runways.get(self.name, fallback)
|
||||
@@ -740,12 +793,19 @@ class NavalControlPoint(ControlPoint, ABC):
|
||||
|
||||
|
||||
class Carrier(NavalControlPoint):
|
||||
|
||||
def __init__(self, name: str, at: Point, cp_id: int):
|
||||
import game.theater.conflicttheater
|
||||
super().__init__(cp_id, name, at, at,
|
||||
game.theater.conflicttheater.SIZE_SMALL, 1,
|
||||
has_frontline=False, cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP)
|
||||
|
||||
super().__init__(
|
||||
cp_id,
|
||||
name,
|
||||
at,
|
||||
at,
|
||||
game.theater.conflicttheater.SIZE_SMALL,
|
||||
1,
|
||||
has_frontline=False,
|
||||
cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP,
|
||||
)
|
||||
|
||||
def capture(self, game: Game, for_player: bool) -> None:
|
||||
raise RuntimeError("Carriers cannot be captured")
|
||||
@@ -763,12 +823,19 @@ class Carrier(NavalControlPoint):
|
||||
|
||||
|
||||
class Lha(NavalControlPoint):
|
||||
|
||||
def __init__(self, name: str, at: Point, cp_id: int):
|
||||
import game.theater.conflicttheater
|
||||
super().__init__(cp_id, name, at, at,
|
||||
game.theater.conflicttheater.SIZE_SMALL, 1,
|
||||
has_frontline=False, cptype=ControlPointType.LHA_GROUP)
|
||||
|
||||
super().__init__(
|
||||
cp_id,
|
||||
name,
|
||||
at,
|
||||
at,
|
||||
game.theater.conflicttheater.SIZE_SMALL,
|
||||
1,
|
||||
has_frontline=False,
|
||||
cptype=ControlPointType.LHA_GROUP,
|
||||
)
|
||||
|
||||
def capture(self, game: Game, for_player: bool) -> None:
|
||||
raise RuntimeError("LHAs cannot be captured")
|
||||
@@ -786,15 +853,22 @@ class Lha(NavalControlPoint):
|
||||
|
||||
|
||||
class OffMapSpawn(ControlPoint):
|
||||
|
||||
def runway_is_operational(self) -> bool:
|
||||
return True
|
||||
|
||||
def __init__(self, cp_id: int, name: str, position: Point):
|
||||
from . import IMPORTANCE_MEDIUM, SIZE_REGULAR
|
||||
super().__init__(cp_id, name, position, at=position,
|
||||
size=SIZE_REGULAR, importance=IMPORTANCE_MEDIUM,
|
||||
has_frontline=False, cptype=ControlPointType.OFF_MAP)
|
||||
|
||||
super().__init__(
|
||||
cp_id,
|
||||
name,
|
||||
position,
|
||||
at=position,
|
||||
size=SIZE_REGULAR,
|
||||
importance=IMPORTANCE_MEDIUM,
|
||||
has_frontline=False,
|
||||
cptype=ControlPointType.OFF_MAP,
|
||||
)
|
||||
|
||||
def capture(self, game: Game, for_player: bool) -> None:
|
||||
raise RuntimeError("Off map control points cannot be captured")
|
||||
@@ -813,8 +887,9 @@ class OffMapSpawn(ControlPoint):
|
||||
def heading(self) -> int:
|
||||
return 0
|
||||
|
||||
def active_runway(self, conditions: Conditions,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
|
||||
def active_runway(
|
||||
self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData]
|
||||
) -> RunwayData:
|
||||
logging.warning("TODO: Off map spawns have no runways.")
|
||||
return RunwayData(self.full_name, runway_heading=0, runway_name="")
|
||||
|
||||
@@ -828,19 +903,27 @@ class OffMapSpawn(ControlPoint):
|
||||
|
||||
|
||||
class Fob(ControlPoint):
|
||||
|
||||
def __init__(self, name: str, at: Point, cp_id: int):
|
||||
import game.theater.conflicttheater
|
||||
super().__init__(cp_id, name, at, at,
|
||||
game.theater.conflicttheater.SIZE_SMALL, 1,
|
||||
has_frontline=True, cptype=ControlPointType.FOB)
|
||||
|
||||
super().__init__(
|
||||
cp_id,
|
||||
name,
|
||||
at,
|
||||
at,
|
||||
game.theater.conflicttheater.SIZE_SMALL,
|
||||
1,
|
||||
has_frontline=True,
|
||||
cptype=ControlPointType.FOB,
|
||||
)
|
||||
self.name = name
|
||||
|
||||
|
||||
def runway_is_operational(self) -> bool:
|
||||
return False
|
||||
|
||||
def active_runway(self, conditions: Conditions,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
|
||||
|
||||
def active_runway(
|
||||
self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData]
|
||||
) -> RunwayData:
|
||||
logging.warning("TODO: FOBs have no runways.")
|
||||
return RunwayData(self.full_name, runway_heading=0, runway_name="")
|
||||
|
||||
@@ -850,6 +933,7 @@ class Fob(ControlPoint):
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
if self.is_friendly(for_player):
|
||||
yield from [
|
||||
FlightType.BARCAP,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ 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:
|
||||
|
||||
@@ -13,7 +13,7 @@ from dcs.vehicles import AirDefence
|
||||
|
||||
from game import Game, db
|
||||
from game.factions.faction import Faction
|
||||
from game.theater import Carrier, Lha, LocationType
|
||||
from game.theater import Carrier, Lha, LocationType, PointWithHeading
|
||||
from game.theater.theatergroundobject import (
|
||||
BuildingGroundObject,
|
||||
CarrierGroundObject,
|
||||
@@ -23,9 +23,11 @@ from game.theater.theatergroundobject import (
|
||||
SamGroundObject,
|
||||
ShipGroundObject,
|
||||
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,
|
||||
@@ -35,10 +37,8 @@ from gen.fleet.ship_group_generator import (
|
||||
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.sam_group_generator import generate_anti_air_group
|
||||
from gen.sam.ewr_group_generator import generate_ewr_group
|
||||
from . import (
|
||||
ConflictTheater,
|
||||
ControlPoint,
|
||||
@@ -76,9 +76,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
|
||||
@@ -96,7 +101,7 @@ class GameGenerator:
|
||||
start_date=self.generator_settings.start_date,
|
||||
settings=self.settings,
|
||||
player_budget=self.generator_settings.player_budget,
|
||||
enemy_budget=self.generator_settings.enemy_budget
|
||||
enemy_budget=self.generator_settings.enemy_budget,
|
||||
)
|
||||
|
||||
GroundObjectGenerator(game, self.generator_settings).generate()
|
||||
@@ -108,7 +113,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
|
||||
@@ -140,31 +145,40 @@ class LocationFinder:
|
||||
self.game = game
|
||||
self.control_point = control_point
|
||||
self.miz_data = MizDataLocationFinder.compute_possible_locations(
|
||||
game.theater.terrain.name, control_point.full_name)
|
||||
game.theater.terrain.name, control_point.full_name
|
||||
)
|
||||
|
||||
def location_for(self, location_type: LocationType) -> Optional[Point]:
|
||||
def location_for(self, location_type: LocationType) -> Optional[PointWithHeading]:
|
||||
position = self.control_point.preset_locations.random_for(location_type)
|
||||
if position is not None:
|
||||
return position
|
||||
|
||||
logging.warning(f"No campaign location for %s at %s",
|
||||
location_type.value, self.control_point)
|
||||
logging.warning(
|
||||
f"No campaign location for %s Mat %s",
|
||||
location_type.value,
|
||||
self.control_point,
|
||||
)
|
||||
position = self.random_from_miz_data(
|
||||
location_type == LocationType.OffshoreStrikeTarget)
|
||||
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)
|
||||
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)
|
||||
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]:
|
||||
def random_from_miz_data(self, offshore: bool) -> Optional[PointWithHeading]:
|
||||
if offshore:
|
||||
locations = self.miz_data.offshore_locations
|
||||
else:
|
||||
@@ -172,13 +186,23 @@ class LocationFinder:
|
||||
if self.miz_data.offshore_locations:
|
||||
preset = random.choice(locations)
|
||||
locations.remove(preset)
|
||||
return preset.position
|
||||
return PointWithHeading.from_point(preset.position, preset.heading)
|
||||
return None
|
||||
|
||||
def random_position(self, location_type: LocationType) -> Optional[Point]:
|
||||
def random_position(
|
||||
self, location_type: LocationType
|
||||
) -> Optional[PointWithHeading]:
|
||||
# TODO: Flesh out preset locations so we never hit this case.
|
||||
logging.warning("Falling back to random location for %s at %s",
|
||||
location_type.value, self.control_point)
|
||||
|
||||
if location_type == LocationType.Coastal:
|
||||
# No coastal locations generated randomly
|
||||
return None
|
||||
|
||||
logging.warning(
|
||||
"Falling back to random location for %s at %s",
|
||||
location_type.value,
|
||||
self.control_point,
|
||||
)
|
||||
|
||||
is_base_defense = location_type in {
|
||||
LocationType.BaseAirDefense,
|
||||
@@ -212,23 +236,28 @@ class LocationFinder:
|
||||
min_range = 10000
|
||||
max_range = 40000
|
||||
|
||||
position = self._find_random_position(min_range, max_range,
|
||||
on_land, is_base_defense,
|
||||
avoid_others)
|
||||
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)
|
||||
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]:
|
||||
def _find_random_position(
|
||||
self,
|
||||
min_range: int,
|
||||
max_range: int,
|
||||
on_ground: bool,
|
||||
is_base_defense: bool,
|
||||
avoid_others: bool,
|
||||
) -> Optional[PointWithHeading]:
|
||||
"""
|
||||
Find a valid ground object location
|
||||
:param on_ground: Whether it should be on ground or on sea (True = on
|
||||
@@ -241,7 +270,7 @@ class LocationFinder:
|
||||
near = self.control_point.position
|
||||
others = self.control_point.ground_objects
|
||||
|
||||
def is_valid(point: Optional[Point]) -> bool:
|
||||
def is_valid(point: Optional[PointWithHeading]) -> bool:
|
||||
if point is None:
|
||||
return False
|
||||
|
||||
@@ -272,15 +301,21 @@ class LocationFinder:
|
||||
|
||||
for _ in range(300):
|
||||
# Check if on land or sea
|
||||
p = near.random_point_within(max_range, min_range)
|
||||
p = PointWithHeading.from_point(
|
||||
near.random_point_within(max_range, min_range), random.randint(0, 360)
|
||||
)
|
||||
if is_valid(p):
|
||||
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
|
||||
@@ -321,15 +356,15 @@ class ControlPointGroundObjectGenerator:
|
||||
self.generate_ship()
|
||||
|
||||
def generate_ship(self) -> None:
|
||||
point = self.location_finder.location_for(
|
||||
LocationType.OffshoreStrikeTarget)
|
||||
point = self.location_finder.location_for(LocationType.OffshoreStrikeTarget)
|
||||
if point is None:
|
||||
return
|
||||
|
||||
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, point, self.control_point
|
||||
)
|
||||
|
||||
group = generate_ship_group(self.game, g, self.faction_name)
|
||||
g.groups = []
|
||||
@@ -352,13 +387,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 +414,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:
|
||||
@@ -422,8 +461,13 @@ class BaseDefenseGenerator:
|
||||
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = EwrGroundObject(namegen.random_objective_name(), group_id,
|
||||
position, self.control_point)
|
||||
g = EwrGroundObject(
|
||||
namegen.random_objective_name(),
|
||||
group_id,
|
||||
position,
|
||||
self.control_point,
|
||||
True,
|
||||
)
|
||||
|
||||
group = generate_ewr_group(self.game, g, self.faction)
|
||||
if group is None:
|
||||
@@ -454,28 +498,35 @@ class BaseDefenseGenerator:
|
||||
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = VehicleGroupGroundObject(namegen.random_objective_name(), group_id,
|
||||
position, self.control_point,
|
||||
for_airbase=True)
|
||||
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}")
|
||||
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)
|
||||
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)
|
||||
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:
|
||||
@@ -485,21 +536,25 @@ class BaseDefenseGenerator:
|
||||
self.control_point.base_defenses.append(g)
|
||||
|
||||
def generate_shorad(self) -> None:
|
||||
position = self.location_finder.location_for(
|
||||
LocationType.BaseAirDefense)
|
||||
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)
|
||||
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}])
|
||||
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}")
|
||||
logging.error(f"Could not generate SHORAD group at {self.control_point}")
|
||||
return
|
||||
g.groups = groups
|
||||
self.control_point.base_defenses.append(g)
|
||||
@@ -509,7 +564,7 @@ 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.
|
||||
@@ -528,9 +583,13 @@ class FobDefenseGenerator(BaseDefenseGenerator):
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -544,11 +603,15 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
if self.faction.missiles:
|
||||
self.generate_missile_sites()
|
||||
|
||||
if self.faction.coastal_defenses:
|
||||
self.generate_coastal_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()
|
||||
skip_ewrs = self.generate_required_ewr()
|
||||
|
||||
if self.control_point.is_global:
|
||||
return
|
||||
@@ -565,6 +628,12 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
skip_sams -= 1
|
||||
else:
|
||||
self.generate_aa_site()
|
||||
# 1 in 4 additional objectives are EWR.
|
||||
elif random.randint(0, 3) == 0:
|
||||
if skip_ewrs > 0:
|
||||
skip_ewrs -= 1
|
||||
else:
|
||||
self.generate_ewr_site()
|
||||
else:
|
||||
self.generate_ground_point()
|
||||
|
||||
@@ -576,18 +645,36 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
"""
|
||||
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},
|
||||
])
|
||||
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))
|
||||
self.generate_aa_at(
|
||||
position,
|
||||
ranges=[
|
||||
{AirDefenseRange.Medium},
|
||||
{AirDefenseRange.Short},
|
||||
],
|
||||
)
|
||||
return len(presets.required_long_range_sams) + len(
|
||||
presets.required_medium_range_sams
|
||||
)
|
||||
|
||||
def generate_required_ewr(self) -> int:
|
||||
"""Generates the EWR sites that are required by the campaign.
|
||||
|
||||
Returns:
|
||||
The number of EWR sites that were generated.
|
||||
"""
|
||||
presets = self.control_point.preset_locations
|
||||
for position in presets.required_ewrs:
|
||||
self.generate_ewr_at(position)
|
||||
return len(presets.required_ewrs)
|
||||
|
||||
def generate_ground_point(self) -> None:
|
||||
try:
|
||||
@@ -618,8 +705,15 @@ 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,
|
||||
point + template_point,
|
||||
unit["heading"],
|
||||
self.control_point,
|
||||
unit["type"],
|
||||
)
|
||||
|
||||
self.control_point.connected_objectives.append(g)
|
||||
|
||||
@@ -627,27 +721,65 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
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},
|
||||
])
|
||||
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_aa_at(
|
||||
self, position: Point,
|
||||
ranges: Iterable[Set[AirDefenseRange]]) -> None:
|
||||
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, for_airbase=False)
|
||||
g = SamGroundObject(
|
||||
namegen.random_objective_name(),
|
||||
group_id,
|
||||
position,
|
||||
self.control_point,
|
||||
for_airbase=False,
|
||||
)
|
||||
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_ewr_site(self) -> None:
|
||||
position = self.location_finder.location_for(LocationType.Ewr)
|
||||
if position is None:
|
||||
return
|
||||
self.generate_ewr_at(position)
|
||||
|
||||
def generate_ewr_at(self, position: Point) -> None:
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = EwrGroundObject(
|
||||
namegen.random_objective_name(),
|
||||
group_id,
|
||||
position,
|
||||
self.control_point,
|
||||
for_airbase=False,
|
||||
)
|
||||
group = generate_ewr_group(self.game, g, self.faction)
|
||||
if group is None:
|
||||
logging.error(
|
||||
"Could not generate ewr group for %s at %s",
|
||||
g.name,
|
||||
self.control_point,
|
||||
)
|
||||
return
|
||||
g.groups = [group]
|
||||
self.control_point.connected_objectives.append(g)
|
||||
|
||||
def generate_missile_sites(self) -> None:
|
||||
for i in range(self.faction.missiles_group_count):
|
||||
self.generate_missile_site()
|
||||
@@ -659,8 +791,9 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = MissileSiteGroundObject(namegen.random_objective_name(), group_id,
|
||||
position, self.control_point)
|
||||
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,6 +801,31 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
self.control_point.connected_objectives.append(g)
|
||||
return
|
||||
|
||||
def generate_coastal_sites(self) -> None:
|
||||
for i in range(self.faction.coastal_group_count):
|
||||
self.generate_coastal_site()
|
||||
|
||||
def generate_coastal_site(self) -> None:
|
||||
position = self.location_finder.location_for(LocationType.Coastal)
|
||||
if position is None:
|
||||
return
|
||||
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = CoastalSiteGroundObject(
|
||||
namegen.random_objective_name(),
|
||||
group_id,
|
||||
position,
|
||||
self.control_point,
|
||||
position.heading,
|
||||
)
|
||||
group = generate_coastal_group(self.game, g, self.faction_name)
|
||||
g.groups = []
|
||||
if group is not None:
|
||||
g.groups.append(group)
|
||||
self.control_point.connected_objectives.append(g)
|
||||
return
|
||||
|
||||
|
||||
class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
|
||||
def generate(self) -> bool:
|
||||
@@ -678,7 +836,7 @@ class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
|
||||
|
||||
def generate_fob(self) -> None:
|
||||
try:
|
||||
category = self.faction.building_set[self.faction.building_set.index('fob')]
|
||||
category = self.faction.building_set[self.faction.building_set.index("fob")]
|
||||
except IndexError:
|
||||
logging.exception("Faction has no fob buildings defined")
|
||||
return
|
||||
@@ -696,14 +854,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"],
|
||||
airbase_group=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 +886,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()
|
||||
|
||||
@@ -33,7 +33,7 @@ NAME_BY_CATEGORY = {
|
||||
"ww2bunker": "Bunker",
|
||||
"village": "Village",
|
||||
"allycamp": "Camp",
|
||||
"EWR":"EWR",
|
||||
"EWR": "EWR",
|
||||
}
|
||||
|
||||
ABBREV_NAME = {
|
||||
@@ -54,34 +54,59 @@ ABBREV_NAME = {
|
||||
}
|
||||
|
||||
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"],
|
||||
"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"],
|
||||
"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"],
|
||||
"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": [],
|
||||
}
|
||||
|
||||
|
||||
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,
|
||||
airbase_group: bool,
|
||||
sea_object: bool,
|
||||
) -> None:
|
||||
super().__init__(name, position)
|
||||
self.category = category
|
||||
self.group_id = group_id
|
||||
@@ -131,6 +156,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
|
||||
@@ -193,9 +219,18 @@ class TheaterGroundObject(MissionTarget):
|
||||
|
||||
|
||||
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,
|
||||
airbase_group=False,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category=category,
|
||||
@@ -205,7 +240,7 @@ class BuildingGroundObject(TheaterGroundObject):
|
||||
control_point=control_point,
|
||||
dcs_identifier=dcs_identifier,
|
||||
airbase_group=airbase_group,
|
||||
sea_object=False
|
||||
sea_object=False,
|
||||
)
|
||||
self.object_id = object_id
|
||||
# Other TGOs track deadness based on the number of alive units, but
|
||||
@@ -234,6 +269,7 @@ class BuildingGroundObject(TheaterGroundObject):
|
||||
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)
|
||||
@@ -249,8 +285,7 @@ class GenericCarrierGroundObject(NavalGroundObject):
|
||||
|
||||
# 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",
|
||||
@@ -260,7 +295,7 @@ class CarrierGroundObject(GenericCarrierGroundObject):
|
||||
control_point=control_point,
|
||||
dcs_identifier="CARRIER",
|
||||
airbase_group=True,
|
||||
sea_object=True
|
||||
sea_object=True,
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -272,8 +307,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",
|
||||
@@ -283,7 +317,7 @@ class LhaGroundObject(GenericCarrierGroundObject):
|
||||
control_point=control_point,
|
||||
dcs_identifier="LHA",
|
||||
airbase_group=True,
|
||||
sea_object=True
|
||||
sea_object=True,
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -294,8 +328,9 @@ 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",
|
||||
@@ -305,7 +340,29 @@ class MissileSiteGroundObject(TheaterGroundObject):
|
||||
control_point=control_point,
|
||||
dcs_identifier="AA",
|
||||
airbase_group=False,
|
||||
sea_object=False
|
||||
sea_object=False,
|
||||
)
|
||||
|
||||
|
||||
class CoastalSiteGroundObject(TheaterGroundObject):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
group_id: int,
|
||||
position: Point,
|
||||
control_point: ControlPoint,
|
||||
heading,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="aa",
|
||||
group_id=group_id,
|
||||
position=position,
|
||||
heading=heading,
|
||||
control_point=control_point,
|
||||
dcs_identifier="AA",
|
||||
airbase_group=False,
|
||||
sea_object=False,
|
||||
)
|
||||
|
||||
|
||||
@@ -317,8 +374,14 @@ class BaseDefenseGroundObject(TheaterGroundObject):
|
||||
# This type gets used both for AA sites (SAM, AAA, or SHORAD). These should each
|
||||
# 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:
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
group_id: int,
|
||||
position: Point,
|
||||
control_point: ControlPoint,
|
||||
for_airbase: bool,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="aa",
|
||||
@@ -328,7 +391,7 @@ class SamGroundObject(BaseDefenseGroundObject):
|
||||
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,6 +408,7 @@ 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 from super().mission_types(for_player)
|
||||
@@ -355,8 +419,14 @@ class SamGroundObject(BaseDefenseGroundObject):
|
||||
|
||||
|
||||
class VehicleGroupGroundObject(BaseDefenseGroundObject):
|
||||
def __init__(self, name: str, group_id: int, position: Point,
|
||||
control_point: ControlPoint, for_airbase: bool) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
group_id: int,
|
||||
position: Point,
|
||||
control_point: ControlPoint,
|
||||
for_airbase: bool,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="aa",
|
||||
@@ -366,13 +436,19 @@ class VehicleGroupGroundObject(BaseDefenseGroundObject):
|
||||
control_point=control_point,
|
||||
dcs_identifier="AA",
|
||||
airbase_group=for_airbase,
|
||||
sea_object=False
|
||||
sea_object=False,
|
||||
)
|
||||
|
||||
|
||||
class EwrGroundObject(BaseDefenseGroundObject):
|
||||
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,
|
||||
for_airbase: bool,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="EWR",
|
||||
@@ -381,8 +457,8 @@ class EwrGroundObject(BaseDefenseGroundObject):
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier="EWR",
|
||||
airbase_group=True,
|
||||
sea_object=False
|
||||
airbase_group=for_airbase,
|
||||
sea_object=False,
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -392,6 +468,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)
|
||||
@@ -402,8 +479,9 @@ class EwrGroundObject(BaseDefenseGroundObject):
|
||||
|
||||
|
||||
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",
|
||||
@@ -413,7 +491,7 @@ class ShipGroundObject(NavalGroundObject):
|
||||
control_point=control_point,
|
||||
dcs_identifier="AA",
|
||||
airbase_group=False,
|
||||
sea_object=True
|
||||
sea_object=True,
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -15,6 +15,7 @@ from shapely.ops import nearest_points, unary_union
|
||||
|
||||
from game.theater import ControlPoint
|
||||
from game.utils import Distance, meters, nautical_miles
|
||||
from gen import Conflict
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
@@ -32,8 +33,9 @@ class ThreatZones:
|
||||
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)
|
||||
|
||||
@singledispatchmethod
|
||||
@@ -49,8 +51,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 +65,9 @@ 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))
|
||||
)
|
||||
|
||||
@singledispatchmethod
|
||||
def threatened_by_air_defense(self, target) -> bool:
|
||||
@@ -76,13 +79,14 @@ 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))
|
||||
)
|
||||
|
||||
@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):
|
||||
if airfield.captured != location.captured:
|
||||
@@ -90,13 +94,14 @@ class ThreatZones:
|
||||
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 +132,15 @@ 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 = []
|
||||
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:
|
||||
@@ -148,11 +152,27 @@ class ThreatZones:
|
||||
threat_zone = point.buffer(threat_range.meters)
|
||||
air_defenses.append(threat_zone)
|
||||
|
||||
for front_line in game.theater.conflicts(player):
|
||||
vector = Conflict.frontline_vector(
|
||||
front_line.control_point_a, front_line.control_point_b, game.theater
|
||||
)
|
||||
|
||||
start = vector[0]
|
||||
end = vector[0].point_from_heading(vector[1], vector[2])
|
||||
|
||||
line = LineString(
|
||||
[
|
||||
ShapelyPoint(start.x, start.y),
|
||||
ShapelyPoint(end.x, end.y),
|
||||
]
|
||||
)
|
||||
doctrine = game.faction_for(player).doctrine
|
||||
air_threats.append(line.buffer(doctrine.cap_engagement_range.meters))
|
||||
|
||||
return cls(
|
||||
airbases=unary_union(airbases),
|
||||
air_defenses=unary_union(air_defenses)
|
||||
airbases=unary_union(air_threats), air_defenses=unary_union(air_defenses)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def dcs_to_shapely_point(point: DcsPoint) -> ShapelyPoint:
|
||||
return ShapelyPoint(point.x, point.y)
|
||||
return ShapelyPoint(point.x, point.y)
|
||||
|
||||
@@ -70,15 +70,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 +107,13 @@ 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_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 +123,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]
|
||||
|
||||
@@ -2,7 +2,7 @@ from pathlib import Path
|
||||
|
||||
|
||||
def _build_version_string() -> str:
|
||||
components = ["2.4.2"]
|
||||
components = ["2.5"]
|
||||
build_number_path = Path("resources/buildnumber")
|
||||
if build_number_path.exists():
|
||||
with build_number_path.open("r") as build_number_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()
|
||||
|
||||
854
gen/aircraft.py
854
gen/aircraft.py
File diff suppressed because it is too large
Load Diff
391
gen/airfields.py
391
gen/airfields.py
File diff suppressed because it is too large
Load Diff
@@ -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,14 +34,19 @@ AWACS_ALT = 13000
|
||||
@dataclass
|
||||
class AwacsInfo:
|
||||
"""AWACS information for the kneeboard."""
|
||||
|
||||
dcsGroupName: str
|
||||
callsign: str
|
||||
freq: RadioFrequency
|
||||
depature_location: Optional[str]
|
||||
start_time: Optional[timedelta]
|
||||
end_time: Optional[timedelta]
|
||||
|
||||
|
||||
@dataclass
|
||||
class TankerInfo:
|
||||
"""Tanker information for the kneeboard."""
|
||||
|
||||
dcsGroupName: str
|
||||
callsign: str
|
||||
variant: str
|
||||
@@ -54,10 +61,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 +89,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.from_cp
|
||||
if self.conflict.from_cp.captured
|
||||
else self.conflict.to_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.to_cp.position.heading_between_point(
|
||||
self.conflict.from_cp.position
|
||||
)
|
||||
+ TANKER_HEADING_OFFSET * i
|
||||
)
|
||||
tanker_position = player_cp.position.point_from_heading(
|
||||
tanker_heading, TANKER_DISTANCE
|
||||
)
|
||||
tanker_group = self.mission.refuel_flight(
|
||||
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 +150,59 @@ 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)
|
||||
)
|
||||
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(
|
||||
dcsGroupName=str(awacs_flight.name),
|
||||
callsign=callsign_for_support_unit(awacs_flight),
|
||||
freq=freq,
|
||||
depature_location=None,
|
||||
start_time=None,
|
||||
end_time=None,
|
||||
)
|
||||
)
|
||||
else:
|
||||
logging.warning("No AWACS for faction")
|
||||
|
||||
362
gen/armor.py
362
gen/armor.py
@@ -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,6 +67,7 @@ INFANTRY_GROUP_SIZE = 5
|
||||
@dataclass(frozen=True)
|
||||
class JtacInfo:
|
||||
"""JTAC information."""
|
||||
|
||||
dcsGroupName: str
|
||||
unit_name: str
|
||||
callsign: str
|
||||
@@ -65,16 +77,16 @@ class JtacInfo:
|
||||
|
||||
|
||||
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 +99,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 +118,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.from_cp, self.conflict.to_cp, self.game.theater
|
||||
)
|
||||
frontline_vector = Conflict.frontline_vector(
|
||||
self.conflict.from_cp,
|
||||
self.conflict.to_cp,
|
||||
self.game.theater
|
||||
)
|
||||
self.conflict.from_cp, self.conflict.to_cp, 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(
|
||||
@@ -137,7 +157,7 @@ class GroundConflictGenerator:
|
||||
enemy_groups,
|
||||
self.conflict.heading + 90,
|
||||
self.conflict.from_cp,
|
||||
self.conflict.to_cp
|
||||
self.conflict.to_cp,
|
||||
)
|
||||
self.plan_action_for_groups(
|
||||
self.enemy_stance,
|
||||
@@ -145,7 +165,7 @@ class GroundConflictGenerator:
|
||||
player_groups,
|
||||
self.conflict.heading - 90,
|
||||
self.conflict.to_cp,
|
||||
self.conflict.from_cp
|
||||
self.conflict.from_cp,
|
||||
)
|
||||
|
||||
# Add JTAC
|
||||
@@ -157,34 +177,38 @@ class GroundConflictGenerator:
|
||||
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.from_cp.name}/{self.conflict.to_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))
|
||||
)
|
||||
|
||||
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
|
||||
@@ -208,44 +232,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 +286,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 +299,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 +315,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 +341,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 +371,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 +382,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 +394,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 +413,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 +456,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 +481,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 +518,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 +549,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 +576,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 +584,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 +601,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 +612,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 +623,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 +637,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 +647,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 +659,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 +670,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 +686,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 +701,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 +763,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}")
|
||||
@@ -718,12 +788,14 @@ class GroundConflictGenerator:
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@@ -96,7 +96,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 +118,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:
|
||||
@@ -173,6 +175,7 @@ class Package:
|
||||
FlightType.SEAD,
|
||||
FlightType.TARCAP,
|
||||
FlightType.BARCAP,
|
||||
FlightType.AEWC,
|
||||
FlightType.SWEEP,
|
||||
FlightType.ESCORT,
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -37,10 +40,13 @@ class FrontLineInfo:
|
||||
self.enemy_base: ControlPoint = front_line.control_point_b
|
||||
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.
|
||||
"""
|
||||
"""Build FrontLineInfo objects from FrontLine type and append to briefing."""
|
||||
for front_line in self.game.theater.conflicts(from_player=True):
|
||||
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]
|
||||
|
||||
31
gen/coastal/coastal_group_generator.py
Normal file
31
gen/coastal/coastal_group_generator.py
Normal 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
58
gen/coastal/silkworm.py
Normal 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,
|
||||
)
|
||||
@@ -12,19 +12,21 @@ 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,
|
||||
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,
|
||||
):
|
||||
|
||||
self.attackers_side = attackers_side
|
||||
self.defenders_side = defenders_side
|
||||
@@ -43,27 +45,49 @@ class Conflict:
|
||||
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]:
|
||||
def frontline_position(
|
||||
cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater
|
||||
) -> Tuple[Point, int]:
|
||||
frontline = FrontLine(from_cp, to_cp, theater)
|
||||
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, from_cp: ControlPoint, to_cp: ControlPoint, 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)
|
||||
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):
|
||||
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)
|
||||
conflict = cls(
|
||||
@@ -76,12 +100,14 @@ class Conflict:
|
||||
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 +118,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 +129,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 +155,3 @@ class Conflict:
|
||||
return pos
|
||||
logging.error("Didn't find ground position ({})!".format(initial))
|
||||
return None
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,10 +4,18 @@ 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.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 +33,7 @@ SHIP_MAP = {
|
||||
"MolniyaGroupGenerator": MolniyaGroupGenerator,
|
||||
"KiloSubGroupGenerator": KiloSubGroupGenerator,
|
||||
"TangoSubGroupGenerator": TangoSubGroupGenerator,
|
||||
"Type54GroupGenerator": Type54GroupGenerator
|
||||
"Type54GroupGenerator": Type54GroupGenerator,
|
||||
}
|
||||
|
||||
|
||||
@@ -39,10 +47,15 @@ def generate_ship_group(game, ground_object, faction_name: str):
|
||||
gen = random.choice(faction.navy_generators)
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -109,22 +109,25 @@ class ProposedMission:
|
||||
flights: List[ProposedFlight]
|
||||
|
||||
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,
|
||||
closest_airfields: ClosestAirfields,
|
||||
global_inventory: GlobalAircraftInventory,
|
||||
is_player: bool,
|
||||
) -> None:
|
||||
self.closest_airfields = closest_airfields
|
||||
self.global_inventory = global_inventory
|
||||
self.is_player = is_player
|
||||
|
||||
def find_aircraft_for_flight(
|
||||
self, flight: ProposedFlight
|
||||
self, flight: ProposedFlight
|
||||
) -> Optional[Tuple[ControlPoint, Type[FlyingType]]]:
|
||||
"""Finds aircraft suitable for the given mission.
|
||||
|
||||
@@ -144,12 +147,12 @@ class AircraftAllocator:
|
||||
on subsequent calls. If the found aircraft are not used, the caller is
|
||||
responsible for returning them to the inventory.
|
||||
"""
|
||||
return self.find_aircraft_of_type(
|
||||
flight, aircraft_for_task(flight.task)
|
||||
)
|
||||
return self.find_aircraft_of_type(flight, aircraft_for_task(flight.task))
|
||||
|
||||
def find_aircraft_of_type(
|
||||
self, flight: ProposedFlight, types: List[Type[FlyingType]],
|
||||
self,
|
||||
flight: ProposedFlight,
|
||||
types: List[Type[FlyingType]],
|
||||
) -> Optional[Tuple[ControlPoint, Type[FlyingType]]]:
|
||||
airfields_in_range = self.closest_airfields.airfields_within(
|
||||
flight.max_distance
|
||||
@@ -171,18 +174,22 @@ class AircraftAllocator:
|
||||
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,
|
||||
is_player: bool,
|
||||
package_country: str,
|
||||
start_type: str,
|
||||
) -> None:
|
||||
self.closest_airfields = closest_airfields
|
||||
self.is_player = is_player
|
||||
self.package_country = package_country
|
||||
self.package = Package(location)
|
||||
self.allocator = AircraftAllocator(closest_airfields, global_inventory,
|
||||
is_player)
|
||||
self.allocator = AircraftAllocator(
|
||||
closest_airfields, global_inventory, is_player
|
||||
)
|
||||
self.global_inventory = global_inventory
|
||||
self.start_type = start_type
|
||||
|
||||
@@ -203,14 +210,23 @@ class PackageBuilder:
|
||||
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,
|
||||
aircraft,
|
||||
plan.num_aircraft,
|
||||
plan.task,
|
||||
start_type,
|
||||
departure=airfield,
|
||||
arrival=airfield,
|
||||
divert=self.find_divert_field(aircraft, airfield),
|
||||
)
|
||||
self.package.add_flight(flight)
|
||||
return True
|
||||
|
||||
def find_divert_field(self, aircraft: 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):
|
||||
if airfield.captured != self.is_player:
|
||||
@@ -323,8 +339,8 @@ class ObjectiveFinder:
|
||||
return self._targets_by_range(self.enemy_ships())
|
||||
|
||||
def _targets_by_range(
|
||||
self,
|
||||
targets: Iterable[MissionTarget]) -> Iterator[MissionTarget]:
|
||||
self, targets: Iterable[MissionTarget]
|
||||
) -> Iterator[MissionTarget]:
|
||||
target_ranges: List[Tuple[MissionTarget, int]] = []
|
||||
for target in targets:
|
||||
ranges: List[int] = []
|
||||
@@ -430,13 +446,33 @@ class ObjectiveFinder:
|
||||
|
||||
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:
|
||||
"""
|
||||
Iterates over all friendly control points and find the one farthest away from the frontline
|
||||
BUT! prefer Cvs. Everybody likes CVs!
|
||||
"""
|
||||
from_frontline = 0
|
||||
|
||||
for c in self.game.theater.controlpoints:
|
||||
if c.is_carrier and c.is_friendly(self.is_player):
|
||||
return c
|
||||
if c.is_friendly(self.is_player) and c.has_frontline:
|
||||
if c.distance_to(self.front_lines().__next__()) > from_frontline:
|
||||
from_frontline = c.distance_to(self.front_lines().__next__())
|
||||
cp = c
|
||||
return cp
|
||||
|
||||
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 +523,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
|
||||
@@ -506,27 +543,47 @@ 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
|
||||
cp = self.objective_finder.farthest_friendly_control_point()
|
||||
yield ProposedMission(
|
||||
cp, [ProposedFlight(FlightType.AEWC, 1, self.MAX_AWEC_RANGE)]
|
||||
)
|
||||
|
||||
# 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),
|
||||
])
|
||||
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),
|
||||
],
|
||||
)
|
||||
|
||||
# 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),
|
||||
ProposedFlight(
|
||||
FlightType.TARCAP, 2, self.MAX_CAP_RANGE, EscortType.AirToAir
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
def propose_missions(self) -> Iterator[ProposedMission]:
|
||||
"""Identifies and iterates over potential mission in priority order."""
|
||||
@@ -537,30 +594,46 @@ class CoalitionMissionPlanner:
|
||||
# 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),
|
||||
])
|
||||
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 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, 2, self.MAX_OCA_RANGE, EscortType.Sead
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
for target in self.objective_finder.oca_targets(min_aircraft=20):
|
||||
flights = [
|
||||
@@ -569,27 +642,37 @@ 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, 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, 2, self.MAX_STRIKE_RANGE, EscortType.Sead
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
def plan_missions(self) -> None:
|
||||
"""Identifies and plans mission for the turn."""
|
||||
@@ -604,19 +687,23 @@ class CoalitionMissionPlanner:
|
||||
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,26 +713,28 @@ 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):
|
||||
@@ -654,8 +743,7 @@ class CoalitionMissionPlanner:
|
||||
threats[EscortType.Sead] = True
|
||||
return threats
|
||||
|
||||
def plan_mission(self, mission: ProposedMission,
|
||||
reserves: bool = False) -> None:
|
||||
def plan_mission(self, mission: ProposedMission, reserves: bool = False) -> None:
|
||||
"""Allocates aircraft for a proposed mission and adds it to the ATO."""
|
||||
|
||||
if self.is_player:
|
||||
@@ -669,7 +757,7 @@ class CoalitionMissionPlanner:
|
||||
self.game.aircraft_inventory,
|
||||
self.is_player,
|
||||
package_country,
|
||||
self.game.settings.default_start_type
|
||||
self.game.settings.default_start_type,
|
||||
)
|
||||
|
||||
# Attempt to plan all the main elements of the mission first. Escorts
|
||||
@@ -683,12 +771,12 @@ class CoalitionMissionPlanner:
|
||||
# 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)
|
||||
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
|
||||
|
||||
# Create flight plans for the main flights of the package so we can
|
||||
@@ -697,8 +785,9 @@ 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)
|
||||
|
||||
@@ -708,14 +797,14 @@ 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)
|
||||
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:
|
||||
@@ -732,8 +821,9 @@ class CoalitionMissionPlanner:
|
||||
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)
|
||||
@@ -744,17 +834,13 @@ class CoalitionMissionPlanner:
|
||||
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
|
||||
count=len(non_dca_packages), earliest=5, latest=90, margin=5
|
||||
)
|
||||
for package in self.ato.packages:
|
||||
tot = TotEstimator(package).earliest_tot()
|
||||
@@ -771,8 +857,7 @@ 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
|
||||
else:
|
||||
@@ -791,8 +876,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}")
|
||||
|
||||
@@ -12,8 +12,8 @@ from dcs.helicopters import (
|
||||
OH_58D,
|
||||
SA342L,
|
||||
SA342M,
|
||||
SH_60B,
|
||||
UH_1H,
|
||||
SH_60B
|
||||
)
|
||||
from dcs.planes import (
|
||||
AJS37,
|
||||
@@ -22,11 +22,14 @@ from dcs.planes import (
|
||||
A_10C,
|
||||
A_10C_2,
|
||||
A_20G,
|
||||
A_50,
|
||||
B_17G,
|
||||
B_1B,
|
||||
B_52H,
|
||||
Bf_109K_4,
|
||||
C_101CC,
|
||||
E_2C,
|
||||
E_3A,
|
||||
FA_18C_hornet,
|
||||
FW_190A8,
|
||||
FW_190D9,
|
||||
@@ -40,9 +43,11 @@ from dcs.planes import (
|
||||
F_4E,
|
||||
F_5E_3,
|
||||
F_86F_Sabre,
|
||||
I_16,
|
||||
JF_17,
|
||||
J_11A,
|
||||
Ju_88A4,
|
||||
KJ_2000,
|
||||
L_39ZA,
|
||||
MQ_9_Reaper,
|
||||
M_2000C,
|
||||
@@ -54,7 +59,6 @@ from dcs.planes import (
|
||||
MiG_27K,
|
||||
MiG_29A,
|
||||
MiG_29G,
|
||||
MiG_29K,
|
||||
MiG_29S,
|
||||
MiG_31,
|
||||
Mirage_2000_5,
|
||||
@@ -83,18 +87,16 @@ from dcs.planes import (
|
||||
Tu_22M3,
|
||||
Tu_95MS,
|
||||
WingLoong_I,
|
||||
I_16
|
||||
I_16,
|
||||
)
|
||||
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.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,14 +113,12 @@ 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,
|
||||
@@ -155,8 +155,8 @@ CAP_CAPABLE = [
|
||||
# Used for CAS (Close air support) and BAI (Battlefield Interdiction)
|
||||
CAS_CAPABLE = [
|
||||
A_10C_2,
|
||||
A_10C,
|
||||
B_1B,
|
||||
A_10C,
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
Su_25TM,
|
||||
@@ -165,8 +165,6 @@ CAS_CAPABLE = [
|
||||
F_15E,
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
Rafale_A_S,
|
||||
Rafale_B,
|
||||
Tornado_GR4,
|
||||
Tornado_IDS,
|
||||
JF_17,
|
||||
@@ -180,6 +178,7 @@ CAS_CAPABLE = [
|
||||
S_3B,
|
||||
Su_34,
|
||||
Su_30,
|
||||
MiG_19P,
|
||||
MiG_29S,
|
||||
MiG_27K,
|
||||
MiG_29A,
|
||||
@@ -227,8 +226,6 @@ SEAD_CAPABLE = [
|
||||
Tornado_IDS,
|
||||
Su_25T,
|
||||
Su_25TM,
|
||||
Rafale_A_S,
|
||||
Rafale_B,
|
||||
F_4E,
|
||||
A_4E_C,
|
||||
AV8BNA,
|
||||
@@ -276,8 +273,6 @@ STRIKE_CAPABLE = [
|
||||
Tu_22M3,
|
||||
F_15E,
|
||||
AJS37,
|
||||
Rafale_A_S,
|
||||
Rafale_B,
|
||||
Tornado_GR4,
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
@@ -296,7 +291,6 @@ STRIKE_CAPABLE = [
|
||||
Su_30,
|
||||
Su_27,
|
||||
MiG_29S,
|
||||
MiG_29K,
|
||||
MiG_29G,
|
||||
MiG_29A,
|
||||
JF_17,
|
||||
@@ -333,8 +327,6 @@ ANTISHIP_CAPABLE = [
|
||||
AJS37,
|
||||
Tu_22M3,
|
||||
FA_18C_hornet,
|
||||
Rafale_A_S,
|
||||
Rafale_B,
|
||||
Su_24M,
|
||||
Su_17M4,
|
||||
JF_17,
|
||||
@@ -367,10 +359,13 @@ TRANSPORT_CAPABLE = [
|
||||
UH_1H,
|
||||
]
|
||||
|
||||
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,
|
||||
]
|
||||
|
||||
|
||||
@@ -396,6 +391,8 @@ 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
|
||||
else:
|
||||
logging.error(f"Unplannable flight type: {task}")
|
||||
return []
|
||||
|
||||
@@ -12,8 +12,9 @@ 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.
|
||||
@@ -52,9 +53,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(
|
||||
|
||||
@@ -20,6 +20,15 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class FlightType(Enum):
|
||||
"""Enumeration of mission types.
|
||||
|
||||
The value of each enumeration is the name that will be shown in the UI.
|
||||
|
||||
These values are persisted to the save game as well since they are a part of
|
||||
each flight and thus a part of the ATO, so changing these values will break
|
||||
save compat.
|
||||
"""
|
||||
|
||||
TARCAP = "TARCAP"
|
||||
BARCAP = "BARCAP"
|
||||
CAS = "CAS"
|
||||
@@ -33,28 +42,29 @@ class FlightType(Enum):
|
||||
SWEEP = "Fighter sweep"
|
||||
OCA_RUNWAY = "OCA/Runway"
|
||||
OCA_AIRCRAFT = "OCA/Aircraft"
|
||||
AEWC = "AEW&C"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
|
||||
|
||||
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)
|
||||
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)
|
||||
JOIN = 16
|
||||
SPLIT = 17
|
||||
LOITER = 18
|
||||
@@ -68,9 +78,13 @@ class FlightWaypointType(Enum):
|
||||
|
||||
|
||||
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:
|
||||
@@ -108,10 +122,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.
|
||||
@@ -135,12 +152,19 @@ class FlightWaypoint:
|
||||
|
||||
|
||||
class Flight:
|
||||
|
||||
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:
|
||||
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:
|
||||
self.package = package
|
||||
self.country = country
|
||||
self.unit_type = unit_type
|
||||
@@ -161,10 +185,9 @@ class Flight:
|
||||
# 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
|
||||
@@ -182,7 +205,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
@@ -23,7 +23,6 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class GroundSpeed:
|
||||
|
||||
@classmethod
|
||||
def for_flight(cls, flight: Flight, altitude: Distance) -> Speed:
|
||||
if not issubclass(flight.unit_type, FlyingType):
|
||||
@@ -55,13 +54,11 @@ class TravelTime:
|
||||
def between_points(a: Point, b: Point, speed: Speed) -> timedelta:
|
||||
error_factor = 1.1
|
||||
distance = meters(a.distance_to_point(b))
|
||||
return timedelta(
|
||||
hours=distance.nautical_miles / speed.knots * error_factor)
|
||||
return timedelta(hours=distance.nautical_miles / speed.knots * error_factor)
|
||||
|
||||
|
||||
# TODO: Most if not all of this should move into FlightPlan.
|
||||
class TotEstimator:
|
||||
|
||||
def __init__(self, package: Package) -> None:
|
||||
self.package = package
|
||||
|
||||
@@ -75,9 +72,9 @@ class TotEstimator:
|
||||
return startup_time
|
||||
|
||||
def earliest_tot(self) -> timedelta:
|
||||
earliest_tot = max((
|
||||
self.earliest_tot_for_flight(f) for f in self.package.flights
|
||||
))
|
||||
earliest_tot = max(
|
||||
(self.earliest_tot_for_flight(f) for f in self.package.flights)
|
||||
)
|
||||
|
||||
# Trim microseconds. DCS doesn't handle sub-second resolution for tasks,
|
||||
# and they're not interesting from a mission planning perspective so we
|
||||
|
||||
@@ -14,7 +14,7 @@ from typing import (
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.unit import Unit
|
||||
from dcs.unitgroup import VehicleGroup
|
||||
from dcs.unitgroup import Group, VehicleGroup
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
@@ -32,12 +32,17 @@ from .flight import Flight, FlightWaypoint, FlightWaypointType
|
||||
@dataclass(frozen=True)
|
||||
class StrikeTarget:
|
||||
name: str
|
||||
target: Union[VehicleGroup, TheaterGroundObject, Unit]
|
||||
target: Union[VehicleGroup, TheaterGroundObject, Unit, Group]
|
||||
|
||||
|
||||
class WaypointBuilder:
|
||||
def __init__(self, flight: Flight, game: Game, player: bool,
|
||||
targets: Optional[List[StrikeTarget]] = None) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
flight: Flight,
|
||||
game: Game,
|
||||
player: bool,
|
||||
targets: Optional[List[StrikeTarget]] = None,
|
||||
) -> None:
|
||||
self.flight = flight
|
||||
self.conditions = game.conditions
|
||||
self.doctrine = game.faction_for(player).doctrine
|
||||
@@ -65,9 +70,7 @@ class WaypointBuilder:
|
||||
FlightWaypointType.NAV,
|
||||
position.x,
|
||||
position.y,
|
||||
meters(
|
||||
500
|
||||
) if self.is_helo else self.doctrine.rendezvous_altitude
|
||||
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
|
||||
)
|
||||
waypoint.name = "NAV"
|
||||
waypoint.alt_type = "BARO"
|
||||
@@ -75,10 +78,7 @@ class WaypointBuilder:
|
||||
waypoint.pretty_name = "Enter theater"
|
||||
else:
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.TAKEOFF,
|
||||
position.x,
|
||||
position.y,
|
||||
meters(0)
|
||||
FlightWaypointType.TAKEOFF, position.x, position.y, meters(0)
|
||||
)
|
||||
waypoint.name = "TAKEOFF"
|
||||
waypoint.alt_type = "RADIO"
|
||||
@@ -98,9 +98,7 @@ class WaypointBuilder:
|
||||
FlightWaypointType.NAV,
|
||||
position.x,
|
||||
position.y,
|
||||
meters(
|
||||
500
|
||||
) if self.is_helo else self.doctrine.rendezvous_altitude
|
||||
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
|
||||
)
|
||||
waypoint.name = "NAV"
|
||||
waypoint.alt_type = "BARO"
|
||||
@@ -108,10 +106,7 @@ class WaypointBuilder:
|
||||
waypoint.pretty_name = "Exit theater"
|
||||
else:
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.LANDING_POINT,
|
||||
position.x,
|
||||
position.y,
|
||||
meters(0)
|
||||
FlightWaypointType.LANDING_POINT, position.x, position.y, meters(0)
|
||||
)
|
||||
waypoint.name = "LANDING"
|
||||
waypoint.alt_type = "RADIO"
|
||||
@@ -119,8 +114,7 @@ class WaypointBuilder:
|
||||
waypoint.pretty_name = "Land"
|
||||
return waypoint
|
||||
|
||||
def divert(self,
|
||||
divert: Optional[ControlPoint]) -> Optional[FlightWaypoint]:
|
||||
def divert(self, divert: Optional[ControlPoint]) -> Optional[FlightWaypoint]:
|
||||
"""Create divert waypoint for the given arrival airfield or carrier.
|
||||
|
||||
Args:
|
||||
@@ -141,10 +135,7 @@ class WaypointBuilder:
|
||||
altitude_type = "RADIO"
|
||||
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.DIVERT,
|
||||
position.x,
|
||||
position.y,
|
||||
altitude
|
||||
FlightWaypointType.DIVERT, position.x, position.y, altitude
|
||||
)
|
||||
waypoint.alt_type = altitude_type
|
||||
waypoint.name = "DIVERT"
|
||||
@@ -158,9 +149,7 @@ class WaypointBuilder:
|
||||
FlightWaypointType.LOITER,
|
||||
position.x,
|
||||
position.y,
|
||||
meters(
|
||||
500
|
||||
) if self.is_helo else self.doctrine.rendezvous_altitude
|
||||
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
|
||||
)
|
||||
waypoint.pretty_name = "Hold"
|
||||
waypoint.description = "Wait until push time"
|
||||
@@ -172,10 +161,10 @@ class WaypointBuilder:
|
||||
FlightWaypointType.JOIN,
|
||||
position.x,
|
||||
position.y,
|
||||
meters(
|
||||
500
|
||||
) if self.is_helo else self.doctrine.ingress_altitude
|
||||
meters(80) if self.is_helo else self.doctrine.ingress_altitude,
|
||||
)
|
||||
if self.is_helo:
|
||||
waypoint.alt_type = "RADIO"
|
||||
waypoint.pretty_name = "Join"
|
||||
waypoint.description = "Rendezvous with package"
|
||||
waypoint.name = "JOIN"
|
||||
@@ -186,25 +175,29 @@ class WaypointBuilder:
|
||||
FlightWaypointType.SPLIT,
|
||||
position.x,
|
||||
position.y,
|
||||
meters(
|
||||
500
|
||||
) if self.is_helo else self.doctrine.ingress_altitude
|
||||
meters(80) if self.is_helo else self.doctrine.ingress_altitude,
|
||||
)
|
||||
if self.is_helo:
|
||||
waypoint.alt_type = "RADIO"
|
||||
waypoint.pretty_name = "Split"
|
||||
waypoint.description = "Depart from package"
|
||||
waypoint.name = "SPLIT"
|
||||
return waypoint
|
||||
|
||||
def ingress(self, ingress_type: FlightWaypointType, position: Point,
|
||||
objective: MissionTarget) -> FlightWaypoint:
|
||||
def ingress(
|
||||
self,
|
||||
ingress_type: FlightWaypointType,
|
||||
position: Point,
|
||||
objective: MissionTarget,
|
||||
) -> FlightWaypoint:
|
||||
waypoint = FlightWaypoint(
|
||||
ingress_type,
|
||||
position.x,
|
||||
position.y,
|
||||
meters(
|
||||
500
|
||||
) if self.is_helo else self.doctrine.ingress_altitude
|
||||
meters(50) if self.is_helo else self.doctrine.ingress_altitude,
|
||||
)
|
||||
if self.is_helo:
|
||||
waypoint.alt_type = "RADIO"
|
||||
waypoint.pretty_name = "INGRESS on " + objective.name
|
||||
waypoint.description = "INGRESS on " + objective.name
|
||||
waypoint.name = "INGRESS"
|
||||
@@ -217,10 +210,10 @@ class WaypointBuilder:
|
||||
FlightWaypointType.EGRESS,
|
||||
position.x,
|
||||
position.y,
|
||||
meters(
|
||||
500
|
||||
) if self.is_helo else self.doctrine.ingress_altitude
|
||||
meters(50) if self.is_helo else self.doctrine.ingress_altitude,
|
||||
)
|
||||
if self.is_helo:
|
||||
waypoint.alt_type = "RADIO"
|
||||
waypoint.pretty_name = "EGRESS from " + target.name
|
||||
waypoint.description = "EGRESS from " + target.name
|
||||
waypoint.name = "EGRESS"
|
||||
@@ -244,7 +237,7 @@ class WaypointBuilder:
|
||||
FlightWaypointType.TARGET_POINT,
|
||||
target.target.position.x,
|
||||
target.target.position.y,
|
||||
meters(0)
|
||||
meters(0),
|
||||
)
|
||||
waypoint.description = description
|
||||
waypoint.pretty_name = description
|
||||
@@ -269,13 +262,14 @@ class WaypointBuilder:
|
||||
return self._target_area(f"ATTACK {target.name}", target, flyover=True)
|
||||
|
||||
@staticmethod
|
||||
def _target_area(name: str, location: MissionTarget,
|
||||
flyover: bool = False) -> FlightWaypoint:
|
||||
def _target_area(
|
||||
name: str, location: MissionTarget, flyover: bool = False
|
||||
) -> FlightWaypoint:
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
location.position.x,
|
||||
location.position.y,
|
||||
meters(0)
|
||||
meters(0),
|
||||
)
|
||||
waypoint.description = name
|
||||
waypoint.pretty_name = name
|
||||
@@ -300,7 +294,7 @@ class WaypointBuilder:
|
||||
FlightWaypointType.CAS,
|
||||
position.x,
|
||||
position.y,
|
||||
meters(500) if self.is_helo else meters(1000)
|
||||
meters(50) if self.is_helo else meters(1000),
|
||||
)
|
||||
waypoint.alt_type = "RADIO"
|
||||
waypoint.description = "Provide CAS"
|
||||
@@ -317,10 +311,7 @@ class WaypointBuilder:
|
||||
altitude: Altitude of the racetrack.
|
||||
"""
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.PATROL_TRACK,
|
||||
position.x,
|
||||
position.y,
|
||||
altitude
|
||||
FlightWaypointType.PATROL_TRACK, position.x, position.y, altitude
|
||||
)
|
||||
waypoint.name = "RACETRACK START"
|
||||
waypoint.description = "Orbit between this point and the next point"
|
||||
@@ -336,18 +327,16 @@ class WaypointBuilder:
|
||||
altitude: Altitude of the racetrack.
|
||||
"""
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.PATROL,
|
||||
position.x,
|
||||
position.y,
|
||||
altitude
|
||||
FlightWaypointType.PATROL, position.x, position.y, altitude
|
||||
)
|
||||
waypoint.name = "RACETRACK END"
|
||||
waypoint.description = "Orbit between this point and the previous point"
|
||||
waypoint.pretty_name = "Race-track end"
|
||||
return waypoint
|
||||
|
||||
def race_track(self, start: Point, end: Point,
|
||||
altitude: Distance) -> Tuple[FlightWaypoint, FlightWaypoint]:
|
||||
def race_track(
|
||||
self, start: Point, end: Point, altitude: Distance
|
||||
) -> Tuple[FlightWaypoint, FlightWaypoint]:
|
||||
"""Creates two waypoint for a racetrack orbit.
|
||||
|
||||
Args:
|
||||
@@ -355,8 +344,25 @@ class WaypointBuilder:
|
||||
end: The ending racetrack waypoint.
|
||||
altitude: The racetrack altitude.
|
||||
"""
|
||||
return (self.race_track_start(start, altitude),
|
||||
self.race_track_end(end, altitude))
|
||||
return (
|
||||
self.race_track_start(start, altitude),
|
||||
self.race_track_end(end, altitude),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def orbit(start: Point, altitude: Distance) -> FlightWaypoint:
|
||||
"""Creates an circular orbit point.
|
||||
|
||||
Args:
|
||||
start: Position of the waypoint.
|
||||
altitude: Altitude of the racetrack.
|
||||
"""
|
||||
|
||||
waypoint = FlightWaypoint(FlightWaypointType.LOITER, start.x, start.y, altitude)
|
||||
waypoint.name = "ORBIT"
|
||||
waypoint.description = "Anchor and hold at this point"
|
||||
waypoint.pretty_name = "Orbit"
|
||||
return waypoint
|
||||
|
||||
@staticmethod
|
||||
def sweep_start(position: Point, altitude: Distance) -> FlightWaypoint:
|
||||
@@ -367,10 +373,7 @@ class WaypointBuilder:
|
||||
altitude: Altitude of the sweep in meters.
|
||||
"""
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.INGRESS_SWEEP,
|
||||
position.x,
|
||||
position.y,
|
||||
altitude
|
||||
FlightWaypointType.INGRESS_SWEEP, position.x, position.y, altitude
|
||||
)
|
||||
waypoint.name = "SWEEP START"
|
||||
waypoint.description = "Proceed to the target and engage enemy aircraft"
|
||||
@@ -386,18 +389,16 @@ class WaypointBuilder:
|
||||
altitude: Altitude of the sweep in meters.
|
||||
"""
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.EGRESS,
|
||||
position.x,
|
||||
position.y,
|
||||
altitude
|
||||
FlightWaypointType.EGRESS, position.x, position.y, altitude
|
||||
)
|
||||
waypoint.name = "SWEEP END"
|
||||
waypoint.description = "End of sweep"
|
||||
waypoint.pretty_name = "Sweep end"
|
||||
return waypoint
|
||||
|
||||
def sweep(self, start: Point, end: Point,
|
||||
altitude: Distance) -> Tuple[FlightWaypoint, FlightWaypoint]:
|
||||
def sweep(
|
||||
self, start: Point, end: Point, altitude: Distance
|
||||
) -> Tuple[FlightWaypoint, FlightWaypoint]:
|
||||
"""Creates two waypoint for a racetrack orbit.
|
||||
|
||||
Args:
|
||||
@@ -405,11 +406,11 @@ class WaypointBuilder:
|
||||
end: The end of the sweep.
|
||||
altitude: The sweep altitude.
|
||||
"""
|
||||
return (self.sweep_start(start, altitude),
|
||||
self.sweep_end(end, altitude))
|
||||
return (self.sweep_start(start, altitude), self.sweep_end(end, altitude))
|
||||
|
||||
def escort(self, ingress: Point, target: MissionTarget, egress: Point) -> \
|
||||
Tuple[FlightWaypoint, FlightWaypoint, FlightWaypoint]:
|
||||
def escort(
|
||||
self, ingress: Point, target: MissionTarget, egress: Point
|
||||
) -> Tuple[FlightWaypoint, FlightWaypoint, FlightWaypoint]:
|
||||
"""Creates the waypoints needed to escort the package.
|
||||
|
||||
Args:
|
||||
@@ -423,17 +424,16 @@ class WaypointBuilder:
|
||||
# description in gen.aircraft.JoinPointBuilder), so instead we give
|
||||
# the escort flights a flight plan including the ingress point, target
|
||||
# area, and egress point.
|
||||
ingress = self.ingress(FlightWaypointType.INGRESS_ESCORT, ingress,
|
||||
target)
|
||||
ingress = self.ingress(FlightWaypointType.INGRESS_ESCORT, ingress, target)
|
||||
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
target.position.x,
|
||||
target.position.y,
|
||||
meters(
|
||||
500
|
||||
) if self.is_helo else self.doctrine.ingress_altitude
|
||||
meters(50) if self.is_helo else self.doctrine.ingress_altitude,
|
||||
)
|
||||
if self.is_helo:
|
||||
waypoint.alt_type = "RADIO"
|
||||
waypoint.name = "TARGET"
|
||||
waypoint.description = "Escort the package"
|
||||
waypoint.pretty_name = "Target area"
|
||||
@@ -450,18 +450,14 @@ class WaypointBuilder:
|
||||
altitude: Altitude of the waypoint.
|
||||
"""
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.NAV,
|
||||
position.x,
|
||||
position.y,
|
||||
altitude
|
||||
FlightWaypointType.NAV, position.x, position.y, altitude
|
||||
)
|
||||
waypoint.name = "NAV"
|
||||
waypoint.description = "NAV"
|
||||
waypoint.pretty_name = "Nav"
|
||||
return waypoint
|
||||
|
||||
def nav_path(self, a: Point, b: Point,
|
||||
altitude: Distance) -> List[FlightWaypoint]:
|
||||
def nav_path(self, a: Point, b: Point, altitude: Distance) -> List[FlightWaypoint]:
|
||||
path = self.clean_nav_points(self.navmesh.shortest_path(a, b))
|
||||
return [self.nav(self.perturb(p), altitude) for p in path]
|
||||
|
||||
@@ -488,10 +484,8 @@ class WaypointBuilder:
|
||||
previous = current
|
||||
current = nxt
|
||||
|
||||
def nav_point_prunable(self, previous: Point, current: Point,
|
||||
nxt: Point) -> bool:
|
||||
previous_threatened = self.threat_zones.path_threatened(previous,
|
||||
current)
|
||||
def nav_point_prunable(self, previous: Point, current: Point, nxt: Point) -> bool:
|
||||
previous_threatened = self.threat_zones.path_threatened(previous, current)
|
||||
next_threatened = self.threat_zones.path_threatened(current, nxt)
|
||||
pruned_threatened = self.threat_zones.path_threatened(previous, nxt)
|
||||
previous_distance = meters(previous.distance_to_point(current))
|
||||
|
||||
@@ -15,11 +15,15 @@ class ForcedOptionsGenerator:
|
||||
self.game = game
|
||||
|
||||
def _set_options_view(self) -> None:
|
||||
self.mission.forced_options.options_view = self.game.settings.map_coalition_visibility
|
||||
self.mission.forced_options.options_view = (
|
||||
self.game.settings.map_coalition_visibility
|
||||
)
|
||||
|
||||
def _set_external_views(self) -> None:
|
||||
if not self.game.settings.external_views_allowed:
|
||||
self.mission.forced_options.external_views = self.game.settings.external_views_allowed
|
||||
self.mission.forced_options.external_views = (
|
||||
self.game.settings.external_views_allowed
|
||||
)
|
||||
|
||||
def _set_labels(self) -> None:
|
||||
# TODO: Fix settings to use the real type.
|
||||
|
||||
@@ -39,12 +39,11 @@ GROUP_SIZES_BY_COMBAT_STANCE = {
|
||||
CombatStance.RETREAT: [2, 4, 6, 8],
|
||||
CombatStance.BREAKTHROUGH: [4, 6, 6, 8],
|
||||
CombatStance.ELIMINATION: [2, 4, 4, 4, 6],
|
||||
CombatStance.AMBUSH: [1, 1, 2, 2, 2, 2, 4]
|
||||
CombatStance.AMBUSH: [1, 1, 2, 2, 2, 2, 4],
|
||||
}
|
||||
|
||||
|
||||
class CombatGroup:
|
||||
|
||||
def __init__(self, role: CombatGroupRole):
|
||||
self.units: List[VehicleType] = []
|
||||
self.role = role
|
||||
@@ -60,11 +59,12 @@ class CombatGroup:
|
||||
|
||||
|
||||
class GroundPlanner:
|
||||
|
||||
def __init__(self, cp:ControlPoint, game):
|
||||
def __init__(self, cp: ControlPoint, game):
|
||||
self.cp = cp
|
||||
self.game = game
|
||||
self.connected_enemy_cp = [cp for cp in self.cp.connected_points if cp.captured != self.cp.captured]
|
||||
self.connected_enemy_cp = [
|
||||
cp for cp in self.cp.connected_points if cp.captured != self.cp.captured
|
||||
]
|
||||
self.tank_groups: List[CombatGroup] = []
|
||||
self.apc_group: List[CombatGroup] = []
|
||||
self.ifv_group: List[CombatGroup] = []
|
||||
@@ -80,7 +80,7 @@ class GroundPlanner:
|
||||
|
||||
def plan_groundwar(self):
|
||||
|
||||
if hasattr(self.cp, 'stance'):
|
||||
if hasattr(self.cp, "stance"):
|
||||
group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[self.cp.stance]
|
||||
else:
|
||||
self.cp.stance = CombatStance.DEFENSIVE
|
||||
@@ -152,12 +152,3 @@ class GroundPlanner:
|
||||
print("For : #" + str(key))
|
||||
for group in self.units_per_cp[key]:
|
||||
print(str(group))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -14,51 +14,44 @@ TYPE_TANKS = [
|
||||
Armor.MBT_Challenger_II,
|
||||
Armor.MBT_M1A2_Abrams,
|
||||
Armor.MBT_M60A3_Patton,
|
||||
Armor.MBT_Merkava_Mk__4,
|
||||
Armor.MBT_Merkava_IV,
|
||||
Armor.ZTZ_96B,
|
||||
|
||||
# WW2
|
||||
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
|
||||
Armor.MT_Pz_Kpfw_IV_Ausf_H,
|
||||
Armor.MT_PzIV_H,
|
||||
Armor.HT_Pz_Kpfw_VI_Tiger_I,
|
||||
Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II,
|
||||
Armor.MT_M4_Sherman,
|
||||
Armor.MT_M4A4_Sherman_Firefly,
|
||||
Armor.StuG_IV,
|
||||
Armor.SPG_StuG_IV,
|
||||
Armor.CT_Centaur_IV,
|
||||
Armor.CT_Cromwell_IV,
|
||||
Armor.HIT_Churchill_VII,
|
||||
Armor.LT_Mk_VII_Tetrarch,
|
||||
|
||||
# Mods
|
||||
frenchpack.DIM__TOYOTA_BLUE,
|
||||
frenchpack.DIM__TOYOTA_GREEN,
|
||||
frenchpack.DIM__TOYOTA_DESERT,
|
||||
frenchpack.DIM__KAMIKAZE,
|
||||
|
||||
frenchpack.AMX_10RCR,
|
||||
frenchpack.AMX_10RCR_SEPAR,
|
||||
frenchpack.AMX_30B2,
|
||||
frenchpack.Leclerc_Serie_XXI,
|
||||
|
||||
]
|
||||
|
||||
TYPE_ATGM = [
|
||||
Armor.ATGM_M1045_HMMWV_TOW,
|
||||
Armor.ATGM_M1134_Stryker,
|
||||
Armor.ATGM_HMMWV,
|
||||
Armor.ATGM_Stryker,
|
||||
Armor.IFV_BMP_2,
|
||||
|
||||
# WW2 (Tank Destroyers)
|
||||
Armor.M30_Cargo_Carrier,
|
||||
Armor.TD_Jagdpanzer_IV,
|
||||
Armor.TD_Jagdpanther_G1,
|
||||
Armor.TD_M10_GMC,
|
||||
|
||||
Unarmed.Carrier_M30_Cargo,
|
||||
Armor.SPG_Jagdpanzer_IV,
|
||||
Armor.SPG_Jagdpanther_G1,
|
||||
Armor.SPG_M10_GMC,
|
||||
# Mods
|
||||
frenchpack.VBAE_CRAB_MMP,
|
||||
frenchpack.VAB_MEPHISTO,
|
||||
frenchpack.TRM_2000_PAMELA,
|
||||
|
||||
]
|
||||
|
||||
TYPE_IFV = [
|
||||
@@ -66,134 +59,124 @@ TYPE_IFV = [
|
||||
Armor.IFV_BMP_2,
|
||||
Armor.IFV_BMP_1,
|
||||
Armor.IFV_Marder,
|
||||
Armor.IFV_MCV_80,
|
||||
Armor.IFV_Warrior,
|
||||
Armor.IFV_LAV_25,
|
||||
Armor.SPG_M1128_Stryker_MGS,
|
||||
Armor.AC_Sd_Kfz_234_2_Puma,
|
||||
Armor.SPG_Stryker_MGS,
|
||||
Armor.IFV_Sd_Kfz_234_2_Puma,
|
||||
Armor.IFV_M2A2_Bradley,
|
||||
Armor.IFV_BMD_1,
|
||||
Armor.ZBD_04A,
|
||||
|
||||
# WW2
|
||||
Armor.AC_Sd_Kfz_234_2_Puma,
|
||||
Armor.LAC_M8_Greyhound,
|
||||
Armor.Daimler_Armoured_Car,
|
||||
|
||||
Armor.IFV_Sd_Kfz_234_2_Puma,
|
||||
Armor.Car_M8_Greyhound_Armored,
|
||||
Armor.Car_Daimler_Armored,
|
||||
# Mods
|
||||
frenchpack.ERC_90,
|
||||
frenchpack.VBAE_CRAB,
|
||||
frenchpack.VAB_T20_13
|
||||
|
||||
frenchpack.VAB_T20_13,
|
||||
]
|
||||
|
||||
TYPE_APC = [
|
||||
Armor.APC_M1043_HMMWV_Armament,
|
||||
Armor.APC_M1126_Stryker_ICV,
|
||||
Armor.APC_HMMWV__Scout,
|
||||
Armor.IFV_M1126_Stryker_ICV,
|
||||
Armor.APC_M113,
|
||||
Armor.APC_BTR_80,
|
||||
Armor.APC_BTR_82A,
|
||||
Armor.APC_MTLB,
|
||||
Armor.APC_M2A1,
|
||||
Armor.APC_Cobra,
|
||||
Armor.APC_Sd_Kfz_251,
|
||||
Armor.APC_AAV_7,
|
||||
Armor.TPz_Fuchs,
|
||||
Armor.ARV_BRDM_2,
|
||||
Armor.ARV_BTR_RD,
|
||||
Armor.FDDM_Grad,
|
||||
|
||||
Armor.APC_M2A1_Halftrack,
|
||||
Armor.APC_Cobra__Scout,
|
||||
Armor.APC_Sd_Kfz_251_Halftrack,
|
||||
Armor.APC_AAV_7_Amphibious,
|
||||
Armor.APC_TPz_Fuchs,
|
||||
Armor.IFV_BRDM_2,
|
||||
Armor.APC_BTR_RD,
|
||||
Artillery.Grad_MRL_FDDM__FC,
|
||||
# WW2
|
||||
Armor.APC_M2A1,
|
||||
Armor.APC_Sd_Kfz_251,
|
||||
|
||||
Armor.APC_M2A1_Halftrack,
|
||||
Armor.APC_Sd_Kfz_251_Halftrack,
|
||||
# Mods
|
||||
frenchpack.VAB__50,
|
||||
frenchpack.VBL__50,
|
||||
frenchpack.VBL_AANF1,
|
||||
|
||||
]
|
||||
|
||||
TYPE_ARTILLERY = [
|
||||
Artillery.MLRS_9A52_Smerch,
|
||||
Artillery.SPH_2S1_Gvozdika,
|
||||
Artillery.SPH_2S3_Akatsia,
|
||||
Artillery.MLRS_BM_21_Grad,
|
||||
Artillery.MLRS_9K57_Uragan_BM_27,
|
||||
Artillery.SPH_M109_Paladin,
|
||||
Artillery.MLRS_M270,
|
||||
Artillery.SPH_2S9_Nona,
|
||||
Artillery.SpGH_Dana,
|
||||
Artillery.SPH_2S19_Msta,
|
||||
Artillery.MLRS_FDDM,
|
||||
|
||||
Artillery.MLRS_9A52_Smerch_HE_300mm,
|
||||
Artillery.SPH_2S1_Gvozdika_122mm,
|
||||
Artillery.SPH_2S3_Akatsia_152mm,
|
||||
Artillery.MLRS_BM_21_Grad_122mm,
|
||||
Artillery.MLRS_BM_27_Uragan_220mm,
|
||||
Artillery.SPH_M109_Paladin_155mm,
|
||||
Artillery.MLRS_M270_227mm,
|
||||
Artillery.SPH_2S9_Nona_120mm_M,
|
||||
Artillery.SPH_Dana_vz77_152mm,
|
||||
Artillery.PLZ_05,
|
||||
Artillery.SPH_2S19_Msta_152mm,
|
||||
Artillery.MLRS_9A52_Smerch_CM_300mm,
|
||||
# WW2
|
||||
Artillery.Sturmpanzer_IV_Brummbär,
|
||||
Artillery.M12_GMC
|
||||
Artillery.SPG_Sturmpanzer_IV_Brummbar,
|
||||
Artillery.SPG_M12_GMC_155mm,
|
||||
]
|
||||
|
||||
TYPE_LOGI = [
|
||||
Unarmed.Transport_M818,
|
||||
Unarmed.Transport_KAMAZ_43101,
|
||||
Unarmed.Transport_Ural_375,
|
||||
Unarmed.Transport_GAZ_66,
|
||||
Unarmed.Transport_GAZ_3307,
|
||||
Unarmed.Transport_GAZ_3308,
|
||||
Unarmed.Transport_Ural_4320_31_Armored,
|
||||
Unarmed.Transport_Ural_4320T,
|
||||
Unarmed.Blitz_3_6_6700A,
|
||||
Unarmed.Kübelwagen_82,
|
||||
Unarmed.Sd_Kfz_7,
|
||||
Unarmed.Sd_Kfz_2,
|
||||
Unarmed.Willys_MB,
|
||||
Unarmed.Land_Rover_109_S3,
|
||||
Unarmed.Land_Rover_101_FC,
|
||||
|
||||
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,
|
||||
|
||||
]
|
||||
|
||||
TYPE_INFANTRY = [
|
||||
Infantry.Infantry_Soldier_Insurgents,
|
||||
Infantry.Soldier_AK,
|
||||
Infantry.Insurgent_AK_74,
|
||||
Infantry.Infantry_AK_74,
|
||||
Infantry.Infantry_M1_Garand,
|
||||
Infantry.Infantry_Mauser_98,
|
||||
Infantry.Infantry_SMLE_No_4_Mk_1,
|
||||
Infantry.Georgian_soldier_with_M4,
|
||||
Infantry.Infantry_Soldier_Rus,
|
||||
Infantry.Infantry_M4_Georgia,
|
||||
Infantry.Infantry_AK_74_Rus,
|
||||
Infantry.Paratrooper_AKS,
|
||||
Infantry.Paratrooper_RPG_16,
|
||||
Infantry.Soldier_M249,
|
||||
Infantry.Infantry_M249,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Soldier_RPG,
|
||||
Infantry.Infantry_RPG,
|
||||
]
|
||||
|
||||
TYPE_SHORAD = [
|
||||
AirDefence.AAA_ZU_23_on_Ural_375,
|
||||
AirDefence.AAA_ZU_23_Insurgent_on_Ural_375,
|
||||
AirDefence.AAA_ZSU_57_2,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka,
|
||||
AirDefence.SAM_SA_8_Osa_9A33,
|
||||
AirDefence.SAM_SA_9_Strela_1_9P31,
|
||||
AirDefence.SAM_SA_13_Strela_10M3_9A35M3,
|
||||
AirDefence.SAM_SA_15_Tor_9A331,
|
||||
AirDefence.SAM_SA_19_Tunguska_2S6,
|
||||
|
||||
AirDefence.SPAAA_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.AAA_Vulcan_M163,
|
||||
AirDefence.SAM_Linebacker_M6,
|
||||
AirDefence.SPAAA_Vulcan_M163,
|
||||
AirDefence.SAM_Linebacker___Bradley_M6,
|
||||
AirDefence.SAM_Chaparral_M48,
|
||||
AirDefence.SAM_Avenger_M1097,
|
||||
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_40mm_Bofors,
|
||||
AirDefence.AAA_S_60_57mm,
|
||||
AirDefence.AAA_M1_37mm,
|
||||
AirDefence.AA_gun_QF_3_7,
|
||||
|
||||
AirDefence.AAA_QF_3_7,
|
||||
]
|
||||
|
||||
@@ -2,10 +2,11 @@ from enum import Enum
|
||||
|
||||
|
||||
class CombatStance(Enum):
|
||||
DEFENSIVE = 0 # Unit will adopt defensive stance with medium group of units
|
||||
AGGRESSIVE = 1 # Unit will attempt to make progress with medium sized group of units
|
||||
RETREAT = 2 # Unit will retreat
|
||||
BREAKTHROUGH = 3 # Unit will attempt a breakthrough, rushing forward very aggresively with big group of armored units, and even less armored units will move aggresively
|
||||
DEFENSIVE = 0 # Unit will adopt defensive stance with medium group of units
|
||||
AGGRESSIVE = (
|
||||
1 # Unit will attempt to make progress with medium sized group of units
|
||||
)
|
||||
RETREAT = 2 # Unit will retreat
|
||||
BREAKTHROUGH = 3 # Unit will attempt a breakthrough, rushing forward very aggresively with big group of armored units, and even less armored units will move aggresively
|
||||
ELIMINATION = 4 # Unit will progress aggresively toward anemy units, attempting to eliminate the ennemy force
|
||||
AMBUSH = 5 # Units will adopt a defensive stance a bit different from 'DEFENSIVE', ATGM & INFANTRY with RPG will be located on frontline with the armored units. (The groups of units will be smaller)
|
||||
|
||||
AMBUSH = 5 # Units will adopt a defensive stance a bit different from 'DEFENSIVE', ATGM & INFANTRY with RPG will be located on frontline with the armored units. (The groups of units will be smaller)
|
||||
|
||||
@@ -11,16 +11,18 @@ import logging
|
||||
import random
|
||||
from typing import Dict, Iterator, Optional, TYPE_CHECKING, Type, List
|
||||
|
||||
from dcs import Mission, Point
|
||||
from dcs import Mission, Point, unitgroup
|
||||
from dcs.country import Country
|
||||
from dcs.statics import fortification_map, warehouse_map
|
||||
from dcs.point import StaticPoint
|
||||
from dcs.statics import fortification_map, warehouse_map, Warehouse
|
||||
from dcs.task import (
|
||||
ActivateBeaconCommand,
|
||||
ActivateICLSCommand,
|
||||
EPLRS,
|
||||
OptAlarmState, FireAtPoint,
|
||||
OptAlarmState,
|
||||
FireAtPoint,
|
||||
)
|
||||
from dcs.unit import Ship, Unit, Vehicle
|
||||
from dcs.unit import Ship, Unit, Vehicle, SingleHeliPad, Static
|
||||
from dcs.unitgroup import Group, ShipGroup, StaticGroup, VehicleGroup
|
||||
from dcs.unittype import StaticType, UnitType
|
||||
from dcs.vehicles import vehicle_map
|
||||
@@ -30,9 +32,12 @@ from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID
|
||||
from game.db import unit_type_from_name
|
||||
from game.theater import ControlPoint, TheaterGroundObject
|
||||
from game.theater.theatergroundobject import (
|
||||
BuildingGroundObject, CarrierGroundObject,
|
||||
BuildingGroundObject,
|
||||
CarrierGroundObject,
|
||||
GenericCarrierGroundObject,
|
||||
LhaGroundObject, ShipGroundObject, MissileSiteGroundObject,
|
||||
LhaGroundObject,
|
||||
ShipGroundObject,
|
||||
MissileSiteGroundObject,
|
||||
)
|
||||
from game.unitmap import UnitMap
|
||||
from game.utils import knots, mps
|
||||
@@ -43,7 +48,6 @@ from .tacan import TacanBand, TacanChannel, TacanRegistry
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
|
||||
FARP_FRONTLINE_DISTANCE = 10000
|
||||
AA_CP_MIN_DISTANCE = 40000
|
||||
|
||||
@@ -53,8 +57,15 @@ class GenericGroundObjectGenerator:
|
||||
|
||||
Currently used only for SAM
|
||||
"""
|
||||
def __init__(self, ground_object: TheaterGroundObject, country: Country,
|
||||
game: Game, mission: Mission, unit_map: UnitMap) -> None:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ground_object: TheaterGroundObject,
|
||||
country: Country,
|
||||
game: Game,
|
||||
mission: Mission,
|
||||
unit_map: UnitMap,
|
||||
) -> None:
|
||||
self.ground_object = ground_object
|
||||
self.country = country
|
||||
self.game = game
|
||||
@@ -72,18 +83,22 @@ class GenericGroundObjectGenerator:
|
||||
|
||||
unit_type = unit_type_from_name(group.units[0].type)
|
||||
if unit_type is None:
|
||||
raise RuntimeError(
|
||||
f"Unrecognized unit type: {group.units[0].type}")
|
||||
raise RuntimeError(f"Unrecognized unit type: {group.units[0].type}")
|
||||
|
||||
vg = self.m.vehicle_group(self.country, group.name, unit_type,
|
||||
position=group.position,
|
||||
heading=group.units[0].heading)
|
||||
vg = self.m.vehicle_group(
|
||||
self.country,
|
||||
group.name,
|
||||
unit_type,
|
||||
position=group.position,
|
||||
heading=group.units[0].heading,
|
||||
)
|
||||
vg.units[0].name = self.m.string(group.units[0].name)
|
||||
vg.units[0].player_can_drive = True
|
||||
for i, u in enumerate(group.units):
|
||||
if i > 0:
|
||||
vehicle = Vehicle(self.m.next_unit_id(),
|
||||
self.m.string(u.name), u.type)
|
||||
vehicle = Vehicle(
|
||||
self.m.next_unit_id(), self.m.string(u.name), u.type
|
||||
)
|
||||
vehicle.position.x = u.position.x
|
||||
vehicle.position.y = u.position.y
|
||||
vehicle.heading = u.heading
|
||||
@@ -96,7 +111,7 @@ class GenericGroundObjectGenerator:
|
||||
|
||||
@staticmethod
|
||||
def enable_eplrs(group: Group, unit_type: Type[UnitType]) -> None:
|
||||
if hasattr(unit_type, 'eplrs'):
|
||||
if hasattr(unit_type, "eplrs"):
|
||||
if unit_type.eplrs:
|
||||
group.points[0].tasks.append(EPLRS(group.id))
|
||||
|
||||
@@ -106,14 +121,13 @@ class GenericGroundObjectGenerator:
|
||||
else:
|
||||
group.points[0].tasks.append(OptAlarmState(1))
|
||||
|
||||
def _register_unit_group(self, persistence_group: Group,
|
||||
miz_group: Group) -> None:
|
||||
self.unit_map.add_ground_object_units(self.ground_object,
|
||||
persistence_group, miz_group)
|
||||
def _register_unit_group(self, persistence_group: Group, miz_group: Group) -> None:
|
||||
self.unit_map.add_ground_object_units(
|
||||
self.ground_object, persistence_group, miz_group
|
||||
)
|
||||
|
||||
|
||||
class MissileSiteGenerator(GenericGroundObjectGenerator):
|
||||
|
||||
def generate(self) -> None:
|
||||
super(MissileSiteGenerator, self).generate()
|
||||
# Note : Only the SCUD missiles group can fire (V1 site cannot fire in game right now)
|
||||
@@ -125,13 +139,19 @@ class MissileSiteGenerator(GenericGroundObjectGenerator):
|
||||
targets = self.possible_missile_targets(vg)
|
||||
if targets:
|
||||
target = random.choice(targets)
|
||||
real_target = target.point_from_heading(random.randint(0, 360), random.randint(0, 2500))
|
||||
real_target = target.point_from_heading(
|
||||
random.randint(0, 360), random.randint(0, 2500)
|
||||
)
|
||||
vg.points[0].add_task(FireAtPoint(real_target))
|
||||
logging.info("Set up fire task for missile group.")
|
||||
else:
|
||||
logging.info("Couldn't setup missile site to fire, no valid target in range.")
|
||||
logging.info(
|
||||
"Couldn't setup missile site to fire, no valid target in range."
|
||||
)
|
||||
else:
|
||||
logging.info("Couldn't setup missile site to fire, group was not generated.")
|
||||
logging.info(
|
||||
"Couldn't setup missile site to fire, group was not generated."
|
||||
)
|
||||
|
||||
def possible_missile_targets(self, vg: Group) -> List[Point]:
|
||||
"""
|
||||
@@ -190,7 +210,8 @@ class BuildingSiteGenerator(GenericGroundObjectGenerator):
|
||||
break
|
||||
else:
|
||||
logging.error(
|
||||
f"{self.ground_object.dcs_identifier} not found in static maps")
|
||||
f"{self.ground_object.dcs_identifier} not found in static maps"
|
||||
)
|
||||
|
||||
def generate_vehicle_group(self, unit_type: UnitType) -> None:
|
||||
if not self.ground_object.is_dead:
|
||||
@@ -228,11 +249,20 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
|
||||
|
||||
Used by both CV(N) groups and LHA groups.
|
||||
"""
|
||||
def __init__(self, ground_object: GenericCarrierGroundObject,
|
||||
control_point: ControlPoint, country: Country, game: Game,
|
||||
mission: Mission, radio_registry: RadioRegistry,
|
||||
tacan_registry: TacanRegistry, icls_alloc: Iterator[int],
|
||||
runways: Dict[str, RunwayData], unit_map: UnitMap) -> None:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ground_object: GenericCarrierGroundObject,
|
||||
control_point: ControlPoint,
|
||||
country: Country,
|
||||
game: Game,
|
||||
mission: Mission,
|
||||
radio_registry: RadioRegistry,
|
||||
tacan_registry: TacanRegistry,
|
||||
icls_alloc: Iterator[int],
|
||||
runways: Dict[str, RunwayData],
|
||||
unit_map: UnitMap,
|
||||
) -> None:
|
||||
super().__init__(ground_object, country, game, mission, unit_map)
|
||||
self.ground_object = ground_object
|
||||
self.control_point = control_point
|
||||
@@ -245,8 +275,7 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
|
||||
# TODO: Require single group?
|
||||
for group in self.ground_object.groups:
|
||||
if not group.units:
|
||||
logging.warning(
|
||||
f"Found empty carrier group in {self.control_point}")
|
||||
logging.warning(f"Found empty carrier group in {self.control_point}")
|
||||
continue
|
||||
|
||||
atc = self.radio_registry.alloc_uhf()
|
||||
@@ -270,25 +299,29 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
|
||||
def get_carrier_type(self, group: Group) -> Type[UnitType]:
|
||||
unit_type = unit_type_from_name(group.units[0].type)
|
||||
if unit_type is None:
|
||||
raise RuntimeError(
|
||||
f"Unrecognized carrier name: {group.units[0].type}")
|
||||
raise RuntimeError(f"Unrecognized carrier name: {group.units[0].type}")
|
||||
return unit_type
|
||||
|
||||
def configure_carrier(self, group: Group,
|
||||
atc_channel: RadioFrequency) -> ShipGroup:
|
||||
def configure_carrier(self, group: Group, atc_channel: RadioFrequency) -> ShipGroup:
|
||||
unit_type = self.get_carrier_type(group)
|
||||
|
||||
ship_group = self.m.ship_group(self.country, group.name, unit_type,
|
||||
position=group.position,
|
||||
heading=group.units[0].heading)
|
||||
ship_group = self.m.ship_group(
|
||||
self.country,
|
||||
group.name,
|
||||
unit_type,
|
||||
position=group.position,
|
||||
heading=group.units[0].heading,
|
||||
)
|
||||
ship_group.set_frequency(atc_channel.hertz)
|
||||
ship_group.units[0].name = self.m.string(group.units[0].name)
|
||||
return ship_group
|
||||
|
||||
def create_ship(self, unit: Unit, atc_channel: RadioFrequency) -> Ship:
|
||||
ship = Ship(self.m.next_unit_id(),
|
||||
self.m.string(unit.name),
|
||||
unit_type_from_name(unit.type))
|
||||
ship = Ship(
|
||||
self.m.next_unit_id(),
|
||||
self.m.string(unit.name),
|
||||
unit_type_from_name(unit.type),
|
||||
)
|
||||
ship.position.x = unit.position.x
|
||||
ship.position.y = unit.position.y
|
||||
ship.heading = unit.heading
|
||||
@@ -303,7 +336,8 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
|
||||
carrier_speed = knots(25) - mps(wind.speed)
|
||||
for attempt in range(5):
|
||||
point = group.points[0].position.point_from_heading(
|
||||
brc, 100000 - attempt * 20000)
|
||||
brc, 100000 - attempt * 20000
|
||||
)
|
||||
if self.game.theater.is_in_sea(point):
|
||||
group.points[0].speed = carrier_speed.meters_per_second
|
||||
group.add_waypoint(point, carrier_speed.kph)
|
||||
@@ -314,21 +348,30 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def activate_beacons(group: ShipGroup, tacan: TacanChannel,
|
||||
callsign: str, icls: int) -> None:
|
||||
group.points[0].tasks.append(ActivateBeaconCommand(
|
||||
channel=tacan.number,
|
||||
modechannel=tacan.band.value,
|
||||
callsign=callsign,
|
||||
unit_id=group.units[0].id,
|
||||
aa=False
|
||||
))
|
||||
group.points[0].tasks.append(ActivateICLSCommand(
|
||||
icls, unit_id=group.units[0].id
|
||||
))
|
||||
def activate_beacons(
|
||||
group: ShipGroup, tacan: TacanChannel, callsign: str, icls: int
|
||||
) -> None:
|
||||
group.points[0].tasks.append(
|
||||
ActivateBeaconCommand(
|
||||
channel=tacan.number,
|
||||
modechannel=tacan.band.value,
|
||||
callsign=callsign,
|
||||
unit_id=group.units[0].id,
|
||||
aa=False,
|
||||
)
|
||||
)
|
||||
group.points[0].tasks.append(
|
||||
ActivateICLSCommand(icls, unit_id=group.units[0].id)
|
||||
)
|
||||
|
||||
def add_runway_data(self, brc: int, atc: RadioFrequency,
|
||||
tacan: TacanChannel, callsign: str, icls: int) -> None:
|
||||
def add_runway_data(
|
||||
self,
|
||||
brc: int,
|
||||
atc: RadioFrequency,
|
||||
tacan: TacanChannel,
|
||||
callsign: str,
|
||||
icls: int,
|
||||
) -> None:
|
||||
# TODO: Make unit name usable.
|
||||
# This relies on one control point mapping exactly
|
||||
# to one LHA, carrier, or other usable "runway".
|
||||
@@ -354,24 +397,28 @@ class CarrierGenerator(GenericCarrierGenerator):
|
||||
def get_carrier_type(self, group: Group) -> UnitType:
|
||||
unit_type = super().get_carrier_type(group)
|
||||
if self.game.settings.supercarrier:
|
||||
unit_type = db.upgrade_to_supercarrier(unit_type,
|
||||
self.control_point.name)
|
||||
unit_type = db.upgrade_to_supercarrier(unit_type, self.control_point.name)
|
||||
return unit_type
|
||||
|
||||
def tacan_callsign(self) -> str:
|
||||
# TODO: Assign these properly.
|
||||
return random.choice([
|
||||
"STE",
|
||||
"CVN",
|
||||
"CVH",
|
||||
"CCV",
|
||||
"ACC",
|
||||
"ARC",
|
||||
"GER",
|
||||
"ABR",
|
||||
"LIN",
|
||||
"TRU",
|
||||
])
|
||||
if self.control_point.name == "Carrier Strike Group 8":
|
||||
return "TRU"
|
||||
else:
|
||||
return random.choice(
|
||||
[
|
||||
"STE",
|
||||
"CVN",
|
||||
"CVH",
|
||||
"CCV",
|
||||
"ACC",
|
||||
"ARC",
|
||||
"GER",
|
||||
"ABR",
|
||||
"LIN",
|
||||
"TRU",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class LhaGenerator(GenericCarrierGenerator):
|
||||
@@ -379,14 +426,16 @@ class LhaGenerator(GenericCarrierGenerator):
|
||||
|
||||
def tacan_callsign(self) -> str:
|
||||
# TODO: Assign these properly.
|
||||
return random.choice([
|
||||
"LHD",
|
||||
"LHA",
|
||||
"LHB",
|
||||
"LHC",
|
||||
"LHD",
|
||||
"LDS",
|
||||
])
|
||||
return random.choice(
|
||||
[
|
||||
"LHD",
|
||||
"LHA",
|
||||
"LHB",
|
||||
"LHC",
|
||||
"LHD",
|
||||
"LDS",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class ShipObjectGenerator(GenericGroundObjectGenerator):
|
||||
@@ -403,22 +452,23 @@ class ShipObjectGenerator(GenericGroundObjectGenerator):
|
||||
|
||||
unit_type = unit_type_from_name(group.units[0].type)
|
||||
if unit_type is None:
|
||||
raise RuntimeError(
|
||||
f"Unrecognized unit type: {group.units[0].type}")
|
||||
raise RuntimeError(f"Unrecognized unit type: {group.units[0].type}")
|
||||
|
||||
self.generate_group(group, unit_type)
|
||||
|
||||
def generate_group(self, group_def: Group,
|
||||
first_unit_type: Type[UnitType]) -> None:
|
||||
group = self.m.ship_group(self.country, group_def.name, first_unit_type,
|
||||
position=group_def.position,
|
||||
heading=group_def.units[0].heading)
|
||||
def generate_group(self, group_def: Group, first_unit_type: Type[UnitType]) -> None:
|
||||
group = self.m.ship_group(
|
||||
self.country,
|
||||
group_def.name,
|
||||
first_unit_type,
|
||||
position=group_def.position,
|
||||
heading=group_def.units[0].heading,
|
||||
)
|
||||
group.units[0].name = self.m.string(group_def.units[0].name)
|
||||
# TODO: Skipping the first unit looks like copy pasta from the carrier.
|
||||
for unit in group_def.units[1:]:
|
||||
unit_type = unit_type_from_name(unit.type)
|
||||
ship = Ship(self.m.next_unit_id(),
|
||||
self.m.string(unit.name), unit_type)
|
||||
ship = Ship(self.m.next_unit_id(), self.m.string(unit.name), unit_type)
|
||||
ship.position.x = unit.position.x
|
||||
ship.position.y = unit.position.y
|
||||
ship.heading = unit.heading
|
||||
@@ -427,6 +477,48 @@ class ShipObjectGenerator(GenericGroundObjectGenerator):
|
||||
self._register_unit_group(group_def, group)
|
||||
|
||||
|
||||
class HelipadGenerator:
|
||||
"""
|
||||
Generates helipads for given control point
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
mission: Mission,
|
||||
cp: ControlPoint,
|
||||
game: Game,
|
||||
radio_registry: RadioRegistry,
|
||||
tacan_registry: TacanRegistry,
|
||||
):
|
||||
self.m = mission
|
||||
self.cp = cp
|
||||
self.game = game
|
||||
self.radio_registry = radio_registry
|
||||
self.tacan_registry = tacan_registry
|
||||
|
||||
def generate(self) -> None:
|
||||
|
||||
if self.cp.captured:
|
||||
country_name = self.game.player_country
|
||||
else:
|
||||
country_name = self.game.enemy_country
|
||||
country = self.m.country(country_name)
|
||||
|
||||
for i, helipad in enumerate(self.cp.helipads):
|
||||
name = self.cp.name + "_helipad_" + str(i)
|
||||
logging.info("Generating helipad : " + name)
|
||||
pad = SingleHeliPad(name=self.m.string(name + "_unit"))
|
||||
pad.position = Point(helipad.x, helipad.y)
|
||||
pad.heading = helipad.heading
|
||||
# pad.heliport_frequency = self.radio_registry.alloc_uhf() TODO : alloc radio & callsign
|
||||
sg = unitgroup.StaticGroup(self.m.next_group_id(), self.m.string(name))
|
||||
sg.add_unit(pad)
|
||||
sp = StaticPoint()
|
||||
sp.position = pad.position
|
||||
sg.add_point(sp)
|
||||
country.add_static_group(sg)
|
||||
|
||||
|
||||
class GroundObjectsGenerator:
|
||||
"""Creates DCS groups and statics for the theater during mission generation.
|
||||
|
||||
@@ -436,9 +528,14 @@ class GroundObjectsGenerator:
|
||||
the appropriate generators.
|
||||
"""
|
||||
|
||||
def __init__(self, mission: Mission, game: Game,
|
||||
radio_registry: RadioRegistry, tacan_registry: TacanRegistry,
|
||||
unit_map: UnitMap) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
mission: Mission,
|
||||
game: Game,
|
||||
radio_registry: RadioRegistry,
|
||||
tacan_registry: TacanRegistry,
|
||||
unit_map: UnitMap,
|
||||
) -> None:
|
||||
self.m = mission
|
||||
self.game = game
|
||||
self.radio_registry = radio_registry
|
||||
@@ -455,31 +552,51 @@ class GroundObjectsGenerator:
|
||||
country_name = self.game.enemy_country
|
||||
country = self.m.country(country_name)
|
||||
|
||||
HelipadGenerator(
|
||||
self.m, cp, self.game, self.radio_registry, self.tacan_registry
|
||||
).generate()
|
||||
|
||||
for ground_object in cp.ground_objects:
|
||||
if isinstance(ground_object, BuildingGroundObject):
|
||||
generator = BuildingSiteGenerator(
|
||||
ground_object, country, self.game, self.m,
|
||||
self.unit_map)
|
||||
ground_object, country, self.game, self.m, self.unit_map
|
||||
)
|
||||
elif isinstance(ground_object, CarrierGroundObject):
|
||||
generator = CarrierGenerator(
|
||||
ground_object, cp, country, self.game, self.m,
|
||||
self.radio_registry, self.tacan_registry,
|
||||
self.icls_alloc, self.runways, self.unit_map)
|
||||
ground_object,
|
||||
cp,
|
||||
country,
|
||||
self.game,
|
||||
self.m,
|
||||
self.radio_registry,
|
||||
self.tacan_registry,
|
||||
self.icls_alloc,
|
||||
self.runways,
|
||||
self.unit_map,
|
||||
)
|
||||
elif isinstance(ground_object, LhaGroundObject):
|
||||
generator = CarrierGenerator(
|
||||
ground_object, cp, country, self.game, self.m,
|
||||
self.radio_registry, self.tacan_registry,
|
||||
self.icls_alloc, self.runways, self.unit_map)
|
||||
ground_object,
|
||||
cp,
|
||||
country,
|
||||
self.game,
|
||||
self.m,
|
||||
self.radio_registry,
|
||||
self.tacan_registry,
|
||||
self.icls_alloc,
|
||||
self.runways,
|
||||
self.unit_map,
|
||||
)
|
||||
elif isinstance(ground_object, ShipGroundObject):
|
||||
generator = ShipObjectGenerator(
|
||||
ground_object, country, self.game, self.m,
|
||||
self.unit_map)
|
||||
ground_object, country, self.game, self.m, self.unit_map
|
||||
)
|
||||
elif isinstance(ground_object, MissileSiteGroundObject):
|
||||
generator = MissileSiteGenerator(
|
||||
ground_object, country, self.game, self.m,
|
||||
self.unit_map)
|
||||
ground_object, country, self.game, self.m, self.unit_map
|
||||
)
|
||||
else:
|
||||
generator = GenericGroundObjectGenerator(
|
||||
ground_object, country, self.game, self.m,
|
||||
self.unit_map)
|
||||
ground_object, country, self.game, self.m, self.unit_map
|
||||
)
|
||||
generator.generate()
|
||||
|
||||
236
gen/kneeboard.py
236
gen/kneeboard.py
@@ -41,6 +41,7 @@ from .flights.flight import FlightWaypoint, FlightWaypointType
|
||||
from .radios import RadioFrequency
|
||||
from .runways import RunwayData
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
@@ -48,18 +49,33 @@ if TYPE_CHECKING:
|
||||
class KneeboardPageWriter:
|
||||
"""Creates kneeboard images."""
|
||||
|
||||
def __init__(self, page_margin: int = 24, line_spacing: int = 12) -> None:
|
||||
self.image = Image.new('RGB', (768, 1024), (0xff, 0xff, 0xff))
|
||||
def __init__(
|
||||
self, page_margin: int = 24, line_spacing: int = 12, dark_theme: bool = False
|
||||
) -> None:
|
||||
if dark_theme:
|
||||
self.foreground_fill = (215, 200, 200)
|
||||
self.background_fill = (10, 5, 5)
|
||||
else:
|
||||
self.foreground_fill = (15, 15, 15)
|
||||
self.background_fill = (255, 252, 252)
|
||||
self.image = Image.new("RGB", (768, 1024), self.background_fill)
|
||||
# These font sizes create a relatively full page for current sorties. If
|
||||
# we start generating more complicated flight plans, or start including
|
||||
# more information in the comm ladder (the latter of which we should
|
||||
# probably do), we'll need to split some of this information off into a
|
||||
# second page.
|
||||
self.title_font = ImageFont.truetype("arial.ttf", 32)
|
||||
self.heading_font = ImageFont.truetype("arial.ttf", 24)
|
||||
self.content_font = ImageFont.truetype("arial.ttf", 20)
|
||||
self.title_font = ImageFont.truetype(
|
||||
"arial.ttf", 32, layout_engine=ImageFont.LAYOUT_BASIC
|
||||
)
|
||||
self.heading_font = ImageFont.truetype(
|
||||
"arial.ttf", 24, layout_engine=ImageFont.LAYOUT_BASIC
|
||||
)
|
||||
self.content_font = ImageFont.truetype(
|
||||
"arial.ttf", 20, layout_engine=ImageFont.LAYOUT_BASIC
|
||||
)
|
||||
self.table_font = ImageFont.truetype(
|
||||
"resources/fonts/Inconsolata.otf", 20)
|
||||
"resources/fonts/Inconsolata.otf", 20, layout_engine=ImageFont.LAYOUT_BASIC
|
||||
)
|
||||
self.draw = ImageDraw.Draw(self.image)
|
||||
self.x = page_margin
|
||||
self.y = page_margin
|
||||
@@ -69,8 +85,9 @@ class KneeboardPageWriter:
|
||||
def position(self) -> Tuple[int, int]:
|
||||
return self.x, self.y
|
||||
|
||||
def text(self, text: str, font=None,
|
||||
fill: Tuple[int, int, int] = (0, 0, 0)) -> None:
|
||||
def text(
|
||||
self, text: str, font=None, fill: Tuple[int, int, int] = (0, 0, 0)
|
||||
) -> None:
|
||||
if font is None:
|
||||
font = self.content_font
|
||||
|
||||
@@ -79,17 +96,18 @@ class KneeboardPageWriter:
|
||||
self.y += height + self.line_spacing
|
||||
|
||||
def title(self, title: str) -> None:
|
||||
self.text(title, font=self.title_font)
|
||||
self.text(title, font=self.title_font, fill=self.foreground_fill)
|
||||
|
||||
def heading(self, text: str) -> None:
|
||||
self.text(text, font=self.heading_font)
|
||||
self.text(text, font=self.heading_font, fill=self.foreground_fill)
|
||||
|
||||
def table(self, cells: List[List[str]],
|
||||
headers: Optional[List[str]] = None) -> None:
|
||||
def table(
|
||||
self, cells: List[List[str]], headers: Optional[List[str]] = None
|
||||
) -> None:
|
||||
if headers is None:
|
||||
headers = []
|
||||
table = tabulate(cells, headers=headers, numalign="right")
|
||||
self.text(table, font=self.table_font)
|
||||
self.text(table, font=self.table_font, fill=self.foreground_fill)
|
||||
|
||||
def write(self, path: Path) -> None:
|
||||
self.image.save(path)
|
||||
@@ -157,29 +175,34 @@ class FlightPlanBuilder:
|
||||
first_waypoint_num = self.target_points[0].number
|
||||
last_waypoint_num = self.target_points[-1].number
|
||||
|
||||
self.rows.append([
|
||||
f"{first_waypoint_num}-{last_waypoint_num}",
|
||||
"Target points",
|
||||
"0",
|
||||
self._waypoint_distance(self.target_points[0].waypoint),
|
||||
self._ground_speed(self.target_points[0].waypoint),
|
||||
self._format_time(self.target_points[0].waypoint.tot),
|
||||
self._format_time(self.target_points[0].waypoint.departure_time),
|
||||
])
|
||||
self.rows.append(
|
||||
[
|
||||
f"{first_waypoint_num}-{last_waypoint_num}",
|
||||
"Target points",
|
||||
"0",
|
||||
self._waypoint_distance(self.target_points[0].waypoint),
|
||||
self._ground_speed(self.target_points[0].waypoint),
|
||||
self._format_time(self.target_points[0].waypoint.tot),
|
||||
self._format_time(self.target_points[0].waypoint.departure_time),
|
||||
]
|
||||
)
|
||||
self.last_waypoint = self.target_points[-1].waypoint
|
||||
|
||||
def add_waypoint_row(self, waypoint: NumberedWaypoint) -> None:
|
||||
self.rows.append([
|
||||
str(waypoint.number),
|
||||
KneeboardPageWriter.wrap_line(
|
||||
waypoint.waypoint.pretty_name,
|
||||
FlightPlanBuilder.WAYPOINT_DESC_MAX_LEN),
|
||||
str(int(waypoint.waypoint.alt.feet)),
|
||||
self._waypoint_distance(waypoint.waypoint),
|
||||
self._ground_speed(waypoint.waypoint),
|
||||
self._format_time(waypoint.waypoint.tot),
|
||||
self._format_time(waypoint.waypoint.departure_time),
|
||||
])
|
||||
self.rows.append(
|
||||
[
|
||||
str(waypoint.number),
|
||||
KneeboardPageWriter.wrap_line(
|
||||
waypoint.waypoint.pretty_name,
|
||||
FlightPlanBuilder.WAYPOINT_DESC_MAX_LEN,
|
||||
),
|
||||
str(int(waypoint.waypoint.alt.feet)),
|
||||
self._waypoint_distance(waypoint.waypoint),
|
||||
self._ground_speed(waypoint.waypoint),
|
||||
self._format_time(waypoint.waypoint.tot),
|
||||
self._format_time(waypoint.waypoint.departure_time),
|
||||
]
|
||||
)
|
||||
|
||||
def _format_time(self, time: Optional[datetime.timedelta]) -> str:
|
||||
if time is None:
|
||||
@@ -191,9 +214,9 @@ class FlightPlanBuilder:
|
||||
if self.last_waypoint is None:
|
||||
return "-"
|
||||
|
||||
distance = meters(self.last_waypoint.position.distance_to_point(
|
||||
waypoint.position
|
||||
))
|
||||
distance = meters(
|
||||
self.last_waypoint.position.distance_to_point(waypoint.position)
|
||||
)
|
||||
return f"{distance.nautical_miles:.1f} NM"
|
||||
|
||||
def _ground_speed(self, waypoint: FlightWaypoint) -> str:
|
||||
@@ -210,9 +233,9 @@ class FlightPlanBuilder:
|
||||
else:
|
||||
return "-"
|
||||
|
||||
distance = meters(self.last_waypoint.position.distance_to_point(
|
||||
waypoint.position
|
||||
))
|
||||
distance = meters(
|
||||
self.last_waypoint.position.distance_to_point(waypoint.position)
|
||||
)
|
||||
duration = (waypoint.tot - last_time).total_seconds() / 3600
|
||||
return f"{int(distance.nautical_miles / duration)} kt"
|
||||
|
||||
@@ -222,66 +245,113 @@ class FlightPlanBuilder:
|
||||
|
||||
class BriefingPage(KneeboardPage):
|
||||
"""A kneeboard page containing briefing information."""
|
||||
def __init__(self, flight: FlightData, comms: List[CommInfo],
|
||||
awacs: List[AwacsInfo], tankers: List[TankerInfo],
|
||||
jtacs: List[JtacInfo], start_time: datetime.datetime) -> None:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
flight: FlightData,
|
||||
comms: List[CommInfo],
|
||||
awacs: List[AwacsInfo],
|
||||
tankers: List[TankerInfo],
|
||||
jtacs: List[JtacInfo],
|
||||
start_time: datetime.datetime,
|
||||
dark_kneeboard: bool,
|
||||
) -> None:
|
||||
self.flight = flight
|
||||
self.comms = list(comms)
|
||||
self.awacs = awacs
|
||||
self.tankers = tankers
|
||||
self.jtacs = jtacs
|
||||
self.start_time = start_time
|
||||
self.dark_kneeboard = dark_kneeboard
|
||||
self.comms.append(CommInfo("Flight", self.flight.intra_flight_channel))
|
||||
|
||||
def write(self, path: Path) -> None:
|
||||
writer = KneeboardPageWriter()
|
||||
writer.title(f"{self.flight.callsign} Mission Info")
|
||||
writer = KneeboardPageWriter(dark_theme=self.dark_kneeboard)
|
||||
if self.flight.custom_name is not None:
|
||||
custom_name_title = ' ("{}")'.format(self.flight.custom_name)
|
||||
else:
|
||||
custom_name_title = ""
|
||||
writer.title(f"{self.flight.callsign} Mission Info{custom_name_title}")
|
||||
|
||||
# TODO: Handle carriers.
|
||||
writer.heading("Airfield Info")
|
||||
writer.table([
|
||||
self.airfield_info_row("Departure", self.flight.departure),
|
||||
self.airfield_info_row("Arrival", self.flight.arrival),
|
||||
self.airfield_info_row("Divert", self.flight.divert),
|
||||
], headers=["", "Airbase", "ATC", "TCN", "I(C)LS", "RWY"])
|
||||
writer.table(
|
||||
[
|
||||
self.airfield_info_row("Departure", self.flight.departure),
|
||||
self.airfield_info_row("Arrival", self.flight.arrival),
|
||||
self.airfield_info_row("Divert", self.flight.divert),
|
||||
],
|
||||
headers=["", "Airbase", "ATC", "TCN", "I(C)LS", "RWY"],
|
||||
)
|
||||
|
||||
writer.heading("Flight Plan")
|
||||
flight_plan_builder = FlightPlanBuilder(self.start_time)
|
||||
for num, waypoint in enumerate(self.flight.waypoints):
|
||||
flight_plan_builder.add_waypoint(num, waypoint)
|
||||
writer.table(flight_plan_builder.build(), headers=[
|
||||
"#", "Action", "Alt", "Dist", "GSPD", "Time", "Departure"
|
||||
])
|
||||
writer.table(
|
||||
flight_plan_builder.build(),
|
||||
headers=["#", "Action", "Alt", "Dist", "GSPD", "Time", "Departure"],
|
||||
)
|
||||
|
||||
flight_plan_builder
|
||||
writer.table([
|
||||
["{}lbs".format(self.flight.bingo_fuel), "{}lbs".format(self.flight.joker_fuel)]
|
||||
], ['Bingo', 'Joker'])
|
||||
writer.table(
|
||||
[
|
||||
[
|
||||
"{}lbs".format(self.flight.bingo_fuel),
|
||||
"{}lbs".format(self.flight.joker_fuel),
|
||||
]
|
||||
],
|
||||
["Bingo", "Joker"],
|
||||
)
|
||||
|
||||
# AEW&C
|
||||
writer.heading("AEW&C")
|
||||
aewc_ladder = []
|
||||
|
||||
for single_aewc in self.awacs:
|
||||
|
||||
if single_aewc.depature_location is None:
|
||||
dep = "-"
|
||||
arr = "-"
|
||||
else:
|
||||
dep = self._format_time(single_aewc.start_time)
|
||||
arr = self._format_time(single_aewc.end_time)
|
||||
|
||||
aewc_ladder.append(
|
||||
[
|
||||
str(single_aewc.callsign),
|
||||
str(single_aewc.freq),
|
||||
str(single_aewc.depature_location),
|
||||
str(dep),
|
||||
str(arr),
|
||||
]
|
||||
)
|
||||
|
||||
writer.table(
|
||||
aewc_ladder,
|
||||
headers=["Callsign", "FREQ", "Depature", "ETD", "ETA"],
|
||||
)
|
||||
|
||||
# Package Section
|
||||
writer.heading("Comm ladder")
|
||||
comm_ladder = []
|
||||
for comm in self.comms:
|
||||
comm_ladder.append([comm.name, '', '', '', self.format_frequency(comm.freq)])
|
||||
comm_ladder.append(
|
||||
[comm.name, "", "", "", self.format_frequency(comm.freq)]
|
||||
)
|
||||
|
||||
for a in self.awacs:
|
||||
comm_ladder.append([
|
||||
a.callsign,
|
||||
'AWACS',
|
||||
'',
|
||||
'',
|
||||
self.format_frequency(a.freq)
|
||||
])
|
||||
for tanker in self.tankers:
|
||||
comm_ladder.append([
|
||||
tanker.callsign,
|
||||
"Tanker",
|
||||
tanker.variant,
|
||||
str(tanker.tacan),
|
||||
self.format_frequency(tanker.freq),
|
||||
])
|
||||
|
||||
writer.table(comm_ladder, headers=["Callsign","Task", "Type", "TACAN", "FREQ"])
|
||||
comm_ladder.append(
|
||||
[
|
||||
tanker.callsign,
|
||||
"Tanker",
|
||||
tanker.variant,
|
||||
str(tanker.tacan),
|
||||
self.format_frequency(tanker.freq),
|
||||
]
|
||||
)
|
||||
|
||||
writer.table(comm_ladder, headers=["Callsign", "Task", "Type", "TACAN", "FREQ"])
|
||||
|
||||
writer.heading("JTAC")
|
||||
jtacs = []
|
||||
@@ -291,8 +361,9 @@ class BriefingPage(KneeboardPage):
|
||||
|
||||
writer.write(path)
|
||||
|
||||
def airfield_info_row(self, row_title: str,
|
||||
runway: Optional[RunwayData]) -> List[str]:
|
||||
def airfield_info_row(
|
||||
self, row_title: str, runway: Optional[RunwayData]
|
||||
) -> List[str]:
|
||||
"""Creates a table row for a given airfield.
|
||||
|
||||
Args:
|
||||
@@ -337,12 +408,21 @@ class BriefingPage(KneeboardPage):
|
||||
channel_name = namer.channel_name(channel.radio_id, channel.channel)
|
||||
return f"{channel_name} {frequency}"
|
||||
|
||||
def _format_time(self, time: Optional[datetime.timedelta]) -> str:
|
||||
if time is None:
|
||||
return ""
|
||||
local_time = self.start_time + time
|
||||
return local_time.strftime(f"%H:%M:%S")
|
||||
|
||||
|
||||
class KneeboardGenerator(MissionInfoGenerator):
|
||||
"""Creates kneeboard pages for each client flight in the mission."""
|
||||
|
||||
def __init__(self, mission: Mission, game: "Game") -> None:
|
||||
super().__init__(mission, game)
|
||||
self.dark_kneeboard = self.game.settings.generate_dark_kneeboard and (
|
||||
self.mission.start_time.hour > 19 or self.mission.start_time.hour < 7
|
||||
)
|
||||
|
||||
def generate(self) -> None:
|
||||
"""Generates a kneeboard per client flight."""
|
||||
@@ -372,7 +452,8 @@ class KneeboardGenerator(MissionInfoGenerator):
|
||||
if not flight.client_units:
|
||||
continue
|
||||
all_flights[flight.aircraft_type].extend(
|
||||
self.generate_flight_kneeboard(flight))
|
||||
self.generate_flight_kneeboard(flight)
|
||||
)
|
||||
return all_flights
|
||||
|
||||
def generate_flight_kneeboard(self, flight: FlightData) -> List[KneeboardPage]:
|
||||
@@ -384,6 +465,7 @@ class KneeboardGenerator(MissionInfoGenerator):
|
||||
self.awacs,
|
||||
self.tankers,
|
||||
self.jtacs,
|
||||
self.mission.start_time
|
||||
self.mission.start_time,
|
||||
self.dark_kneeboard,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -9,7 +9,7 @@ from gen.locations.preset_locations import PresetLocation
|
||||
class PresetControlPointLocations:
|
||||
"""A repository of preset locations for a given control point"""
|
||||
|
||||
# List of possible ashore locations to generate objects (Represented in miz file by an APC_AAV_7)
|
||||
# List of possible ashore locations to generate objects (Represented in miz file by an APC_AAV_7_Amphibious)
|
||||
ashore_locations: List[PresetLocation] = field(default_factory=list)
|
||||
|
||||
# List of possible offshore locations to generate ship groups (Represented in miz file by an Oliver Hazard Perry)
|
||||
|
||||
@@ -9,9 +9,10 @@ from gen.locations.preset_locations import PresetLocation
|
||||
|
||||
|
||||
class MizDataLocationFinder:
|
||||
|
||||
@staticmethod
|
||||
def compute_possible_locations(terrain_name: str, cp_name: str) -> PresetControlPointLocations:
|
||||
def compute_possible_locations(
|
||||
terrain_name: str, cp_name: str
|
||||
) -> PresetControlPointLocations:
|
||||
"""
|
||||
Extract the list of preset locations from miz data
|
||||
:param terrain_name: Terrain/Map name
|
||||
@@ -32,28 +33,55 @@ class MizDataLocationFinder:
|
||||
|
||||
for vehicle_group in m.country("USA").vehicle_group:
|
||||
if len(vehicle_group.units) > 0:
|
||||
ashore_locations.append(PresetLocation(vehicle_group.position,
|
||||
vehicle_group.units[0].heading,
|
||||
vehicle_group.name))
|
||||
ashore_locations.append(
|
||||
PresetLocation(
|
||||
vehicle_group.position,
|
||||
vehicle_group.units[0].heading,
|
||||
vehicle_group.name,
|
||||
)
|
||||
)
|
||||
|
||||
for ship_group in m.country("USA").ship_group:
|
||||
if len(ship_group.units) > 0 and ship_group.units[0].type == ships.Oliver_Hazzard_Perry_class.id:
|
||||
offshore_locations.append(PresetLocation(ship_group.position,
|
||||
ship_group.units[0].heading,
|
||||
ship_group.name))
|
||||
if (
|
||||
len(ship_group.units) > 0
|
||||
and ship_group.units[0].type == ships.FFG_Oliver_Hazzard_Perry.id
|
||||
):
|
||||
offshore_locations.append(
|
||||
PresetLocation(
|
||||
ship_group.position,
|
||||
ship_group.units[0].heading,
|
||||
ship_group.name,
|
||||
)
|
||||
)
|
||||
|
||||
for static_group in m.country("USA").static_group:
|
||||
if len(static_group.units) > 0:
|
||||
powerplants_locations.append(PresetLocation(static_group.position,
|
||||
static_group.units[0].heading,
|
||||
static_group.name))
|
||||
powerplants_locations.append(
|
||||
PresetLocation(
|
||||
static_group.position,
|
||||
static_group.units[0].heading,
|
||||
static_group.name,
|
||||
)
|
||||
)
|
||||
|
||||
if m.country("Iran") is not None:
|
||||
for vehicle_group in m.country("Iran").vehicle_group:
|
||||
if len(vehicle_group.units) > 0 and vehicle_group.units[0].type == MissilesSS.SS_N_2_Silkworm.id:
|
||||
antiship_locations.append(PresetLocation(vehicle_group.position,
|
||||
vehicle_group.units[0].heading,
|
||||
vehicle_group.name))
|
||||
if (
|
||||
len(vehicle_group.units) > 0
|
||||
and vehicle_group.units[0].type
|
||||
== MissilesSS.AShM_SS_N_2_Silkworm.id
|
||||
):
|
||||
antiship_locations.append(
|
||||
PresetLocation(
|
||||
vehicle_group.position,
|
||||
vehicle_group.units[0].heading,
|
||||
vehicle_group.name,
|
||||
)
|
||||
)
|
||||
|
||||
return PresetControlPointLocations(ashore_locations, offshore_locations,
|
||||
antiship_locations, powerplants_locations)
|
||||
return PresetControlPointLocations(
|
||||
ashore_locations,
|
||||
offshore_locations,
|
||||
antiship_locations,
|
||||
powerplants_locations,
|
||||
)
|
||||
|
||||
@@ -6,10 +6,16 @@ from dcs import Point
|
||||
@dataclass
|
||||
class PresetLocation:
|
||||
"""A preset location"""
|
||||
|
||||
position: Point
|
||||
heading: int
|
||||
id: str
|
||||
|
||||
def __str__(self):
|
||||
return "-" * 10 + "X: {}\n Y: {}\nHdg: {}°\nId: {}".format(self.position.x, self.position.y, self.heading,
|
||||
self.id) + "-" * 10
|
||||
return (
|
||||
"-" * 10
|
||||
+ "X: {}\n Y: {}\nHdg: {}°\nId: {}".format(
|
||||
self.position.x, self.position.y, self.heading, self.id
|
||||
)
|
||||
+ "-" * 10
|
||||
)
|
||||
|
||||
@@ -4,15 +4,12 @@ from game import db
|
||||
from gen.missiles.scud_site import ScudGenerator
|
||||
from gen.missiles.v1_group import V1GroupGenerator
|
||||
|
||||
MISSILES_MAP = {
|
||||
"V1GroupGenerator": V1GroupGenerator,
|
||||
"ScudGenerator": ScudGenerator
|
||||
}
|
||||
MISSILES_MAP = {"V1GroupGenerator": V1GroupGenerator, "ScudGenerator": ScudGenerator}
|
||||
|
||||
|
||||
def generate_missile_group(game, ground_object, faction_name: str):
|
||||
"""
|
||||
This generate a ship group
|
||||
This generate a missiles group
|
||||
:return: Nothing, but put the group reference inside the ground object
|
||||
"""
|
||||
faction = db.FACTIONS[faction_name]
|
||||
@@ -25,5 +22,9 @@ def generate_missile_group(game, ground_object, faction_name: str):
|
||||
generator.generate()
|
||||
return generator.get_generated_group()
|
||||
else:
|
||||
logging.info("Unable to generate missile group, generator : " + str(gen) + "does not exists")
|
||||
return None
|
||||
logging.info(
|
||||
"Unable to generate missile group, generator : "
|
||||
+ str(gen)
|
||||
+ "does not exists"
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -6,7 +6,6 @@ from gen.sam.group_generator import GroupGenerator
|
||||
|
||||
|
||||
class ScudGenerator(GroupGenerator):
|
||||
|
||||
def __init__(self, game, ground_object, faction):
|
||||
super(ScudGenerator, self).__init__(game, ground_object)
|
||||
self.faction = faction
|
||||
@@ -14,17 +13,50 @@ class ScudGenerator(GroupGenerator):
|
||||
def generate(self):
|
||||
|
||||
# Scuds
|
||||
self.add_unit(MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M, "V1#0", self.position.x, self.position.y + random.randint(1, 8), self.heading)
|
||||
self.add_unit(MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M, "V1#1", self.position.x + 50, self.position.y + random.randint(1, 8), self.heading)
|
||||
self.add_unit(MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M, "V1#2", self.position.x + 100, self.position.y + random.randint(1, 8), self.heading)
|
||||
self.add_unit(
|
||||
MissilesSS.SSM_SS_1C_Scud_B,
|
||||
"V1#0",
|
||||
self.position.x,
|
||||
self.position.y + random.randint(1, 8),
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
MissilesSS.SSM_SS_1C_Scud_B,
|
||||
"V1#1",
|
||||
self.position.x + 50,
|
||||
self.position.y + random.randint(1, 8),
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
MissilesSS.SSM_SS_1C_Scud_B,
|
||||
"V1#2",
|
||||
self.position.x + 100,
|
||||
self.position.y + random.randint(1, 8),
|
||||
self.heading,
|
||||
)
|
||||
|
||||
# Commander
|
||||
self.add_unit(Unarmed.Transport_UAZ_469, "Kubel#0", self.position.x - 35, self.position.y - 20,
|
||||
self.heading)
|
||||
self.add_unit(
|
||||
Unarmed.LUV_UAZ_469_Jeep,
|
||||
"Kubel#0",
|
||||
self.position.x - 35,
|
||||
self.position.y - 20,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
# Shorad
|
||||
self.add_unit(AirDefence.SPAAA_ZSU_23_4_Shilka, "SHILKA#0", self.position.x - 55, self.position.y - 38,
|
||||
self.heading)
|
||||
self.add_unit(
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
|
||||
"SHILKA#0",
|
||||
self.position.x - 55,
|
||||
self.position.y - 38,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
self.add_unit(AirDefence.SAM_SA_9_Strela_1_9P31, "STRELA#0",
|
||||
self.position.x + 200, self.position.y + 15, 90)
|
||||
self.add_unit(
|
||||
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL,
|
||||
"STRELA#0",
|
||||
self.position.x + 200,
|
||||
self.position.y + 15,
|
||||
90,
|
||||
)
|
||||
|
||||
@@ -6,7 +6,6 @@ from gen.sam.group_generator import GroupGenerator
|
||||
|
||||
|
||||
class V1GroupGenerator(GroupGenerator):
|
||||
|
||||
def __init__(self, game, ground_object, faction):
|
||||
super(V1GroupGenerator, self).__init__(game, ground_object)
|
||||
self.faction = faction
|
||||
@@ -14,19 +13,54 @@ class V1GroupGenerator(GroupGenerator):
|
||||
def generate(self):
|
||||
|
||||
# Ramps
|
||||
self.add_unit(MissilesSS.V_1_ramp, "V1#0", self.position.x, self.position.y + random.randint(1, 8), self.heading)
|
||||
self.add_unit(MissilesSS.V_1_ramp, "V1#1", self.position.x + 50, self.position.y + random.randint(1, 8), self.heading)
|
||||
self.add_unit(MissilesSS.V_1_ramp, "V1#2", self.position.x + 100, self.position.y + random.randint(1, 8), self.heading)
|
||||
self.add_unit(
|
||||
MissilesSS.SSM_V_1_Launcher,
|
||||
"V1#0",
|
||||
self.position.x,
|
||||
self.position.y + random.randint(1, 8),
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
MissilesSS.SSM_V_1_Launcher,
|
||||
"V1#1",
|
||||
self.position.x + 50,
|
||||
self.position.y + random.randint(1, 8),
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
MissilesSS.SSM_V_1_Launcher,
|
||||
"V1#2",
|
||||
self.position.x + 100,
|
||||
self.position.y + random.randint(1, 8),
|
||||
self.heading,
|
||||
)
|
||||
|
||||
# Commander
|
||||
self.add_unit(Unarmed.Kübelwagen_82, "Kubel#0", self.position.x - 35, self.position.y - 20,
|
||||
self.heading)
|
||||
self.add_unit(
|
||||
Unarmed.LUV_Kubelwagen_82,
|
||||
"Kubel#0",
|
||||
self.position.x - 35,
|
||||
self.position.y - 20,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
# Self defense flak
|
||||
flak_unit = random.choice([AirDefence.AAA_Flak_Vierling_38, AirDefence.AAA_Flak_38])
|
||||
flak_unit = random.choice(
|
||||
[AirDefence.AAA_Flak_Vierling_38_Quad_20mm, AirDefence.AAA_Flak_38_20mm]
|
||||
)
|
||||
|
||||
self.add_unit(flak_unit, "FLAK#0", self.position.x - 55, self.position.y - 38,
|
||||
self.heading)
|
||||
self.add_unit(
|
||||
flak_unit,
|
||||
"FLAK#0",
|
||||
self.position.x - 55,
|
||||
self.position.y - 38,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
self.add_unit(Unarmed.Blitz_3_6_6700A, "Blitz#0",
|
||||
self.position.x + 200, self.position.y + 15, 90)
|
||||
self.add_unit(
|
||||
Unarmed.Truck_Opel_Blitz,
|
||||
"Blitz#0",
|
||||
self.position.x + 200,
|
||||
self.position.y + 15,
|
||||
90,
|
||||
)
|
||||
|
||||
304
gen/naming.py
304
gen/naming.py
@@ -9,39 +9,242 @@ from game import db
|
||||
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
ALPHA_MILITARY = ["Alpha", "Bravo", "Charlie", "Delta", "Echo", "Foxtrot",
|
||||
"Golf", "Hotel", "India", "Juliet", "Kilo", "Lima", "Mike",
|
||||
"November", "Oscar", "Papa", "Quebec", "Romeo", "Sierra",
|
||||
"Tango", "Uniform", "Victor", "Whisky", "XRay", "Yankee",
|
||||
"Zulu", "Zero"]
|
||||
ALPHA_MILITARY = [
|
||||
"Alpha",
|
||||
"Bravo",
|
||||
"Charlie",
|
||||
"Delta",
|
||||
"Echo",
|
||||
"Foxtrot",
|
||||
"Golf",
|
||||
"Hotel",
|
||||
"India",
|
||||
"Juliet",
|
||||
"Kilo",
|
||||
"Lima",
|
||||
"Mike",
|
||||
"November",
|
||||
"Oscar",
|
||||
"Papa",
|
||||
"Quebec",
|
||||
"Romeo",
|
||||
"Sierra",
|
||||
"Tango",
|
||||
"Uniform",
|
||||
"Victor",
|
||||
"Whisky",
|
||||
"XRay",
|
||||
"Yankee",
|
||||
"Zulu",
|
||||
"Zero",
|
||||
]
|
||||
|
||||
ANIMALS = [
|
||||
"SHARK", "TORTOISE", "BAT", "PANGOLIN", "AARDWOLF",
|
||||
"MONKEY", "BUFFALO", "DOG", "BOBCAT", "LYNX", "PANTHER", "TIGER",
|
||||
"LION", "OWL", "BUTTERFLY", "BISON", "DUCK", "COBRA", "MAMBA",
|
||||
"DOLPHIN", "PHEASANT", "ARMADILLLO", "RACOON", "ZEBRA", "COW", "COYOTE", "FOX",
|
||||
"LIGHTFOOT", "COTTONMOUTH", "TAURUS", "VIPER", "CASTOR", "GIRAFFE", "SNAKE",
|
||||
"MONSTER", "ALBATROSS", "HAWK", "DOVE", "MOCKINGBIRD", "GECKO", "ORYX", "GORILLA",
|
||||
"HARAMBE", "GOOSE", "MAVERICK", "HARE", "JACKAL", "LEOPARD", "CAT", "MUSK", "ORCA",
|
||||
"OCELOT", "BEAR", "PANDA", "GULL", "PENGUIN", "PYTHON", "RAVEN", "DEER", "MOOSE",
|
||||
"REINDEER", "SHEEP", "GAZELLE", "INSECT", "VULTURE", "WALLABY", "KANGAROO", "KOALA",
|
||||
"KIWI", "WHALE", "FISH", "RHINO", "HIPPO", "RAT", "WOODPECKER", "WORM", "BABOON",
|
||||
"YAK", "SCORPIO", "HORSE", "POODLE", "CENTIPEDE", "CHICKEN", "CHEETAH", "CHAMELEON",
|
||||
"CATFISH", "CATERPILLAR", "CARACAL", "CAMEL", "CAIMAN", "BARRACUDA", "BANDICOOT",
|
||||
"ALLIGATOR", "BONGO", "CORAL", "ELEPHANT", "ANTELOPE", "CRAB", "DACHSHUND", "DODO",
|
||||
"FLAMINGO", "FERRET", "FALCON", "BULLDOG", "DONKEY", "IGUANA", "TAMARIN", "HARRIER",
|
||||
"GRIZZLY", "GREYHOUND", "GRASSHOPPER", "JAGUAR", "LADYBUG", "KOMODO", "DRAGON", "LIZARD",
|
||||
"LLAMA", "LOBSTER", "OCTOPUS", "MANATEE", "MAGPIE", "MACAW", "OSTRICH", "OYSTER",
|
||||
"MOLE", "MULE", "MOTH", "MONGOOSE", "MOLLY", "MEERKAT", "MOUSE", "PEACOCK", "PIKE", "ROBIN",
|
||||
"RAGDOLL", "PLATYPUS", "PELICAN", "PARROT", "PORCUPINE", "PIRANHA", "PUMA", "PUG", "TAPIR",
|
||||
"TERMITE", "URCHIN", "SHRIMP", "TURKEY", "TOUCAN", "TETRA", "HUSKY", "STARFISH", "SWAN",
|
||||
"FROG", "SQUIRREL", "WALRUS", "WARTHOG", "CORGI", "WEASEL", "WOMBAT", "WOLVERINE", "MAMMOTH",
|
||||
"TOAD", "WOLF", "ZEBU", "SEAL", "SKATE", "JELLYFISH", "MOSQUITO", "LOCUST", "SLUG", "SNAIL",
|
||||
"HEDGEHOG", "PIGLET", "FENNEC", "BADGER", "ALPACA", "DINGO", "COLT", "SKUNK", "BUNNY", "IMPALA",
|
||||
"GUANACO", "CAPYBARA", "ELK", "MINK", "PRONGHORN", "CROW", "BUMBLEBEE", "FAWN", "OTTER", "WATERBUCK",
|
||||
"JERBOA", "KITTEN", "ARGALI", "OX", "MARE", "FINCH", "BASILISK", "GOPHER", "HAMSTER", "CANARY", "WOODCHUCK",
|
||||
"ANACONDA"
|
||||
]
|
||||
"SHARK",
|
||||
"TORTOISE",
|
||||
"BAT",
|
||||
"PANGOLIN",
|
||||
"AARDWOLF",
|
||||
"MONKEY",
|
||||
"BUFFALO",
|
||||
"DOG",
|
||||
"BOBCAT",
|
||||
"LYNX",
|
||||
"PANTHER",
|
||||
"TIGER",
|
||||
"LION",
|
||||
"OWL",
|
||||
"BUTTERFLY",
|
||||
"BISON",
|
||||
"DUCK",
|
||||
"COBRA",
|
||||
"MAMBA",
|
||||
"DOLPHIN",
|
||||
"PHEASANT",
|
||||
"ARMADILLO",
|
||||
"RACOON",
|
||||
"ZEBRA",
|
||||
"COW",
|
||||
"COYOTE",
|
||||
"FOX",
|
||||
"LIGHTFOOT",
|
||||
"COTTONMOUTH",
|
||||
"TAURUS",
|
||||
"VIPER",
|
||||
"CASTOR",
|
||||
"GIRAFFE",
|
||||
"SNAKE",
|
||||
"MONSTER",
|
||||
"ALBATROSS",
|
||||
"HAWK",
|
||||
"DOVE",
|
||||
"MOCKINGBIRD",
|
||||
"GECKO",
|
||||
"ORYX",
|
||||
"GORILLA",
|
||||
"HARAMBE",
|
||||
"GOOSE",
|
||||
"MAVERICK",
|
||||
"HARE",
|
||||
"JACKAL",
|
||||
"LEOPARD",
|
||||
"CAT",
|
||||
"MUSK",
|
||||
"ORCA",
|
||||
"OCELOT",
|
||||
"BEAR",
|
||||
"PANDA",
|
||||
"GULL",
|
||||
"PENGUIN",
|
||||
"PYTHON",
|
||||
"RAVEN",
|
||||
"DEER",
|
||||
"MOOSE",
|
||||
"REINDEER",
|
||||
"SHEEP",
|
||||
"GAZELLE",
|
||||
"INSECT",
|
||||
"VULTURE",
|
||||
"WALLABY",
|
||||
"KANGAROO",
|
||||
"KOALA",
|
||||
"KIWI",
|
||||
"WHALE",
|
||||
"FISH",
|
||||
"RHINO",
|
||||
"HIPPO",
|
||||
"RAT",
|
||||
"WOODPECKER",
|
||||
"WORM",
|
||||
"BABOON",
|
||||
"YAK",
|
||||
"SCORPIO",
|
||||
"HORSE",
|
||||
"POODLE",
|
||||
"CENTIPEDE",
|
||||
"CHICKEN",
|
||||
"CHEETAH",
|
||||
"CHAMELEON",
|
||||
"CATFISH",
|
||||
"CATERPILLAR",
|
||||
"CARACAL",
|
||||
"CAMEL",
|
||||
"CAIMAN",
|
||||
"BARRACUDA",
|
||||
"BANDICOOT",
|
||||
"ALLIGATOR",
|
||||
"BONGO",
|
||||
"CORAL",
|
||||
"ELEPHANT",
|
||||
"ANTELOPE",
|
||||
"CRAB",
|
||||
"DACHSHUND",
|
||||
"DODO",
|
||||
"FLAMINGO",
|
||||
"FERRET",
|
||||
"FALCON",
|
||||
"BULLDOG",
|
||||
"DONKEY",
|
||||
"IGUANA",
|
||||
"TAMARIN",
|
||||
"HARRIER",
|
||||
"GRIZZLY",
|
||||
"GREYHOUND",
|
||||
"GRASSHOPPER",
|
||||
"JAGUAR",
|
||||
"LADYBUG",
|
||||
"KOMODO",
|
||||
"DRAGON",
|
||||
"LIZARD",
|
||||
"LLAMA",
|
||||
"LOBSTER",
|
||||
"OCTOPUS",
|
||||
"MANATEE",
|
||||
"MAGPIE",
|
||||
"MACAW",
|
||||
"OSTRICH",
|
||||
"OYSTER",
|
||||
"MOLE",
|
||||
"MULE",
|
||||
"MOTH",
|
||||
"MONGOOSE",
|
||||
"MOLLY",
|
||||
"MEERKAT",
|
||||
"MOUSE",
|
||||
"PEACOCK",
|
||||
"PIKE",
|
||||
"ROBIN",
|
||||
"RAGDOLL",
|
||||
"PLATYPUS",
|
||||
"PELICAN",
|
||||
"PARROT",
|
||||
"PORCUPINE",
|
||||
"PIRANHA",
|
||||
"PUMA",
|
||||
"PUG",
|
||||
"TAPIR",
|
||||
"TERMITE",
|
||||
"URCHIN",
|
||||
"SHRIMP",
|
||||
"TURKEY",
|
||||
"TOUCAN",
|
||||
"TETRA",
|
||||
"HUSKY",
|
||||
"STARFISH",
|
||||
"SWAN",
|
||||
"FROG",
|
||||
"SQUIRREL",
|
||||
"WALRUS",
|
||||
"WARTHOG",
|
||||
"CORGI",
|
||||
"WEASEL",
|
||||
"WOMBAT",
|
||||
"WOLVERINE",
|
||||
"MAMMOTH",
|
||||
"TOAD",
|
||||
"WOLF",
|
||||
"ZEBU",
|
||||
"SEAL",
|
||||
"SKATE",
|
||||
"JELLYFISH",
|
||||
"MOSQUITO",
|
||||
"LOCUST",
|
||||
"SLUG",
|
||||
"SNAIL",
|
||||
"HEDGEHOG",
|
||||
"PIGLET",
|
||||
"FENNEC",
|
||||
"BADGER",
|
||||
"ALPACA",
|
||||
"DINGO",
|
||||
"COLT",
|
||||
"SKUNK",
|
||||
"BUNNY",
|
||||
"IMPALA",
|
||||
"GUANACO",
|
||||
"CAPYBARA",
|
||||
"ELK",
|
||||
"MINK",
|
||||
"PRONGHORN",
|
||||
"CROW",
|
||||
"BUMBLEBEE",
|
||||
"FAWN",
|
||||
"OTTER",
|
||||
"WATERBUCK",
|
||||
"JERBOA",
|
||||
"KITTEN",
|
||||
"ARGALI",
|
||||
"OX",
|
||||
"MARE",
|
||||
"FINCH",
|
||||
"BASILISK",
|
||||
"GOPHER",
|
||||
"HAMSTER",
|
||||
"CANARY",
|
||||
"WOODCHUCK",
|
||||
"ANACONDA",
|
||||
]
|
||||
|
||||
|
||||
class NameGenerator:
|
||||
number = 0
|
||||
@@ -72,21 +275,36 @@ class NameGenerator:
|
||||
name_str = flight.custom_name
|
||||
else:
|
||||
name_str = "{} {}".format(
|
||||
flight.package.target.name, flight.flight_type)
|
||||
flight.package.target.name, flight.flight_type
|
||||
)
|
||||
except AttributeError: # Here to maintain save compatibility with 2.3
|
||||
name_str = "{} {}".format(
|
||||
flight.package.target.name, flight.flight_type)
|
||||
return "{}|{}|{}|{}|{}|".format(name_str, country.id, cls.aircraft_number, parent_base_id, db.unit_type_name(flight.unit_type))
|
||||
name_str = "{} {}".format(flight.package.target.name, flight.flight_type)
|
||||
return "{}|{}|{}|{}|{}|".format(
|
||||
name_str,
|
||||
country.id,
|
||||
cls.aircraft_number,
|
||||
parent_base_id,
|
||||
db.unit_type_name(flight.unit_type),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def next_unit_name(cls, country: Country, parent_base_id: int, unit_type: UnitType):
|
||||
cls.number += 1
|
||||
return "unit|{}|{}|{}|{}|".format(country.id, cls.number, parent_base_id, db.unit_type_name(unit_type))
|
||||
return "unit|{}|{}|{}|{}|".format(
|
||||
country.id, cls.number, parent_base_id, db.unit_type_name(unit_type)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def next_infantry_name(cls, country: Country, parent_base_id: int, unit_type: UnitType):
|
||||
def next_infantry_name(
|
||||
cls, country: Country, parent_base_id: int, unit_type: UnitType
|
||||
):
|
||||
cls.infantry_number += 1
|
||||
return "infantry|{}|{}|{}|{}|".format(country.id, cls.infantry_number, parent_base_id, db.unit_type_name(unit_type))
|
||||
return "infantry|{}|{}|{}|{}|".format(
|
||||
country.id,
|
||||
cls.infantry_number,
|
||||
parent_base_id,
|
||||
db.unit_type_name(unit_type),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def next_basedefense_name():
|
||||
@@ -100,7 +318,9 @@ class NameGenerator:
|
||||
@classmethod
|
||||
def next_tanker_name(cls, country: Country, unit_type: UnitType):
|
||||
cls.number += 1
|
||||
return "tanker|{}|{}|0|{}".format(country.id, cls.number, db.unit_type_name(unit_type))
|
||||
return "tanker|{}|{}|0|{}".format(
|
||||
country.id, cls.number, db.unit_type_name(unit_type)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def next_carrier_name(cls, country: Country):
|
||||
@@ -112,7 +332,11 @@ class NameGenerator:
|
||||
if len(cls.ANIMALS) == 0:
|
||||
for i in range(10):
|
||||
new_name_generated = True
|
||||
alpha_mil_name = random.choice(ALPHA_MILITARY).upper() + "#" + str(random.randint(0, 100))
|
||||
alpha_mil_name = (
|
||||
random.choice(ALPHA_MILITARY).upper()
|
||||
+ "#"
|
||||
+ str(random.randint(0, 100))
|
||||
)
|
||||
for existing_name in cls.existing_alphas:
|
||||
if existing_name == alpha_mil_name:
|
||||
new_name_generated = False
|
||||
|
||||
@@ -68,9 +68,10 @@ class Radio:
|
||||
|
||||
def range(self) -> Iterator[RadioFrequency]:
|
||||
"""Returns an iterator over the usable frequencies of this radio."""
|
||||
return (RadioFrequency(x) for x in range(
|
||||
self.minimum.hertz, self.maximum.hertz, self.step.hertz
|
||||
))
|
||||
return (
|
||||
RadioFrequency(x)
|
||||
for x in range(self.minimum.hertz, self.maximum.hertz, self.step.hertz)
|
||||
)
|
||||
|
||||
@property
|
||||
def last_channel(self) -> RadioFrequency:
|
||||
@@ -99,14 +100,12 @@ RADIOS: List[Radio] = [
|
||||
Radio("SCR-522", MHz(100), MHz(156), step=MHz(1)),
|
||||
Radio("A.R.I. 1063", MHz(100), MHz(156), step=MHz(1)),
|
||||
Radio("BC-1206", kHz(200), kHz(400), step=kHz(10)),
|
||||
|
||||
# Note: The M2000C V/UHF can operate in both ranges, but has a gap between
|
||||
# 150 MHz and 225 MHz. We can't allocate in that gap, and the current
|
||||
# system doesn't model gaps, so just pretend it ends at 150 MHz for now. We
|
||||
# can model gaps later if needed.
|
||||
Radio("TRT ERA 7000 V/UHF", MHz(118), MHz(150), step=MHz(1)),
|
||||
Radio("TRT ERA 7200 UHF", MHz(225), MHz(400), step=MHz(1)),
|
||||
|
||||
# Tomcat radios
|
||||
# # https://www.heatblur.se/F-14Manual/general.html#an-arc-159-uhf-1-radio
|
||||
Radio("AN/ARC-159", MHz(225), MHz(400), step=MHz(1)),
|
||||
@@ -114,31 +113,23 @@ RADIOS: List[Radio] = [
|
||||
# to 400 MHz range, but we can't model gaps with the current implementation.
|
||||
# https://www.heatblur.se/F-14Manual/general.html#an-arc-182-v-uhf-2-radio
|
||||
Radio("AN/ARC-182", MHz(108), MHz(174), step=MHz(1)),
|
||||
|
||||
# Also capable of [103, 156) at 25 kHz intervals, but we can't do gaps.
|
||||
Radio("FR 22", MHz(225), MHz(400), step=kHz(50)),
|
||||
|
||||
# P-51 / P-47 Radio
|
||||
# 4 preset channels (A/B/C/D)
|
||||
Radio("SCR522", MHz(100), MHz(156), step=kHz(25)),
|
||||
|
||||
Radio("R&S M3AR VHF", MHz(120), MHz(174), step=MHz(1)),
|
||||
Radio("R&S M3AR UHF", MHz(225), MHz(400), step=MHz(1)),
|
||||
|
||||
# MiG-15bis
|
||||
Radio("RSI-6K HF", MHz(3, 750), MHz(5), step=kHz(25)),
|
||||
|
||||
# MiG-19P
|
||||
Radio("RSIU-4V", MHz(100), MHz(150), step=MHz(1)),
|
||||
|
||||
# MiG-21bis
|
||||
Radio("RSIU-5V", MHz(118), MHz(140), step=MHz(1)),
|
||||
|
||||
# Ka-50
|
||||
# Note: Also capable of 100MHz-150MHz, but we can't model gaps.
|
||||
Radio("R-800L1", MHz(220), MHz(400), step=kHz(25)),
|
||||
Radio("R-828", MHz(20), MHz(60), step=kHz(25)),
|
||||
|
||||
# UH-1H
|
||||
Radio("AN/ARC-51BX", MHz(225), MHz(400), step=kHz(50)),
|
||||
Radio("AN/ARC-131", MHz(30), MHz(76), step=kHz(50)),
|
||||
@@ -218,7 +209,8 @@ class RadioRegistry:
|
||||
# https://github.com/Khopa/dcs_liberation/issues/598
|
||||
channel = radio.last_channel
|
||||
logging.warning(
|
||||
f"No more free channels for {radio.name}. Reusing {channel}.")
|
||||
f"No more free channels for {radio.name}. Reusing {channel}."
|
||||
)
|
||||
return channel
|
||||
|
||||
def alloc_uhf(self) -> RadioFrequency:
|
||||
|
||||
@@ -25,8 +25,9 @@ class RunwayData:
|
||||
icls: Optional[int] = None
|
||||
|
||||
@classmethod
|
||||
def for_airfield(cls, airport: Airport, runway_heading: int,
|
||||
runway_name: str) -> RunwayData:
|
||||
def for_airfield(
|
||||
cls, airport: Airport, runway_heading: int, runway_name: str
|
||||
) -> RunwayData:
|
||||
"""Creates RunwayData for the given runway of an airfield.
|
||||
|
||||
Args:
|
||||
@@ -56,7 +57,7 @@ class RunwayData:
|
||||
atc=atc,
|
||||
tacan=tacan,
|
||||
tacan_callsign=tacan_callsign,
|
||||
ils=ils
|
||||
ils=ils,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -20,15 +20,19 @@ class BoforsGenerator(AirDefenseGroupGenerator):
|
||||
grid_x = random.randint(2, 3)
|
||||
grid_y = random.randint(2, 3)
|
||||
|
||||
spacing = random.randint(10,40)
|
||||
spacing = random.randint(10, 40)
|
||||
|
||||
index = 0
|
||||
for i in range(grid_x):
|
||||
for j in range(grid_y):
|
||||
index = index+1
|
||||
self.add_unit(AirDefence.AAA_Bofors_40mm, "AAA#" + str(index),
|
||||
self.position.x + spacing*i,
|
||||
self.position.y + spacing*j, self.heading)
|
||||
index = index + 1
|
||||
self.add_unit(
|
||||
AirDefence.AAA_40mm_Bofors,
|
||||
"AAA#" + str(index),
|
||||
self.position.x + spacing * i,
|
||||
self.position.y + spacing * j,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
|
||||
@@ -8,12 +8,12 @@ from gen.sam.airdefensegroupgenerator import (
|
||||
)
|
||||
|
||||
GFLAK = [
|
||||
AirDefence.AAA_Flak_Vierling_38,
|
||||
AirDefence.AAA_Flak_Vierling_38_Quad_20mm,
|
||||
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_Flak_38,
|
||||
AirDefence.AAA_Flak_38_20mm,
|
||||
]
|
||||
|
||||
|
||||
@@ -37,34 +37,64 @@ class FlakGenerator(AirDefenseGroupGenerator):
|
||||
|
||||
for i in range(grid_x):
|
||||
for j in range(grid_y):
|
||||
index = index+1
|
||||
self.add_unit(unit_type, "AAA#" + str(index),
|
||||
self.position.x + spacing*i + random.randint(1,5),
|
||||
self.position.y + spacing*j + random.randint(1,5), self.heading)
|
||||
index = index + 1
|
||||
self.add_unit(
|
||||
unit_type,
|
||||
"AAA#" + str(index),
|
||||
self.position.x + spacing * i + random.randint(1, 5),
|
||||
self.position.y + spacing * j + random.randint(1, 5),
|
||||
self.heading,
|
||||
)
|
||||
|
||||
if(mixed):
|
||||
if mixed:
|
||||
unit_type = random.choice(GFLAK)
|
||||
|
||||
# Search lights
|
||||
search_pos = self.get_circular_position(random.randint(2,3), 80)
|
||||
search_pos = self.get_circular_position(random.randint(2, 3), 80)
|
||||
for index, pos in enumerate(search_pos):
|
||||
self.add_unit(AirDefence.Flak_Searchlight_37, "SearchLight#" + str(index), pos[0], pos[1], self.heading)
|
||||
self.add_unit(
|
||||
AirDefence.SL_Flakscheinwerfer_37,
|
||||
"SearchLight#" + str(index),
|
||||
pos[0],
|
||||
pos[1],
|
||||
self.heading,
|
||||
)
|
||||
|
||||
# Support
|
||||
self.add_unit(AirDefence.Maschinensatz_33, "MC33#", self.position.x-20, self.position.y-20, self.heading)
|
||||
self.add_unit(AirDefence.AAA_Kdo_G_40, "KDO#", self.position.x - 25, self.position.y - 20,
|
||||
self.heading)
|
||||
self.add_unit(
|
||||
AirDefence.PU_Maschinensatz_33,
|
||||
"MC33#",
|
||||
self.position.x - 20,
|
||||
self.position.y - 20,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
AirDefence.AAA_SP_Kdo_G_40,
|
||||
"KDO#",
|
||||
self.position.x - 25,
|
||||
self.position.y - 20,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
# Commander
|
||||
self.add_unit(Unarmed.Kübelwagen_82, "Kubel#", self.position.x - 35, self.position.y - 20,
|
||||
self.heading)
|
||||
self.add_unit(
|
||||
Unarmed.LUV_Kubelwagen_82,
|
||||
"Kubel#",
|
||||
self.position.x - 35,
|
||||
self.position.y - 20,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
# Some Opel Blitz trucks
|
||||
for i in range(int(max(1,grid_x/2))):
|
||||
for j in range(int(max(1,grid_x/2))):
|
||||
self.add_unit(Unarmed.Blitz_3_6_6700A, "BLITZ#" + str(index),
|
||||
self.position.x + 125 + 15*i + random.randint(1,5),
|
||||
self.position.y + 15*j + random.randint(1,5), 75)
|
||||
for i in range(int(max(1, grid_x / 2))):
|
||||
for j in range(int(max(1, grid_x / 2))):
|
||||
self.add_unit(
|
||||
Unarmed.Truck_Opel_Blitz,
|
||||
"BLITZ#" + str(index),
|
||||
self.position.x + 125 + 15 * i + random.randint(1, 5),
|
||||
self.position.y + 15 * j + random.randint(1, 5),
|
||||
75,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
|
||||
@@ -24,12 +24,22 @@ class Flak18Generator(AirDefenseGroupGenerator):
|
||||
for i in range(3):
|
||||
for j in range(2):
|
||||
index = index + 1
|
||||
self.add_unit(AirDefence.AAA_8_8cm_Flak_18, "AAA#" + str(index),
|
||||
self.position.x + spacing * i + random.randint(1, 5),
|
||||
self.position.y + spacing * j + random.randint(1, 5), self.heading)
|
||||
self.add_unit(
|
||||
AirDefence.AAA_8_8cm_Flak_18,
|
||||
"AAA#" + str(index),
|
||||
self.position.x + spacing * i + random.randint(1, 5),
|
||||
self.position.y + spacing * j + random.randint(1, 5),
|
||||
self.heading,
|
||||
)
|
||||
|
||||
# Add a commander truck
|
||||
self.add_unit(Unarmed.Blitz_3_6_6700A, "Blitz#", self.position.x - 35, self.position.y - 20, self.heading)
|
||||
self.add_unit(
|
||||
Unarmed.Truck_Opel_Blitz,
|
||||
"Blitz#",
|
||||
self.position.x - 35,
|
||||
self.position.y - 20,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
|
||||
@@ -19,15 +19,25 @@ class KS19Generator(AirDefenseGroupGenerator):
|
||||
|
||||
spacing = random.randint(10, 40)
|
||||
|
||||
self.add_unit(highdigitsams.AAA_SON_9_Fire_Can, "TR", self.position.x - 20, self.position.y - 20, self.heading)
|
||||
self.add_unit(
|
||||
highdigitsams.AAA_SON_9_Fire_Can,
|
||||
"TR",
|
||||
self.position.x - 20,
|
||||
self.position.y - 20,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
index = 0
|
||||
for i in range(3):
|
||||
for j in range(3):
|
||||
index = index + 1
|
||||
self.add_unit(highdigitsams.AAA_100mm_KS_19, "AAA#" + str(index),
|
||||
self.position.x + spacing * i,
|
||||
self.position.y + spacing * j, self.heading)
|
||||
self.add_unit(
|
||||
highdigitsams.AAA_100mm_KS_19,
|
||||
"AAA#" + str(index),
|
||||
self.position.x + spacing * i,
|
||||
self.position.y + spacing * j,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
|
||||
@@ -20,21 +20,63 @@ class AllyWW2FlakGenerator(AirDefenseGroupGenerator):
|
||||
|
||||
positions = self.get_circular_position(4, launcher_distance=30, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.AA_gun_QF_3_7, "AA#" + str(i), position[0], position[1], position[2])
|
||||
self.add_unit(
|
||||
AirDefence.AAA_QF_3_7,
|
||||
"AA#" + str(i),
|
||||
position[0],
|
||||
position[1],
|
||||
position[2],
|
||||
)
|
||||
|
||||
positions = self.get_circular_position(8, launcher_distance=60, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.AAA_M1_37mm, "AA#" + str(4 + i), position[0], position[1], position[2])
|
||||
self.add_unit(
|
||||
AirDefence.AAA_M1_37mm,
|
||||
"AA#" + str(4 + i),
|
||||
position[0],
|
||||
position[1],
|
||||
position[2],
|
||||
)
|
||||
|
||||
positions = self.get_circular_position(8, launcher_distance=90, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.AAA_M45_Quadmount, "AA#" + str(12 + i), position[0], position[1], position[2])
|
||||
self.add_unit(
|
||||
AirDefence.AAA_M45_Quadmount_HB_12_7mm,
|
||||
"AA#" + str(12 + i),
|
||||
position[0],
|
||||
position[1],
|
||||
position[2],
|
||||
)
|
||||
|
||||
# Add a commander truck
|
||||
self.add_unit(Unarmed.Willys_MB, "CMD#1", self.position.x, self.position.y - 20, random.randint(0, 360))
|
||||
self.add_unit(Armor.M30_Cargo_Carrier, "LOG#1", self.position.x, self.position.y + 20, random.randint(0, 360))
|
||||
self.add_unit(Armor.M4_Tractor, "LOG#2", self.position.x + 20, self.position.y, random.randint(0, 360))
|
||||
self.add_unit(Unarmed.Bedford_MWD, "LOG#3", self.position.x - 20, self.position.y, random.randint(0, 360))
|
||||
self.add_unit(
|
||||
Unarmed.Car_Willys_Jeep,
|
||||
"CMD#1",
|
||||
self.position.x,
|
||||
self.position.y - 20,
|
||||
random.randint(0, 360),
|
||||
)
|
||||
self.add_unit(
|
||||
Unarmed.Carrier_M30_Cargo,
|
||||
"LOG#1",
|
||||
self.position.x,
|
||||
self.position.y + 20,
|
||||
random.randint(0, 360),
|
||||
)
|
||||
self.add_unit(
|
||||
Unarmed.Tractor_M4_Hi_Speed,
|
||||
"LOG#2",
|
||||
self.position.x + 20,
|
||||
self.position.y,
|
||||
random.randint(0, 360),
|
||||
)
|
||||
self.add_unit(
|
||||
Unarmed.Truck_Bedford,
|
||||
"LOG#3",
|
||||
self.position.x - 20,
|
||||
self.position.y,
|
||||
random.randint(0, 360),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
|
||||
@@ -16,9 +16,17 @@ class ZSU57Generator(AirDefenseGroupGenerator):
|
||||
|
||||
def generate(self):
|
||||
num_launchers = 5
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=110, coverage=360)
|
||||
positions = self.get_circular_position(
|
||||
num_launchers, launcher_distance=110, coverage=360
|
||||
)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.AAA_ZSU_57_2, "SPAA#" + str(i), position[0], position[1], position[2])
|
||||
self.add_unit(
|
||||
AirDefence.SPAAA_ZSU_57_2,
|
||||
"SPAA#" + str(i),
|
||||
position[0],
|
||||
position[1],
|
||||
position[2],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
|
||||
@@ -20,15 +20,19 @@ class ZU23InsurgentGenerator(AirDefenseGroupGenerator):
|
||||
grid_x = random.randint(2, 3)
|
||||
grid_y = random.randint(2, 3)
|
||||
|
||||
spacing = random.randint(10,40)
|
||||
spacing = random.randint(10, 40)
|
||||
|
||||
index = 0
|
||||
for i in range(grid_x):
|
||||
for j in range(grid_y):
|
||||
index = index+1
|
||||
self.add_unit(AirDefence.AAA_ZU_23_Insurgent_Closed, "AAA#" + str(index),
|
||||
self.position.x + spacing*i,
|
||||
self.position.y + spacing*j, self.heading)
|
||||
index = index + 1
|
||||
self.add_unit(
|
||||
AirDefence.AAA_ZU_23_Closed_Emplacement_Insurgent,
|
||||
"AAA#" + str(index),
|
||||
self.position.x + spacing * i,
|
||||
self.position.y + spacing * j,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
|
||||
@@ -28,8 +28,9 @@ class AirDefenseGroupGenerator(GroupGenerator, ABC):
|
||||
self.auxiliary_groups: List[VehicleGroup] = []
|
||||
|
||||
def add_auxiliary_group(self, name_suffix: str) -> VehicleGroup:
|
||||
group = VehicleGroup(self.game.next_group_id(),
|
||||
"|".join([self.go.group_name, name_suffix]))
|
||||
group = VehicleGroup(
|
||||
self.game.next_group_id(), "|".join([self.go.group_name, name_suffix])
|
||||
)
|
||||
self.auxiliary_groups.append(group)
|
||||
return group
|
||||
|
||||
@@ -37,7 +38,8 @@ class AirDefenseGroupGenerator(GroupGenerator, ABC):
|
||||
raise RuntimeError(
|
||||
"Deprecated call to AirDefenseGroupGenerator.get_generated_group "
|
||||
"misses auxiliary groups. Use AirDefenseGroupGenerator.groups "
|
||||
"instead.")
|
||||
"instead."
|
||||
)
|
||||
|
||||
@property
|
||||
def groups(self) -> Iterator[VehicleGroup]:
|
||||
|
||||
@@ -12,13 +12,13 @@ from gen.sam.group_generator import GroupGenerator
|
||||
class EarlyColdWarFlakGenerator(AirDefenseGroupGenerator):
|
||||
"""
|
||||
This generator attempt to mimic an early cold-war era flak AAA site.
|
||||
The Flak 18 88mm is used as the main long range gun and 2 Bofors 40mm guns provide short range protection.
|
||||
The Flak 18 88mm is used as the main long range gun, S-60 is used as a mid range gun and 2 Bofors 40mm guns provide short range protection.
|
||||
|
||||
This does not include search lights and telemeter computer (Kdo.G 40) because these are paid units only available in WW2 asset pack
|
||||
"""
|
||||
|
||||
name = "Early Cold War Flak Site"
|
||||
price = 58
|
||||
price = 74
|
||||
|
||||
def generate(self):
|
||||
|
||||
@@ -29,18 +29,54 @@ class EarlyColdWarFlakGenerator(AirDefenseGroupGenerator):
|
||||
for i in range(3):
|
||||
for j in range(2):
|
||||
index = index + 1
|
||||
self.add_unit(AirDefence.AAA_8_8cm_Flak_18, "AAA#" + str(index),
|
||||
self.position.x + spacing * i + random.randint(1, 5),
|
||||
self.position.y + spacing * j + random.randint(1, 5), self.heading)
|
||||
self.add_unit(
|
||||
AirDefence.AAA_8_8cm_Flak_18,
|
||||
"AAA#" + str(index),
|
||||
self.position.x + spacing * i + random.randint(1, 5),
|
||||
self.position.y + spacing * j + random.randint(1, 5),
|
||||
self.heading,
|
||||
)
|
||||
|
||||
# Medium range guns
|
||||
self.add_unit(
|
||||
AirDefence.AAA_S_60_57mm,
|
||||
"SHO#1",
|
||||
self.position.x - 40,
|
||||
self.position.y - 40,
|
||||
self.heading + 180,
|
||||
),
|
||||
self.add_unit(
|
||||
AirDefence.AAA_S_60_57mm,
|
||||
"SHO#2",
|
||||
self.position.x + spacing * 2 + 40,
|
||||
self.position.y + spacing + 40,
|
||||
self.heading,
|
||||
),
|
||||
|
||||
# Short range guns
|
||||
self.add_unit(AirDefence.AAA_Bofors_40mm, "SHO#1",
|
||||
self.position.x - 40, self.position.y - 40, self.heading + 180),
|
||||
self.add_unit(AirDefence.AAA_Bofors_40mm, "SHO#2",
|
||||
self.position.x + spacing * 2 + 40, self.position.y + spacing + 40, self.heading),
|
||||
self.add_unit(
|
||||
AirDefence.AAA_ZU_23_Closed_Emplacement,
|
||||
"SHO#3",
|
||||
self.position.x - 80,
|
||||
self.position.y - 40,
|
||||
self.heading + 180,
|
||||
),
|
||||
self.add_unit(
|
||||
AirDefence.AAA_ZU_23_Closed_Emplacement,
|
||||
"SHO#4",
|
||||
self.position.x + spacing * 2 + 80,
|
||||
self.position.y + spacing + 40,
|
||||
self.heading,
|
||||
),
|
||||
|
||||
# Add a truck
|
||||
self.add_unit(Unarmed.Transport_KAMAZ_43101, "Truck#", self.position.x - 60, self.position.y - 20, self.heading)
|
||||
self.add_unit(
|
||||
Unarmed.Truck_KAMAZ_43101,
|
||||
"Truck#",
|
||||
self.position.x - 60,
|
||||
self.position.y - 20,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
@@ -50,7 +86,7 @@ class EarlyColdWarFlakGenerator(AirDefenseGroupGenerator):
|
||||
class ColdWarFlakGenerator(AirDefenseGroupGenerator):
|
||||
"""
|
||||
This generator attempt to mimic a cold-war era flak AAA site.
|
||||
The Flak 18 88mm is used as the main long range gun while 2 Zu-23 guns provide short range protection.
|
||||
The Flak 18 88mm is used as the main long range gun, 2 S-60 57mm gun improve mid range firepower, while 2 Zu-23 guns even provide short range protection.
|
||||
The site is also fitted with a P-19 radar for early detection.
|
||||
"""
|
||||
|
||||
@@ -66,18 +102,54 @@ class ColdWarFlakGenerator(AirDefenseGroupGenerator):
|
||||
for i in range(3):
|
||||
for j in range(2):
|
||||
index = index + 1
|
||||
self.add_unit(AirDefence.AAA_8_8cm_Flak_18, "AAA#" + str(index),
|
||||
self.position.x + spacing * i + random.randint(1, 5),
|
||||
self.position.y + spacing * j + random.randint(1, 5), self.heading)
|
||||
self.add_unit(
|
||||
AirDefence.AAA_8_8cm_Flak_18,
|
||||
"AAA#" + str(index),
|
||||
self.position.x + spacing * i + random.randint(1, 5),
|
||||
self.position.y + spacing * j + random.randint(1, 5),
|
||||
self.heading,
|
||||
)
|
||||
|
||||
# Medium range guns
|
||||
self.add_unit(
|
||||
AirDefence.AAA_S_60_57mm,
|
||||
"SHO#1",
|
||||
self.position.x - 40,
|
||||
self.position.y - 40,
|
||||
self.heading + 180,
|
||||
),
|
||||
self.add_unit(
|
||||
AirDefence.AAA_S_60_57mm,
|
||||
"SHO#2",
|
||||
self.position.x + spacing * 2 + 40,
|
||||
self.position.y + spacing + 40,
|
||||
self.heading,
|
||||
),
|
||||
|
||||
# Short range guns
|
||||
self.add_unit(AirDefence.AAA_ZU_23_Closed, "SHO#1",
|
||||
self.position.x - 40, self.position.y - 40, self.heading + 180),
|
||||
self.add_unit(AirDefence.AAA_ZU_23_Closed, "SHO#2",
|
||||
self.position.x + spacing * 2 + 40, self.position.y + spacing + 40, self.heading),
|
||||
self.add_unit(
|
||||
AirDefence.AAA_ZU_23_Closed_Emplacement,
|
||||
"SHO#3",
|
||||
self.position.x - 80,
|
||||
self.position.y - 40,
|
||||
self.heading + 180,
|
||||
),
|
||||
self.add_unit(
|
||||
AirDefence.AAA_ZU_23_Closed_Emplacement,
|
||||
"SHO#4",
|
||||
self.position.x + spacing * 2 + 80,
|
||||
self.position.y + spacing + 40,
|
||||
self.heading,
|
||||
),
|
||||
|
||||
# Add a P19 Radar for EWR
|
||||
self.add_unit(AirDefence.SAM_SR_P_19, "SR#0", self.position.x - 60, self.position.y - 20, self.heading)
|
||||
self.add_unit(
|
||||
AirDefence.SAM_P19_Flat_Face_SR__SA_2_3,
|
||||
"SR#0",
|
||||
self.position.x - 60,
|
||||
self.position.y - 20,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
|
||||
63
gen/sam/ewr_group_generator.py
Normal file
63
gen/sam/ewr_group_generator.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import random
|
||||
from typing import List, Optional, Type
|
||||
|
||||
from dcs.unitgroup import VehicleGroup
|
||||
|
||||
from game import Game
|
||||
from game.factions.faction import Faction
|
||||
from game.theater.theatergroundobject import EwrGroundObject
|
||||
from gen.sam.ewrs import (
|
||||
BigBirdGenerator,
|
||||
BoxSpringGenerator,
|
||||
DogEarGenerator,
|
||||
FlatFaceGenerator,
|
||||
HawkEwrGenerator,
|
||||
PatriotEwrGenerator,
|
||||
RolandEwrGenerator,
|
||||
SnowDriftGenerator,
|
||||
StraightFlushGenerator,
|
||||
TallRackGenerator,
|
||||
)
|
||||
from gen.sam.group_generator import GroupGenerator
|
||||
|
||||
EWR_MAP = {
|
||||
"BoxSpringGenerator": BoxSpringGenerator,
|
||||
"TallRackGenerator": TallRackGenerator,
|
||||
"DogEarGenerator": DogEarGenerator,
|
||||
"RolandEwrGenerator": RolandEwrGenerator,
|
||||
"FlatFaceGenerator": FlatFaceGenerator,
|
||||
"PatriotEwrGenerator": PatriotEwrGenerator,
|
||||
"BigBirdGenerator": BigBirdGenerator,
|
||||
"SnowDriftGenerator": SnowDriftGenerator,
|
||||
"StraightFlushGenerator": StraightFlushGenerator,
|
||||
"HawkEwrGenerator": HawkEwrGenerator,
|
||||
}
|
||||
|
||||
|
||||
def get_faction_possible_ewrs_generator(
|
||||
faction: Faction,
|
||||
) -> List[Type[GroupGenerator]]:
|
||||
"""
|
||||
Return the list of possible EWR generators for the given faction
|
||||
:param faction: Faction name to search units for
|
||||
"""
|
||||
return [EWR_MAP[s] for s in faction.ewrs]
|
||||
|
||||
|
||||
def generate_ewr_group(
|
||||
game: Game, ground_object: EwrGroundObject, faction: Faction
|
||||
) -> Optional[VehicleGroup]:
|
||||
"""Generates an early warning radar group.
|
||||
|
||||
:param game: The Game.
|
||||
:param ground_object: The ground object which will own the EWR group.
|
||||
:param faction: Owner faction.
|
||||
:return: The generated group, or None if one could not be generated.
|
||||
"""
|
||||
generators = get_faction_possible_ewrs_generator(faction)
|
||||
if len(generators) > 0:
|
||||
generator_class = random.choice(generators)
|
||||
generator = generator_class(game, ground_object)
|
||||
generator.generate()
|
||||
return generator.get_generated_group()
|
||||
return None
|
||||
@@ -10,8 +10,9 @@ class EwrGenerator(GroupGenerator):
|
||||
raise NotImplementedError
|
||||
|
||||
def generate(self) -> None:
|
||||
self.add_unit(self.unit_type, "EWR", self.position.x, self.position.y,
|
||||
self.heading)
|
||||
self.add_unit(
|
||||
self.unit_type, "EWR", self.position.x, self.position.y, self.heading
|
||||
)
|
||||
|
||||
|
||||
class BoxSpringGenerator(EwrGenerator):
|
||||
@@ -32,7 +33,7 @@ class DogEarGenerator(EwrGenerator):
|
||||
This is the SA-8 search radar, but used as an early warning radar.
|
||||
"""
|
||||
|
||||
unit_type = AirDefence.CP_9S80M1_Sborka
|
||||
unit_type = AirDefence.MCC_SR_Sborka_Dog_Ear_SR
|
||||
|
||||
|
||||
class RolandEwrGenerator(EwrGenerator):
|
||||
@@ -50,7 +51,7 @@ class FlatFaceGenerator(EwrGenerator):
|
||||
This is the SA-3 search radar, but used as an early warning radar.
|
||||
"""
|
||||
|
||||
unit_type = AirDefence.SAM_SR_P_19
|
||||
unit_type = AirDefence.SAM_P19_Flat_Face_SR__SA_2_3
|
||||
|
||||
|
||||
class PatriotEwrGenerator(EwrGenerator):
|
||||
@@ -59,7 +60,7 @@ class PatriotEwrGenerator(EwrGenerator):
|
||||
This is the Patriot search/track radar, but used as an early warning radar.
|
||||
"""
|
||||
|
||||
unit_type = AirDefence.SAM_Patriot_STR_AN_MPQ_53
|
||||
unit_type = AirDefence.SAM_Patriot_STR
|
||||
|
||||
|
||||
class BigBirdGenerator(EwrGenerator):
|
||||
@@ -68,7 +69,7 @@ class BigBirdGenerator(EwrGenerator):
|
||||
This is the SA-10 track radar, but used as an early warning radar.
|
||||
"""
|
||||
|
||||
unit_type = AirDefence.SAM_SA_10_S_300PS_SR_64H6E
|
||||
unit_type = AirDefence.SAM_SA_10_S_300_Grumble_Big_Bird_SR
|
||||
|
||||
|
||||
class SnowDriftGenerator(EwrGenerator):
|
||||
@@ -77,7 +78,7 @@ class SnowDriftGenerator(EwrGenerator):
|
||||
This is the SA-11 search radar, but used as an early warning radar.
|
||||
"""
|
||||
|
||||
unit_type = AirDefence.SAM_SA_11_Buk_SR_9S18M1
|
||||
unit_type = AirDefence.SAM_SA_11_Buk_Gadfly_Snow_Drift_SR
|
||||
|
||||
|
||||
class StraightFlushGenerator(EwrGenerator):
|
||||
@@ -86,7 +87,7 @@ class StraightFlushGenerator(EwrGenerator):
|
||||
This is the SA-6 search/track radar, but used as an early warning radar.
|
||||
"""
|
||||
|
||||
unit_type = AirDefence.SAM_SA_6_Kub_STR_9S91
|
||||
unit_type = AirDefence.SAM_SA_6_Kub_Long_Track_STR
|
||||
|
||||
|
||||
class HawkEwrGenerator(EwrGenerator):
|
||||
@@ -95,4 +96,4 @@ class HawkEwrGenerator(EwrGenerator):
|
||||
This is the Hawk search radar, but used as an early warning radar.
|
||||
"""
|
||||
|
||||
unit_type = AirDefence.SAM_Hawk_SR_AN_MPQ_50
|
||||
unit_type = AirDefence.SAM_Hawk_SR__AN_MPQ_50
|
||||
|
||||
@@ -17,27 +17,93 @@ class FreyaGenerator(AirDefenseGroupGenerator):
|
||||
def generate(self):
|
||||
|
||||
# TODO : would be better with the Concrete structure that is supposed to protect it
|
||||
self.add_unit(AirDefence.EWR_FuMG_401_Freya_LZ, "EWR#1", self.position.x, self.position.y, self.heading)
|
||||
self.add_unit(
|
||||
AirDefence.EWR_FuMG_401_Freya_LZ,
|
||||
"EWR#1",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
positions = self.get_circular_position(4, launcher_distance=50, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.AAA_Flak_Vierling_38, "AA#" + str(i), position[0], position[1], position[2])
|
||||
self.add_unit(
|
||||
AirDefence.AAA_Flak_Vierling_38_Quad_20mm,
|
||||
"AA#" + str(i),
|
||||
position[0],
|
||||
position[1],
|
||||
position[2],
|
||||
)
|
||||
|
||||
positions = self.get_circular_position(4, launcher_distance=100, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.AAA_8_8cm_Flak_18, "AA#" + str(4+i), position[0], position[1], position[2])
|
||||
self.add_unit(
|
||||
AirDefence.AAA_8_8cm_Flak_18,
|
||||
"AA#" + str(4 + i),
|
||||
position[0],
|
||||
position[1],
|
||||
position[2],
|
||||
)
|
||||
|
||||
# Command/Logi
|
||||
self.add_unit(Unarmed.Kübelwagen_82, "Kubel#1", self.position.x - 20, self.position.y - 20, self.heading)
|
||||
self.add_unit(Unarmed.Sd_Kfz_7, "Sdkfz#1", self.position.x + 20, self.position.y + 22, self.heading)
|
||||
self.add_unit(Unarmed.Sd_Kfz_2, "Sdkfz#2", self.position.x - 22, self.position.y + 20, self.heading)
|
||||
self.add_unit(
|
||||
Unarmed.LUV_Kubelwagen_82,
|
||||
"Kubel#1",
|
||||
self.position.x - 20,
|
||||
self.position.y - 20,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
Unarmed.Carrier_Sd_Kfz_7_Tractor,
|
||||
"Sdkfz#1",
|
||||
self.position.x + 20,
|
||||
self.position.y + 22,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
Unarmed.LUV_Kettenrad,
|
||||
"Sdkfz#2",
|
||||
self.position.x - 22,
|
||||
self.position.y + 20,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
# Maschinensatz_33 and Kdo.g 40 Telemeter
|
||||
self.add_unit(AirDefence.Maschinensatz_33, "Energy#1", self.position.x + 20, self.position.y - 20, self.heading)
|
||||
self.add_unit(AirDefence.AAA_Kdo_G_40, "Telemeter#1", self.position.x + 20, self.position.y - 10, self.heading)
|
||||
self.add_unit(Infantry.Infantry_Mauser_98, "Inf#1", self.position.x + 20, self.position.y - 14, self.heading)
|
||||
self.add_unit(Infantry.Infantry_Mauser_98, "Inf#2", self.position.x + 20, self.position.y - 22, self.heading)
|
||||
self.add_unit(Infantry.Infantry_Mauser_98, "Inf#3", self.position.x + 20, self.position.y - 24, self.heading + 45)
|
||||
# PU_Maschinensatz_33 and Kdo.g 40 Telemeter
|
||||
self.add_unit(
|
||||
AirDefence.PU_Maschinensatz_33,
|
||||
"Energy#1",
|
||||
self.position.x + 20,
|
||||
self.position.y - 20,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
AirDefence.AAA_SP_Kdo_G_40,
|
||||
"Telemeter#1",
|
||||
self.position.x + 20,
|
||||
self.position.y - 10,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
Infantry.Infantry_Mauser_98,
|
||||
"Inf#1",
|
||||
self.position.x + 20,
|
||||
self.position.y - 14,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
Infantry.Infantry_Mauser_98,
|
||||
"Inf#2",
|
||||
self.position.x + 20,
|
||||
self.position.y - 22,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
Infantry.Infantry_Mauser_98,
|
||||
"Inf#3",
|
||||
self.position.x + 20,
|
||||
self.position.y - 24,
|
||||
self.heading + 45,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
|
||||
@@ -23,14 +23,12 @@ if TYPE_CHECKING:
|
||||
# care about in the format we want if we just generate our own group description
|
||||
# types rather than pydcs groups.
|
||||
class GroupGenerator:
|
||||
|
||||
def __init__(self, game: Game, ground_object: TheaterGroundObject) -> None:
|
||||
self.game = game
|
||||
self.go = ground_object
|
||||
self.position = ground_object.position
|
||||
self.heading = random.randint(0, 359)
|
||||
self.vg = unitgroup.VehicleGroup(self.game.next_group_id(),
|
||||
self.go.group_name)
|
||||
self.vg = unitgroup.VehicleGroup(self.game.next_group_id(), self.go.group_name)
|
||||
wp = self.vg.add_waypoint(self.position, PointAction.OffRoad, 0)
|
||||
wp.ETA_locked = True
|
||||
|
||||
@@ -40,16 +38,27 @@ class GroupGenerator:
|
||||
def get_generated_group(self) -> unitgroup.VehicleGroup:
|
||||
return self.vg
|
||||
|
||||
def add_unit(self, unit_type: Type[VehicleType], name: str, pos_x: float,
|
||||
pos_y: float, heading: int) -> Vehicle:
|
||||
return self.add_unit_to_group(self.vg, unit_type, name,
|
||||
Point(pos_x, pos_y), heading)
|
||||
def add_unit(
|
||||
self,
|
||||
unit_type: Type[VehicleType],
|
||||
name: str,
|
||||
pos_x: float,
|
||||
pos_y: float,
|
||||
heading: int,
|
||||
) -> Vehicle:
|
||||
return self.add_unit_to_group(
|
||||
self.vg, unit_type, name, Point(pos_x, pos_y), heading
|
||||
)
|
||||
|
||||
def add_unit_to_group(self, group: unitgroup.VehicleGroup,
|
||||
unit_type: Type[VehicleType], name: str,
|
||||
position: Point, heading: int) -> Vehicle:
|
||||
unit = Vehicle(self.game.next_unit_id(),
|
||||
f"{group.name}|{name}", unit_type.id)
|
||||
def add_unit_to_group(
|
||||
self,
|
||||
group: unitgroup.VehicleGroup,
|
||||
unit_type: Type[VehicleType],
|
||||
name: str,
|
||||
position: Point,
|
||||
heading: int,
|
||||
) -> Vehicle:
|
||||
unit = Vehicle(self.game.next_unit_id(), f"{group.name}|{name}", unit_type.id)
|
||||
unit.position = position
|
||||
unit.heading = heading
|
||||
group.add_unit(unit)
|
||||
@@ -82,31 +91,36 @@ class GroupGenerator:
|
||||
current_offset = self.heading
|
||||
current_offset -= outer_offset * (math.ceil(num_units / 2) - 1)
|
||||
for x in range(1, num_units + 1):
|
||||
positions.append((
|
||||
self.position.x + launcher_distance * math.cos(math.radians(current_offset)),
|
||||
self.position.y + launcher_distance * math.sin(math.radians(current_offset)),
|
||||
current_offset,
|
||||
))
|
||||
positions.append(
|
||||
(
|
||||
self.position.x
|
||||
+ launcher_distance * math.cos(math.radians(current_offset)),
|
||||
self.position.y
|
||||
+ launcher_distance * math.sin(math.radians(current_offset)),
|
||||
current_offset,
|
||||
)
|
||||
)
|
||||
current_offset += outer_offset
|
||||
return positions
|
||||
|
||||
|
||||
class ShipGroupGenerator(GroupGenerator):
|
||||
"""Abstract class for other ship generator classes"""
|
||||
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
|
||||
|
||||
def __init__(
|
||||
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
||||
):
|
||||
self.game = game
|
||||
self.go = ground_object
|
||||
self.position = ground_object.position
|
||||
self.heading = random.randint(0, 359)
|
||||
self.faction = faction
|
||||
self.vg = unitgroup.ShipGroup(self.game.next_group_id(),
|
||||
self.go.group_name)
|
||||
self.vg = unitgroup.ShipGroup(self.game.next_group_id(), self.go.group_name)
|
||||
wp = self.vg.add_waypoint(self.position, 0)
|
||||
wp.ETA_locked = True
|
||||
|
||||
|
||||
def add_unit(self, unit_type, name, pos_x, pos_y, heading) -> Ship:
|
||||
unit = Ship(self.game.next_unit_id(),
|
||||
f"{self.go.group_name}|{name}", unit_type)
|
||||
unit = Ship(self.game.next_unit_id(), f"{self.go.group_name}|{name}", unit_type)
|
||||
unit.position.x = pos_x
|
||||
unit.position.y = pos_y
|
||||
unit.heading = heading
|
||||
|
||||
@@ -19,10 +19,24 @@ class AvengerGenerator(AirDefenseGroupGenerator):
|
||||
def generate(self):
|
||||
num_launchers = random.randint(2, 3)
|
||||
|
||||
self.add_unit(Unarmed.Transport_M818, "TRUCK", self.position.x, self.position.y, self.heading)
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=110, coverage=180)
|
||||
self.add_unit(
|
||||
Unarmed.Truck_M818_6x6,
|
||||
"TRUCK",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
self.heading,
|
||||
)
|
||||
positions = self.get_circular_position(
|
||||
num_launchers, launcher_distance=110, coverage=180
|
||||
)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.SAM_Avenger_M1097, "SPAA#" + str(i), position[0], position[1], position[2])
|
||||
self.add_unit(
|
||||
AirDefence.SAM_Avenger__Stinger,
|
||||
"SPAA#" + str(i),
|
||||
position[0],
|
||||
position[1],
|
||||
position[2],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
|
||||
@@ -19,10 +19,24 @@ class ChaparralGenerator(AirDefenseGroupGenerator):
|
||||
def generate(self):
|
||||
num_launchers = random.randint(2, 4)
|
||||
|
||||
self.add_unit(Unarmed.Transport_M818, "TRUCK", self.position.x, self.position.y, self.heading)
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=110, coverage=180)
|
||||
self.add_unit(
|
||||
Unarmed.Truck_M818_6x6,
|
||||
"TRUCK",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
self.heading,
|
||||
)
|
||||
positions = self.get_circular_position(
|
||||
num_launchers, launcher_distance=110, coverage=180
|
||||
)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.SAM_Chaparral_M48, "SPAA#" + str(i), position[0], position[1], position[2])
|
||||
self.add_unit(
|
||||
AirDefence.SAM_Chaparral_M48,
|
||||
"SPAA#" + str(i),
|
||||
position[0],
|
||||
position[1],
|
||||
position[2],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
|
||||
@@ -17,10 +17,28 @@ class GepardGenerator(AirDefenseGroupGenerator):
|
||||
price = 50
|
||||
|
||||
def generate(self):
|
||||
self.add_unit(AirDefence.SPAAA_Gepard, "SPAAA", self.position.x, self.position.y, self.heading)
|
||||
self.add_unit(
|
||||
AirDefence.SPAAA_Gepard,
|
||||
"SPAAA",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
self.heading,
|
||||
)
|
||||
if random.randint(0, 1) == 1:
|
||||
self.add_unit(AirDefence.SPAAA_Gepard, "SPAAA2", self.position.x, self.position.y, self.heading)
|
||||
self.add_unit(Unarmed.Transport_M818, "TRUCK", self.position.x + 80, self.position.y, self.heading)
|
||||
self.add_unit(
|
||||
AirDefence.SPAAA_Gepard,
|
||||
"SPAAA2",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
Unarmed.Truck_M818_6x6,
|
||||
"TRUCK",
|
||||
self.position.x + 80,
|
||||
self.position.y,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user