mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Compare commits
176 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0e5a707fb | ||
|
|
4555a4968d | ||
|
|
ae34e4749b | ||
|
|
635eee9590 | ||
|
|
f0558c4c1e | ||
|
|
637ca8fbca | ||
|
|
e4e65df976 | ||
|
|
29579a2aec | ||
|
|
e32b43cffb | ||
|
|
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 | ||
|
|
e40b916b07 | ||
|
|
2ffe3bf722 | ||
|
|
1bc994c102 | ||
|
|
21be4d38e1 | ||
|
|
3b58c571b3 | ||
|
|
e0430cf607 | ||
|
|
f7889b785d | ||
|
|
27829a024a | ||
|
|
a0fda2552f | ||
|
|
65c185ebd2 | ||
|
|
fb425d3524 | ||
|
|
5792eb354c | ||
|
|
45300b64c5 | ||
|
|
dce7d91511 | ||
|
|
4b7ef46f82 | ||
|
|
b67e6d20f1 | ||
|
|
3d1afa74d4 | ||
|
|
0ae6575087 | ||
|
|
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`).
|
- DCS Liberation save file (the `.liberation` file you save from the DCS Liberation window). By default these are located in your DCS saved games directory (`%USERPROFILE%/Saved Games/DCS`).
|
||||||
- The generated mission file (the `.miz` file that you load in DCS to play the turn). By default these are located in your missions directory (`%USERPROFILE%/Saved Games/DCS/Missions`).
|
- The generated mission file (the `.miz` file that you load in DCS to play the turn). By default these are located in your missions directory (`%USERPROFILE%/Saved Games/DCS/Missions`).
|
||||||
- A tacview track file, especially when demonstrating an issue with AI behavior. By default these are locaed in your Tacview tracks directory (`%USERPROFILE%/Documents/Tacview`).
|
- A tacview track file, especially when demonstrating an issue with AI behavior. By default these are located in your Tacview tracks directory (`%USERPROFILE%/Documents/Tacview`).
|
||||||
|
- The state.json file from the finished mission when the problem is related to results processing. By default these are located in your Liberation install directory.
|
||||||
|
|
||||||
**Version information (please complete the following information):**
|
**Version information (please complete the following information):**
|
||||||
- DCS Liberation [e.g. 2.3.1]:
|
- DCS Liberation [e.g. 2.3.1]:
|
||||||
|
|||||||
13
.github/workflows/black.yml
vendored
Normal file
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"
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,6 +11,7 @@ a.py
|
|||||||
resources/tools/a.miz
|
resources/tools/a.miz
|
||||||
# User-specific stuff
|
# User-specific stuff
|
||||||
.idea/
|
.idea/
|
||||||
|
.env
|
||||||
|
|
||||||
/kneeboards
|
/kneeboards
|
||||||
/liberation_preferences.json
|
/liberation_preferences.json
|
||||||
|
|||||||
6
.pre-commit-config.yaml
Normal file
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
|
||||||
71
changelog.md
71
changelog.md
@@ -1,3 +1,74 @@
|
|||||||
|
# 2.5.1
|
||||||
|
|
||||||
|
## Features/Improvements
|
||||||
|
|
||||||
|
* **[UI]** Engagement ranges are now displayed by default.
|
||||||
|
* **[UI]** Engagement range display generalized to work for all patrolling flight plans (BARCAP, TARCAP, and CAS).
|
||||||
|
* **[Flight Planner]** Front lines no longer project threat zones to avoid pushing BARCAPs back so much. TARCAPs will be forcibly planned but strike packages will not route around front lines even if it is reasonable to do so.
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
* **[Campaigns]** EWRs associated with a base will now only be generated near the base.
|
||||||
|
* **[Flight Planner]** Fixed error when generating AEW&C flight plans in campaigns with no front lines.
|
||||||
|
|
||||||
|
# 2.5.0
|
||||||
|
|
||||||
|
Saves from 2.4 are not compatible with 2.5.
|
||||||
|
|
||||||
|
## Features/Improvements
|
||||||
|
|
||||||
|
* **[Engine]** DCS 2.7 Support
|
||||||
|
* **[UI]** Improved FOB menu, added a custom banner, and do not display aircraft recruitment menu
|
||||||
|
* **[Flight Planner]** Added AEW&C missions. (by siKruger)
|
||||||
|
* **[Kneeboard]** Added dark kneeboard option (by GvonH)
|
||||||
|
* **[Campaigns]** Multiple EWR sites may now be generated, and EWR sites may be generated outside bases (by SnappyComebacks)
|
||||||
|
* **[Mission Generation]** Cloudy and rainy (but not thunderstorm) weather will use the cloud presets from DCS 2.7.
|
||||||
|
* **[Plugins]** Added LotATC export plugin (by drsoran)
|
||||||
|
* **[Plugins]** Added Splash Damage Plugin (by Wheelijoe)
|
||||||
|
* **[Loadouts]** Replaced Litening with ATFLIR for all default F/A-18C loadouts.
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
* **[Flight Planner]** Front lines now project threat zones, so TARCAP/escorts will not be pruned for flights near the front. Packages may also route around the front line when practical.
|
||||||
|
* **[Flight Planner]** Fixed error when planning BAI at SAMs with dead subgroups.
|
||||||
|
* **[Flight Planner]** Mig-19 was not allowed for CAS roles fixed
|
||||||
|
* **[Flight Planner]** Increased size of navigation planning area to avoid plannign failures with distant waypoints.
|
||||||
|
* **[Flight Planner]** Fixed UI refresh when unchecking the "default loadout" box in the loadout editor.
|
||||||
|
* **[Objective names]** Fixed typos in objective name : ARMADILLLO -> ARMADILLO (by SnappyComebacks)
|
||||||
|
* **[Payloads]** F-86 Sabre was missing a custom payload
|
||||||
|
* **[Payloads]** Added GAR-8 period restrictions (by Mustang-25)
|
||||||
|
* **[Campaign]** Date now progresses.
|
||||||
|
* **[Campaign]** Added game over message when a coalition runs out of functioning airbases.
|
||||||
|
* **[Mission Generation]** Fixed "invalid face handle" error in kneeboard generation that occurred on some machines.
|
||||||
|
|
||||||
|
## Regressions
|
||||||
|
|
||||||
|
* **[Mod Support]** Stopped support for 2.5.5 Rafale Mode, and removed factions that were using it
|
||||||
|
* **[Mod Support]** Su-57 mod support might be out of date
|
||||||
|
|
||||||
|
# 2.4.3
|
||||||
|
|
||||||
|
## Features/Improvements
|
||||||
|
|
||||||
|
* **[New Game Wizard]** Added the possibility to setup custom start date
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
* **[Mods]** Updated C-130J mod data to version 6.4
|
||||||
|
* **[Mods]** Updated F-22A mod to latest version
|
||||||
|
|
||||||
|
# 2.4.2
|
||||||
|
|
||||||
|
## Features/Improvements
|
||||||
|
|
||||||
|
* **[Factions]** Introduction dates and fallback weapons added for US, Russian, UK, and French weapons. Huge thanks to @TheCandianVendingMachine for the massive amount of data entry!
|
||||||
|
* **[Campaigns]** Added 1995 start dates.
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
* **[Economy]** Pending ground unit purchases will also be transferred when a connected base is captured.
|
||||||
|
* **[UI]** Fixed rounding of budget in recruitment menu.
|
||||||
|
|
||||||
# 2.4.1
|
# 2.4.1
|
||||||
|
|
||||||
## Fixes
|
## Fixes
|
||||||
|
|||||||
@@ -2,20 +2,21 @@ from dcs.vehicles import AirDefence
|
|||||||
|
|
||||||
AAA_UNITS = [
|
AAA_UNITS = [
|
||||||
AirDefence.SPAAA_Gepard,
|
AirDefence.SPAAA_Gepard,
|
||||||
AirDefence.SPAAA_ZSU_23_4_Shilka,
|
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
|
||||||
AirDefence.AAA_Vulcan_M163,
|
AirDefence.SPAAA_Vulcan_M163,
|
||||||
AirDefence.AAA_ZU_23_Closed,
|
AirDefence.AAA_ZU_23_Closed_Emplacement,
|
||||||
AirDefence.AAA_ZU_23_Emplacement,
|
AirDefence.AAA_ZU_23_Emplacement,
|
||||||
AirDefence.AAA_ZU_23_on_Ural_375,
|
AirDefence.SPAAA_ZU_23_2_Mounted_Ural_375,
|
||||||
AirDefence.AAA_ZU_23_Insurgent_Closed,
|
AirDefence.AAA_ZU_23_Closed_Emplacement_Insurgent,
|
||||||
AirDefence.AAA_ZU_23_Insurgent_on_Ural_375,
|
AirDefence.SPAAA_ZU_23_2_Insurgent_Mounted_Ural_375,
|
||||||
AirDefence.AAA_ZU_23_Insurgent,
|
AirDefence.AAA_ZU_23_Insurgent,
|
||||||
AirDefence.AAA_8_8cm_Flak_18,
|
AirDefence.AAA_8_8cm_Flak_18,
|
||||||
AirDefence.AAA_Flak_38,
|
AirDefence.AAA_Flak_38_20mm,
|
||||||
AirDefence.AAA_8_8cm_Flak_36,
|
AirDefence.AAA_8_8cm_Flak_36,
|
||||||
AirDefence.AAA_8_8cm_Flak_37,
|
AirDefence.AAA_8_8cm_Flak_37,
|
||||||
AirDefence.AAA_Flak_Vierling_38,
|
AirDefence.AAA_Flak_Vierling_38_Quad_20mm,
|
||||||
AirDefence.AAA_Kdo_G_40,
|
AirDefence.AAA_SP_Kdo_G_40,
|
||||||
AirDefence.AAA_8_8cm_Flak_41,
|
AirDefence.AAA_8_8cm_Flak_41,
|
||||||
AirDefence.AAA_Bofors_40mm
|
AirDefence.AAA_40mm_Bofors,
|
||||||
]
|
AirDefence.AAA_S_60_57mm,
|
||||||
|
]
|
||||||
|
|||||||
@@ -1,17 +1,70 @@
|
|||||||
import inspect
|
import inspect
|
||||||
import dcs
|
import dcs
|
||||||
|
|
||||||
DEFAULT_AVAILABLE_BUILDINGS = ['fuel', 'ammo', 'comms', 'oil', 'ware', 'farp', 'fob', 'power', 'factory', 'derrick']
|
DEFAULT_AVAILABLE_BUILDINGS = [
|
||||||
|
"fuel",
|
||||||
|
"ammo",
|
||||||
|
"comms",
|
||||||
|
"oil",
|
||||||
|
"ware",
|
||||||
|
"farp",
|
||||||
|
"fob",
|
||||||
|
"power",
|
||||||
|
"factory",
|
||||||
|
"derrick",
|
||||||
|
]
|
||||||
|
|
||||||
WW2_FREE = ['fuel', 'factory', 'ware', 'fob']
|
WW2_FREE = ["fuel", "factory", "ware", "fob"]
|
||||||
WW2_GERMANY_BUILDINGS = ['fuel', 'factory', 'ww2bunker', 'ww2bunker', 'ww2bunker', 'allycamp', 'allycamp', 'fob']
|
WW2_GERMANY_BUILDINGS = [
|
||||||
WW2_ALLIES_BUILDINGS = ['fuel', 'factory', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'fob']
|
"fuel",
|
||||||
|
"factory",
|
||||||
|
"ww2bunker",
|
||||||
|
"ww2bunker",
|
||||||
|
"ww2bunker",
|
||||||
|
"allycamp",
|
||||||
|
"allycamp",
|
||||||
|
"fob",
|
||||||
|
]
|
||||||
|
WW2_ALLIES_BUILDINGS = [
|
||||||
|
"fuel",
|
||||||
|
"factory",
|
||||||
|
"allycamp",
|
||||||
|
"allycamp",
|
||||||
|
"allycamp",
|
||||||
|
"allycamp",
|
||||||
|
"allycamp",
|
||||||
|
"fob",
|
||||||
|
]
|
||||||
|
|
||||||
FORTIFICATION_BUILDINGS = ['Siegfried Line', 'Concertina wire', 'Concertina Wire', 'Czech hedgehogs 1', 'Czech hedgehogs 2',
|
FORTIFICATION_BUILDINGS = [
|
||||||
'Dragonteeth 1', 'Dragonteeth 2', 'Dragonteeth 3', 'Dragonteeth 4', 'Dragonteeth 5',
|
"Siegfried Line",
|
||||||
'Haystack 1', 'Haystack 2', 'Haystack 3', 'Haystack 4', 'Hemmkurvenvenhindernis',
|
"Concertina wire",
|
||||||
'Log posts 1', 'Log posts 2', 'Log posts 3', 'Log ramps 1', 'Log ramps 2', 'Log ramps 3',
|
"Concertina Wire",
|
||||||
'Belgian Gate', 'Container white']
|
"Czech hedgehogs 1",
|
||||||
|
"Czech hedgehogs 2",
|
||||||
|
"Dragonteeth 1",
|
||||||
|
"Dragonteeth 2",
|
||||||
|
"Dragonteeth 3",
|
||||||
|
"Dragonteeth 4",
|
||||||
|
"Dragonteeth 5",
|
||||||
|
"Haystack 1",
|
||||||
|
"Haystack 2",
|
||||||
|
"Haystack 3",
|
||||||
|
"Haystack 4",
|
||||||
|
"Hemmkurvenvenhindernis",
|
||||||
|
"Log posts 1",
|
||||||
|
"Log posts 2",
|
||||||
|
"Log posts 3",
|
||||||
|
"Log ramps 1",
|
||||||
|
"Log ramps 2",
|
||||||
|
"Log ramps 3",
|
||||||
|
"Belgian Gate",
|
||||||
|
"Container white",
|
||||||
|
]
|
||||||
|
|
||||||
FORTIFICATION_UNITS = [c for c in vars(dcs.vehicles.Fortification).values() if inspect.isclass(c)]
|
FORTIFICATION_UNITS = [
|
||||||
FORTIFICATION_UNITS_ID = [c.id for c in vars(dcs.vehicles.Fortification).values() if inspect.isclass(c)]
|
c for c in vars(dcs.vehicles.Fortification).values() if inspect.isclass(c)
|
||||||
|
]
|
||||||
|
FORTIFICATION_UNITS_ID = [
|
||||||
|
c.id for c in vars(dcs.vehicles.Fortification).values() if inspect.isclass(c)
|
||||||
|
]
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from dcs.planes import (
|
|||||||
P_51D,
|
P_51D,
|
||||||
P_51D_30_NA,
|
P_51D_30_NA,
|
||||||
SpitfireLFMkIX,
|
SpitfireLFMkIX,
|
||||||
SpitfireLFMkIXCW
|
SpitfireLFMkIXCW,
|
||||||
)
|
)
|
||||||
|
|
||||||
from pydcs_extensions.a4ec.a4ec import A_4E_C
|
from pydcs_extensions.a4ec.a4ec import A_4E_C
|
||||||
@@ -26,7 +26,6 @@ This list contains the aircraft that do not use the guns as the last resort weap
|
|||||||
They'll RTB when they don't have gun ammo left
|
They'll RTB when they don't have gun ammo left
|
||||||
"""
|
"""
|
||||||
GUNFIGHTERS = [
|
GUNFIGHTERS = [
|
||||||
|
|
||||||
# Cold War
|
# Cold War
|
||||||
MiG_15bis,
|
MiG_15bis,
|
||||||
MiG_19P,
|
MiG_19P,
|
||||||
@@ -34,11 +33,9 @@ GUNFIGHTERS = [
|
|||||||
F_86F_Sabre,
|
F_86F_Sabre,
|
||||||
A_4E_C,
|
A_4E_C,
|
||||||
F_5E_3,
|
F_5E_3,
|
||||||
|
|
||||||
# Trainers
|
# Trainers
|
||||||
C_101CC,
|
C_101CC,
|
||||||
L_39ZA,
|
L_39ZA,
|
||||||
|
|
||||||
# WW2
|
# WW2
|
||||||
P_51D_30_NA,
|
P_51D_30_NA,
|
||||||
P_51D,
|
P_51D,
|
||||||
@@ -51,5 +48,4 @@ GUNFIGHTERS = [
|
|||||||
FW_190D9,
|
FW_190D9,
|
||||||
FW_190A8,
|
FW_190A8,
|
||||||
I_16,
|
I_16,
|
||||||
|
]
|
||||||
]
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from dcs.ships import (
|
from dcs.ships import (
|
||||||
CGN_1144_2_Pyotr_Velikiy,
|
Battlecruiser_1144_2_Pyotr_Velikiy,
|
||||||
CG_1164_Moskva,
|
Cruiser_1164_Moskva,
|
||||||
CVN_70_Carl_Vinson,
|
CVN_70_Carl_Vinson,
|
||||||
CVN_71_Theodore_Roosevelt,
|
CVN_71_Theodore_Roosevelt,
|
||||||
CVN_72_Abraham_Lincoln,
|
CVN_72_Abraham_Lincoln,
|
||||||
@@ -8,67 +8,65 @@ from dcs.ships import (
|
|||||||
CVN_74_John_C__Stennis,
|
CVN_74_John_C__Stennis,
|
||||||
CV_1143_5_Admiral_Kuznetsov,
|
CV_1143_5_Admiral_Kuznetsov,
|
||||||
CV_1143_5_Admiral_Kuznetsov_2017,
|
CV_1143_5_Admiral_Kuznetsov_2017,
|
||||||
FFG_11540_Neustrashimy,
|
Frigate_11540_Neustrashimy,
|
||||||
FFL_1124_4_Grisha,
|
Corvette_1124_4_Grisha,
|
||||||
FF_1135M_Rezky,
|
Frigate_1135M_Rezky,
|
||||||
FSG_1241_1MP_Molniya,
|
Corvette_1241_1_Molniya,
|
||||||
LHA_1_Tarawa,
|
LHA_1_Tarawa,
|
||||||
Oliver_Hazzard_Perry_class,
|
FFG_Oliver_Hazzard_Perry,
|
||||||
Ticonderoga_class,
|
CG_Ticonderoga,
|
||||||
Type_052B_Destroyer,
|
Type_052B_Destroyer,
|
||||||
Type_052C_Destroyer,
|
Type_052C_Destroyer,
|
||||||
Type_054A_Frigate,
|
Type_054A_Frigate,
|
||||||
USS_Arleigh_Burke_IIa,
|
DDG_Arleigh_Burke_IIa,
|
||||||
)
|
)
|
||||||
from dcs.vehicles import AirDefence
|
from dcs.vehicles import AirDefence
|
||||||
|
|
||||||
UNITS_WITH_RADAR = [
|
UNITS_WITH_RADAR = [
|
||||||
|
|
||||||
# Radars
|
# Radars
|
||||||
AirDefence.SAM_SA_15_Tor_9A331,
|
AirDefence.SAM_SA_15_Tor_Gauntlet,
|
||||||
AirDefence.SAM_SA_11_Buk_CC_9S470M1,
|
AirDefence.SAM_SA_11_Buk_Gadfly_C2,
|
||||||
AirDefence.SAM_Patriot_AMG_AN_MRC_137,
|
AirDefence.SAM_Patriot_CR__AMG_AN_MRC_137,
|
||||||
AirDefence.SAM_Patriot_ECS_AN_MSQ_104,
|
AirDefence.SAM_Patriot_ECS,
|
||||||
AirDefence.SPAAA_Gepard,
|
AirDefence.SPAAA_Gepard,
|
||||||
AirDefence.AAA_Vulcan_M163,
|
AirDefence.SPAAA_Vulcan_M163,
|
||||||
AirDefence.SPAAA_ZSU_23_4_Shilka,
|
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
|
||||||
AirDefence.EWR_1L13,
|
AirDefence.EWR_1L13,
|
||||||
AirDefence.SAM_SA_6_Kub_STR_9S91,
|
AirDefence.SAM_SA_6_Kub_Long_Track_STR,
|
||||||
AirDefence.SAM_SA_10_S_300PS_TR_30N6,
|
AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR,
|
||||||
AirDefence.SAM_SA_10_S_300PS_SR_5N66M,
|
AirDefence.SAM_SA_10_S_300_Grumble_Clam_Shell_SR,
|
||||||
AirDefence.EWR_55G6,
|
AirDefence.EWR_55G6,
|
||||||
AirDefence.SAM_SA_10_S_300PS_SR_64H6E,
|
AirDefence.SAM_SA_10_S_300_Grumble_Big_Bird_SR,
|
||||||
AirDefence.SAM_SA_11_Buk_SR_9S18M1,
|
AirDefence.SAM_SA_11_Buk_Gadfly_Snow_Drift_SR,
|
||||||
AirDefence.CP_9S80M1_Sborka,
|
AirDefence.MCC_SR_Sborka_Dog_Ear_SR,
|
||||||
AirDefence.SAM_Hawk_TR_AN_MPQ_46,
|
AirDefence.SAM_Hawk_TR__AN_MPQ_46,
|
||||||
AirDefence.SAM_Hawk_SR_AN_MPQ_50,
|
AirDefence.SAM_Hawk_SR__AN_MPQ_50,
|
||||||
AirDefence.SAM_Patriot_STR_AN_MPQ_53,
|
AirDefence.SAM_Patriot_STR,
|
||||||
AirDefence.SAM_Hawk_CWAR_AN_MPQ_55,
|
AirDefence.SAM_Hawk_CWAR_AN_MPQ_55,
|
||||||
AirDefence.SAM_SR_P_19,
|
AirDefence.SAM_P19_Flat_Face_SR__SA_2_3,
|
||||||
AirDefence.SAM_Roland_EWR,
|
AirDefence.SAM_Roland_EWR,
|
||||||
AirDefence.SAM_SA_3_S_125_TR_SNR,
|
AirDefence.SAM_SA_3_S_125_Low_Blow_TR,
|
||||||
AirDefence.SAM_SA_2_TR_SNR_75_Fan_Song,
|
AirDefence.SAM_SA_2_S_75_Fan_Song_TR,
|
||||||
AirDefence.HQ_7_Self_Propelled_STR,
|
AirDefence.HQ_7_Self_Propelled_STR,
|
||||||
|
|
||||||
# Ships
|
# Ships
|
||||||
CVN_70_Carl_Vinson,
|
CVN_70_Carl_Vinson,
|
||||||
Oliver_Hazzard_Perry_class,
|
FFG_Oliver_Hazzard_Perry,
|
||||||
Ticonderoga_class,
|
CG_Ticonderoga,
|
||||||
FFL_1124_4_Grisha,
|
Corvette_1124_4_Grisha,
|
||||||
CV_1143_5_Admiral_Kuznetsov,
|
CV_1143_5_Admiral_Kuznetsov,
|
||||||
FSG_1241_1MP_Molniya,
|
Corvette_1241_1_Molniya,
|
||||||
CG_1164_Moskva,
|
Cruiser_1164_Moskva,
|
||||||
FFG_11540_Neustrashimy,
|
Frigate_11540_Neustrashimy,
|
||||||
CGN_1144_2_Pyotr_Velikiy,
|
Battlecruiser_1144_2_Pyotr_Velikiy,
|
||||||
FF_1135M_Rezky,
|
Frigate_1135M_Rezky,
|
||||||
CV_1143_5_Admiral_Kuznetsov_2017,
|
CV_1143_5_Admiral_Kuznetsov_2017,
|
||||||
CVN_74_John_C__Stennis,
|
CVN_74_John_C__Stennis,
|
||||||
CVN_71_Theodore_Roosevelt,
|
CVN_71_Theodore_Roosevelt,
|
||||||
CVN_72_Abraham_Lincoln,
|
CVN_72_Abraham_Lincoln,
|
||||||
CVN_73_George_Washington,
|
CVN_73_George_Washington,
|
||||||
USS_Arleigh_Burke_IIa,
|
DDG_Arleigh_Burke_IIa,
|
||||||
LHA_1_Tarawa,
|
LHA_1_Tarawa,
|
||||||
Type_052B_Destroyer,
|
Type_052B_Destroyer,
|
||||||
Type_054A_Frigate,
|
Type_054A_Frigate,
|
||||||
Type_052C_Destroyer
|
Type_052C_Destroyer,
|
||||||
]
|
]
|
||||||
|
|||||||
1237
game/data/weapons.py
1237
game/data/weapons.py
File diff suppressed because it is too large
Load Diff
847
game/db.py
847
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.
|
# them when they've already dead. Dedup.
|
||||||
killed_ground_units=list(set(data["killed_ground_units"])),
|
killed_ground_units=list(set(data["killed_ground_units"])),
|
||||||
destroyed_statics=data["destroyed_objects_positions"],
|
destroyed_statics=data["destroyed_objects_positions"],
|
||||||
base_capture_events=data["base_capture_events"]
|
base_capture_events=data["base_capture_events"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Debriefing:
|
class Debriefing:
|
||||||
def __init__(self, state_data: Dict[str, Any], game: Game,
|
def __init__(
|
||||||
unit_map: UnitMap) -> None:
|
self, state_data: Dict[str, Any], game: Game, unit_map: UnitMap
|
||||||
|
) -> None:
|
||||||
self.state_data = StateData.from_json(state_data)
|
self.state_data = StateData.from_json(state_data)
|
||||||
self.unit_map = unit_map
|
self.unit_map = unit_map
|
||||||
|
|
||||||
@@ -135,12 +136,9 @@ class Debriefing:
|
|||||||
yield from self.ground_losses.enemy_airfields
|
yield from self.ground_losses.enemy_airfields
|
||||||
|
|
||||||
def casualty_count(self, control_point: ControlPoint) -> int:
|
def casualty_count(self, control_point: ControlPoint) -> int:
|
||||||
return len(
|
return len([x for x in self.front_line_losses if x.origin == control_point])
|
||||||
[x for x in self.front_line_losses if x.origin == control_point]
|
|
||||||
)
|
|
||||||
|
|
||||||
def front_line_losses_by_type(
|
def front_line_losses_by_type(self, player: bool) -> Dict[Type[UnitType], int]:
|
||||||
self, player: bool) -> Dict[Type[UnitType], int]:
|
|
||||||
losses_by_type: Dict[Type[UnitType], int] = defaultdict(int)
|
losses_by_type: Dict[Type[UnitType], int] = defaultdict(int)
|
||||||
if player:
|
if player:
|
||||||
losses = self.ground_losses.player_front_line
|
losses = self.ground_losses.player_front_line
|
||||||
@@ -221,8 +219,10 @@ class Debriefing:
|
|||||||
# deaths, so we expect to see quite a few unclaimed dead ground
|
# deaths, so we expect to see quite a few unclaimed dead ground
|
||||||
# units. We should start tracking those and covert this to a
|
# units. We should start tracking those and covert this to a
|
||||||
# warning.
|
# warning.
|
||||||
logging.debug(f"Death of untracked ground unit {unit_name} will "
|
logging.debug(
|
||||||
"have no effect. This may be normal behavior.")
|
f"Death of untracked ground unit {unit_name} will "
|
||||||
|
"have no effect. This may be normal behavior."
|
||||||
|
)
|
||||||
|
|
||||||
return losses
|
return losses
|
||||||
|
|
||||||
@@ -234,15 +234,16 @@ class Debriefing:
|
|||||||
for idx, base in enumerate(i.split("||")[0] for i in reversed_captures):
|
for idx, base in enumerate(i.split("||")[0] for i in reversed_captures):
|
||||||
if base not in [x[1] for x in last_base_cap_indexes]:
|
if base not in [x[1] for x in last_base_cap_indexes]:
|
||||||
last_base_cap_indexes.append((idx, base))
|
last_base_cap_indexes.append((idx, base))
|
||||||
return [reversed_captures[idx[0]] for idx in last_base_cap_indexes]
|
return [reversed_captures[idx[0]] for idx in last_base_cap_indexes]
|
||||||
|
|
||||||
|
|
||||||
class PollDebriefingFileThread(threading.Thread):
|
class PollDebriefingFileThread(threading.Thread):
|
||||||
"""Thread class with a stop() method. The thread itself has to check
|
"""Thread class with a stop() method. The thread itself has to check
|
||||||
regularly for the stopped() condition."""
|
regularly for the stopped() condition."""
|
||||||
|
|
||||||
def __init__(self, callback: Callable[[Debriefing], None],
|
def __init__(
|
||||||
game: Game, unit_map: UnitMap) -> None:
|
self, callback: Callable[[Debriefing], None], game: Game, unit_map: UnitMap
|
||||||
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._stop_event = threading.Event()
|
self._stop_event = threading.Event()
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
@@ -261,7 +262,10 @@ class PollDebriefingFileThread(threading.Thread):
|
|||||||
else:
|
else:
|
||||||
last_modified = 0
|
last_modified = 0
|
||||||
while not self.stopped():
|
while not self.stopped():
|
||||||
if os.path.isfile("state.json") and os.path.getmtime("state.json") > last_modified:
|
if (
|
||||||
|
os.path.isfile("state.json")
|
||||||
|
and os.path.getmtime("state.json") > last_modified
|
||||||
|
):
|
||||||
with open("state.json", "r") as json_file:
|
with open("state.json", "r") as json_file:
|
||||||
json_data = json.load(json_file)
|
json_data = json.load(json_file)
|
||||||
debriefing = Debriefing(json_data, self.game, self.unit_map)
|
debriefing = Debriefing(json_data, self.game, self.unit_map)
|
||||||
@@ -270,8 +274,9 @@ class PollDebriefingFileThread(threading.Thread):
|
|||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
|
|
||||||
|
|
||||||
def wait_for_debriefing(callback: Callable[[Debriefing], None],
|
def wait_for_debriefing(
|
||||||
game: Game, unit_map) -> PollDebriefingFileThread:
|
callback: Callable[[Debriefing], None], game: Game, unit_map
|
||||||
|
) -> PollDebriefingFileThread:
|
||||||
thread = PollDebriefingFileThread(callback, game, unit_map)
|
thread = PollDebriefingFileThread(callback, game, unit_map)
|
||||||
thread.start()
|
thread.start()
|
||||||
return thread
|
return thread
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from typing import Dict, Iterator, List, TYPE_CHECKING, Tuple, Type
|
|||||||
|
|
||||||
from dcs.mapping import Point
|
from dcs.mapping import Point
|
||||||
from dcs.task import Task
|
from dcs.task import Task
|
||||||
from dcs.unittype import UnitType
|
from dcs.unittype import UnitType, VehicleType
|
||||||
|
|
||||||
from game import persistency
|
from game import persistency
|
||||||
from game.debriefing import AirLosses, Debriefing
|
from game.debriefing import AirLosses, Debriefing
|
||||||
@@ -37,7 +37,15 @@ class Event:
|
|||||||
to_cp = None # type: ControlPoint
|
to_cp = None # type: ControlPoint
|
||||||
difficulty = 1 # type: int
|
difficulty = 1 # type: int
|
||||||
|
|
||||||
def __init__(self, game, from_cp: ControlPoint, target_cp: ControlPoint, location: Point, attacker_name: str, defender_name: str):
|
def __init__(
|
||||||
|
self,
|
||||||
|
game,
|
||||||
|
from_cp: ControlPoint,
|
||||||
|
target_cp: ControlPoint,
|
||||||
|
location: Point,
|
||||||
|
attacker_name: str,
|
||||||
|
defender_name: str,
|
||||||
|
):
|
||||||
self.game = game
|
self.game = game
|
||||||
self.from_cp = from_cp
|
self.from_cp = from_cp
|
||||||
self.to_cp = target_cp
|
self.to_cp = target_cp
|
||||||
@@ -57,12 +65,14 @@ class Event:
|
|||||||
Operation.prepare(self.game)
|
Operation.prepare(self.game)
|
||||||
unit_map = Operation.generate()
|
unit_map = Operation.generate()
|
||||||
Operation.current_mission.save(
|
Operation.current_mission.save(
|
||||||
persistency.mission_path_for("liberation_nextturn.miz"))
|
persistency.mission_path_for("liberation_nextturn.miz")
|
||||||
|
)
|
||||||
return unit_map
|
return unit_map
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _transfer_aircraft(ato: AirTaskingOrder, losses: AirLosses,
|
def _transfer_aircraft(
|
||||||
for_player: bool) -> None:
|
ato: AirTaskingOrder, losses: AirLosses, for_player: bool
|
||||||
|
) -> None:
|
||||||
for package in ato.packages:
|
for package in ato.packages:
|
||||||
for flight in package.flights:
|
for flight in package.flights:
|
||||||
# No need to transfer to the same location.
|
# No need to transfer to the same location.
|
||||||
@@ -77,13 +87,16 @@ class Event:
|
|||||||
if flight.arrival.captured != for_player:
|
if flight.arrival.captured != for_player:
|
||||||
logging.info(
|
logging.info(
|
||||||
f"Not transferring {flight} because {flight.arrival} "
|
f"Not transferring {flight} because {flight.arrival} "
|
||||||
"was captured")
|
"was captured"
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
transfer_count = losses.surviving_flight_members(flight)
|
transfer_count = losses.surviving_flight_members(flight)
|
||||||
if transfer_count < 0:
|
if transfer_count < 0:
|
||||||
logging.error(f"{flight} had {flight.count} aircraft but "
|
logging.error(
|
||||||
f"{transfer_count} losses were recorded.")
|
f"{flight} had {flight.count} aircraft but "
|
||||||
|
f"{transfer_count} losses were recorded."
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
aircraft = flight.unit_type
|
aircraft = flight.unit_type
|
||||||
@@ -91,7 +104,8 @@ class Event:
|
|||||||
if available < transfer_count:
|
if available < transfer_count:
|
||||||
logging.error(
|
logging.error(
|
||||||
f"Found killed {aircraft} from {flight.departure} but "
|
f"Found killed {aircraft} from {flight.departure} but "
|
||||||
f"that airbase has only {available} available.")
|
f"that airbase has only {available} available."
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
flight.departure.base.aircraft[aircraft] -= transfer_count
|
flight.departure.base.aircraft[aircraft] -= transfer_count
|
||||||
@@ -101,10 +115,12 @@ class Event:
|
|||||||
flight.arrival.base.aircraft[aircraft] += transfer_count
|
flight.arrival.base.aircraft[aircraft] += transfer_count
|
||||||
|
|
||||||
def complete_aircraft_transfers(self, debriefing: Debriefing) -> None:
|
def complete_aircraft_transfers(self, debriefing: Debriefing) -> None:
|
||||||
self._transfer_aircraft(self.game.blue_ato, debriefing.air_losses,
|
self._transfer_aircraft(
|
||||||
for_player=True)
|
self.game.blue_ato, debriefing.air_losses, for_player=True
|
||||||
self._transfer_aircraft(self.game.red_ato, debriefing.air_losses,
|
)
|
||||||
for_player=False)
|
self._transfer_aircraft(
|
||||||
|
self.game.red_ato, debriefing.air_losses, for_player=False
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def commit_air_losses(debriefing: Debriefing) -> None:
|
def commit_air_losses(debriefing: Debriefing) -> None:
|
||||||
@@ -115,7 +131,8 @@ class Event:
|
|||||||
if available <= 0:
|
if available <= 0:
|
||||||
logging.error(
|
logging.error(
|
||||||
f"Found killed {aircraft} from {cp} but that airbase has "
|
f"Found killed {aircraft} from {cp} but that airbase has "
|
||||||
"none available.")
|
"none available."
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
logging.info(f"{aircraft} destroyed from {cp}")
|
logging.info(f"{aircraft} destroyed from {cp}")
|
||||||
@@ -130,7 +147,8 @@ class Event:
|
|||||||
if available <= 0:
|
if available <= 0:
|
||||||
logging.error(
|
logging.error(
|
||||||
f"Found killed {unit_type} from {control_point} but that "
|
f"Found killed {unit_type} from {control_point} but that "
|
||||||
"airbase has none available.")
|
"airbase has none available."
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
logging.info(f"{unit_type} destroyed from {control_point}")
|
logging.info(f"{unit_type} destroyed from {control_point}")
|
||||||
@@ -149,11 +167,14 @@ class Event:
|
|||||||
def commit_building_losses(self, debriefing: Debriefing) -> None:
|
def commit_building_losses(self, debriefing: Debriefing) -> None:
|
||||||
for loss in debriefing.building_losses:
|
for loss in debriefing.building_losses:
|
||||||
loss.ground_object.kill()
|
loss.ground_object.kill()
|
||||||
self.game.informations.append(Information(
|
self.game.informations.append(
|
||||||
"Building destroyed",
|
Information(
|
||||||
f"{loss.ground_object.dcs_identifier} has been destroyed at "
|
"Building destroyed",
|
||||||
f"location {loss.ground_object.obj_name}", self.game.turn
|
f"{loss.ground_object.dcs_identifier} has been destroyed at "
|
||||||
))
|
f"location {loss.ground_object.obj_name}",
|
||||||
|
self.game.turn,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def commit_damaged_runways(debriefing: Debriefing) -> None:
|
def commit_damaged_runways(debriefing: Debriefing) -> None:
|
||||||
@@ -171,9 +192,9 @@ class Event:
|
|||||||
|
|
||||||
# ------------------------------
|
# ------------------------------
|
||||||
# Captured bases
|
# Captured bases
|
||||||
#if self.game.player_country in db.BLUEFOR_FACTIONS:
|
# if self.game.player_country in db.BLUEFOR_FACTIONS:
|
||||||
coalition = 2 # Value in DCS mission event for BLUE
|
coalition = 2 # Value in DCS mission event for BLUE
|
||||||
#else:
|
# else:
|
||||||
# coalition = 1 # Value in DCS mission event for RED
|
# coalition = 1 # Value in DCS mission event for RED
|
||||||
|
|
||||||
for captured in debriefing.base_capture_events:
|
for captured in debriefing.base_capture_events:
|
||||||
@@ -187,12 +208,22 @@ class Event:
|
|||||||
|
|
||||||
if cp.captured and new_owner_coalition != coalition:
|
if cp.captured and new_owner_coalition != coalition:
|
||||||
for_player = False
|
for_player = False
|
||||||
info = Information(cp.name + " lost !", "The ennemy took control of " + cp.name + "\nShame on us !", self.game.turn)
|
info = Information(
|
||||||
|
cp.name + " lost !",
|
||||||
|
"The ennemy took control of "
|
||||||
|
+ cp.name
|
||||||
|
+ "\nShame on us !",
|
||||||
|
self.game.turn,
|
||||||
|
)
|
||||||
self.game.informations.append(info)
|
self.game.informations.append(info)
|
||||||
captured_cps.append(cp)
|
captured_cps.append(cp)
|
||||||
elif not(cp.captured) and new_owner_coalition == coalition:
|
elif not (cp.captured) and new_owner_coalition == coalition:
|
||||||
for_player = True
|
for_player = True
|
||||||
info = Information(cp.name + " captured !", "We took control of " + cp.name + "! Great job !", self.game.turn)
|
info = Information(
|
||||||
|
cp.name + " captured !",
|
||||||
|
"We took control of " + cp.name + "! Great job !",
|
||||||
|
self.game.turn,
|
||||||
|
)
|
||||||
self.game.informations.append(info)
|
self.game.informations.append(info)
|
||||||
captured_cps.append(cp)
|
captured_cps.append(cp)
|
||||||
else:
|
else:
|
||||||
@@ -218,7 +249,12 @@ class Event:
|
|||||||
for cp in self.game.theater.player_points():
|
for cp in self.game.theater.player_points():
|
||||||
enemy_cps = [e for e in cp.connected_points if not e.captured]
|
enemy_cps = [e for e in cp.connected_points if not e.captured]
|
||||||
for enemy_cp in enemy_cps:
|
for enemy_cp in enemy_cps:
|
||||||
print("Compute frontline progression for : " + cp.name + " to " + enemy_cp.name)
|
print(
|
||||||
|
"Compute frontline progression for : "
|
||||||
|
+ cp.name
|
||||||
|
+ " to "
|
||||||
|
+ enemy_cp.name
|
||||||
|
)
|
||||||
|
|
||||||
delta = 0.0
|
delta = 0.0
|
||||||
player_won = True
|
player_won = True
|
||||||
@@ -234,7 +270,11 @@ class Event:
|
|||||||
|
|
||||||
ratio = (1.0 + enemy_casualties) / (1.0 + ally_casualties)
|
ratio = (1.0 + enemy_casualties) / (1.0 + ally_casualties)
|
||||||
|
|
||||||
player_aggresive = cp.stances[enemy_cp.id] in [CombatStance.AGGRESSIVE, CombatStance.ELIMINATION, CombatStance.BREAKTHROUGH]
|
player_aggresive = cp.stances[enemy_cp.id] in [
|
||||||
|
CombatStance.AGGRESSIVE,
|
||||||
|
CombatStance.ELIMINATION,
|
||||||
|
CombatStance.BREAKTHROUGH,
|
||||||
|
]
|
||||||
|
|
||||||
if ally_units_alive == 0:
|
if ally_units_alive == 0:
|
||||||
player_won = False
|
player_won = False
|
||||||
@@ -259,11 +299,17 @@ class Event:
|
|||||||
delta = DEFEAT_INFLUENCE
|
delta = DEFEAT_INFLUENCE
|
||||||
elif ally_casualties > enemy_casualties:
|
elif ally_casualties > enemy_casualties:
|
||||||
|
|
||||||
if ally_units_alive > 2*enemy_units_alive and player_aggresive:
|
if (
|
||||||
|
ally_units_alive > 2 * enemy_units_alive
|
||||||
|
and player_aggresive
|
||||||
|
):
|
||||||
# Even with casualties if the enemy is overwhelmed, they are going to lose ground
|
# Even with casualties if the enemy is overwhelmed, they are going to lose ground
|
||||||
player_won = True
|
player_won = True
|
||||||
delta = MINOR_DEFEAT_INFLUENCE
|
delta = MINOR_DEFEAT_INFLUENCE
|
||||||
elif ally_units_alive > 3*enemy_units_alive and player_aggresive:
|
elif (
|
||||||
|
ally_units_alive > 3 * enemy_units_alive
|
||||||
|
and player_aggresive
|
||||||
|
):
|
||||||
player_won = True
|
player_won = True
|
||||||
delta = STRONG_DEFEAT_INFLUENCE
|
delta = STRONG_DEFEAT_INFLUENCE
|
||||||
else:
|
else:
|
||||||
@@ -275,7 +321,10 @@ class Event:
|
|||||||
delta = STRONG_DEFEAT_INFLUENCE
|
delta = STRONG_DEFEAT_INFLUENCE
|
||||||
|
|
||||||
# No progress with defensive strategies
|
# No progress with defensive strategies
|
||||||
if player_won and cp.stances[enemy_cp.id] in [CombatStance.DEFENSIVE, CombatStance.AMBUSH]:
|
if player_won and cp.stances[enemy_cp.id] in [
|
||||||
|
CombatStance.DEFENSIVE,
|
||||||
|
CombatStance.AMBUSH,
|
||||||
|
]:
|
||||||
print("Defensive stance, progress is limited")
|
print("Defensive stance, progress is limited")
|
||||||
delta = MINOR_DEFEAT_INFLUENCE
|
delta = MINOR_DEFEAT_INFLUENCE
|
||||||
|
|
||||||
@@ -283,62 +332,95 @@ class Event:
|
|||||||
print(cp.name + " won ! factor > " + str(delta))
|
print(cp.name + " won ! factor > " + str(delta))
|
||||||
cp.base.affect_strength(delta)
|
cp.base.affect_strength(delta)
|
||||||
enemy_cp.base.affect_strength(-delta)
|
enemy_cp.base.affect_strength(-delta)
|
||||||
info = Information("Frontline Report",
|
info = Information(
|
||||||
"Our ground forces from " + cp.name + " are making progress toward " + enemy_cp.name,
|
"Frontline Report",
|
||||||
self.game.turn)
|
"Our ground forces from "
|
||||||
|
+ cp.name
|
||||||
|
+ " are making progress toward "
|
||||||
|
+ enemy_cp.name,
|
||||||
|
self.game.turn,
|
||||||
|
)
|
||||||
self.game.informations.append(info)
|
self.game.informations.append(info)
|
||||||
else:
|
else:
|
||||||
print(cp.name + " lost ! factor > " + str(delta))
|
print(cp.name + " lost ! factor > " + str(delta))
|
||||||
enemy_cp.base.affect_strength(delta)
|
enemy_cp.base.affect_strength(delta)
|
||||||
cp.base.affect_strength(-delta)
|
cp.base.affect_strength(-delta)
|
||||||
info = Information("Frontline Report",
|
info = Information(
|
||||||
"Our ground forces from " + cp.name + " are losing ground against the enemy forces from " + enemy_cp.name,
|
"Frontline Report",
|
||||||
self.game.turn)
|
"Our ground forces from "
|
||||||
|
+ cp.name
|
||||||
|
+ " are losing ground against the enemy forces from "
|
||||||
|
+ enemy_cp.name,
|
||||||
|
self.game.turn,
|
||||||
|
)
|
||||||
self.game.informations.append(info)
|
self.game.informations.append(info)
|
||||||
|
|
||||||
def redeploy_units(self, cp):
|
def redeploy_units(self, cp: ControlPoint) -> None:
|
||||||
""""
|
""" "
|
||||||
Auto redeploy units to newly captured base
|
Auto redeploy units to newly captured base
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ally_connected_cps = [ocp for ocp in cp.connected_points if cp.captured == ocp.captured]
|
ally_connected_cps = [
|
||||||
enemy_connected_cps = [ocp for ocp in cp.connected_points if cp.captured != ocp.captured]
|
ocp for ocp in cp.connected_points if cp.captured == ocp.captured
|
||||||
|
]
|
||||||
|
enemy_connected_cps = [
|
||||||
|
ocp for ocp in cp.connected_points if cp.captured != ocp.captured
|
||||||
|
]
|
||||||
|
|
||||||
# If the newly captured cp does not have enemy connected cp,
|
# If the newly captured cp does not have enemy connected cp,
|
||||||
# then it is not necessary to redeploy frontline units there.
|
# then it is not necessary to redeploy frontline units there.
|
||||||
if len(enemy_connected_cps) == 0:
|
if len(enemy_connected_cps) == 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# From each ally cp, send reinforcements
|
||||||
|
for ally_cp in ally_connected_cps:
|
||||||
|
self.redeploy_between(cp, ally_cp)
|
||||||
|
|
||||||
|
def redeploy_between(self, destination: ControlPoint, source: ControlPoint) -> None:
|
||||||
|
total_units_redeployed = 0
|
||||||
|
moved_units = {}
|
||||||
|
|
||||||
|
if source.has_active_frontline or not destination.captured:
|
||||||
|
# If there are still active front lines to defend at the
|
||||||
|
# transferring CP we should not transfer all units.
|
||||||
|
#
|
||||||
|
# Opfor also does not transfer all of their units.
|
||||||
|
# TODO: Balance the CPs rather than moving half from everywhere.
|
||||||
|
move_factor = 0.5
|
||||||
else:
|
else:
|
||||||
# From each ally cp, send reinforcements
|
# Otherwise we can move everything.
|
||||||
for ally_cp in ally_connected_cps:
|
move_factor = 1
|
||||||
total_units_redeployed = 0
|
|
||||||
own_enemy_cp = [ocp for ocp in ally_cp.connected_points if ally_cp.captured != ocp.captured]
|
|
||||||
|
|
||||||
moved_units = {}
|
for frontline_unit, count in source.base.armor.items():
|
||||||
|
moved_units[frontline_unit] = int(count * move_factor)
|
||||||
|
total_units_redeployed = total_units_redeployed + int(count * move_factor)
|
||||||
|
|
||||||
# If the connected base, does not have any more enemy cp connected.
|
destination.base.commision_units(moved_units)
|
||||||
# Or if it is not the opponent redeploying forces there (enemy AI will never redeploy all their forces at once)
|
source.base.commit_losses(moved_units)
|
||||||
if len(own_enemy_cp) > 0 or not cp.captured:
|
|
||||||
for frontline_unit, count in ally_cp.base.armor.items():
|
|
||||||
moved_units[frontline_unit] = int(count/2)
|
|
||||||
total_units_redeployed = total_units_redeployed + int(count/2)
|
|
||||||
else: # So if the old base, does not have any more enemy cp connected, or if it is an enemy base
|
|
||||||
for frontline_unit, count in ally_cp.base.armor.items():
|
|
||||||
moved_units[frontline_unit] = count
|
|
||||||
total_units_redeployed = total_units_redeployed + count
|
|
||||||
|
|
||||||
cp.base.commision_units(moved_units)
|
# Also transfer pending deliveries.
|
||||||
ally_cp.base.commit_losses(moved_units)
|
for unit_type, count in source.pending_unit_deliveries.units.items():
|
||||||
|
if not issubclass(unit_type, VehicleType):
|
||||||
|
continue
|
||||||
|
if count <= 0:
|
||||||
|
# Don't transfer *sales*...
|
||||||
|
continue
|
||||||
|
move_count = int(count * move_factor)
|
||||||
|
source.pending_unit_deliveries.sell({unit_type: move_count})
|
||||||
|
destination.pending_unit_deliveries.order({unit_type: move_count})
|
||||||
|
total_units_redeployed += move_count
|
||||||
|
|
||||||
if total_units_redeployed > 0:
|
if total_units_redeployed > 0:
|
||||||
info = Information("Units redeployed", "", self.game.turn)
|
text = (
|
||||||
info.text = str(total_units_redeployed) + " units have been redeployed from " + ally_cp.name + " to " + cp.name
|
f"{total_units_redeployed} units have been redeployed from "
|
||||||
self.game.informations.append(info)
|
f"{source.name} to {destination.name}"
|
||||||
logging.info(info.text)
|
)
|
||||||
|
info = Information("Units redeployed", text, self.game.turn)
|
||||||
|
self.game.informations.append(info)
|
||||||
|
logging.info(text)
|
||||||
|
|
||||||
|
|
||||||
class UnitsDeliveryEvent:
|
class UnitsDeliveryEvent:
|
||||||
|
|
||||||
def __init__(self, control_point: ControlPoint) -> None:
|
def __init__(self, control_point: ControlPoint) -> None:
|
||||||
self.to_cp = control_point
|
self.to_cp = control_point
|
||||||
self.units: Dict[Type[UnitType], int] = {}
|
self.units: Dict[Type[UnitType], int] = {}
|
||||||
@@ -366,8 +448,7 @@ class UnitsDeliveryEvent:
|
|||||||
logging.error(f"Could not refund {unit_type.id}, price unknown")
|
logging.error(f"Could not refund {unit_type.id}, price unknown")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
logging.info(
|
logging.info(f"Refunding {count} {unit_type.id} at {self.to_cp.name}")
|
||||||
f"Refunding {count} {unit_type.id} at {self.to_cp.name}")
|
|
||||||
game.adjust_budget(price * count, player=self.to_cp.captured)
|
game.adjust_budget(price * count, player=self.to_cp.captured)
|
||||||
|
|
||||||
def available_next_turn(self, unit_type: Type[UnitType]) -> int:
|
def available_next_turn(self, unit_type: Type[UnitType]) -> int:
|
||||||
@@ -385,13 +466,13 @@ class UnitsDeliveryEvent:
|
|||||||
aircraft = unit_type.id
|
aircraft = unit_type.id
|
||||||
name = self.to_cp.name
|
name = self.to_cp.name
|
||||||
if count >= 0:
|
if count >= 0:
|
||||||
bought_units[unit_type] = count
|
bought_units[unit_type] = count
|
||||||
game.message(
|
game.message(
|
||||||
f"{coalition} reinforcements: {aircraft} x {count} at {name}")
|
f"{coalition} reinforcements: {aircraft} x {count} at {name}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
sold_units[unit_type] = -count
|
sold_units[unit_type] = -count
|
||||||
game.message(
|
game.message(f"{coalition} sold: {aircraft} x {-count} at {name}")
|
||||||
f"{coalition} sold: {aircraft} x {-count} at {name}")
|
|
||||||
self.to_cp.base.commision_units(bought_units)
|
self.to_cp.base.commision_units(bought_units)
|
||||||
self.to_cp.base.commit_losses(sold_units)
|
self.to_cp.base.commit_losses(sold_units)
|
||||||
self.units = {}
|
self.units = {}
|
||||||
|
|||||||
@@ -7,5 +7,6 @@ class FrontlineAttackEvent(Event):
|
|||||||
Currently the same as its parent, but here for legacy compatibility as well as to allow for
|
Currently the same as its parent, but here for legacy compatibility as well as to allow for
|
||||||
future unique Event handling
|
future unique Event handling
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "Frontline attack"
|
return "Frontline attack"
|
||||||
|
|||||||
@@ -10,8 +10,18 @@ from dcs.planes import plane_map
|
|||||||
from dcs.unittype import FlyingType, ShipType, VehicleType, UnitType
|
from dcs.unittype import FlyingType, ShipType, VehicleType, UnitType
|
||||||
from dcs.vehicles import Armor, Unarmed, Infantry, Artillery, AirDefence
|
from dcs.vehicles import Armor, Unarmed, Infantry, Artillery, AirDefence
|
||||||
|
|
||||||
from game.data.building_data import WW2_ALLIES_BUILDINGS, DEFAULT_AVAILABLE_BUILDINGS, WW2_GERMANY_BUILDINGS, WW2_FREE
|
from game.data.building_data import (
|
||||||
from game.data.doctrine import Doctrine, MODERN_DOCTRINE, COLDWAR_DOCTRINE, WWII_DOCTRINE
|
WW2_ALLIES_BUILDINGS,
|
||||||
|
DEFAULT_AVAILABLE_BUILDINGS,
|
||||||
|
WW2_GERMANY_BUILDINGS,
|
||||||
|
WW2_FREE,
|
||||||
|
)
|
||||||
|
from game.data.doctrine import (
|
||||||
|
Doctrine,
|
||||||
|
MODERN_DOCTRINE,
|
||||||
|
COLDWAR_DOCTRINE,
|
||||||
|
WWII_DOCTRINE,
|
||||||
|
)
|
||||||
from pydcs_extensions.mod_units import MODDED_VEHICLES, MODDED_AIRPLANES
|
from pydcs_extensions.mod_units import MODDED_VEHICLES, MODDED_AIRPLANES
|
||||||
|
|
||||||
|
|
||||||
@@ -60,6 +70,9 @@ class Faction:
|
|||||||
# Possible Missile site generators for this faction
|
# Possible Missile site generators for this faction
|
||||||
missiles: List[str] = field(default_factory=list)
|
missiles: List[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
# Possible costal site generators for this faction
|
||||||
|
coastal_defenses: List[str] = field(default_factory=list)
|
||||||
|
|
||||||
# Required mods or asset packs
|
# Required mods or asset packs
|
||||||
requirements: Dict[str, str] = field(default_factory=dict)
|
requirements: Dict[str, str] = field(default_factory=dict)
|
||||||
|
|
||||||
@@ -90,6 +103,9 @@ class Faction:
|
|||||||
# How many missiles group should we try to generate per CP on startup for this faction
|
# How many missiles group should we try to generate per CP on startup for this faction
|
||||||
missiles_group_count: int = field(default=1)
|
missiles_group_count: int = field(default=1)
|
||||||
|
|
||||||
|
# How many coastal group should we try to generate per CP on startup for this faction
|
||||||
|
coastal_group_count: int = field(default=1)
|
||||||
|
|
||||||
# Whether this faction has JTAC access
|
# Whether this faction has JTAC access
|
||||||
has_jtac: bool = field(default=False)
|
has_jtac: bool = field(default=False)
|
||||||
|
|
||||||
@@ -103,8 +119,7 @@ class Faction:
|
|||||||
building_set: List[str] = field(default_factory=list)
|
building_set: List[str] = field(default_factory=list)
|
||||||
|
|
||||||
# List of default livery overrides
|
# List of default livery overrides
|
||||||
liveries_overrides: Dict[Type[UnitType], List[str]] = field(
|
liveries_overrides: Dict[Type[UnitType], List[str]] = field(default_factory=dict)
|
||||||
default_factory=dict)
|
|
||||||
|
|
||||||
#: Set to True if the faction should force the "Unrestricted satnav" option
|
#: Set to True if the faction should force the "Unrestricted satnav" option
|
||||||
#: for the mission. This option enables GPS for capable aircraft regardless
|
#: for the mission. This option enables GPS for capable aircraft regardless
|
||||||
@@ -122,7 +137,11 @@ class Faction:
|
|||||||
|
|
||||||
faction.country = json.get("country", "/")
|
faction.country = json.get("country", "/")
|
||||||
if faction.country not in [c.name for c in country_dict.values()]:
|
if faction.country not in [c.name for c in country_dict.values()]:
|
||||||
raise AssertionError("Faction's country (\"{}\") is not a valid DCS country ID".format(faction.country))
|
raise AssertionError(
|
||||||
|
'Faction\'s country ("{}") is not a valid DCS country ID'.format(
|
||||||
|
faction.country
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
faction.name = json.get("name", "")
|
faction.name = json.get("name", "")
|
||||||
if not faction.name:
|
if not faction.name:
|
||||||
@@ -135,14 +154,10 @@ class Faction:
|
|||||||
faction.awacs = load_all_aircraft(json.get("awacs", []))
|
faction.awacs = load_all_aircraft(json.get("awacs", []))
|
||||||
faction.tankers = load_all_aircraft(json.get("tankers", []))
|
faction.tankers = load_all_aircraft(json.get("tankers", []))
|
||||||
|
|
||||||
faction.frontline_units = load_all_vehicles(
|
faction.frontline_units = load_all_vehicles(json.get("frontline_units", []))
|
||||||
json.get("frontline_units", []))
|
faction.artillery_units = load_all_vehicles(json.get("artillery_units", []))
|
||||||
faction.artillery_units = load_all_vehicles(
|
faction.infantry_units = load_all_vehicles(json.get("infantry_units", []))
|
||||||
json.get("artillery_units", []))
|
faction.logistics_units = load_all_vehicles(json.get("logistics_units", []))
|
||||||
faction.infantry_units = load_all_vehicles(
|
|
||||||
json.get("infantry_units", []))
|
|
||||||
faction.logistics_units = load_all_vehicles(
|
|
||||||
json.get("logistics_units", []))
|
|
||||||
|
|
||||||
faction.ewrs = json.get("ewrs", [])
|
faction.ewrs = json.get("ewrs", [])
|
||||||
|
|
||||||
@@ -153,16 +168,14 @@ class Faction:
|
|||||||
faction.air_defenses.extend(json.get("shorads", []))
|
faction.air_defenses.extend(json.get("shorads", []))
|
||||||
|
|
||||||
faction.missiles = json.get("missiles", [])
|
faction.missiles = json.get("missiles", [])
|
||||||
|
faction.coastal_defenses = json.get("coastal_defenses", [])
|
||||||
faction.requirements = json.get("requirements", {})
|
faction.requirements = json.get("requirements", {})
|
||||||
|
|
||||||
faction.carrier_names = json.get("carrier_names", [])
|
faction.carrier_names = json.get("carrier_names", [])
|
||||||
faction.helicopter_carrier_names = json.get(
|
faction.helicopter_carrier_names = json.get("helicopter_carrier_names", [])
|
||||||
"helicopter_carrier_names", [])
|
|
||||||
faction.navy_generators = json.get("navy_generators", [])
|
faction.navy_generators = json.get("navy_generators", [])
|
||||||
faction.aircraft_carrier = load_all_ships(
|
faction.aircraft_carrier = load_all_ships(json.get("aircraft_carrier", []))
|
||||||
json.get("aircraft_carrier", []))
|
faction.helicopter_carrier = load_all_ships(json.get("helicopter_carrier", []))
|
||||||
faction.helicopter_carrier = load_all_ships(
|
|
||||||
json.get("helicopter_carrier", []))
|
|
||||||
faction.destroyers = load_all_ships(json.get("destroyers", []))
|
faction.destroyers = load_all_ships(json.get("destroyers", []))
|
||||||
faction.cruisers = load_all_ships(json.get("cruisers", []))
|
faction.cruisers = load_all_ships(json.get("cruisers", []))
|
||||||
faction.has_jtac = json.get("has_jtac", False)
|
faction.has_jtac = json.get("has_jtac", False)
|
||||||
@@ -173,6 +186,7 @@ class Faction:
|
|||||||
faction.jtac_unit = None
|
faction.jtac_unit = None
|
||||||
faction.navy_group_count = int(json.get("navy_group_count", 1))
|
faction.navy_group_count = int(json.get("navy_group_count", 1))
|
||||||
faction.missiles_group_count = int(json.get("missiles_group_count", 0))
|
faction.missiles_group_count = int(json.get("missiles_group_count", 0))
|
||||||
|
faction.coastal_group_count = int(json.get("coastal_group_count", 0))
|
||||||
|
|
||||||
# Load doctrine
|
# Load doctrine
|
||||||
doctrine = json.get("doctrine", "modern")
|
doctrine = json.get("doctrine", "modern")
|
||||||
@@ -212,13 +226,18 @@ class Faction:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def units(self) -> List[Type[UnitType]]:
|
def units(self) -> List[Type[UnitType]]:
|
||||||
return (self.infantry_units + self.aircrafts + self.awacs +
|
return (
|
||||||
self.artillery_units + self.frontline_units +
|
self.infantry_units
|
||||||
self.tankers + self.logistics_units)
|
+ self.aircrafts
|
||||||
|
+ self.awacs
|
||||||
|
+ self.artillery_units
|
||||||
|
+ self.frontline_units
|
||||||
|
+ self.tankers
|
||||||
|
+ self.logistics_units
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def unit_loader(
|
def unit_loader(unit: str, class_repository: List[Any]) -> Optional[Type[UnitType]]:
|
||||||
unit: str, class_repository: List[Any]) -> Optional[Type[UnitType]]:
|
|
||||||
"""
|
"""
|
||||||
Find unit by name
|
Find unit by name
|
||||||
:param unit: Unit name as string
|
:param unit: Unit name as string
|
||||||
@@ -242,9 +261,10 @@ def unit_loader(
|
|||||||
|
|
||||||
|
|
||||||
def load_aircraft(name: str) -> Optional[Type[FlyingType]]:
|
def load_aircraft(name: str) -> Optional[Type[FlyingType]]:
|
||||||
return cast(Optional[FlyingType], unit_loader(
|
return cast(
|
||||||
name, [dcs.planes, dcs.helicopters, MODDED_AIRPLANES]
|
Optional[FlyingType],
|
||||||
))
|
unit_loader(name, [dcs.planes, dcs.helicopters, MODDED_AIRPLANES]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def load_all_aircraft(data) -> List[Type[FlyingType]]:
|
def load_all_aircraft(data) -> List[Type[FlyingType]]:
|
||||||
@@ -257,9 +277,12 @@ def load_all_aircraft(data) -> List[Type[FlyingType]]:
|
|||||||
|
|
||||||
|
|
||||||
def load_vehicle(name: str) -> Optional[Type[VehicleType]]:
|
def load_vehicle(name: str) -> Optional[Type[VehicleType]]:
|
||||||
return cast(Optional[FlyingType], unit_loader(
|
return cast(
|
||||||
name, [Infantry, Unarmed, Armor, AirDefence, Artillery, MODDED_VEHICLES]
|
Optional[FlyingType],
|
||||||
))
|
unit_loader(
|
||||||
|
name, [Infantry, Unarmed, Armor, AirDefence, Artillery, MODDED_VEHICLES]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def load_all_vehicles(data) -> List[Type[VehicleType]]:
|
def load_all_vehicles(data) -> List[Type[VehicleType]]:
|
||||||
|
|||||||
118
game/game.py
118
game/game.py
@@ -78,10 +78,16 @@ class TurnState(Enum):
|
|||||||
|
|
||||||
|
|
||||||
class Game:
|
class Game:
|
||||||
def __init__(self, player_name: str, enemy_name: str,
|
def __init__(
|
||||||
theater: ConflictTheater, start_date: datetime,
|
self,
|
||||||
settings: Settings, player_budget: float,
|
player_name: str,
|
||||||
enemy_budget: float) -> None:
|
enemy_name: str,
|
||||||
|
theater: ConflictTheater,
|
||||||
|
start_date: datetime,
|
||||||
|
settings: Settings,
|
||||||
|
player_budget: float,
|
||||||
|
enemy_budget: float,
|
||||||
|
) -> None:
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
self.events: List[Event] = []
|
self.events: List[Event] = []
|
||||||
self.theater = theater
|
self.theater = theater
|
||||||
@@ -90,6 +96,7 @@ class Game:
|
|||||||
self.enemy_name = enemy_name
|
self.enemy_name = enemy_name
|
||||||
self.enemy_country = db.FACTIONS[enemy_name].country
|
self.enemy_country = db.FACTIONS[enemy_name].country
|
||||||
self.turn = 0
|
self.turn = 0
|
||||||
|
# NB: This is the *start* date. It is never updated.
|
||||||
self.date = date(start_date.year, start_date.month, start_date.day)
|
self.date = date(start_date.year, start_date.month, start_date.day)
|
||||||
self.game_stats = GameStats()
|
self.game_stats = GameStats()
|
||||||
self.game_stats.update(self)
|
self.game_stats.update(self)
|
||||||
@@ -112,9 +119,7 @@ class Game:
|
|||||||
self.blue_ato = AirTaskingOrder()
|
self.blue_ato = AirTaskingOrder()
|
||||||
self.red_ato = AirTaskingOrder()
|
self.red_ato = AirTaskingOrder()
|
||||||
|
|
||||||
self.aircraft_inventory = GlobalAircraftInventory(
|
self.aircraft_inventory = GlobalAircraftInventory(self.theater.controlpoints)
|
||||||
self.theater.controlpoints
|
|
||||||
)
|
|
||||||
|
|
||||||
self.sanitize_sides()
|
self.sanitize_sides()
|
||||||
|
|
||||||
@@ -147,8 +152,9 @@ class Game:
|
|||||||
self.on_load()
|
self.on_load()
|
||||||
|
|
||||||
def generate_conditions(self) -> Conditions:
|
def generate_conditions(self) -> Conditions:
|
||||||
return Conditions.generate(self.theater, self.date,
|
return Conditions.generate(
|
||||||
self.current_turn_time_of_day, self.settings)
|
self.theater, self.current_day, self.current_turn_time_of_day, self.settings
|
||||||
|
)
|
||||||
|
|
||||||
def sanitize_sides(self):
|
def sanitize_sides(self):
|
||||||
"""
|
"""
|
||||||
@@ -184,13 +190,24 @@ class Game:
|
|||||||
return random.randint(1, 100) <= prob * mult
|
return random.randint(1, 100) <= prob * mult
|
||||||
|
|
||||||
def _generate_player_event(self, event_class, player_cp, enemy_cp):
|
def _generate_player_event(self, event_class, player_cp, enemy_cp):
|
||||||
self.events.append(event_class(self, player_cp, enemy_cp, enemy_cp.position, self.player_name, self.enemy_name))
|
self.events.append(
|
||||||
|
event_class(
|
||||||
|
self,
|
||||||
|
player_cp,
|
||||||
|
enemy_cp,
|
||||||
|
enemy_cp.position,
|
||||||
|
self.player_name,
|
||||||
|
self.enemy_name,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def _generate_events(self):
|
def _generate_events(self):
|
||||||
for front_line in self.theater.conflicts(True):
|
for front_line in self.theater.conflicts(True):
|
||||||
self._generate_player_event(FrontlineAttackEvent,
|
self._generate_player_event(
|
||||||
front_line.control_point_a,
|
FrontlineAttackEvent,
|
||||||
front_line.control_point_b)
|
front_line.control_point_a,
|
||||||
|
front_line.control_point_b,
|
||||||
|
)
|
||||||
|
|
||||||
def adjust_budget(self, amount: float, player: bool) -> None:
|
def adjust_budget(self, amount: float, player: bool) -> None:
|
||||||
if player:
|
if player:
|
||||||
@@ -208,7 +225,7 @@ class Game:
|
|||||||
self.enemy_budget += Income(self, player=False).total
|
self.enemy_budget += Income(self, player=False).total
|
||||||
|
|
||||||
def initiate_event(self, event: Event) -> UnitMap:
|
def initiate_event(self, event: Event) -> UnitMap:
|
||||||
#assert event in self.events
|
# assert event in self.events
|
||||||
logging.info("Generating {} (regular)".format(event))
|
logging.info("Generating {} (regular)".format(event))
|
||||||
return event.generate()
|
return event.generate()
|
||||||
|
|
||||||
@@ -223,7 +240,11 @@ class Game:
|
|||||||
|
|
||||||
def is_player_attack(self, event):
|
def is_player_attack(self, event):
|
||||||
if isinstance(event, Event):
|
if isinstance(event, Event):
|
||||||
return event and event.attacker_name and event.attacker_name == self.player_name
|
return (
|
||||||
|
event
|
||||||
|
and event.attacker_name
|
||||||
|
and event.attacker_name == self.player_name
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(f"{event} was passed when an Event type was expected")
|
raise RuntimeError(f"{event} was passed when an Event type was expected")
|
||||||
|
|
||||||
@@ -235,7 +256,9 @@ class Game:
|
|||||||
|
|
||||||
def pass_turn(self, no_action: bool = False) -> None:
|
def pass_turn(self, no_action: bool = False) -> None:
|
||||||
logging.info("Pass turn")
|
logging.info("Pass turn")
|
||||||
self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0))
|
self.informations.append(
|
||||||
|
Information("End of turn #" + str(self.turn), "-" * 40, 0)
|
||||||
|
)
|
||||||
self.turn += 1
|
self.turn += 1
|
||||||
|
|
||||||
for control_point in self.theater.controlpoints:
|
for control_point in self.theater.controlpoints:
|
||||||
@@ -261,11 +284,18 @@ class Game:
|
|||||||
persistency.autosave(self)
|
persistency.autosave(self)
|
||||||
|
|
||||||
def check_win_loss(self):
|
def check_win_loss(self):
|
||||||
captured_states = {i.captured for i in self.theater.controlpoints}
|
player_airbases = {
|
||||||
if True not in captured_states:
|
cp for cp in self.theater.player_points() if cp.runway_is_operational()
|
||||||
|
}
|
||||||
|
if not player_airbases:
|
||||||
return TurnState.LOSS
|
return TurnState.LOSS
|
||||||
if False not in captured_states:
|
|
||||||
|
enemy_airbases = {
|
||||||
|
cp for cp in self.theater.enemy_points() if cp.runway_is_operational()
|
||||||
|
}
|
||||||
|
if not enemy_airbases:
|
||||||
return TurnState.WIN
|
return TurnState.WIN
|
||||||
|
|
||||||
return TurnState.CONTINUE
|
return TurnState.CONTINUE
|
||||||
|
|
||||||
def initialize_turn(self) -> None:
|
def initialize_turn(self) -> None:
|
||||||
@@ -281,7 +311,7 @@ class Game:
|
|||||||
|
|
||||||
# Check for win or loss condition
|
# Check for win or loss condition
|
||||||
turn_state = self.check_win_loss()
|
turn_state = self.check_win_loss()
|
||||||
if turn_state in (TurnState.LOSS,TurnState.WIN):
|
if turn_state in (TurnState.LOSS, TurnState.WIN):
|
||||||
return self.process_win_loss(turn_state)
|
return self.process_win_loss(turn_state)
|
||||||
|
|
||||||
# Plan flights & combat for next turn
|
# Plan flights & combat for next turn
|
||||||
@@ -305,8 +335,11 @@ class Game:
|
|||||||
|
|
||||||
self.plan_procurement(blue_planner, red_planner)
|
self.plan_procurement(blue_planner, red_planner)
|
||||||
|
|
||||||
def plan_procurement(self, blue_planner: CoalitionMissionPlanner,
|
def plan_procurement(
|
||||||
red_planner: CoalitionMissionPlanner) -> None:
|
self,
|
||||||
|
blue_planner: CoalitionMissionPlanner,
|
||||||
|
red_planner: CoalitionMissionPlanner,
|
||||||
|
) -> None:
|
||||||
# The first turn needs to buy a *lot* of aircraft to fill CAPs, so it
|
# The first turn needs to buy a *lot* of aircraft to fill CAPs, so it
|
||||||
# gets much more of the budget that turn. Otherwise budget (after
|
# gets much more of the budget that turn. Otherwise budget (after
|
||||||
# repairs) is split evenly between air and ground. For the default
|
# repairs) is split evenly between air and ground. For the default
|
||||||
@@ -320,7 +353,7 @@ class Game:
|
|||||||
manage_runways=self.settings.automate_runway_repair,
|
manage_runways=self.settings.automate_runway_repair,
|
||||||
manage_front_line=self.settings.automate_front_line_reinforcements,
|
manage_front_line=self.settings.automate_front_line_reinforcements,
|
||||||
manage_aircraft=self.settings.automate_aircraft_reinforcements,
|
manage_aircraft=self.settings.automate_aircraft_reinforcements,
|
||||||
front_line_budget_share=ground_portion
|
front_line_budget_share=ground_portion,
|
||||||
).spend_budget(self.budget, blue_planner.procurement_requests)
|
).spend_budget(self.budget, blue_planner.procurement_requests)
|
||||||
|
|
||||||
self.enemy_budget = ProcurementAi(
|
self.enemy_budget = ProcurementAi(
|
||||||
@@ -330,7 +363,7 @@ class Game:
|
|||||||
manage_runways=True,
|
manage_runways=True,
|
||||||
manage_front_line=True,
|
manage_front_line=True,
|
||||||
manage_aircraft=True,
|
manage_aircraft=True,
|
||||||
front_line_budget_share=ground_portion
|
front_line_budget_share=ground_portion,
|
||||||
).spend_budget(self.enemy_budget, red_planner.procurement_requests)
|
).spend_budget(self.enemy_budget, red_planner.procurement_requests)
|
||||||
|
|
||||||
def message(self, text: str) -> None:
|
def message(self, text: str) -> None:
|
||||||
@@ -361,10 +394,12 @@ class Game:
|
|||||||
def compute_threat_zones(self) -> None:
|
def compute_threat_zones(self) -> None:
|
||||||
self.blue_threat_zone = ThreatZones.for_faction(self, player=True)
|
self.blue_threat_zone = ThreatZones.for_faction(self, player=True)
|
||||||
self.red_threat_zone = ThreatZones.for_faction(self, player=False)
|
self.red_threat_zone = ThreatZones.for_faction(self, player=False)
|
||||||
self.blue_navmesh = NavMesh.from_threat_zones(self.red_threat_zone,
|
self.blue_navmesh = NavMesh.from_threat_zones(
|
||||||
self.theater)
|
self.red_threat_zone, self.theater
|
||||||
self.red_navmesh = NavMesh.from_threat_zones(self.blue_threat_zone,
|
)
|
||||||
self.theater)
|
self.red_navmesh = NavMesh.from_threat_zones(
|
||||||
|
self.blue_threat_zone, self.theater
|
||||||
|
)
|
||||||
|
|
||||||
def threat_zone_for(self, player: bool) -> ThreatZones:
|
def threat_zone_for(self, player: bool) -> ThreatZones:
|
||||||
if player:
|
if player:
|
||||||
@@ -386,9 +421,9 @@ class Game:
|
|||||||
|
|
||||||
# By default, use the existing frontline conflict position
|
# By default, use the existing frontline conflict position
|
||||||
for front_line in self.theater.conflicts():
|
for front_line in self.theater.conflicts():
|
||||||
position = Conflict.frontline_position(front_line.control_point_a,
|
position = Conflict.frontline_position(
|
||||||
front_line.control_point_b,
|
front_line.control_point_a, front_line.control_point_b, self.theater
|
||||||
self.theater)
|
)
|
||||||
zones.append(position[0])
|
zones.append(position[0])
|
||||||
zones.append(front_line.control_point_a.position)
|
zones.append(front_line.control_point_a.position)
|
||||||
zones.append(front_line.control_point_b.position)
|
zones.append(front_line.control_point_b.position)
|
||||||
@@ -413,7 +448,10 @@ class Game:
|
|||||||
d = cp.position.distance_to_point(cp2.position)
|
d = cp.position.distance_to_point(cp2.position)
|
||||||
if d < min_distance:
|
if d < min_distance:
|
||||||
min_distance = d
|
min_distance = d
|
||||||
cpoint = Point((cp.position.x + cp2.position.x) / 2, (cp.position.y + cp2.position.y) / 2)
|
cpoint = Point(
|
||||||
|
(cp.position.x + cp2.position.x) / 2,
|
||||||
|
(cp.position.y + cp2.position.y) / 2,
|
||||||
|
)
|
||||||
zones.append(cp.position)
|
zones.append(cp.position)
|
||||||
zones.append(cp2.position)
|
zones.append(cp2.position)
|
||||||
break
|
break
|
||||||
@@ -422,8 +460,7 @@ class Game:
|
|||||||
if cpoint is not None:
|
if cpoint is not None:
|
||||||
zones.append(cpoint)
|
zones.append(cpoint)
|
||||||
|
|
||||||
packages = itertools.chain(self.blue_ato.packages,
|
packages = itertools.chain(self.blue_ato.packages, self.red_ato.packages)
|
||||||
self.red_ato.packages)
|
|
||||||
for package in packages:
|
for package in packages:
|
||||||
if package.primary_task is FlightType.BARCAP:
|
if package.primary_task is FlightType.BARCAP:
|
||||||
# BARCAPs will be planned at most locations on smaller theaters,
|
# BARCAPs will be planned at most locations on smaller theaters,
|
||||||
@@ -460,7 +497,10 @@ class Game:
|
|||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
for z in self.__culling_zones:
|
for z in self.__culling_zones:
|
||||||
if z.distance_to_point(pos) < self.settings.perf_culling_distance * 1000:
|
if (
|
||||||
|
z.distance_to_point(pos)
|
||||||
|
< self.settings.perf_culling_distance * 1000
|
||||||
|
):
|
||||||
return False
|
return False
|
||||||
for p in self.__culling_points:
|
for p in self.__culling_points:
|
||||||
if p.distance_to_point(pos) < 2500:
|
if p.distance_to_point(pos) < 2500:
|
||||||
@@ -502,6 +542,10 @@ class Game:
|
|||||||
|
|
||||||
def process_win_loss(self, turn_state: TurnState):
|
def process_win_loss(self, turn_state: TurnState):
|
||||||
if turn_state is TurnState.WIN:
|
if turn_state is TurnState.WIN:
|
||||||
return self.message("Congratulations, you are victorious! Start a new campaign to continue.")
|
return self.message(
|
||||||
|
"Congratulations, you are victorious! Start a new campaign to continue."
|
||||||
|
)
|
||||||
elif turn_state is TurnState.LOSS:
|
elif turn_state is TurnState.LOSS:
|
||||||
return self.message("Game Over, you lose. Start a new campaign to continue.")
|
return self.message(
|
||||||
|
"Game Over, you lose. Start a new campaign to continue."
|
||||||
|
)
|
||||||
|
|||||||
@@ -46,10 +46,10 @@ class Income:
|
|||||||
for tgo in tgos:
|
for tgo in tgos:
|
||||||
if not tgo.is_dead:
|
if not tgo.is_dead:
|
||||||
count += 1
|
count += 1
|
||||||
self.buildings.append(BuildingIncome(name, category, count,
|
self.buildings.append(
|
||||||
REWARDS[category]))
|
BuildingIncome(name, category, count, REWARDS[category])
|
||||||
|
)
|
||||||
|
|
||||||
self.from_bases = sum(cp.income_per_turn for cp in self.control_points)
|
self.from_bases = sum(cp.income_per_turn for cp in self.control_points)
|
||||||
self.total_buildings = sum(b.income for b in self.buildings)
|
self.total_buildings = sum(b.income for b in self.buildings)
|
||||||
self.total = ((self.total_buildings + self.from_bases) *
|
self.total = (self.total_buildings + self.from_bases) * self.multiplier
|
||||||
self.multiplier)
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
class Information():
|
|
||||||
|
|
||||||
|
class Information:
|
||||||
def __init__(self, title="", text="", turn=0):
|
def __init__(self, title="", text="", turn=0):
|
||||||
self.title = title
|
self.title = title
|
||||||
self.text = text
|
self.text = text
|
||||||
@@ -9,9 +9,11 @@ class Information():
|
|||||||
self.timestamp = datetime.datetime.now()
|
self.timestamp = datetime.datetime.now()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '[{}][{}] {} {}'.format(
|
return "[{}][{}] {} {}".format(
|
||||||
self.timestamp.strftime("%Y-%m-%d %H:%M:%S") if self.timestamp is not None else '',
|
self.timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
if self.timestamp is not None
|
||||||
|
else "",
|
||||||
self.turn,
|
self.turn,
|
||||||
self.title,
|
self.title,
|
||||||
self.text
|
self.text,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ class ControlPointAircraftInventory:
|
|||||||
|
|
||||||
class GlobalAircraftInventory:
|
class GlobalAircraftInventory:
|
||||||
"""Game-wide aircraft inventory."""
|
"""Game-wide aircraft inventory."""
|
||||||
|
|
||||||
def __init__(self, control_points: Iterable[ControlPoint]) -> None:
|
def __init__(self, control_points: Iterable[ControlPoint]) -> None:
|
||||||
self.inventories: Dict[ControlPoint, ControlPointAircraftInventory] = {
|
self.inventories: Dict[ControlPoint, ControlPointAircraftInventory] = {
|
||||||
cp: ControlPointAircraftInventory(cp) for cp in control_points
|
cp: ControlPointAircraftInventory(cp) for cp in control_points
|
||||||
@@ -100,8 +101,8 @@ class GlobalAircraftInventory:
|
|||||||
inventory.add_aircraft(aircraft, count)
|
inventory.add_aircraft(aircraft, count)
|
||||||
|
|
||||||
def for_control_point(
|
def for_control_point(
|
||||||
self,
|
self, control_point: ControlPoint
|
||||||
control_point: ControlPoint) -> ControlPointAircraftInventory:
|
) -> ControlPointAircraftInventory:
|
||||||
"""Returns the inventory specific to the given control point."""
|
"""Returns the inventory specific to the given control point."""
|
||||||
return self.inventories[control_point]
|
return self.inventories[control_point]
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ class DestroyedUnit:
|
|||||||
y: int
|
y: int
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
def __init__(self, x , y, name):
|
def __init__(self, x, y, name):
|
||||||
self.x = x
|
self.x = x
|
||||||
self.y = y
|
self.y = y
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class FrontlineData:
|
|||||||
This Data structure will store information about an existing frontline
|
This Data structure will store information about an existing frontline
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, from_cp:ControlPoint, to_cp: ControlPoint):
|
def __init__(self, from_cp: ControlPoint, to_cp: ControlPoint):
|
||||||
self.to_cp = to_cp
|
self.to_cp = to_cp
|
||||||
self.from_cp = from_cp
|
self.from_cp = from_cp
|
||||||
self.enemy_units_position = []
|
self.enemy_units_position = []
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
class FactionTurnMetadata:
|
class FactionTurnMetadata:
|
||||||
"""
|
"""
|
||||||
Store metadata about a faction
|
Store metadata about a faction
|
||||||
@@ -20,8 +21,8 @@ class GameTurnMetadata:
|
|||||||
Store metadata about a game turn
|
Store metadata about a game turn
|
||||||
"""
|
"""
|
||||||
|
|
||||||
allied_units:FactionTurnMetadata
|
allied_units: FactionTurnMetadata
|
||||||
enemy_units:FactionTurnMetadata
|
enemy_units: FactionTurnMetadata
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.allied_units = FactionTurnMetadata()
|
self.allied_units = FactionTurnMetadata()
|
||||||
@@ -53,4 +54,3 @@ class GameStats:
|
|||||||
turn_data.enemy_units.vehicles_count += sum(cp.base.armor.values())
|
turn_data.enemy_units.vehicles_count += sum(cp.base.armor.values())
|
||||||
|
|
||||||
self.data_per_turn.append(turn_data)
|
self.data_per_turn.append(turn_data)
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ from game.threatzones import ThreatZones
|
|||||||
from game.utils import nautical_miles
|
from game.utils import nautical_miles
|
||||||
|
|
||||||
|
|
||||||
|
class NavMeshError(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class NavMeshPoly:
|
class NavMeshPoly:
|
||||||
def __init__(self, ident: int, poly: Polygon, threatened: bool) -> None:
|
def __init__(self, ident: int, poly: Polygon, threatened: bool) -> None:
|
||||||
self.ident = ident
|
self.ident = ident
|
||||||
@@ -114,16 +118,18 @@ class NavMesh:
|
|||||||
return self.travel_cost(a, b)
|
return self.travel_cost(a, b)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def reconstruct_path(came_from: Dict[NavPoint, Optional[NavPoint]],
|
def reconstruct_path(
|
||||||
origin: NavPoint,
|
came_from: Dict[NavPoint, Optional[NavPoint]],
|
||||||
destination: NavPoint) -> List[Point]:
|
origin: NavPoint,
|
||||||
|
destination: NavPoint,
|
||||||
|
) -> List[Point]:
|
||||||
current = destination
|
current = destination
|
||||||
path: List[Point] = []
|
path: List[Point] = []
|
||||||
while current != origin:
|
while current != origin:
|
||||||
path.append(current.world_point)
|
path.append(current.world_point)
|
||||||
previous = came_from[current]
|
previous = came_from[current]
|
||||||
if previous is None:
|
if previous is None:
|
||||||
raise RuntimeError(
|
raise NavMeshError(
|
||||||
f"Could not reconstruct path to {destination} from {origin}"
|
f"Could not reconstruct path to {destination} from {origin}"
|
||||||
)
|
)
|
||||||
current = previous
|
current = previous
|
||||||
@@ -138,19 +144,19 @@ class NavMesh:
|
|||||||
def shortest_path(self, origin: Point, destination: Point) -> List[Point]:
|
def shortest_path(self, origin: Point, destination: Point) -> List[Point]:
|
||||||
origin_poly = self.localize(origin)
|
origin_poly = self.localize(origin)
|
||||||
if origin_poly is None:
|
if origin_poly is None:
|
||||||
raise ValueError(f"Origin point {origin} is outside the navmesh")
|
raise NavMeshError(f"Origin point {origin} is outside the navmesh")
|
||||||
destination_poly = self.localize(destination)
|
destination_poly = self.localize(destination)
|
||||||
if destination_poly is None:
|
if destination_poly is None:
|
||||||
raise ValueError(
|
raise NavMeshError(
|
||||||
f"Origin point {destination} is outside the navmesh")
|
f"Destination point {destination} is outside the navmesh"
|
||||||
|
)
|
||||||
|
|
||||||
return self._shortest_path(
|
return self._shortest_path(
|
||||||
NavPoint(self.dcs_to_shapely_point(origin), origin_poly),
|
NavPoint(self.dcs_to_shapely_point(origin), origin_poly),
|
||||||
NavPoint(self.dcs_to_shapely_point(destination), destination_poly)
|
NavPoint(self.dcs_to_shapely_point(destination), destination_poly),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _shortest_path(self, origin: NavPoint,
|
def _shortest_path(self, origin: NavPoint, destination: NavPoint) -> List[Point]:
|
||||||
destination: NavPoint) -> List[Point]:
|
|
||||||
# Adapted from
|
# Adapted from
|
||||||
# https://www.redblobgames.com/pathfinding/a-star/implementation.py.
|
# https://www.redblobgames.com/pathfinding/a-star/implementation.py.
|
||||||
frontier = NavFrontier()
|
frontier = NavFrontier()
|
||||||
@@ -167,9 +173,7 @@ class NavMesh:
|
|||||||
if current.poly == destination.poly:
|
if current.poly == destination.poly:
|
||||||
# Made it to the correct nav poly. Add the leg from the border
|
# Made it to the correct nav poly. Add the leg from the border
|
||||||
# to the target.
|
# to the target.
|
||||||
cost = best_known[current] + self.travel_cost(
|
cost = best_known[current] + self.travel_cost(current, destination)
|
||||||
current, destination
|
|
||||||
)
|
|
||||||
if cost < best_known[destination]:
|
if cost < best_known[destination]:
|
||||||
best_known[destination] = cost
|
best_known[destination] = cost
|
||||||
estimated = cost
|
estimated = cost
|
||||||
@@ -185,14 +189,10 @@ class NavMesh:
|
|||||||
raise RuntimeError
|
raise RuntimeError
|
||||||
_, neighbor_point = nearest_points(current.point, boundary)
|
_, neighbor_point = nearest_points(current.point, boundary)
|
||||||
neighbor_nav = NavPoint(neighbor_point, neighbor)
|
neighbor_nav = NavPoint(neighbor_point, neighbor)
|
||||||
cost = best_known[current] + self.travel_cost(
|
cost = best_known[current] + self.travel_cost(current, neighbor_nav)
|
||||||
current, neighbor_nav
|
|
||||||
)
|
|
||||||
if cost < best_known[neighbor_nav]:
|
if cost < best_known[neighbor_nav]:
|
||||||
best_known[neighbor_nav] = cost
|
best_known[neighbor_nav] = cost
|
||||||
estimated = cost + self.travel_heuristic(
|
estimated = cost + self.travel_heuristic(neighbor_nav, destination)
|
||||||
neighbor_nav, destination
|
|
||||||
)
|
|
||||||
frontier.push(neighbor_nav, estimated)
|
frontier.push(neighbor_nav, estimated)
|
||||||
came_from[neighbor_nav] = current
|
came_from[neighbor_nav] = current
|
||||||
|
|
||||||
@@ -209,13 +209,16 @@ class NavMesh:
|
|||||||
# threatened airbases at the map edges have room to retreat from the
|
# threatened airbases at the map edges have room to retreat from the
|
||||||
# threat without running off the navmesh.
|
# threat without running off the navmesh.
|
||||||
return box(*LineString(points).bounds).buffer(
|
return box(*LineString(points).bounds).buffer(
|
||||||
nautical_miles(100).meters, resolution=1)
|
nautical_miles(200).meters, resolution=1
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_navpolys(polys: List[Polygon],
|
def create_navpolys(
|
||||||
threat_zones: ThreatZones) -> List[NavMeshPoly]:
|
polys: List[Polygon], threat_zones: ThreatZones
|
||||||
return [NavMeshPoly(i, p, threat_zones.threatened(p))
|
) -> List[NavMeshPoly]:
|
||||||
for i, p in enumerate(polys)]
|
return [
|
||||||
|
NavMeshPoly(i, p, threat_zones.threatened(p)) for i, p in enumerate(polys)
|
||||||
|
]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def associate_neighbors(polys: List[NavMeshPoly]) -> None:
|
def associate_neighbors(polys: List[NavMeshPoly]) -> None:
|
||||||
@@ -234,8 +237,7 @@ class NavMesh:
|
|||||||
point = (int(x), int(y))
|
point = (int(x), int(y))
|
||||||
neighbors = {}
|
neighbors = {}
|
||||||
for potential_neighbor in points_map[point]:
|
for potential_neighbor in points_map[point]:
|
||||||
intersection = navpoly.poly.intersection(
|
intersection = navpoly.poly.intersection(potential_neighbor.poly)
|
||||||
potential_neighbor.poly)
|
|
||||||
if not intersection.is_empty:
|
if not intersection.is_empty:
|
||||||
potential_neighbor.neighbors[navpoly] = intersection
|
potential_neighbor.neighbors[navpoly] = intersection
|
||||||
neighbors[potential_neighbor] = intersection
|
neighbors[potential_neighbor] = intersection
|
||||||
@@ -243,8 +245,9 @@ class NavMesh:
|
|||||||
points_map[point].add(navpoly)
|
points_map[point].add(navpoly)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_threat_zones(cls, threat_zones: ThreatZones,
|
def from_threat_zones(
|
||||||
theater: ConflictTheater) -> NavMesh:
|
cls, threat_zones: ThreatZones, theater: ConflictTheater
|
||||||
|
) -> NavMesh:
|
||||||
# Simplify the threat poly to reduce the number of nav zones. Increase
|
# Simplify the threat poly to reduce the number of nav zones. Increase
|
||||||
# the size of the zone and then simplify it with the buffer size as the
|
# the size of the zone and then simplify it with the buffer size as the
|
||||||
# error margin. This will create a simpler poly around the threat zone.
|
# error margin. This will create a simpler poly around the threat zone.
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
class Operation:
|
class Operation:
|
||||||
"""Static class for managing the final Mission generation"""
|
"""Static class for managing the final Mission generation"""
|
||||||
|
|
||||||
current_mission = None # type: Mission
|
current_mission = None # type: Mission
|
||||||
airgen = None # type: AircraftConflictGenerator
|
airgen = None # type: AircraftConflictGenerator
|
||||||
triggersgen = None # type: TriggersGenerator
|
triggersgen = None # type: TriggersGenerator
|
||||||
@@ -84,7 +85,7 @@ class Operation:
|
|||||||
cls.game.enemy_name,
|
cls.game.enemy_name,
|
||||||
cls.game.player_country,
|
cls.game.player_country,
|
||||||
cls.game.enemy_country,
|
cls.game.enemy_country,
|
||||||
frontline.position
|
frontline.position,
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -93,7 +94,7 @@ class Operation:
|
|||||||
player_cp, enemy_cp = cls.game.theater.closest_opposing_control_points()
|
player_cp, enemy_cp = cls.game.theater.closest_opposing_control_points()
|
||||||
mid_point = player_cp.position.point_from_heading(
|
mid_point = player_cp.position.point_from_heading(
|
||||||
player_cp.position.heading_between_point(enemy_cp.position),
|
player_cp.position.heading_between_point(enemy_cp.position),
|
||||||
player_cp.position.distance_to_point(enemy_cp.position) / 2
|
player_cp.position.distance_to_point(enemy_cp.position) / 2,
|
||||||
)
|
)
|
||||||
return Conflict(
|
return Conflict(
|
||||||
cls.game.theater,
|
cls.game.theater,
|
||||||
@@ -103,7 +104,7 @@ class Operation:
|
|||||||
cls.game.enemy_name,
|
cls.game.enemy_name,
|
||||||
cls.game.player_country,
|
cls.game.player_country,
|
||||||
cls.game.enemy_country,
|
cls.game.enemy_country,
|
||||||
mid_point
|
mid_point,
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -118,9 +119,11 @@ class Operation:
|
|||||||
p_country = cls.game.player_country
|
p_country = cls.game.player_country
|
||||||
e_country = cls.game.enemy_country
|
e_country = cls.game.enemy_country
|
||||||
cls.current_mission.coalition["blue"].add_country(
|
cls.current_mission.coalition["blue"].add_country(
|
||||||
country_dict[db.country_id_from_name(p_country)]())
|
country_dict[db.country_id_from_name(p_country)]()
|
||||||
|
)
|
||||||
cls.current_mission.coalition["red"].add_country(
|
cls.current_mission.coalition["red"].add_country(
|
||||||
country_dict[db.country_id_from_name(e_country)]())
|
country_dict[db.country_id_from_name(e_country)]()
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def inject_lua_trigger(cls, contents: str, comment: str) -> None:
|
def inject_lua_trigger(cls, contents: str, comment: str) -> None:
|
||||||
@@ -133,12 +136,11 @@ class Operation:
|
|||||||
cls.plugin_scripts.append(mnemonic)
|
cls.plugin_scripts.append(mnemonic)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def inject_plugin_script(cls, plugin_mnemonic: str, script: str,
|
def inject_plugin_script(
|
||||||
script_mnemonic: str) -> None:
|
cls, plugin_mnemonic: str, script: str, script_mnemonic: str
|
||||||
|
) -> None:
|
||||||
if script_mnemonic in cls.plugin_scripts:
|
if script_mnemonic in cls.plugin_scripts:
|
||||||
logging.debug(
|
logging.debug(f"Skipping already loaded {script} for {plugin_mnemonic}")
|
||||||
f"Skipping already loaded {script} for {plugin_mnemonic}"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
cls.plugin_scripts.append(script_mnemonic)
|
cls.plugin_scripts.append(script_mnemonic)
|
||||||
|
|
||||||
@@ -146,15 +148,12 @@ class Operation:
|
|||||||
|
|
||||||
script_path = Path(plugin_path, script)
|
script_path = Path(plugin_path, script)
|
||||||
if not script_path.exists():
|
if not script_path.exists():
|
||||||
logging.error(
|
logging.error(f"Cannot find {script_path} for plugin {plugin_mnemonic}")
|
||||||
f"Cannot find {script_path} for plugin {plugin_mnemonic}"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
trigger = TriggerStart(comment=f"Load {script_mnemonic}")
|
trigger = TriggerStart(comment=f"Load {script_mnemonic}")
|
||||||
filename = script_path.resolve()
|
filename = script_path.resolve()
|
||||||
fileref = cls.current_mission.map_resource.add_resource_file(
|
fileref = cls.current_mission.map_resource.add_resource_file(filename)
|
||||||
filename)
|
|
||||||
trigger.add_action(DoScriptFile(fileref))
|
trigger.add_action(DoScriptFile(fileref))
|
||||||
cls.current_mission.triggerrules.triggers.append(trigger)
|
cls.current_mission.triggerrules.triggers.append(trigger)
|
||||||
|
|
||||||
@@ -166,11 +165,11 @@ class Operation:
|
|||||||
jtacs: List[JtacInfo],
|
jtacs: List[JtacInfo],
|
||||||
airgen: AircraftConflictGenerator,
|
airgen: AircraftConflictGenerator,
|
||||||
):
|
):
|
||||||
"""Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings)
|
"""Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings)"""
|
||||||
"""
|
|
||||||
gens: List[MissionInfoGenerator] = [
|
gens: List[MissionInfoGenerator] = [
|
||||||
KneeboardGenerator(cls.current_mission, cls.game),
|
KneeboardGenerator(cls.current_mission, cls.game),
|
||||||
BriefingGenerator(cls.current_mission, cls.game)
|
BriefingGenerator(cls.current_mission, cls.game),
|
||||||
]
|
]
|
||||||
for gen in gens:
|
for gen in gens:
|
||||||
for dynamic_runway in groundobjectgen.runways.values():
|
for dynamic_runway in groundobjectgen.runways.values():
|
||||||
@@ -179,9 +178,8 @@ class Operation:
|
|||||||
for tanker in airsupportgen.air_support.tankers:
|
for tanker in airsupportgen.air_support.tankers:
|
||||||
gen.add_tanker(tanker)
|
gen.add_tanker(tanker)
|
||||||
|
|
||||||
if cls.player_awacs_enabled:
|
for aewc in airsupportgen.air_support.awacs:
|
||||||
for awacs in airsupportgen.air_support.awacs:
|
gen.add_awacs(aewc)
|
||||||
gen.add_awacs(awacs)
|
|
||||||
|
|
||||||
for jtac in jtacs:
|
for jtac in jtacs:
|
||||||
gen.add_jtac(jtac)
|
gen.add_jtac(jtac)
|
||||||
@@ -208,8 +206,9 @@ class Operation:
|
|||||||
cls.radio_registry.reserve(frequency)
|
cls.radio_registry.reserve(frequency)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def assign_channels_to_flights(cls, flights: List[FlightData],
|
def assign_channels_to_flights(
|
||||||
air_support: AirSupport) -> None:
|
cls, flights: List[FlightData], air_support: AirSupport
|
||||||
|
) -> None:
|
||||||
"""Assigns preset radio channels for client flights."""
|
"""Assigns preset radio channels for client flights."""
|
||||||
for flight in flights:
|
for flight in flights:
|
||||||
if not flight.client_units:
|
if not flight.client_units:
|
||||||
@@ -217,8 +216,7 @@ class Operation:
|
|||||||
cls.assign_channels_to_flight(flight, air_support)
|
cls.assign_channels_to_flight(flight, air_support)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def assign_channels_to_flight(flight: FlightData,
|
def assign_channels_to_flight(flight: FlightData, air_support: AirSupport) -> None:
|
||||||
air_support: AirSupport) -> None:
|
|
||||||
"""Assigns preset radio channels for a client flight."""
|
"""Assigns preset radio channels for a client flight."""
|
||||||
airframe = flight.aircraft_type
|
airframe = flight.aircraft_type
|
||||||
|
|
||||||
@@ -234,7 +232,9 @@ class Operation:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _create_tacan_registry(cls, unique_map_frequencies: Set[RadioFrequency]) -> None:
|
def _create_tacan_registry(
|
||||||
|
cls, unique_map_frequencies: Set[RadioFrequency]
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Dedup beacon/radio frequencies, since some maps have some frequencies
|
Dedup beacon/radio frequencies, since some maps have some frequencies
|
||||||
used multiple times.
|
used multiple times.
|
||||||
@@ -246,13 +246,14 @@ class Operation:
|
|||||||
unique_map_frequencies.add(beacon.frequency)
|
unique_map_frequencies.add(beacon.frequency)
|
||||||
if beacon.is_tacan:
|
if beacon.is_tacan:
|
||||||
if beacon.channel is None:
|
if beacon.channel is None:
|
||||||
logging.error(
|
logging.error(f"TACAN beacon has no channel: {beacon.callsign}")
|
||||||
f"TACAN beacon has no channel: {beacon.callsign}")
|
|
||||||
else:
|
else:
|
||||||
cls.tacan_registry.reserve(beacon.tacan_channel)
|
cls.tacan_registry.reserve(beacon.tacan_channel)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _create_radio_registry(cls, unique_map_frequencies: Set[RadioFrequency]) -> None:
|
def _create_radio_registry(
|
||||||
|
cls, unique_map_frequencies: Set[RadioFrequency]
|
||||||
|
) -> None:
|
||||||
cls.radio_registry = RadioRegistry()
|
cls.radio_registry = RadioRegistry()
|
||||||
for data in AIRFIELD_DATA.values():
|
for data in AIRFIELD_DATA.values():
|
||||||
if data.theater == cls.game.theater.terrain.name and data.atc:
|
if data.theater == cls.game.theater.terrain.name and data.atc:
|
||||||
@@ -270,7 +271,7 @@ class Operation:
|
|||||||
cls.game,
|
cls.game,
|
||||||
cls.radio_registry,
|
cls.radio_registry,
|
||||||
cls.tacan_registry,
|
cls.tacan_registry,
|
||||||
cls.unit_map
|
cls.unit_map,
|
||||||
)
|
)
|
||||||
cls.groundobjectgen.generate()
|
cls.groundobjectgen.generate()
|
||||||
|
|
||||||
@@ -284,10 +285,13 @@ class Operation:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
pos = Point(d["x"], d["z"])
|
pos = Point(d["x"], d["z"])
|
||||||
if utype is not None and not cls.game.position_culled(pos) and cls.game.settings.perf_destroyed_units:
|
if (
|
||||||
|
utype is not None
|
||||||
|
and not cls.game.position_culled(pos)
|
||||||
|
and cls.game.settings.perf_destroyed_units
|
||||||
|
):
|
||||||
cls.current_mission.static_group(
|
cls.current_mission.static_group(
|
||||||
country=cls.current_mission.country(
|
country=cls.current_mission.country(cls.game.player_country),
|
||||||
cls.game.player_country),
|
|
||||||
name="",
|
name="",
|
||||||
_type=utype,
|
_type=utype,
|
||||||
hidden=True,
|
hidden=True,
|
||||||
@@ -302,13 +306,13 @@ class Operation:
|
|||||||
cls.create_unit_map()
|
cls.create_unit_map()
|
||||||
cls.create_radio_registries()
|
cls.create_radio_registries()
|
||||||
# Set mission time and weather conditions.
|
# Set mission time and weather conditions.
|
||||||
EnvironmentGenerator(cls.current_mission,
|
EnvironmentGenerator(cls.current_mission, cls.game.conditions).generate()
|
||||||
cls.game.conditions).generate()
|
|
||||||
cls._generate_ground_units()
|
cls._generate_ground_units()
|
||||||
cls._generate_destroyed_units()
|
cls._generate_destroyed_units()
|
||||||
cls._generate_air_units()
|
cls._generate_air_units()
|
||||||
cls.assign_channels_to_flights(cls.airgen.flights,
|
cls.assign_channels_to_flights(
|
||||||
cls.airsupportgen.air_support)
|
cls.airgen.flights, cls.airsupportgen.air_support
|
||||||
|
)
|
||||||
cls._generate_ground_conflicts()
|
cls._generate_ground_conflicts()
|
||||||
|
|
||||||
# Triggers
|
# Triggers
|
||||||
@@ -317,14 +321,16 @@ class Operation:
|
|||||||
|
|
||||||
# Setup combined arms parameters
|
# Setup combined arms parameters
|
||||||
cls.current_mission.groundControl.pilot_can_control_vehicles = cls.ca_slots > 0
|
cls.current_mission.groundControl.pilot_can_control_vehicles = cls.ca_slots > 0
|
||||||
if cls.game.player_country in [country.name for country in cls.current_mission.coalition["blue"].countries.values()]:
|
if cls.game.player_country in [
|
||||||
|
country.name
|
||||||
|
for country in cls.current_mission.coalition["blue"].countries.values()
|
||||||
|
]:
|
||||||
cls.current_mission.groundControl.blue_tactical_commander = cls.ca_slots
|
cls.current_mission.groundControl.blue_tactical_commander = cls.ca_slots
|
||||||
else:
|
else:
|
||||||
cls.current_mission.groundControl.red_tactical_commander = cls.ca_slots
|
cls.current_mission.groundControl.red_tactical_commander = cls.ca_slots
|
||||||
|
|
||||||
# Options
|
# Options
|
||||||
forcedoptionsgen = ForcedOptionsGenerator(
|
forcedoptionsgen = ForcedOptionsGenerator(cls.current_mission, cls.game)
|
||||||
cls.current_mission, cls.game)
|
|
||||||
forcedoptionsgen.generate()
|
forcedoptionsgen.generate()
|
||||||
|
|
||||||
# Generate Visuals Smoke Effects
|
# Generate Visuals Smoke Effects
|
||||||
@@ -341,13 +347,11 @@ class Operation:
|
|||||||
plugin.inject_scripts(cls)
|
plugin.inject_scripts(cls)
|
||||||
plugin.inject_configuration(cls)
|
plugin.inject_configuration(cls)
|
||||||
|
|
||||||
cls.assign_channels_to_flights(cls.airgen.flights,
|
cls.assign_channels_to_flights(
|
||||||
cls.airsupportgen.air_support)
|
cls.airgen.flights, cls.airsupportgen.air_support
|
||||||
|
)
|
||||||
cls.notify_info_generators(
|
cls.notify_info_generators(
|
||||||
cls.groundobjectgen,
|
cls.groundobjectgen, cls.airsupportgen, cls.jtacs, cls.airgen
|
||||||
cls.airsupportgen,
|
|
||||||
cls.jtacs,
|
|
||||||
cls.airgen
|
|
||||||
)
|
)
|
||||||
cls.reset_naming_ids()
|
cls.reset_naming_ids()
|
||||||
return cls.unit_map
|
return cls.unit_map
|
||||||
@@ -359,29 +363,40 @@ class Operation:
|
|||||||
# Air Support (Tanker & Awacs)
|
# Air Support (Tanker & Awacs)
|
||||||
assert cls.radio_registry and cls.tacan_registry
|
assert cls.radio_registry and cls.tacan_registry
|
||||||
cls.airsupportgen = AirSupportConflictGenerator(
|
cls.airsupportgen = AirSupportConflictGenerator(
|
||||||
cls.current_mission, cls.air_conflict(), cls.game, cls.radio_registry,
|
cls.current_mission,
|
||||||
cls.tacan_registry)
|
cls.air_conflict(),
|
||||||
|
cls.game,
|
||||||
|
cls.radio_registry,
|
||||||
|
cls.tacan_registry,
|
||||||
|
)
|
||||||
cls.airsupportgen.generate()
|
cls.airsupportgen.generate()
|
||||||
|
|
||||||
# Generate Aircraft Activity on the map
|
# Generate Aircraft Activity on the map
|
||||||
cls.airgen = AircraftConflictGenerator(
|
cls.airgen = AircraftConflictGenerator(
|
||||||
cls.current_mission, cls.game.settings, cls.game,
|
cls.current_mission,
|
||||||
cls.radio_registry, cls.unit_map)
|
cls.game.settings,
|
||||||
|
cls.game,
|
||||||
|
cls.radio_registry,
|
||||||
|
cls.unit_map,
|
||||||
|
air_support=cls.airsupportgen.air_support,
|
||||||
|
)
|
||||||
|
|
||||||
cls.airgen.clear_parking_slots()
|
cls.airgen.clear_parking_slots()
|
||||||
|
|
||||||
cls.airgen.generate_flights(
|
cls.airgen.generate_flights(
|
||||||
cls.current_mission.country(cls.game.player_country),
|
cls.current_mission.country(cls.game.player_country),
|
||||||
cls.game.blue_ato,
|
cls.game.blue_ato,
|
||||||
cls.groundobjectgen.runways
|
cls.groundobjectgen.runways,
|
||||||
)
|
)
|
||||||
cls.airgen.generate_flights(
|
cls.airgen.generate_flights(
|
||||||
cls.current_mission.country(cls.game.enemy_country),
|
cls.current_mission.country(cls.game.enemy_country),
|
||||||
cls.game.red_ato,
|
cls.game.red_ato,
|
||||||
cls.groundobjectgen.runways
|
cls.groundobjectgen.runways,
|
||||||
)
|
)
|
||||||
cls.airgen.spawn_unused_aircraft(
|
cls.airgen.spawn_unused_aircraft(
|
||||||
cls.current_mission.country(cls.game.player_country),
|
cls.current_mission.country(cls.game.player_country),
|
||||||
cls.current_mission.country(cls.game.enemy_country))
|
cls.current_mission.country(cls.game.enemy_country),
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _generate_ground_conflicts(cls) -> None:
|
def _generate_ground_conflicts(cls) -> None:
|
||||||
@@ -396,17 +411,19 @@ class Operation:
|
|||||||
cls.current_mission.country(cls.game.enemy_country),
|
cls.current_mission.country(cls.game.enemy_country),
|
||||||
player_cp,
|
player_cp,
|
||||||
enemy_cp,
|
enemy_cp,
|
||||||
cls.game.theater
|
cls.game.theater,
|
||||||
)
|
)
|
||||||
# Generate frontline ops
|
# Generate frontline ops
|
||||||
player_gp = cls.game.ground_planners[player_cp.id].units_per_cp[enemy_cp.id]
|
player_gp = cls.game.ground_planners[player_cp.id].units_per_cp[enemy_cp.id]
|
||||||
enemy_gp = cls.game.ground_planners[enemy_cp.id].units_per_cp[player_cp.id]
|
enemy_gp = cls.game.ground_planners[enemy_cp.id].units_per_cp[player_cp.id]
|
||||||
ground_conflict_gen = GroundConflictGenerator(
|
ground_conflict_gen = GroundConflictGenerator(
|
||||||
cls.current_mission,
|
cls.current_mission,
|
||||||
conflict, cls.game,
|
conflict,
|
||||||
player_gp, enemy_gp,
|
cls.game,
|
||||||
|
player_gp,
|
||||||
|
enemy_gp,
|
||||||
player_cp.stances[enemy_cp.id],
|
player_cp.stances[enemy_cp.id],
|
||||||
cls.unit_map
|
cls.unit_map,
|
||||||
)
|
)
|
||||||
ground_conflict_gen.generate()
|
ground_conflict_gen.generate()
|
||||||
cls.jtacs.extend(ground_conflict_gen.jtacs)
|
cls.jtacs.extend(ground_conflict_gen.jtacs)
|
||||||
@@ -416,9 +433,12 @@ class Operation:
|
|||||||
namegen.reset_numbers()
|
namegen.reset_numbers()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def generate_lua(cls, airgen: AircraftConflictGenerator,
|
def generate_lua(
|
||||||
airsupportgen: AirSupportConflictGenerator,
|
cls,
|
||||||
jtacs: List[JtacInfo]) -> None:
|
airgen: AircraftConflictGenerator,
|
||||||
|
airsupportgen: AirSupportConflictGenerator,
|
||||||
|
jtacs: List[JtacInfo],
|
||||||
|
) -> None:
|
||||||
# TODO: Refactor this
|
# TODO: Refactor this
|
||||||
luaData = {
|
luaData = {
|
||||||
"AircraftCarriers": {},
|
"AircraftCarriers": {},
|
||||||
@@ -426,6 +446,8 @@ class Operation:
|
|||||||
"AWACs": {},
|
"AWACs": {},
|
||||||
"JTACs": {},
|
"JTACs": {},
|
||||||
"TargetPoints": {},
|
"TargetPoints": {},
|
||||||
|
"RedAA": {},
|
||||||
|
"BlueAA": {},
|
||||||
} # type: ignore
|
} # type: ignore
|
||||||
|
|
||||||
for tanker in airsupportgen.air_support.tankers:
|
for tanker in airsupportgen.air_support.tankers:
|
||||||
@@ -434,7 +456,7 @@ class Operation:
|
|||||||
"callsign": tanker.callsign,
|
"callsign": tanker.callsign,
|
||||||
"variant": tanker.variant,
|
"variant": tanker.variant,
|
||||||
"radio": tanker.freq.mhz,
|
"radio": tanker.freq.mhz,
|
||||||
"tacan": str(tanker.tacan.number) + tanker.tacan.band.name
|
"tacan": str(tanker.tacan.number) + tanker.tacan.band.name,
|
||||||
}
|
}
|
||||||
|
|
||||||
if airsupportgen.air_support.awacs:
|
if airsupportgen.air_support.awacs:
|
||||||
@@ -442,7 +464,7 @@ class Operation:
|
|||||||
luaData["AWACs"][awacs.callsign] = {
|
luaData["AWACs"][awacs.callsign] = {
|
||||||
"dcsGroupName": awacs.dcsGroupName,
|
"dcsGroupName": awacs.dcsGroupName,
|
||||||
"callsign": awacs.callsign,
|
"callsign": awacs.callsign,
|
||||||
"radio": awacs.freq.mhz
|
"radio": awacs.freq.mhz,
|
||||||
}
|
}
|
||||||
|
|
||||||
for jtac in jtacs:
|
for jtac in jtacs:
|
||||||
@@ -451,14 +473,16 @@ class Operation:
|
|||||||
"callsign": jtac.callsign,
|
"callsign": jtac.callsign,
|
||||||
"zone": jtac.region,
|
"zone": jtac.region,
|
||||||
"dcsUnit": jtac.unit_name,
|
"dcsUnit": jtac.unit_name,
|
||||||
"laserCode": jtac.code
|
"laserCode": jtac.code,
|
||||||
}
|
}
|
||||||
|
|
||||||
for flight in airgen.flights:
|
for flight in airgen.flights:
|
||||||
if flight.friendly and flight.flight_type in [FlightType.ANTISHIP,
|
if flight.friendly and flight.flight_type in [
|
||||||
FlightType.DEAD,
|
FlightType.ANTISHIP,
|
||||||
FlightType.SEAD,
|
FlightType.DEAD,
|
||||||
FlightType.STRIKE]:
|
FlightType.SEAD,
|
||||||
|
FlightType.STRIKE,
|
||||||
|
]:
|
||||||
flightType = str(flight.flight_type)
|
flightType = str(flight.flight_type)
|
||||||
flightTarget = flight.package.target
|
flightTarget = flight.package.target
|
||||||
if flightTarget:
|
if flightTarget:
|
||||||
@@ -466,23 +490,47 @@ class Operation:
|
|||||||
flightTargetType = None
|
flightTargetType = None
|
||||||
if isinstance(flightTarget, TheaterGroundObject):
|
if isinstance(flightTarget, TheaterGroundObject):
|
||||||
flightTargetName = flightTarget.obj_name
|
flightTargetName = flightTarget.obj_name
|
||||||
flightTargetType = flightType + \
|
flightTargetType = (
|
||||||
f" TGT ({flightTarget.category})"
|
flightType + f" TGT ({flightTarget.category})"
|
||||||
elif hasattr(flightTarget, 'name'):
|
)
|
||||||
|
elif hasattr(flightTarget, "name"):
|
||||||
flightTargetName = flightTarget.name
|
flightTargetName = flightTarget.name
|
||||||
flightTargetType = flightType + " TGT (Airbase)"
|
flightTargetType = flightType + " TGT (Airbase)"
|
||||||
luaData["TargetPoints"][flightTargetName] = {
|
luaData["TargetPoints"][flightTargetName] = {
|
||||||
"name": flightTargetName,
|
"name": flightTargetName,
|
||||||
"type": flightTargetType,
|
"type": flightTargetType,
|
||||||
"position": {"x": flightTarget.position.x,
|
"position": {
|
||||||
"y": flightTarget.position.y}
|
"x": flightTarget.position.x,
|
||||||
|
"y": flightTarget.position.y,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for cp in cls.game.theater.controlpoints:
|
||||||
|
for ground_object in cp.ground_objects:
|
||||||
|
if ground_object.might_have_aa and not ground_object.is_dead:
|
||||||
|
for g in ground_object.groups:
|
||||||
|
threat_range = ground_object.threat_range(g)
|
||||||
|
|
||||||
|
if not threat_range:
|
||||||
|
continue
|
||||||
|
|
||||||
|
faction = "BlueAA" if cp.captured else "RedAA"
|
||||||
|
|
||||||
|
luaData[faction][g.name] = {
|
||||||
|
"name": ground_object.name,
|
||||||
|
"range": threat_range.meters,
|
||||||
|
"position": {
|
||||||
|
"x": ground_object.position.x,
|
||||||
|
"y": ground_object.position.y,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
# set a LUA table with data from Liberation that we want to set
|
# set a LUA table with data from Liberation that we want to set
|
||||||
# at the moment it contains Liberation's install path, and an overridable definition for the JTACAutoLase function
|
# at the moment it contains Liberation's install path, and an overridable definition for the JTACAutoLase function
|
||||||
# later, we'll add data about the units and points having been generated, in order to facilitate the configuration of the plugin lua scripts
|
# later, we'll add data about the units and points having been generated, in order to facilitate the configuration of the plugin lua scripts
|
||||||
state_location = "[[" + os.path.abspath(".") + "]]"
|
state_location = "[[" + os.path.abspath(".") + "]]"
|
||||||
lua = """
|
lua = (
|
||||||
|
"""
|
||||||
-- setting configuration table
|
-- setting configuration table
|
||||||
env.info("DCSLiberation|: setting configuration table")
|
env.info("DCSLiberation|: setting configuration table")
|
||||||
|
|
||||||
@@ -490,9 +538,12 @@ class Operation:
|
|||||||
dcsLiberation = {}
|
dcsLiberation = {}
|
||||||
|
|
||||||
-- the base location for state.json; if non-existent, it'll be replaced with LIBERATION_EXPORT_DIR, TEMP, or DCS working directory
|
-- the base location for state.json; if non-existent, it'll be replaced with LIBERATION_EXPORT_DIR, TEMP, or DCS working directory
|
||||||
dcsLiberation.installPath=""" + state_location + """
|
dcsLiberation.installPath="""
|
||||||
|
+ state_location
|
||||||
|
+ """
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
)
|
||||||
# Process the tankers
|
# Process the tankers
|
||||||
lua += """
|
lua += """
|
||||||
|
|
||||||
@@ -566,7 +617,33 @@ class Operation:
|
|||||||
-- list the aircraft carriers generated by Liberation
|
-- list the aircraft carriers generated by Liberation
|
||||||
-- dcsLiberation.Carriers = {}
|
-- dcsLiberation.Carriers = {}
|
||||||
|
|
||||||
-- later, we'll add more data to the table
|
-- list the Red AA generated by Liberation
|
||||||
|
dcsLiberation.RedAA = {
|
||||||
|
"""
|
||||||
|
for key in luaData["RedAA"]:
|
||||||
|
data = luaData["RedAA"][key]
|
||||||
|
name = data["name"]
|
||||||
|
radius = data["range"]
|
||||||
|
positionX = data["position"]["x"]
|
||||||
|
positionY = data["position"]["y"]
|
||||||
|
lua += f" {{dcsGroupName='{key}', name='{name}', range='{radius}', positionX='{positionX}', positionY='{positionY}' }}, \n"
|
||||||
|
lua += "}"
|
||||||
|
|
||||||
|
lua += """
|
||||||
|
|
||||||
|
-- list the Blue AA generated by Liberation
|
||||||
|
dcsLiberation.BlueAA = {
|
||||||
|
"""
|
||||||
|
for key in luaData["BlueAA"]:
|
||||||
|
data = luaData["BlueAA"][key]
|
||||||
|
name = data["name"]
|
||||||
|
radius = data["range"]
|
||||||
|
positionX = data["position"]["x"]
|
||||||
|
positionY = data["position"]["y"]
|
||||||
|
lua += f" {{dcsGroupName='{key}', name='{name}', range='{radius}', positionX='{positionX}', positionY='{positionY}' }}, \n"
|
||||||
|
lua += "}"
|
||||||
|
|
||||||
|
lua += """
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -67,4 +67,3 @@ def autosave(game) -> bool:
|
|||||||
except Exception:
|
except Exception:
|
||||||
logging.exception("Could not save game")
|
logging.exception("Could not save game")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
class LuaPluginWorkOrder:
|
class LuaPluginWorkOrder:
|
||||||
|
def __init__(
|
||||||
def __init__(self, parent_mnemonic: str, filename: str, mnemonic: str,
|
self, parent_mnemonic: str, filename: str, mnemonic: str, disable: bool
|
||||||
disable: bool) -> None:
|
) -> None:
|
||||||
self.parent_mnemonic = parent_mnemonic
|
self.parent_mnemonic = parent_mnemonic
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
self.mnemonic = mnemonic
|
self.mnemonic = mnemonic
|
||||||
@@ -26,8 +26,9 @@ class LuaPluginWorkOrder:
|
|||||||
if self.disable:
|
if self.disable:
|
||||||
operation.bypass_plugin_script(self.mnemonic)
|
operation.bypass_plugin_script(self.mnemonic)
|
||||||
else:
|
else:
|
||||||
operation.inject_plugin_script(self.parent_mnemonic, self.filename,
|
operation.inject_plugin_script(
|
||||||
self.mnemonic)
|
self.parent_mnemonic, self.filename, self.mnemonic
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PluginSettings:
|
class PluginSettings:
|
||||||
@@ -45,8 +46,7 @@ class PluginSettings:
|
|||||||
# Plugin options are saved in the game's Settings, but it's possible for
|
# Plugin options are saved in the game's Settings, but it's possible for
|
||||||
# plugins to change across loads. If new plugins are added or new
|
# plugins to change across loads. If new plugins are added or new
|
||||||
# options added to those plugins, initialize the new settings.
|
# options added to those plugins, initialize the new settings.
|
||||||
self.settings.initialize_plugin_option(self.identifier,
|
self.settings.initialize_plugin_option(self.identifier, self.enabled_by_default)
|
||||||
self.enabled_by_default)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def enabled(self) -> bool:
|
def enabled(self) -> bool:
|
||||||
@@ -57,8 +57,7 @@ class PluginSettings:
|
|||||||
|
|
||||||
|
|
||||||
class LuaPluginOption(PluginSettings):
|
class LuaPluginOption(PluginSettings):
|
||||||
def __init__(self, identifier: str, name: str,
|
def __init__(self, identifier: str, name: str, enabled_by_default: bool) -> None:
|
||||||
enabled_by_default: bool) -> None:
|
|
||||||
super().__init__(identifier, enabled_by_default)
|
super().__init__(identifier, enabled_by_default)
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
@@ -80,24 +79,34 @@ class LuaPluginDefinition:
|
|||||||
options = []
|
options = []
|
||||||
for option in data.get("specificOptions"):
|
for option in data.get("specificOptions"):
|
||||||
option_id = option["mnemonic"]
|
option_id = option["mnemonic"]
|
||||||
options.append(LuaPluginOption(
|
options.append(
|
||||||
identifier=f"{name}.{option_id}",
|
LuaPluginOption(
|
||||||
name=option.get("nameInUI", name),
|
identifier=f"{name}.{option_id}",
|
||||||
enabled_by_default=option.get("defaultValue")
|
name=option.get("nameInUI", name),
|
||||||
))
|
enabled_by_default=option.get("defaultValue"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
work_orders = []
|
work_orders = []
|
||||||
for work_order in data.get("scriptsWorkOrders"):
|
for work_order in data.get("scriptsWorkOrders"):
|
||||||
work_orders.append(LuaPluginWorkOrder(
|
work_orders.append(
|
||||||
name, work_order.get("file"), work_order["mnemonic"],
|
LuaPluginWorkOrder(
|
||||||
work_order.get("disable", False)
|
name,
|
||||||
))
|
work_order.get("file"),
|
||||||
|
work_order["mnemonic"],
|
||||||
|
work_order.get("disable", False),
|
||||||
|
)
|
||||||
|
)
|
||||||
config_work_orders = []
|
config_work_orders = []
|
||||||
for work_order in data.get("configurationWorkOrders"):
|
for work_order in data.get("configurationWorkOrders"):
|
||||||
config_work_orders.append(LuaPluginWorkOrder(
|
config_work_orders.append(
|
||||||
name, work_order.get("file"), work_order["mnemonic"],
|
LuaPluginWorkOrder(
|
||||||
work_order.get("disable", False)
|
name,
|
||||||
))
|
work_order.get("file"),
|
||||||
|
work_order["mnemonic"],
|
||||||
|
work_order.get("disable", False),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
identifier=name,
|
identifier=name,
|
||||||
@@ -106,16 +115,14 @@ class LuaPluginDefinition:
|
|||||||
enabled_by_default=data.get("defaultValue", False),
|
enabled_by_default=data.get("defaultValue", False),
|
||||||
options=options,
|
options=options,
|
||||||
work_orders=work_orders,
|
work_orders=work_orders,
|
||||||
config_work_orders=config_work_orders
|
config_work_orders=config_work_orders,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class LuaPlugin(PluginSettings):
|
class LuaPlugin(PluginSettings):
|
||||||
|
|
||||||
def __init__(self, definition: LuaPluginDefinition) -> None:
|
def __init__(self, definition: LuaPluginDefinition) -> None:
|
||||||
self.definition = definition
|
self.definition = definition
|
||||||
super().__init__(self.definition.identifier,
|
super().__init__(self.definition.identifier, self.definition.enabled_by_default)
|
||||||
self.definition.enabled_by_default)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
@@ -155,12 +162,12 @@ class LuaPlugin(PluginSettings):
|
|||||||
for option in self.options:
|
for option in self.options:
|
||||||
enabled = str(option.enabled).lower()
|
enabled = str(option.enabled).lower()
|
||||||
name = option.identifier
|
name = option.identifier
|
||||||
option_decls.append(
|
option_decls.append(f" dcsLiberation.plugins.{name} = {enabled}")
|
||||||
f" dcsLiberation.plugins.{name} = {enabled}")
|
|
||||||
|
|
||||||
joined_options = "\n".join(option_decls)
|
joined_options = "\n".join(option_decls)
|
||||||
|
|
||||||
lua = textwrap.dedent(f"""\
|
lua = textwrap.dedent(
|
||||||
|
f"""\
|
||||||
-- {self.identifier} plugin configuration.
|
-- {self.identifier} plugin configuration.
|
||||||
|
|
||||||
if dcsLiberation then
|
if dcsLiberation then
|
||||||
@@ -171,10 +178,10 @@ class LuaPlugin(PluginSettings):
|
|||||||
{joined_options}
|
{joined_options}
|
||||||
end
|
end
|
||||||
|
|
||||||
""")
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
operation.inject_lua_trigger(
|
operation.inject_lua_trigger(lua, f"{self.identifier} plugin configuration")
|
||||||
lua, f"{self.identifier} plugin configuration")
|
|
||||||
|
|
||||||
for work_order in self.definition.config_work_orders:
|
for work_order in self.definition.config_work_orders:
|
||||||
work_order.work(operation)
|
work_order.work(operation)
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ class LuaPluginManager:
|
|||||||
if not plugin_path.exists():
|
if not plugin_path.exists():
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Invalid plugin configuration: required plugin {name} "
|
f"Invalid plugin configuration: required plugin {name} "
|
||||||
f"does not exist at {plugin_path}")
|
f"does not exist at {plugin_path}"
|
||||||
|
)
|
||||||
logging.info(f"Loading plugin {name} from {plugin_path}")
|
logging.info(f"Loading plugin {name} from {plugin_path}")
|
||||||
plugin = LuaPlugin.from_json(name, plugin_path)
|
plugin = LuaPlugin.from_json(name, plugin_path)
|
||||||
if plugin is not None:
|
if plugin is not None:
|
||||||
|
|||||||
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:
|
class ProcurementAi:
|
||||||
def __init__(self, game: Game, for_player: bool, faction: Faction,
|
def __init__(
|
||||||
manage_runways: bool, manage_front_line: bool,
|
self,
|
||||||
manage_aircraft: bool, front_line_budget_share: float) -> None:
|
game: Game,
|
||||||
|
for_player: bool,
|
||||||
|
faction: Faction,
|
||||||
|
manage_runways: bool,
|
||||||
|
manage_front_line: bool,
|
||||||
|
manage_aircraft: bool,
|
||||||
|
front_line_budget_share: float,
|
||||||
|
) -> None:
|
||||||
if front_line_budget_share > 1.0:
|
if front_line_budget_share > 1.0:
|
||||||
raise ValueError
|
raise ValueError
|
||||||
|
|
||||||
@@ -51,8 +58,8 @@ class ProcurementAi:
|
|||||||
self.threat_zones = self.game.threat_zone_for(not self.is_player)
|
self.threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||||
|
|
||||||
def spend_budget(
|
def spend_budget(
|
||||||
self, budget: float,
|
self, budget: float, aircraft_requests: List[AircraftProcurementRequest]
|
||||||
aircraft_requests: List[AircraftProcurementRequest]) -> float:
|
) -> float:
|
||||||
if self.manage_runways:
|
if self.manage_runways:
|
||||||
budget = self.repair_runways(budget)
|
budget = self.repair_runways(budget)
|
||||||
if self.manage_front_line:
|
if self.manage_front_line:
|
||||||
@@ -100,25 +107,30 @@ class ProcurementAi:
|
|||||||
budget -= db.RUNWAY_REPAIR_COST
|
budget -= db.RUNWAY_REPAIR_COST
|
||||||
if self.is_player:
|
if self.is_player:
|
||||||
self.game.message(
|
self.game.message(
|
||||||
"OPFOR has begun repairing the runway at "
|
"OPFOR has begun repairing the runway at " f"{control_point}"
|
||||||
f"{control_point}"
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.game.message(
|
self.game.message(
|
||||||
"We have begun repairing the runway at "
|
"We have begun repairing the runway at " f"{control_point}"
|
||||||
f"{control_point}"
|
|
||||||
)
|
)
|
||||||
return budget
|
return budget
|
||||||
|
|
||||||
def random_affordable_ground_unit(
|
def random_affordable_ground_unit(
|
||||||
self, budget: float,
|
self, budget: float, cp: ControlPoint
|
||||||
cp: ControlPoint) -> Optional[Type[VehicleType]]:
|
) -> Optional[Type[VehicleType]]:
|
||||||
affordable_units = [u for u in self.faction.frontline_units + self.faction.artillery_units if
|
affordable_units = [
|
||||||
db.PRICES[u] <= budget]
|
u
|
||||||
|
for u in self.faction.frontline_units + self.faction.artillery_units
|
||||||
|
if db.PRICES[u] <= budget
|
||||||
|
]
|
||||||
|
|
||||||
total_number_aa = cp.base.total_frontline_aa + cp.pending_frontline_aa_deliveries_count
|
total_number_aa = (
|
||||||
total_non_aa = cp.base.total_armor + cp.pending_deliveries_count - total_number_aa
|
cp.base.total_frontline_aa + cp.pending_frontline_aa_deliveries_count
|
||||||
max_aa = math.ceil(total_non_aa/8)
|
)
|
||||||
|
total_non_aa = (
|
||||||
|
cp.base.total_armor + cp.pending_deliveries_count - total_number_aa
|
||||||
|
)
|
||||||
|
max_aa = math.ceil(total_non_aa / 8)
|
||||||
|
|
||||||
# Limit the number of AA units the AI will buy
|
# Limit the number of AA units the AI will buy
|
||||||
if not total_number_aa < max_aa:
|
if not total_number_aa < max_aa:
|
||||||
@@ -150,8 +162,12 @@ class ProcurementAi:
|
|||||||
return budget
|
return budget
|
||||||
|
|
||||||
def _affordable_aircraft_of_types(
|
def _affordable_aircraft_of_types(
|
||||||
self, types: List[Type[FlyingType]], airbase: ControlPoint,
|
self,
|
||||||
number: int, max_price: float) -> Optional[Type[FlyingType]]:
|
types: List[Type[FlyingType]],
|
||||||
|
airbase: ControlPoint,
|
||||||
|
number: int,
|
||||||
|
max_price: float,
|
||||||
|
) -> Optional[Type[FlyingType]]:
|
||||||
best_choice: Optional[Type[FlyingType]] = None
|
best_choice: Optional[Type[FlyingType]] = None
|
||||||
for unit in [u for u in self.faction.aircrafts if u in types]:
|
for unit in [u for u in self.faction.aircrafts if u in types]:
|
||||||
if db.PRICES[unit] * number > max_price:
|
if db.PRICES[unit] * number > max_price:
|
||||||
@@ -168,15 +184,15 @@ class ProcurementAi:
|
|||||||
return best_choice
|
return best_choice
|
||||||
|
|
||||||
def affordable_aircraft_for(
|
def affordable_aircraft_for(
|
||||||
self, request: AircraftProcurementRequest,
|
self, request: AircraftProcurementRequest, airbase: ControlPoint, budget: float
|
||||||
airbase: ControlPoint, budget: float) -> Optional[Type[FlyingType]]:
|
) -> Optional[Type[FlyingType]]:
|
||||||
return self._affordable_aircraft_of_types(
|
return self._affordable_aircraft_of_types(
|
||||||
aircraft_for_task(request.task_capability),
|
aircraft_for_task(request.task_capability), airbase, request.number, budget
|
||||||
airbase, request.number, budget)
|
)
|
||||||
|
|
||||||
def purchase_aircraft(
|
def purchase_aircraft(
|
||||||
self, budget: float,
|
self, budget: float, aircraft_requests: List[AircraftProcurementRequest]
|
||||||
aircraft_requests: List[AircraftProcurementRequest]) -> float:
|
) -> float:
|
||||||
for request in aircraft_requests:
|
for request in aircraft_requests:
|
||||||
for airbase in self.best_airbases_for(request):
|
for airbase in self.best_airbases_for(request):
|
||||||
unit = self.affordable_aircraft_for(request, airbase, budget)
|
unit = self.affordable_aircraft_for(request, airbase, budget)
|
||||||
@@ -201,11 +217,9 @@ class ProcurementAi:
|
|||||||
return self.game.theater.enemy_points()
|
return self.game.theater.enemy_points()
|
||||||
|
|
||||||
def best_airbases_for(
|
def best_airbases_for(
|
||||||
self,
|
self, request: AircraftProcurementRequest
|
||||||
request: AircraftProcurementRequest) -> Iterator[ControlPoint]:
|
) -> Iterator[ControlPoint]:
|
||||||
distance_cache = ObjectiveDistanceCache.get_closest_airfields(
|
distance_cache = ObjectiveDistanceCache.get_closest_airfields(request.near)
|
||||||
request.near
|
|
||||||
)
|
|
||||||
threatened = []
|
threatened = []
|
||||||
for cp in distance_cache.airfields_within(request.range):
|
for cp in distance_cache.airfields_within(request.range):
|
||||||
if not cp.is_friendly(self.is_player):
|
if not cp.is_friendly(self.is_player):
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ class Settings:
|
|||||||
automate_front_line_reinforcements: bool = False
|
automate_front_line_reinforcements: bool = False
|
||||||
automate_aircraft_reinforcements: bool = False
|
automate_aircraft_reinforcements: bool = False
|
||||||
restrict_weapons_by_date: bool = False
|
restrict_weapons_by_date: bool = False
|
||||||
|
disable_legacy_aewc: bool = False
|
||||||
|
generate_dark_kneeboard: bool = False
|
||||||
|
|
||||||
# Performance oriented
|
# Performance oriented
|
||||||
perf_red_alert_state: bool = True
|
perf_red_alert_state: bool = True
|
||||||
@@ -58,8 +60,7 @@ class Settings:
|
|||||||
def plugin_settings_key(identifier: str) -> str:
|
def plugin_settings_key(identifier: str) -> str:
|
||||||
return f"plugins.{identifier}"
|
return f"plugins.{identifier}"
|
||||||
|
|
||||||
def initialize_plugin_option(self, identifier: str,
|
def initialize_plugin_option(self, identifier: str, default_value: bool) -> None:
|
||||||
default_value: bool) -> None:
|
|
||||||
try:
|
try:
|
||||||
self.plugin_option(identifier)
|
self.plugin_option(identifier)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import math
|
|||||||
import typing
|
import typing
|
||||||
from typing import Dict, Type
|
from typing import Dict, Type
|
||||||
|
|
||||||
from dcs.task import CAP, CAS, Embarking, PinpointStrike, Task
|
from dcs.task import AWACS, CAP, CAS, Embarking, PinpointStrike, Task
|
||||||
from dcs.unittype import FlyingType, UnitType, VehicleType
|
from dcs.unittype import FlyingType, UnitType, VehicleType
|
||||||
from dcs.vehicles import AirDefence, Armor
|
from dcs.vehicles import AirDefence, Armor
|
||||||
|
|
||||||
@@ -22,7 +22,6 @@ BASE_MIN_STRENGTH = 0
|
|||||||
|
|
||||||
|
|
||||||
class Base:
|
class Base:
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.aircraft: Dict[Type[FlyingType], int] = {}
|
self.aircraft: Dict[Type[FlyingType], int] = {}
|
||||||
self.armor: Dict[Type[VehicleType], int] = {}
|
self.armor: Dict[Type[VehicleType], int] = {}
|
||||||
@@ -57,23 +56,43 @@ class Base:
|
|||||||
return sum(self.aa.values())
|
return sum(self.aa.values())
|
||||||
|
|
||||||
def total_units(self, task: Task) -> int:
|
def total_units(self, task: Task) -> int:
|
||||||
return sum([c for t, c in itertools.chain(self.aircraft.items(), self.armor.items(), self.aa.items()) if t in db.UNIT_BY_TASK[task]])
|
return sum(
|
||||||
|
[
|
||||||
|
c
|
||||||
|
for t, c in itertools.chain(
|
||||||
|
self.aircraft.items(), self.armor.items(), self.aa.items()
|
||||||
|
)
|
||||||
|
if t in db.UNIT_BY_TASK[task]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
def total_units_of_type(self, unit_type) -> int:
|
def total_units_of_type(self, unit_type) -> int:
|
||||||
return sum([c for t, c in itertools.chain(self.aircraft.items(), self.armor.items(), self.aa.items()) if t == unit_type])
|
return sum(
|
||||||
|
[
|
||||||
|
c
|
||||||
|
for t, c in itertools.chain(
|
||||||
|
self.aircraft.items(), self.armor.items(), self.aa.items()
|
||||||
|
)
|
||||||
|
if t == unit_type
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def all_units(self):
|
def all_units(self):
|
||||||
return itertools.chain(self.aircraft.items(), self.armor.items(), self.aa.items())
|
return itertools.chain(
|
||||||
|
self.aircraft.items(), self.armor.items(), self.aa.items()
|
||||||
|
)
|
||||||
|
|
||||||
def _find_best_unit(self, available_units: Dict[UnitType, int],
|
def _find_best_unit(
|
||||||
for_type: Task, count: int) -> Dict[UnitType, int]:
|
self, available_units: Dict[UnitType, int], for_type: Task, count: int
|
||||||
|
) -> Dict[UnitType, int]:
|
||||||
if count <= 0:
|
if count <= 0:
|
||||||
logging.warning("{}: no units for {}".format(self, for_type))
|
logging.warning("{}: no units for {}".format(self, for_type))
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
sorted_units = [key for key in available_units if
|
sorted_units = [
|
||||||
key in db.UNIT_BY_TASK[for_type]]
|
key for key in available_units if key in db.UNIT_BY_TASK[for_type]
|
||||||
|
]
|
||||||
sorted_units.sort(key=lambda x: db.PRICES[x], reverse=True)
|
sorted_units.sort(key=lambda x: db.PRICES[x], reverse=True)
|
||||||
|
|
||||||
result: Dict[UnitType, int] = {}
|
result: Dict[UnitType, int] = {}
|
||||||
@@ -94,14 +113,18 @@ class Base:
|
|||||||
logging.info("{} for {} ({}): {}".format(self, for_type, count, result))
|
logging.info("{} for {} ({}): {}".format(self, for_type, count, result))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _find_best_planes(self, for_type: Task, count: int) -> typing.Dict[FlyingType, int]:
|
def _find_best_planes(
|
||||||
|
self, for_type: Task, count: int
|
||||||
|
) -> typing.Dict[FlyingType, int]:
|
||||||
return self._find_best_unit(self.aircraft, for_type, count)
|
return self._find_best_unit(self.aircraft, for_type, count)
|
||||||
|
|
||||||
def _find_best_armor(self, for_type: Task, count: int) -> typing.Dict[Armor, int]:
|
def _find_best_armor(self, for_type: Task, count: int) -> typing.Dict[Armor, int]:
|
||||||
return self._find_best_unit(self.armor, for_type, count)
|
return self._find_best_unit(self.armor, for_type, count)
|
||||||
|
|
||||||
def append_commision_points(self, for_type, points: float) -> int:
|
def append_commision_points(self, for_type, points: float) -> int:
|
||||||
self.commision_points[for_type] = self.commision_points.get(for_type, 0) + points
|
self.commision_points[for_type] = (
|
||||||
|
self.commision_points.get(for_type, 0) + points
|
||||||
|
)
|
||||||
points = self.commision_points[for_type]
|
points = self.commision_points[for_type]
|
||||||
if points >= 1:
|
if points >= 1:
|
||||||
self.commision_points[for_type] = points - math.floor(points)
|
self.commision_points[for_type] = points - math.floor(points)
|
||||||
@@ -110,7 +133,9 @@ class Base:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
def filter_units(self, applicable_units: typing.Collection):
|
def filter_units(self, applicable_units: typing.Collection):
|
||||||
self.aircraft = {k: v for k, v in self.aircraft.items() if k in applicable_units}
|
self.aircraft = {
|
||||||
|
k: v for k, v in self.aircraft.items() if k in applicable_units
|
||||||
|
}
|
||||||
self.armor = {k: v for k, v in self.armor.items() if k in applicable_units}
|
self.armor = {k: v for k, v in self.armor.items() if k in applicable_units}
|
||||||
|
|
||||||
def commision_units(self, units: typing.Dict[typing.Any, int]):
|
def commision_units(self, units: typing.Dict[typing.Any, int]):
|
||||||
@@ -122,7 +147,12 @@ class Base:
|
|||||||
for_task = db.unit_task(unit_type)
|
for_task = db.unit_task(unit_type)
|
||||||
|
|
||||||
target_dict = None
|
target_dict = None
|
||||||
if for_task == CAS or for_task == CAP or for_task == Embarking:
|
if (
|
||||||
|
for_task == AWACS
|
||||||
|
or for_task == CAS
|
||||||
|
or for_task == CAP
|
||||||
|
or for_task == Embarking
|
||||||
|
):
|
||||||
target_dict = self.aircraft
|
target_dict = self.aircraft
|
||||||
elif for_task == PinpointStrike:
|
elif for_task == PinpointStrike:
|
||||||
target_dict = self.armor
|
target_dict = self.armor
|
||||||
@@ -149,7 +179,7 @@ class Base:
|
|||||||
if unit_type not in target_array:
|
if unit_type not in target_array:
|
||||||
print("Base didn't find event type {}".format(unit_type))
|
print("Base didn't find event type {}".format(unit_type))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
target_array[unit_type] = max(target_array[unit_type] - count, 0)
|
target_array[unit_type] = max(target_array[unit_type] - count, 0)
|
||||||
if target_array[unit_type] == 0:
|
if target_array[unit_type] == 0:
|
||||||
del target_array[unit_type]
|
del target_array[unit_type]
|
||||||
@@ -166,12 +196,20 @@ class Base:
|
|||||||
|
|
||||||
def scramble_count(self, multiplier: float, task: Task = None) -> int:
|
def scramble_count(self, multiplier: float, task: Task = None) -> int:
|
||||||
if task:
|
if task:
|
||||||
count = sum([v for k, v in self.aircraft.items() if db.unit_task(k) == task])
|
count = sum(
|
||||||
|
[v for k, v in self.aircraft.items() if db.unit_task(k) == task]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
count = self.total_aircraft
|
count = self.total_aircraft
|
||||||
|
|
||||||
count = int(math.ceil(count * PLANES_SCRAMBLE_FACTOR * self.strength))
|
count = int(math.ceil(count * PLANES_SCRAMBLE_FACTOR * self.strength))
|
||||||
return min(min(max(count, PLANES_SCRAMBLE_MIN_BASE), int(PLANES_SCRAMBLE_MAX_BASE * multiplier)), count)
|
return min(
|
||||||
|
min(
|
||||||
|
max(count, PLANES_SCRAMBLE_MIN_BASE),
|
||||||
|
int(PLANES_SCRAMBLE_MAX_BASE * multiplier),
|
||||||
|
),
|
||||||
|
count,
|
||||||
|
)
|
||||||
|
|
||||||
def assemble_count(self):
|
def assemble_count(self):
|
||||||
return int(self.total_armor * 0.5)
|
return int(self.total_armor * 0.5)
|
||||||
@@ -202,4 +240,8 @@ class Base:
|
|||||||
return self._find_best_armor(PinpointStrike, count)
|
return self._find_best_armor(PinpointStrike, count)
|
||||||
|
|
||||||
def assemble_aa(self, count=None) -> typing.Dict[AirDefence, int]:
|
def assemble_aa(self, count=None) -> typing.Dict[AirDefence, int]:
|
||||||
return self._find_best_unit(self.aa, AirDefence, count and min(count, self.total_aa) or self.assemble_aa_count())
|
return self._find_best_unit(
|
||||||
|
self.aa,
|
||||||
|
AirDefence,
|
||||||
|
count and min(count, self.total_aa) or self.assemble_aa_count(),
|
||||||
|
)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from dcs.planes import F_15C
|
|||||||
from dcs.ships import (
|
from dcs.ships import (
|
||||||
CVN_74_John_C__Stennis,
|
CVN_74_John_C__Stennis,
|
||||||
LHA_1_Tarawa,
|
LHA_1_Tarawa,
|
||||||
USS_Arleigh_Burke_IIa,
|
DDG_Arleigh_Burke_IIa,
|
||||||
)
|
)
|
||||||
from dcs.statics import Fortification
|
from dcs.statics import Fortification
|
||||||
from dcs.terrain import (
|
from dcs.terrain import (
|
||||||
@@ -55,6 +55,7 @@ from .controlpoint import (
|
|||||||
Fob,
|
Fob,
|
||||||
)
|
)
|
||||||
from .landmap import Landmap, load_landmap, poly_contains
|
from .landmap import Landmap, load_landmap, poly_contains
|
||||||
|
from ..point_with_heading import PointWithHeading
|
||||||
from ..utils import Distance, meters, nautical_miles
|
from ..utils import Distance, meters, nautical_miles
|
||||||
|
|
||||||
Numeric = Union[int, float]
|
Numeric = Union[int, float]
|
||||||
@@ -71,6 +72,7 @@ IMPORTANCE_HIGH = 1.4
|
|||||||
|
|
||||||
FRONTLINE_MIN_CP_DISTANCE = 5000
|
FRONTLINE_MIN_CP_DISTANCE = 5000
|
||||||
|
|
||||||
|
|
||||||
def pairwise(iterable):
|
def pairwise(iterable):
|
||||||
"""
|
"""
|
||||||
itertools recipe
|
itertools recipe
|
||||||
@@ -91,30 +93,33 @@ class MizCampaignLoader:
|
|||||||
LHA_UNIT_TYPE = LHA_1_Tarawa.id
|
LHA_UNIT_TYPE = LHA_1_Tarawa.id
|
||||||
FRONT_LINE_UNIT_TYPE = Armor.APC_M113.id
|
FRONT_LINE_UNIT_TYPE = Armor.APC_M113.id
|
||||||
|
|
||||||
FOB_UNIT_TYPE = Unarmed.CP_SKP_11_ATC_Mobile_Command_Post.id
|
FOB_UNIT_TYPE = Unarmed.Truck_SKP_11_Mobile_ATC.id
|
||||||
|
FARP_HELIPAD = "SINGLE_HELIPAD"
|
||||||
|
|
||||||
EWR_UNIT_TYPE = AirDefence.EWR_55G6.id
|
EWR_UNIT_TYPE = AirDefence.EWR_55G6.id
|
||||||
SAM_UNIT_TYPE = AirDefence.SAM_SA_10_S_300PS_SR_64H6E.id
|
SAM_UNIT_TYPE = AirDefence.SAM_SA_10_S_300_Grumble_Big_Bird_SR.id
|
||||||
GARRISON_UNIT_TYPE = AirDefence.SAM_SA_19_Tunguska_2S6.id
|
GARRISON_UNIT_TYPE = AirDefence.SAM_SA_19_Tunguska_Grison.id
|
||||||
OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id
|
OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id
|
||||||
SHIP_UNIT_TYPE = USS_Arleigh_Burke_IIa.id
|
SHIP_UNIT_TYPE = DDG_Arleigh_Burke_IIa.id
|
||||||
MISSILE_SITE_UNIT_TYPE = MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M.id
|
MISSILE_SITE_UNIT_TYPE = MissilesSS.SSM_SS_1C_Scud_B.id
|
||||||
COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.SS_N_2_Silkworm.id
|
COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.AShM_SS_N_2_Silkworm.id
|
||||||
|
|
||||||
# Multiple options for the required SAMs so campaign designers can more
|
# Multiple options for the required SAMs so campaign designers can more
|
||||||
# accurately see the coverage of their IADS for the expected type.
|
# accurately see the coverage of their IADS for the expected type.
|
||||||
REQUIRED_LONG_RANGE_SAM_UNIT_TYPES = {
|
REQUIRED_LONG_RANGE_SAM_UNIT_TYPES = {
|
||||||
AirDefence.SAM_Patriot_LN_M901.id,
|
AirDefence.SAM_Patriot_LN.id,
|
||||||
AirDefence.SAM_SA_10_S_300PS_LN_5P85C.id,
|
AirDefence.SAM_SA_10_S_300_Grumble_TEL_C.id,
|
||||||
AirDefence.SAM_SA_10_S_300PS_LN_5P85D.id,
|
AirDefence.SAM_SA_10_S_300_Grumble_TEL_D.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
REQUIRED_MEDIUM_RANGE_SAM_UNIT_TYPES = {
|
REQUIRED_MEDIUM_RANGE_SAM_UNIT_TYPES = {
|
||||||
AirDefence.SAM_Hawk_LN_M192.id,
|
AirDefence.SAM_Hawk_LN_M192.id,
|
||||||
AirDefence.SAM_SA_2_LN_SM_90.id,
|
AirDefence.SAM_SA_2_S_75_Guideline_LN.id,
|
||||||
AirDefence.SAM_SA_3_S_125_LN_5P73.id,
|
AirDefence.SAM_SA_3_S_125_Goa_LN.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
REQUIRED_EWR_UNIT_TYPE = AirDefence.EWR_1L13.id
|
||||||
|
|
||||||
BASE_DEFENSE_RADIUS = nautical_miles(2)
|
BASE_DEFENSE_RADIUS = nautical_miles(2)
|
||||||
|
|
||||||
def __init__(self, miz: Path, theater: ConflictTheater) -> None:
|
def __init__(self, miz: Path, theater: ConflictTheater) -> None:
|
||||||
@@ -156,7 +161,8 @@ class MizCampaignLoader:
|
|||||||
|
|
||||||
def country(self, blue: bool) -> Country:
|
def country(self, blue: bool) -> Country:
|
||||||
country = self.mission.country(
|
country = self.mission.country(
|
||||||
self.BLUE_COUNTRY.name if blue else self.RED_COUNTRY.name)
|
self.BLUE_COUNTRY.name if blue else self.RED_COUNTRY.name
|
||||||
|
)
|
||||||
# Should be guaranteed because we initialized them.
|
# Should be guaranteed because we initialized them.
|
||||||
assert country
|
assert country
|
||||||
return country
|
return country
|
||||||
@@ -183,7 +189,7 @@ class MizCampaignLoader:
|
|||||||
for group in self.country(blue).ship_group:
|
for group in self.country(blue).ship_group:
|
||||||
if group.units[0].type == self.LHA_UNIT_TYPE:
|
if group.units[0].type == self.LHA_UNIT_TYPE:
|
||||||
yield group
|
yield group
|
||||||
|
|
||||||
def fobs(self, blue: bool) -> Iterator[VehicleGroup]:
|
def fobs(self, blue: bool) -> Iterator[VehicleGroup]:
|
||||||
for group in self.country(blue).vehicle_group:
|
for group in self.country(blue).vehicle_group:
|
||||||
if group.units[0].type == self.FOB_UNIT_TYPE:
|
if group.units[0].type == self.FOB_UNIT_TYPE:
|
||||||
@@ -243,6 +249,18 @@ class MizCampaignLoader:
|
|||||||
if group.units[0].type in self.REQUIRED_MEDIUM_RANGE_SAM_UNIT_TYPES:
|
if group.units[0].type in self.REQUIRED_MEDIUM_RANGE_SAM_UNIT_TYPES:
|
||||||
yield group
|
yield group
|
||||||
|
|
||||||
|
@property
|
||||||
|
def required_ewrs(self) -> Iterator[VehicleGroup]:
|
||||||
|
for group in self.red.vehicle_group:
|
||||||
|
if group.units[0].type in self.REQUIRED_EWR_UNIT_TYPE:
|
||||||
|
yield group
|
||||||
|
|
||||||
|
@property
|
||||||
|
def helipads(self) -> Iterator[StaticGroup]:
|
||||||
|
for group in self.blue.static_group:
|
||||||
|
if group.units[0].type == self.FARP_HELIPAD:
|
||||||
|
yield group
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def control_points(self) -> Dict[int, ControlPoint]:
|
def control_points(self) -> Dict[int, ControlPoint]:
|
||||||
control_points = {}
|
control_points = {}
|
||||||
@@ -253,22 +271,23 @@ class MizCampaignLoader:
|
|||||||
|
|
||||||
for blue in (False, True):
|
for blue in (False, True):
|
||||||
for group in self.off_map_spawns(blue):
|
for group in self.off_map_spawns(blue):
|
||||||
control_point = OffMapSpawn(next(self.control_point_id),
|
control_point = OffMapSpawn(
|
||||||
str(group.name), group.position)
|
next(self.control_point_id), str(group.name), group.position
|
||||||
|
)
|
||||||
control_point.captured = blue
|
control_point.captured = blue
|
||||||
control_point.captured_invert = group.late_activation
|
control_point.captured_invert = group.late_activation
|
||||||
control_points[control_point.id] = control_point
|
control_points[control_point.id] = control_point
|
||||||
for group in self.carriers(blue):
|
for group in self.carriers(blue):
|
||||||
# TODO: Name the carrier.
|
# TODO: Name the carrier.
|
||||||
control_point = Carrier(
|
control_point = Carrier(
|
||||||
"carrier", group.position, next(self.control_point_id))
|
"carrier", group.position, next(self.control_point_id)
|
||||||
|
)
|
||||||
control_point.captured = blue
|
control_point.captured = blue
|
||||||
control_point.captured_invert = group.late_activation
|
control_point.captured_invert = group.late_activation
|
||||||
control_points[control_point.id] = control_point
|
control_points[control_point.id] = control_point
|
||||||
for group in self.lhas(blue):
|
for group in self.lhas(blue):
|
||||||
# TODO: Name the LHA.
|
# TODO: Name the LHA.db
|
||||||
control_point = Lha(
|
control_point = Lha("lha", group.position, next(self.control_point_id))
|
||||||
"lha", group.position, next(self.control_point_id))
|
|
||||||
control_point.captured = blue
|
control_point.captured = blue
|
||||||
control_point.captured_invert = group.late_activation
|
control_point.captured_invert = group.late_activation
|
||||||
control_points[control_point.id] = control_point
|
control_points[control_point.id] = control_point
|
||||||
@@ -297,24 +316,24 @@ class MizCampaignLoader:
|
|||||||
# final waypoint at the destination CP. Intermediate waypoints
|
# final waypoint at the destination CP. Intermediate waypoints
|
||||||
# define the curve of the front line.
|
# define the curve of the front line.
|
||||||
waypoints = [p.position for p in group.points]
|
waypoints = [p.position for p in group.points]
|
||||||
origin = self.theater.closest_control_point(waypoints[0])
|
origin = self.theater.closest_control_point(waypoints[0])
|
||||||
if origin is None:
|
if origin is None:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"No control point near the first waypoint of {group.name}")
|
f"No control point near the first waypoint of {group.name}"
|
||||||
|
)
|
||||||
destination = self.theater.closest_control_point(waypoints[-1])
|
destination = self.theater.closest_control_point(waypoints[-1])
|
||||||
if destination is None:
|
if destination is None:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"No control point near the final waypoint of {group.name}")
|
f"No control point near the final waypoint of {group.name}"
|
||||||
|
)
|
||||||
|
|
||||||
# Snap the begin and end points to the control points.
|
# Snap the begin and end points to the control points.
|
||||||
waypoints[0] = origin.position
|
waypoints[0] = origin.position
|
||||||
waypoints[-1] = destination.position
|
waypoints[-1] = destination.position
|
||||||
front_line_id = f"{origin.id}|{destination.id}"
|
front_line_id = f"{origin.id}|{destination.id}"
|
||||||
front_lines[front_line_id] = ComplexFrontLine(origin, waypoints)
|
front_lines[front_line_id] = ComplexFrontLine(origin, waypoints)
|
||||||
self.control_points[origin.id].connect(
|
self.control_points[origin.id].connect(self.control_points[destination.id])
|
||||||
self.control_points[destination.id])
|
self.control_points[destination.id].connect(self.control_points[origin.id])
|
||||||
self.control_points[destination.id].connect(
|
|
||||||
self.control_points[origin.id])
|
|
||||||
return front_lines
|
return front_lines
|
||||||
|
|
||||||
def objective_info(self, group: Group) -> Tuple[ControlPoint, Distance]:
|
def objective_info(self, group: Group) -> Tuple[ControlPoint, Distance]:
|
||||||
@@ -326,49 +345,80 @@ class MizCampaignLoader:
|
|||||||
for group in self.garrisons:
|
for group in self.garrisons:
|
||||||
closest, distance = self.objective_info(group)
|
closest, distance = self.objective_info(group)
|
||||||
if distance < self.BASE_DEFENSE_RADIUS:
|
if distance < self.BASE_DEFENSE_RADIUS:
|
||||||
closest.preset_locations.base_garrisons.append(group.position)
|
closest.preset_locations.base_garrisons.append(
|
||||||
|
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logging.warning(
|
logging.warning(f"Found garrison unit too far from base: {group.name}")
|
||||||
f"Found garrison unit too far from base: {group.name}")
|
|
||||||
|
|
||||||
for group in self.sams:
|
for group in self.sams:
|
||||||
closest, distance = self.objective_info(group)
|
closest, distance = self.objective_info(group)
|
||||||
if distance < self.BASE_DEFENSE_RADIUS:
|
if distance < self.BASE_DEFENSE_RADIUS:
|
||||||
closest.preset_locations.base_air_defense.append(group.position)
|
closest.preset_locations.base_air_defense.append(
|
||||||
|
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
closest.preset_locations.strike_locations.append(group.position)
|
closest.preset_locations.strike_locations.append(
|
||||||
|
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||||
|
)
|
||||||
|
|
||||||
for group in self.ewrs:
|
for group in self.ewrs:
|
||||||
closest, distance = self.objective_info(group)
|
closest, distance = self.objective_info(group)
|
||||||
closest.preset_locations.ewrs.append(group.position)
|
if distance < self.BASE_DEFENSE_RADIUS:
|
||||||
|
closest.preset_locations.base_ewrs.append(
|
||||||
|
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
closest.preset_locations.ewrs.append(
|
||||||
|
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||||
|
)
|
||||||
|
|
||||||
for group in self.offshore_strike_targets:
|
for group in self.offshore_strike_targets:
|
||||||
closest, distance = self.objective_info(group)
|
closest, distance = self.objective_info(group)
|
||||||
closest.preset_locations.offshore_strike_locations.append(
|
closest.preset_locations.offshore_strike_locations.append(
|
||||||
group.position)
|
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||||
|
)
|
||||||
|
|
||||||
for group in self.ships:
|
for group in self.ships:
|
||||||
closest, distance = self.objective_info(group)
|
closest, distance = self.objective_info(group)
|
||||||
closest.preset_locations.ships.append(group.position)
|
closest.preset_locations.ships.append(
|
||||||
|
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||||
|
)
|
||||||
|
|
||||||
for group in self.missile_sites:
|
for group in self.missile_sites:
|
||||||
closest, distance = self.objective_info(group)
|
closest, distance = self.objective_info(group)
|
||||||
closest.preset_locations.missile_sites.append(group.position)
|
closest.preset_locations.missile_sites.append(
|
||||||
|
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||||
|
)
|
||||||
|
|
||||||
for group in self.coastal_defenses:
|
for group in self.coastal_defenses:
|
||||||
closest, distance = self.objective_info(group)
|
closest, distance = self.objective_info(group)
|
||||||
closest.preset_locations.coastal_defenses.append(group.position)
|
closest.preset_locations.coastal_defenses.append(
|
||||||
|
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||||
|
)
|
||||||
|
|
||||||
for group in self.required_long_range_sams:
|
for group in self.required_long_range_sams:
|
||||||
closest, distance = self.objective_info(group)
|
closest, distance = self.objective_info(group)
|
||||||
closest.preset_locations.required_long_range_sams.append(
|
closest.preset_locations.required_long_range_sams.append(
|
||||||
group.position
|
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||||
)
|
)
|
||||||
|
|
||||||
for group in self.required_medium_range_sams:
|
for group in self.required_medium_range_sams:
|
||||||
closest, distance = self.objective_info(group)
|
closest, distance = self.objective_info(group)
|
||||||
closest.preset_locations.required_medium_range_sams.append(
|
closest.preset_locations.required_medium_range_sams.append(
|
||||||
group.position
|
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||||
|
)
|
||||||
|
|
||||||
|
for group in self.required_ewrs:
|
||||||
|
closest, distance = self.objective_info(group)
|
||||||
|
closest.preset_locations.required_ewrs.append(
|
||||||
|
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||||
|
)
|
||||||
|
|
||||||
|
for group in self.helipads:
|
||||||
|
closest, distance = self.objective_info(group)
|
||||||
|
closest.helipads.append(
|
||||||
|
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||||
)
|
)
|
||||||
|
|
||||||
def populate_theater(self) -> None:
|
def populate_theater(self) -> None:
|
||||||
@@ -423,8 +473,9 @@ class ConflictTheater:
|
|||||||
logging.warning("Replacing existing frontline data")
|
logging.warning("Replacing existing frontline data")
|
||||||
self._frontline_data = data
|
self._frontline_data = data
|
||||||
|
|
||||||
def add_controlpoint(self, point: ControlPoint,
|
def add_controlpoint(
|
||||||
connected_to: Optional[List[ControlPoint]] = None):
|
self, point: ControlPoint, connected_to: Optional[List[ControlPoint]] = None
|
||||||
|
):
|
||||||
if connected_to is None:
|
if connected_to is None:
|
||||||
connected_to = []
|
connected_to = []
|
||||||
for connected_point in connected_to:
|
for connected_point in connected_to:
|
||||||
@@ -486,8 +537,8 @@ class ConflictTheater:
|
|||||||
for inclusion_zone in self.landmap.inclusion_zones:
|
for inclusion_zone in self.landmap.inclusion_zones:
|
||||||
nearest_pair = ops.nearest_points(point, inclusion_zone)
|
nearest_pair = ops.nearest_points(point, inclusion_zone)
|
||||||
nearest_points.append(nearest_pair[1])
|
nearest_points.append(nearest_pair[1])
|
||||||
min_distance = point.distance(nearest_points[0]) # type: geometry.Point
|
min_distance = point.distance(nearest_points[0]) # type: geometry.Point
|
||||||
nearest_point = nearest_points[0] # type: geometry.Point
|
nearest_point = nearest_points[0] # type: geometry.Point
|
||||||
for pt in nearest_points[1:]:
|
for pt in nearest_points[1:]:
|
||||||
distance = point.distance(pt)
|
distance = point.distance(pt)
|
||||||
if distance < min_distance:
|
if distance < min_distance:
|
||||||
@@ -498,7 +549,7 @@ class ConflictTheater:
|
|||||||
nearest_point = Point(nearest_point.x, nearest_point.y)
|
nearest_point = Point(nearest_point.x, nearest_point.y)
|
||||||
new_point = point.point_from_heading(
|
new_point = point.point_from_heading(
|
||||||
point.heading_between_point(nearest_point),
|
point.heading_between_point(nearest_point),
|
||||||
point.distance_to_point(nearest_point) + extend_dist
|
point.distance_to_point(nearest_point) + extend_dist,
|
||||||
)
|
)
|
||||||
return new_point
|
return new_point
|
||||||
|
|
||||||
@@ -512,7 +563,9 @@ class ConflictTheater:
|
|||||||
|
|
||||||
def conflicts(self, from_player=True) -> Iterator[FrontLine]:
|
def conflicts(self, from_player=True) -> Iterator[FrontLine]:
|
||||||
for cp in [x for x in self.controlpoints if x.captured == from_player]:
|
for cp in [x for x in self.controlpoints if x.captured == from_player]:
|
||||||
for connected_point in [x for x in cp.connected_points if x.captured != from_player]:
|
for connected_point in [
|
||||||
|
x for x in cp.connected_points if x.captured != from_player
|
||||||
|
]:
|
||||||
yield FrontLine(cp, connected_point, self)
|
yield FrontLine(cp, connected_point, self)
|
||||||
|
|
||||||
def enemy_points(self) -> List[ControlPoint]:
|
def enemy_points(self) -> List[ControlPoint]:
|
||||||
@@ -547,7 +600,7 @@ class ConflictTheater:
|
|||||||
closest = conflict
|
closest = conflict
|
||||||
closest_distance = distance
|
closest_distance = distance
|
||||||
return closest
|
return closest
|
||||||
|
|
||||||
def closest_opposing_control_points(self) -> Tuple[ControlPoint, ControlPoint]:
|
def closest_opposing_control_points(self) -> Tuple[ControlPoint, ControlPoint]:
|
||||||
"""
|
"""
|
||||||
Returns a tuple of the two nearest opposing ControlPoints in theater.
|
Returns a tuple of the two nearest opposing ControlPoints in theater.
|
||||||
@@ -567,17 +620,22 @@ class ConflictTheater:
|
|||||||
distances[cp.id] = dist
|
distances[cp.id] = dist
|
||||||
closest_cp_id = min(distances, key=distances.get) # type: ignore
|
closest_cp_id = min(distances, key=distances.get) # type: ignore
|
||||||
|
|
||||||
all_cp_min_distances[(control_point.id, closest_cp_id)] = distances[closest_cp_id]
|
all_cp_min_distances[(control_point.id, closest_cp_id)] = distances[
|
||||||
|
closest_cp_id
|
||||||
|
]
|
||||||
closest_opposing_cps = [
|
closest_opposing_cps = [
|
||||||
self.find_control_point_by_id(i)
|
self.find_control_point_by_id(i)
|
||||||
for i
|
for i in min(
|
||||||
in min(all_cp_min_distances, key=all_cp_min_distances.get) # type: ignore
|
all_cp_min_distances, key=all_cp_min_distances.get
|
||||||
] # type: List[ControlPoint]
|
) # type: ignore
|
||||||
|
] # type: List[ControlPoint]
|
||||||
assert len(closest_opposing_cps) == 2
|
assert len(closest_opposing_cps) == 2
|
||||||
if closest_opposing_cps[0].captured:
|
if closest_opposing_cps[0].captured:
|
||||||
return cast(Tuple[ControlPoint, ControlPoint], tuple(closest_opposing_cps))
|
return cast(Tuple[ControlPoint, ControlPoint], tuple(closest_opposing_cps))
|
||||||
else:
|
else:
|
||||||
return cast(Tuple[ControlPoint, ControlPoint], tuple(reversed(closest_opposing_cps)))
|
return cast(
|
||||||
|
Tuple[ControlPoint, ControlPoint], tuple(reversed(closest_opposing_cps))
|
||||||
|
)
|
||||||
|
|
||||||
def find_control_point_by_id(self, id: int) -> ControlPoint:
|
def find_control_point_by_id(self, id: int) -> ControlPoint:
|
||||||
for i in self.controlpoints:
|
for i in self.controlpoints:
|
||||||
@@ -613,7 +671,7 @@ class ConflictTheater:
|
|||||||
cp.captured_invert = False
|
cp.captured_invert = False
|
||||||
|
|
||||||
return cp
|
return cp
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_json(directory: Path, data: Dict[str, Any]) -> ConflictTheater:
|
def from_json(directory: Path, data: Dict[str, Any]) -> ConflictTheater:
|
||||||
theaters = {
|
theaters = {
|
||||||
@@ -649,7 +707,7 @@ class ConflictTheater:
|
|||||||
cps[l[1]].connect(cps[l[0]])
|
cps[l[1]].connect(cps[l[0]])
|
||||||
|
|
||||||
return t
|
return t
|
||||||
|
|
||||||
|
|
||||||
class CaucasusTheater(ConflictTheater):
|
class CaucasusTheater(ConflictTheater):
|
||||||
terrain = caucasus.Caucasus()
|
terrain = caucasus.Caucasus()
|
||||||
@@ -672,9 +730,8 @@ class PersianGulfTheater(ConflictTheater):
|
|||||||
terrain = persiangulf.PersianGulf()
|
terrain = persiangulf.PersianGulf()
|
||||||
overview_image = "persiangulf.gif"
|
overview_image = "persiangulf.gif"
|
||||||
reference_points = (
|
reference_points = (
|
||||||
ReferencePoint(persiangulf.Jiroft_Airport.position,
|
ReferencePoint(persiangulf.Jiroft.position, Point(1692, 1343)),
|
||||||
Point(1692, 1343)),
|
ReferencePoint(persiangulf.Liwa_AFB.position, Point(358, 3238)),
|
||||||
ReferencePoint(persiangulf.Liwa_Airbase.position, Point(358, 3238)),
|
|
||||||
)
|
)
|
||||||
landmap = load_landmap("resources\\gulflandmap.p")
|
landmap = load_landmap("resources\\gulflandmap.p")
|
||||||
daytime_map = {
|
daytime_map = {
|
||||||
@@ -722,7 +779,7 @@ class TheChannelTheater(ConflictTheater):
|
|||||||
overview_image = "thechannel.gif"
|
overview_image = "thechannel.gif"
|
||||||
reference_points = (
|
reference_points = (
|
||||||
ReferencePoint(thechannel.Abbeville_Drucat.position, Point(2005, 2390)),
|
ReferencePoint(thechannel.Abbeville_Drucat.position, Point(2005, 2390)),
|
||||||
ReferencePoint(thechannel.Detling.position, Point(706, 382))
|
ReferencePoint(thechannel.Detling.position, Point(706, 382)),
|
||||||
)
|
)
|
||||||
landmap = load_landmap("resources\\channellandmap.p")
|
landmap = load_landmap("resources\\channellandmap.p")
|
||||||
daytime_map = {
|
daytime_map = {
|
||||||
@@ -791,7 +848,7 @@ class FrontLine(MissionTarget):
|
|||||||
self,
|
self,
|
||||||
control_point_a: ControlPoint,
|
control_point_a: ControlPoint,
|
||||||
control_point_b: ControlPoint,
|
control_point_b: ControlPoint,
|
||||||
theater: ConflictTheater
|
theater: ConflictTheater,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.control_point_a = control_point_a
|
self.control_point_a = control_point_a
|
||||||
self.control_point_b = control_point_b
|
self.control_point_b = control_point_b
|
||||||
@@ -807,6 +864,7 @@ class FrontLine(MissionTarget):
|
|||||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||||
yield from [
|
yield from [
|
||||||
FlightType.CAS,
|
FlightType.CAS,
|
||||||
|
FlightType.AEWC,
|
||||||
# TODO: FlightType.TROOP_TRANSPORT
|
# TODO: FlightType.TROOP_TRANSPORT
|
||||||
# TODO: FlightType.EVAC
|
# TODO: FlightType.EVAC
|
||||||
]
|
]
|
||||||
@@ -935,10 +993,9 @@ class FrontLine(MissionTarget):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load_json_frontlines(
|
def load_json_frontlines(
|
||||||
theater: ConflictTheater
|
theater: ConflictTheater,
|
||||||
) -> Optional[Dict[str, ComplexFrontLine]]:
|
) -> Optional[Dict[str, ComplexFrontLine]]:
|
||||||
"""Load complex frontlines from json"""
|
"""Load complex frontlines from json"""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import heapq
|
|||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
import re
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
@@ -28,6 +27,7 @@ from gen.ground_forces.combat_stance import CombatStance
|
|||||||
from gen.runways import RunwayAssigner, RunwayData
|
from gen.runways import RunwayAssigner, RunwayData
|
||||||
from .base import Base
|
from .base import Base
|
||||||
from .missiontarget import MissionTarget
|
from .missiontarget import MissionTarget
|
||||||
|
from game.point_with_heading import PointWithHeading
|
||||||
from .theatergroundobject import (
|
from .theatergroundobject import (
|
||||||
BaseDefenseGroundObject,
|
BaseDefenseGroundObject,
|
||||||
EwrGroundObject,
|
EwrGroundObject,
|
||||||
@@ -63,6 +63,7 @@ class LocationType(Enum):
|
|||||||
BaseAirDefense = "base air defense"
|
BaseAirDefense = "base air defense"
|
||||||
Coastal = "coastal defense"
|
Coastal = "coastal defense"
|
||||||
Ewr = "EWR"
|
Ewr = "EWR"
|
||||||
|
BaseEwr = "Base EWR"
|
||||||
Garrison = "garrison"
|
Garrison = "garrison"
|
||||||
MissileSite = "missile site"
|
MissileSite = "missile site"
|
||||||
OffshoreStrikeTarget = "offshore strike target"
|
OffshoreStrikeTarget = "offshore strike target"
|
||||||
@@ -77,38 +78,44 @@ class PresetLocations:
|
|||||||
"""Defines the preset locations loaded from the campaign mission file."""
|
"""Defines the preset locations loaded from the campaign mission file."""
|
||||||
|
|
||||||
#: Locations used for spawning ground defenses for bases.
|
#: Locations used for spawning ground defenses for bases.
|
||||||
base_garrisons: List[Point] = field(default_factory=list)
|
base_garrisons: List[PointWithHeading] = field(default_factory=list)
|
||||||
|
|
||||||
#: Locations used for spawning air defenses for bases. Used by SAMs, AAA,
|
#: Locations used for spawning air defenses for bases. Used by SAMs, AAA,
|
||||||
#: and SHORADs.
|
#: and SHORADs.
|
||||||
base_air_defense: List[Point] = field(default_factory=list)
|
base_air_defense: List[PointWithHeading] = field(default_factory=list)
|
||||||
|
|
||||||
#: Locations used by EWRs.
|
#: Locations used by EWRs.
|
||||||
ewrs: List[Point] = field(default_factory=list)
|
ewrs: List[PointWithHeading] = field(default_factory=list)
|
||||||
|
|
||||||
|
#: Locations used by Base EWRs.
|
||||||
|
base_ewrs: List[PointWithHeading] = field(default_factory=list)
|
||||||
|
|
||||||
#: Locations used by non-carrier ships. Carriers and LHAs are not random.
|
#: Locations used by non-carrier ships. Carriers and LHAs are not random.
|
||||||
ships: List[Point] = field(default_factory=list)
|
ships: List[PointWithHeading] = field(default_factory=list)
|
||||||
|
|
||||||
#: Locations used by coastal defenses.
|
#: Locations used by coastal defenses.
|
||||||
coastal_defenses: List[Point] = field(default_factory=list)
|
coastal_defenses: List[PointWithHeading] = field(default_factory=list)
|
||||||
|
|
||||||
#: Locations used by ground based strike objectives.
|
#: Locations used by ground based strike objectives.
|
||||||
strike_locations: List[Point] = field(default_factory=list)
|
strike_locations: List[PointWithHeading] = field(default_factory=list)
|
||||||
|
|
||||||
#: Locations used by offshore strike objectives.
|
#: Locations used by offshore strike objectives.
|
||||||
offshore_strike_locations: List[Point] = field(default_factory=list)
|
offshore_strike_locations: List[PointWithHeading] = field(default_factory=list)
|
||||||
|
|
||||||
#: Locations used by missile sites like scuds and V-2s.
|
#: Locations used by missile sites like scuds and V-2s.
|
||||||
missile_sites: List[Point] = field(default_factory=list)
|
missile_sites: List[PointWithHeading] = field(default_factory=list)
|
||||||
|
|
||||||
#: Locations of long range SAMs which should always be spawned.
|
#: Locations of long range SAMs which should always be spawned.
|
||||||
required_long_range_sams: List[Point] = field(default_factory=list)
|
required_long_range_sams: List[PointWithHeading] = field(default_factory=list)
|
||||||
|
|
||||||
#: Locations of medium range SAMs which should always be spawned.
|
#: Locations of medium range SAMs which should always be spawned.
|
||||||
required_medium_range_sams: List[Point] = field(default_factory=list)
|
required_medium_range_sams: List[PointWithHeading] = field(default_factory=list)
|
||||||
|
|
||||||
|
#: Locations of EWRs which should always be spawned.
|
||||||
|
required_ewrs: List[PointWithHeading] = field(default_factory=list)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _random_from(points: List[Point]) -> Optional[Point]:
|
def _random_from(points: List[PointWithHeading]) -> Optional[PointWithHeading]:
|
||||||
"""Finds, removes, and returns a random position from the given list."""
|
"""Finds, removes, and returns a random position from the given list."""
|
||||||
if not points:
|
if not points:
|
||||||
return None
|
return None
|
||||||
@@ -116,7 +123,7 @@ class PresetLocations:
|
|||||||
points.remove(point)
|
points.remove(point)
|
||||||
return point
|
return point
|
||||||
|
|
||||||
def random_for(self, location_type: LocationType) -> Optional[Point]:
|
def random_for(self, location_type: LocationType) -> Optional[PointWithHeading]:
|
||||||
"""Returns a position suitable for the given location type.
|
"""Returns a position suitable for the given location type.
|
||||||
|
|
||||||
The location, if found, will be claimed by the caller and not available
|
The location, if found, will be claimed by the caller and not available
|
||||||
@@ -128,6 +135,8 @@ class PresetLocations:
|
|||||||
return self._random_from(self.coastal_defenses)
|
return self._random_from(self.coastal_defenses)
|
||||||
if location_type == LocationType.Ewr:
|
if location_type == LocationType.Ewr:
|
||||||
return self._random_from(self.ewrs)
|
return self._random_from(self.ewrs)
|
||||||
|
if location_type == LocationType.BaseEwr:
|
||||||
|
return self._random_from(self.base_ewrs)
|
||||||
if location_type == LocationType.Garrison:
|
if location_type == LocationType.Garrison:
|
||||||
return self._random_from(self.base_garrisons)
|
return self._random_from(self.base_garrisons)
|
||||||
if location_type == LocationType.MissileSite:
|
if location_type == LocationType.MissileSite:
|
||||||
@@ -231,10 +240,17 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
# TODO: Only airbases have IDs.
|
# TODO: Only airbases have IDs.
|
||||||
# TODO: has_frontline is only reasonable for airbases.
|
# TODO: has_frontline is only reasonable for airbases.
|
||||||
# TODO: cptype is obsolete.
|
# TODO: cptype is obsolete.
|
||||||
def __init__(self, cp_id: int, name: str, position: Point,
|
def __init__(
|
||||||
at: db.StartingPosition, size: int,
|
self,
|
||||||
importance: float, has_frontline=True,
|
cp_id: int,
|
||||||
cptype=ControlPointType.AIRBASE):
|
name: str,
|
||||||
|
position: Point,
|
||||||
|
at: db.StartingPosition,
|
||||||
|
size: int,
|
||||||
|
importance: float,
|
||||||
|
has_frontline=True,
|
||||||
|
cptype=ControlPointType.AIRBASE,
|
||||||
|
):
|
||||||
super().__init__(name, position)
|
super().__init__(name, position)
|
||||||
# TODO: Should be Airbase specific.
|
# TODO: Should be Airbase specific.
|
||||||
self.id = cp_id
|
self.id = cp_id
|
||||||
@@ -243,6 +259,7 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
self.connected_objectives: List[TheaterGroundObject] = []
|
self.connected_objectives: List[TheaterGroundObject] = []
|
||||||
self.base_defenses: List[BaseDefenseGroundObject] = []
|
self.base_defenses: List[BaseDefenseGroundObject] = []
|
||||||
self.preset_locations = PresetLocations()
|
self.preset_locations = PresetLocations()
|
||||||
|
self.helipads: List[PointWithHeading] = []
|
||||||
|
|
||||||
# TODO: Should be Airbase specific.
|
# TODO: Should be Airbase specific.
|
||||||
self.size = size
|
self.size = size
|
||||||
@@ -257,17 +274,17 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
# TODO: Should be Airbase specific.
|
# TODO: Should be Airbase specific.
|
||||||
self.stances: Dict[int, CombatStance] = {}
|
self.stances: Dict[int, CombatStance] = {}
|
||||||
from ..event import UnitsDeliveryEvent
|
from ..event import UnitsDeliveryEvent
|
||||||
|
|
||||||
self.pending_unit_deliveries = UnitsDeliveryEvent(self)
|
self.pending_unit_deliveries = UnitsDeliveryEvent(self)
|
||||||
|
|
||||||
self.target_position: Optional[Point] = None
|
self.target_position: Optional[Point] = None
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{__class__}: {self.name}>"
|
return f"<{__class__}: {self.name}>"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ground_objects(self) -> List[TheaterGroundObject]:
|
def ground_objects(self) -> List[TheaterGroundObject]:
|
||||||
return list(
|
return list(itertools.chain(self.connected_objectives, self.base_defenses))
|
||||||
itertools.chain(self.connected_objectives, self.base_defenses))
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@@ -342,15 +359,18 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
Get the carrier group name if the airbase is a carrier
|
Get the carrier group name if the airbase is a carrier
|
||||||
:return: Carrier group name
|
:return: Carrier group name
|
||||||
"""
|
"""
|
||||||
if self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP,
|
if self.cptype in [
|
||||||
ControlPointType.LHA_GROUP]:
|
ControlPointType.AIRCRAFT_CARRIER_GROUP,
|
||||||
|
ControlPointType.LHA_GROUP,
|
||||||
|
]:
|
||||||
for g in self.ground_objects:
|
for g in self.ground_objects:
|
||||||
if g.dcs_identifier == "CARRIER":
|
if g.dcs_identifier == "CARRIER":
|
||||||
for group in g.groups:
|
for group in g.groups:
|
||||||
for u in group.units:
|
for u in group.units:
|
||||||
if db.unit_type_from_name(u.type) in [
|
if db.unit_type_from_name(u.type) in [
|
||||||
CVN_74_John_C__Stennis,
|
CVN_74_John_C__Stennis,
|
||||||
CV_1143_5_Admiral_Kuznetsov]:
|
CV_1143_5_Admiral_Kuznetsov,
|
||||||
|
]:
|
||||||
return group.name
|
return group.name
|
||||||
elif g.dcs_identifier == "LHA":
|
elif g.dcs_identifier == "LHA":
|
||||||
for group in g.groups:
|
for group in g.groups:
|
||||||
@@ -376,20 +396,19 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
# TODO: Should be Airbase specific.
|
# TODO: Should be Airbase specific.
|
||||||
def clear_base_defenses(self) -> None:
|
def clear_base_defenses(self) -> None:
|
||||||
for base_defense in self.base_defenses:
|
for base_defense in self.base_defenses:
|
||||||
|
p = PointWithHeading.from_point(base_defense.position, base_defense.heading)
|
||||||
if isinstance(base_defense, EwrGroundObject):
|
if isinstance(base_defense, EwrGroundObject):
|
||||||
self.preset_locations.ewrs.append(base_defense.position)
|
self.preset_locations.base_ewrs.append(p)
|
||||||
elif isinstance(base_defense, SamGroundObject):
|
elif isinstance(base_defense, SamGroundObject):
|
||||||
self.preset_locations.base_air_defense.append(
|
self.preset_locations.base_air_defense.append(p)
|
||||||
base_defense.position)
|
|
||||||
elif isinstance(base_defense, VehicleGroupGroundObject):
|
elif isinstance(base_defense, VehicleGroupGroundObject):
|
||||||
self.preset_locations.base_garrisons.append(
|
self.preset_locations.base_garrisons.append(p)
|
||||||
base_defense.position)
|
|
||||||
else:
|
else:
|
||||||
logging.error(
|
logging.error(
|
||||||
"Could not determine preset location type for "
|
"Could not determine preset location type for "
|
||||||
f"{base_defense}. Assuming garrison type.")
|
f"{base_defense}. Assuming garrison type."
|
||||||
self.preset_locations.base_garrisons.append(
|
)
|
||||||
base_defense.position)
|
self.preset_locations.base_garrisons.append(p)
|
||||||
self.base_defenses = []
|
self.base_defenses = []
|
||||||
|
|
||||||
def capture_equipment(self, game: Game) -> None:
|
def capture_equipment(self, game: Game) -> None:
|
||||||
@@ -398,15 +417,18 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
game.adjust_budget(total, player=not self.captured)
|
game.adjust_budget(total, player=not self.captured)
|
||||||
game.message(
|
game.message(
|
||||||
f"{self.name} is not connected to any friendly points. Ground "
|
f"{self.name} is not connected to any friendly points. Ground "
|
||||||
f"vehicles have been captured and sold for ${total}M.")
|
f"vehicles have been captured and sold for ${total}M."
|
||||||
|
)
|
||||||
|
|
||||||
def retreat_ground_units(self, game: Game):
|
def retreat_ground_units(self, game: Game):
|
||||||
# When there are multiple valid destinations, deliver units to whichever
|
# When there are multiple valid destinations, deliver units to whichever
|
||||||
# base is least defended first. The closest approximation of unit
|
# base is least defended first. The closest approximation of unit
|
||||||
# strength we have is price
|
# strength we have is price
|
||||||
destinations = [GroundUnitDestination(cp)
|
destinations = [
|
||||||
for cp in self.connected_points
|
GroundUnitDestination(cp)
|
||||||
if cp.captured == self.captured]
|
for cp in self.connected_points
|
||||||
|
if cp.captured == self.captured
|
||||||
|
]
|
||||||
if not destinations:
|
if not destinations:
|
||||||
self.capture_equipment(game)
|
self.capture_equipment(game)
|
||||||
return
|
return
|
||||||
@@ -419,8 +441,9 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
destination.control_point.base.commision_units({unit_type: 1})
|
destination.control_point.base.commision_units({unit_type: 1})
|
||||||
destination = heapq.heappushpop(destinations, destination)
|
destination = heapq.heappushpop(destinations, destination)
|
||||||
|
|
||||||
def capture_aircraft(self, game: Game, airframe: Type[FlyingType],
|
def capture_aircraft(
|
||||||
count: int) -> None:
|
self, game: Game, airframe: Type[FlyingType], count: int
|
||||||
|
) -> None:
|
||||||
try:
|
try:
|
||||||
value = PRICES[airframe] * count
|
value = PRICES[airframe] * count
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@@ -431,11 +454,12 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
game.message(
|
game.message(
|
||||||
f"No valid retreat destination in range of {self.name} for "
|
f"No valid retreat destination in range of {self.name} for "
|
||||||
f"{airframe.id}. {count} aircraft have been captured and sold for "
|
f"{airframe.id}. {count} aircraft have been captured and sold for "
|
||||||
f"${value}M.")
|
f"${value}M."
|
||||||
|
)
|
||||||
|
|
||||||
def aircraft_retreat_destination(
|
def aircraft_retreat_destination(
|
||||||
self, game: Game,
|
self, game: Game, airframe: Type[FlyingType]
|
||||||
airframe: Type[FlyingType]) -> Optional[ControlPoint]:
|
) -> Optional[ControlPoint]:
|
||||||
closest = ObjectiveDistanceCache.get_closest_airfields(self)
|
closest = ObjectiveDistanceCache.get_closest_airfields(self)
|
||||||
# TODO: Should be airframe dependent.
|
# TODO: Should be airframe dependent.
|
||||||
max_retreat_distance = nautical_miles(200)
|
max_retreat_distance = nautical_miles(200)
|
||||||
@@ -451,8 +475,9 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
return airbase
|
return airbase
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _retreat_air_units(self, game: Game, airframe: Type[FlyingType],
|
def _retreat_air_units(
|
||||||
count: int) -> None:
|
self, game: Game, airframe: Type[FlyingType], count: int
|
||||||
|
) -> None:
|
||||||
while count:
|
while count:
|
||||||
logging.debug(f"Retreating {count} {airframe.id} from {self.name}")
|
logging.debug(f"Retreating {count} {airframe.id} from {self.name}")
|
||||||
destination = self.aircraft_retreat_destination(game, airframe)
|
destination = self.aircraft_retreat_destination(game, airframe)
|
||||||
@@ -485,6 +510,7 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
|
|
||||||
self.clear_base_defenses()
|
self.clear_base_defenses()
|
||||||
from .start_generator import BaseDefenseGenerator
|
from .start_generator import BaseDefenseGenerator
|
||||||
|
|
||||||
BaseDefenseGenerator(game, self).generate()
|
BaseDefenseGenerator(game, self).generate()
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@@ -514,16 +540,19 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
if issubclass(unit_bought, FlyingType):
|
if issubclass(unit_bought, FlyingType):
|
||||||
on_order += self.pending_unit_deliveries.units[unit_bought]
|
on_order += self.pending_unit_deliveries.units[unit_bought]
|
||||||
|
|
||||||
return PendingOccupancy(self.base.total_aircraft, on_order,
|
return PendingOccupancy(
|
||||||
self.aircraft_transferring(game))
|
self.base.total_aircraft, on_order, self.aircraft_transferring(game)
|
||||||
|
)
|
||||||
|
|
||||||
def unclaimed_parking(self, game: Game) -> int:
|
def unclaimed_parking(self, game: Game) -> int:
|
||||||
return (self.total_aircraft_parking -
|
return (
|
||||||
self.expected_aircraft_next_turn(game).total)
|
self.total_aircraft_parking - self.expected_aircraft_next_turn(game).total
|
||||||
|
)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def active_runway(self, conditions: Conditions,
|
def active_runway(
|
||||||
dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
|
self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData]
|
||||||
|
) -> RunwayData:
|
||||||
...
|
...
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -574,7 +603,13 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
Get number of pending frontline aa units
|
Get number of pending frontline aa units
|
||||||
"""
|
"""
|
||||||
if self.pending_unit_deliveries:
|
if self.pending_unit_deliveries:
|
||||||
return sum([v for k,v in self.pending_unit_deliveries.units.items() if k in TYPE_SHORAD])
|
return sum(
|
||||||
|
[
|
||||||
|
v
|
||||||
|
for k, v in self.pending_unit_deliveries.units.items()
|
||||||
|
if k in TYPE_SHORAD
|
||||||
|
]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@@ -598,22 +633,45 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
continue
|
continue
|
||||||
on_order += self.pending_unit_deliveries.units[unit_bought]
|
on_order += self.pending_unit_deliveries.units[unit_bought]
|
||||||
|
|
||||||
return PendingOccupancy(self.base.total_armor, on_order,
|
return PendingOccupancy(
|
||||||
# Ground unit transfers not yet implemented.
|
self.base.total_armor,
|
||||||
transferring=0)
|
on_order,
|
||||||
|
# Ground unit transfers not yet implemented.
|
||||||
|
transferring=0,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def income_per_turn(self) -> int:
|
def income_per_turn(self) -> int:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||||
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
|
if self.is_friendly(for_player):
|
||||||
|
yield from [
|
||||||
|
FlightType.AEWC,
|
||||||
|
]
|
||||||
|
yield from super().mission_types(for_player)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_active_frontline(self) -> bool:
|
||||||
|
return any(not c.is_friendly(self.captured) for c in self.connected_points)
|
||||||
|
|
||||||
|
|
||||||
class Airfield(ControlPoint):
|
class Airfield(ControlPoint):
|
||||||
|
def __init__(
|
||||||
def __init__(self, airport: Airport, size: int,
|
self, airport: Airport, size: int, importance: float, has_frontline=True
|
||||||
importance: float, has_frontline=True):
|
):
|
||||||
super().__init__(airport.id, airport.name, airport.position, airport,
|
super().__init__(
|
||||||
size, importance, has_frontline,
|
airport.id,
|
||||||
cptype=ControlPointType.AIRBASE)
|
airport.name,
|
||||||
|
airport.position,
|
||||||
|
airport,
|
||||||
|
size,
|
||||||
|
importance,
|
||||||
|
has_frontline,
|
||||||
|
cptype=ControlPointType.AIRBASE,
|
||||||
|
)
|
||||||
self.airport = airport
|
self.airport = airport
|
||||||
self._runway_status = RunwayStatus()
|
self._runway_status = RunwayStatus()
|
||||||
|
|
||||||
@@ -627,6 +685,7 @@ class Airfield(ControlPoint):
|
|||||||
|
|
||||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||||
from gen.flights.flight import FlightType
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
if self.is_friendly(for_player):
|
if self.is_friendly(for_player):
|
||||||
yield from [
|
yield from [
|
||||||
# TODO: FlightType.INTERCEPTION
|
# TODO: FlightType.INTERCEPTION
|
||||||
@@ -657,8 +716,9 @@ class Airfield(ControlPoint):
|
|||||||
def damage_runway(self) -> None:
|
def damage_runway(self) -> None:
|
||||||
self.runway_status.damage()
|
self.runway_status.damage()
|
||||||
|
|
||||||
def active_runway(self, conditions: Conditions,
|
def active_runway(
|
||||||
dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
|
self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData]
|
||||||
|
) -> RunwayData:
|
||||||
assigner = RunwayAssigner(conditions)
|
assigner = RunwayAssigner(conditions)
|
||||||
return assigner.get_preferred_runway(self.airport)
|
return assigner.get_preferred_runway(self.airport)
|
||||||
|
|
||||||
@@ -676,13 +736,13 @@ class Airfield(ControlPoint):
|
|||||||
|
|
||||||
|
|
||||||
class NavalControlPoint(ControlPoint, ABC):
|
class NavalControlPoint(ControlPoint, ABC):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_fleet(self) -> bool:
|
def is_fleet(self) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||||
from gen.flights.flight import FlightType
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
if self.is_friendly(for_player):
|
if self.is_friendly(for_player):
|
||||||
yield from [
|
yield from [
|
||||||
# TODO: FlightType.INTERCEPTION
|
# TODO: FlightType.INTERCEPTION
|
||||||
@@ -706,14 +766,17 @@ class NavalControlPoint(ControlPoint, ABC):
|
|||||||
for group in g.groups:
|
for group in g.groups:
|
||||||
for u in group.units:
|
for u in group.units:
|
||||||
if db.unit_type_from_name(u.type) in [
|
if db.unit_type_from_name(u.type) in [
|
||||||
CVN_74_John_C__Stennis, LHA_1_Tarawa,
|
CVN_74_John_C__Stennis,
|
||||||
CV_1143_5_Admiral_Kuznetsov,
|
LHA_1_Tarawa,
|
||||||
Type_071_Amphibious_Transport_Dock]:
|
CV_1143_5_Admiral_Kuznetsov,
|
||||||
|
Type_071_Amphibious_Transport_Dock,
|
||||||
|
]:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def active_runway(self, conditions: Conditions,
|
def active_runway(
|
||||||
dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
|
self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData]
|
||||||
|
) -> RunwayData:
|
||||||
# TODO: Assign TACAN and ICLS earlier so we don't need this.
|
# TODO: Assign TACAN and ICLS earlier so we don't need this.
|
||||||
fallback = RunwayData(self.full_name, runway_heading=0, runway_name="")
|
fallback = RunwayData(self.full_name, runway_heading=0, runway_name="")
|
||||||
return dynamic_runways.get(self.name, fallback)
|
return dynamic_runways.get(self.name, fallback)
|
||||||
@@ -736,12 +799,19 @@ class NavalControlPoint(ControlPoint, ABC):
|
|||||||
|
|
||||||
|
|
||||||
class Carrier(NavalControlPoint):
|
class Carrier(NavalControlPoint):
|
||||||
|
|
||||||
def __init__(self, name: str, at: Point, cp_id: int):
|
def __init__(self, name: str, at: Point, cp_id: int):
|
||||||
import game.theater.conflicttheater
|
import game.theater.conflicttheater
|
||||||
super().__init__(cp_id, name, at, at,
|
|
||||||
game.theater.conflicttheater.SIZE_SMALL, 1,
|
super().__init__(
|
||||||
has_frontline=False, cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP)
|
cp_id,
|
||||||
|
name,
|
||||||
|
at,
|
||||||
|
at,
|
||||||
|
game.theater.conflicttheater.SIZE_SMALL,
|
||||||
|
1,
|
||||||
|
has_frontline=False,
|
||||||
|
cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP,
|
||||||
|
)
|
||||||
|
|
||||||
def capture(self, game: Game, for_player: bool) -> None:
|
def capture(self, game: Game, for_player: bool) -> None:
|
||||||
raise RuntimeError("Carriers cannot be captured")
|
raise RuntimeError("Carriers cannot be captured")
|
||||||
@@ -759,12 +829,19 @@ class Carrier(NavalControlPoint):
|
|||||||
|
|
||||||
|
|
||||||
class Lha(NavalControlPoint):
|
class Lha(NavalControlPoint):
|
||||||
|
|
||||||
def __init__(self, name: str, at: Point, cp_id: int):
|
def __init__(self, name: str, at: Point, cp_id: int):
|
||||||
import game.theater.conflicttheater
|
import game.theater.conflicttheater
|
||||||
super().__init__(cp_id, name, at, at,
|
|
||||||
game.theater.conflicttheater.SIZE_SMALL, 1,
|
super().__init__(
|
||||||
has_frontline=False, cptype=ControlPointType.LHA_GROUP)
|
cp_id,
|
||||||
|
name,
|
||||||
|
at,
|
||||||
|
at,
|
||||||
|
game.theater.conflicttheater.SIZE_SMALL,
|
||||||
|
1,
|
||||||
|
has_frontline=False,
|
||||||
|
cptype=ControlPointType.LHA_GROUP,
|
||||||
|
)
|
||||||
|
|
||||||
def capture(self, game: Game, for_player: bool) -> None:
|
def capture(self, game: Game, for_player: bool) -> None:
|
||||||
raise RuntimeError("LHAs cannot be captured")
|
raise RuntimeError("LHAs cannot be captured")
|
||||||
@@ -782,15 +859,22 @@ class Lha(NavalControlPoint):
|
|||||||
|
|
||||||
|
|
||||||
class OffMapSpawn(ControlPoint):
|
class OffMapSpawn(ControlPoint):
|
||||||
|
|
||||||
def runway_is_operational(self) -> bool:
|
def runway_is_operational(self) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def __init__(self, cp_id: int, name: str, position: Point):
|
def __init__(self, cp_id: int, name: str, position: Point):
|
||||||
from . import IMPORTANCE_MEDIUM, SIZE_REGULAR
|
from . import IMPORTANCE_MEDIUM, SIZE_REGULAR
|
||||||
super().__init__(cp_id, name, position, at=position,
|
|
||||||
size=SIZE_REGULAR, importance=IMPORTANCE_MEDIUM,
|
super().__init__(
|
||||||
has_frontline=False, cptype=ControlPointType.OFF_MAP)
|
cp_id,
|
||||||
|
name,
|
||||||
|
position,
|
||||||
|
at=position,
|
||||||
|
size=SIZE_REGULAR,
|
||||||
|
importance=IMPORTANCE_MEDIUM,
|
||||||
|
has_frontline=False,
|
||||||
|
cptype=ControlPointType.OFF_MAP,
|
||||||
|
)
|
||||||
|
|
||||||
def capture(self, game: Game, for_player: bool) -> None:
|
def capture(self, game: Game, for_player: bool) -> None:
|
||||||
raise RuntimeError("Off map control points cannot be captured")
|
raise RuntimeError("Off map control points cannot be captured")
|
||||||
@@ -809,8 +893,9 @@ class OffMapSpawn(ControlPoint):
|
|||||||
def heading(self) -> int:
|
def heading(self) -> int:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def active_runway(self, conditions: Conditions,
|
def active_runway(
|
||||||
dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
|
self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData]
|
||||||
|
) -> RunwayData:
|
||||||
logging.warning("TODO: Off map spawns have no runways.")
|
logging.warning("TODO: Off map spawns have no runways.")
|
||||||
return RunwayData(self.full_name, runway_heading=0, runway_name="")
|
return RunwayData(self.full_name, runway_heading=0, runway_name="")
|
||||||
|
|
||||||
@@ -824,19 +909,27 @@ class OffMapSpawn(ControlPoint):
|
|||||||
|
|
||||||
|
|
||||||
class Fob(ControlPoint):
|
class Fob(ControlPoint):
|
||||||
|
|
||||||
def __init__(self, name: str, at: Point, cp_id: int):
|
def __init__(self, name: str, at: Point, cp_id: int):
|
||||||
import game.theater.conflicttheater
|
import game.theater.conflicttheater
|
||||||
super().__init__(cp_id, name, at, at,
|
|
||||||
game.theater.conflicttheater.SIZE_SMALL, 1,
|
super().__init__(
|
||||||
has_frontline=True, cptype=ControlPointType.FOB)
|
cp_id,
|
||||||
|
name,
|
||||||
|
at,
|
||||||
|
at,
|
||||||
|
game.theater.conflicttheater.SIZE_SMALL,
|
||||||
|
1,
|
||||||
|
has_frontline=True,
|
||||||
|
cptype=ControlPointType.FOB,
|
||||||
|
)
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
def runway_is_operational(self) -> bool:
|
def runway_is_operational(self) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def active_runway(self, conditions: Conditions,
|
def active_runway(
|
||||||
dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
|
self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData]
|
||||||
|
) -> RunwayData:
|
||||||
logging.warning("TODO: FOBs have no runways.")
|
logging.warning("TODO: FOBs have no runways.")
|
||||||
return RunwayData(self.full_name, runway_heading=0, runway_name="")
|
return RunwayData(self.full_name, runway_heading=0, runway_name="")
|
||||||
|
|
||||||
@@ -846,6 +939,7 @@ class Fob(ControlPoint):
|
|||||||
|
|
||||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||||
from gen.flights.flight import FlightType
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
if self.is_friendly(for_player):
|
if self.is_friendly(for_player):
|
||||||
yield from [
|
yield from [
|
||||||
FlightType.BARCAP,
|
FlightType.BARCAP,
|
||||||
|
|||||||
@@ -46,4 +46,3 @@ def poly_centroid(poly) -> Tuple[float, float]:
|
|||||||
x = sum(x_list) / len(poly)
|
x = sum(x_list) / len(poly)
|
||||||
y = sum(y_list) / len(poly)
|
y = sum(y_list) / len(poly)
|
||||||
return (x, y)
|
return (x, y)
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class MissionTarget:
|
|||||||
|
|
||||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||||
from gen.flights.flight import FlightType
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
if self.is_friendly(for_player):
|
if self.is_friendly(for_player):
|
||||||
yield FlightType.BARCAP
|
yield FlightType.BARCAP
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from dcs.vehicles import AirDefence
|
|||||||
|
|
||||||
from game import Game, db
|
from game import Game, db
|
||||||
from game.factions.faction import Faction
|
from game.factions.faction import Faction
|
||||||
from game.theater import Carrier, Lha, LocationType
|
from game.theater import Carrier, Lha, LocationType, PointWithHeading
|
||||||
from game.theater.theatergroundobject import (
|
from game.theater.theatergroundobject import (
|
||||||
BuildingGroundObject,
|
BuildingGroundObject,
|
||||||
CarrierGroundObject,
|
CarrierGroundObject,
|
||||||
@@ -23,9 +23,11 @@ from game.theater.theatergroundobject import (
|
|||||||
SamGroundObject,
|
SamGroundObject,
|
||||||
ShipGroundObject,
|
ShipGroundObject,
|
||||||
VehicleGroupGroundObject,
|
VehicleGroupGroundObject,
|
||||||
|
CoastalSiteGroundObject,
|
||||||
)
|
)
|
||||||
from game.version import VERSION
|
from game.version import VERSION
|
||||||
from gen import namegen
|
from gen import namegen
|
||||||
|
from gen.coastal.coastal_group_generator import generate_coastal_group
|
||||||
from gen.defenses.armor_group_generator import generate_armor_group
|
from gen.defenses.armor_group_generator import generate_armor_group
|
||||||
from gen.fleet.ship_group_generator import (
|
from gen.fleet.ship_group_generator import (
|
||||||
generate_carrier_group,
|
generate_carrier_group,
|
||||||
@@ -35,10 +37,8 @@ from gen.fleet.ship_group_generator import (
|
|||||||
from gen.locations.preset_location_finder import MizDataLocationFinder
|
from gen.locations.preset_location_finder import MizDataLocationFinder
|
||||||
from gen.missiles.missiles_group_generator import generate_missile_group
|
from gen.missiles.missiles_group_generator import generate_missile_group
|
||||||
from gen.sam.airdefensegroupgenerator import AirDefenseRange
|
from gen.sam.airdefensegroupgenerator import AirDefenseRange
|
||||||
from gen.sam.sam_group_generator import (
|
from gen.sam.sam_group_generator import generate_anti_air_group
|
||||||
generate_anti_air_group,
|
from gen.sam.ewr_group_generator import generate_ewr_group
|
||||||
generate_ewr_group,
|
|
||||||
)
|
|
||||||
from . import (
|
from . import (
|
||||||
ConflictTheater,
|
ConflictTheater,
|
||||||
ControlPoint,
|
ControlPoint,
|
||||||
@@ -76,9 +76,14 @@ class GeneratorSettings:
|
|||||||
|
|
||||||
|
|
||||||
class GameGenerator:
|
class GameGenerator:
|
||||||
def __init__(self, player: str, enemy: str, theater: ConflictTheater,
|
def __init__(
|
||||||
settings: Settings,
|
self,
|
||||||
generator_settings: GeneratorSettings) -> None:
|
player: str,
|
||||||
|
enemy: str,
|
||||||
|
theater: ConflictTheater,
|
||||||
|
settings: Settings,
|
||||||
|
generator_settings: GeneratorSettings,
|
||||||
|
) -> None:
|
||||||
self.player = player
|
self.player = player
|
||||||
self.enemy = enemy
|
self.enemy = enemy
|
||||||
self.theater = theater
|
self.theater = theater
|
||||||
@@ -96,7 +101,7 @@ class GameGenerator:
|
|||||||
start_date=self.generator_settings.start_date,
|
start_date=self.generator_settings.start_date,
|
||||||
settings=self.settings,
|
settings=self.settings,
|
||||||
player_budget=self.generator_settings.player_budget,
|
player_budget=self.generator_settings.player_budget,
|
||||||
enemy_budget=self.generator_settings.enemy_budget
|
enemy_budget=self.generator_settings.enemy_budget,
|
||||||
)
|
)
|
||||||
|
|
||||||
GroundObjectGenerator(game, self.generator_settings).generate()
|
GroundObjectGenerator(game, self.generator_settings).generate()
|
||||||
@@ -108,7 +113,7 @@ class GameGenerator:
|
|||||||
# Auto-capture half the bases if midgame.
|
# Auto-capture half the bases if midgame.
|
||||||
if self.generator_settings.midgame:
|
if self.generator_settings.midgame:
|
||||||
control_points = self.theater.controlpoints
|
control_points = self.theater.controlpoints
|
||||||
for control_point in control_points[:len(control_points) // 2]:
|
for control_point in control_points[: len(control_points) // 2]:
|
||||||
control_point.captured = True
|
control_point.captured = True
|
||||||
|
|
||||||
# Remove carrier and lha, invert situation if needed
|
# Remove carrier and lha, invert situation if needed
|
||||||
@@ -140,31 +145,40 @@ class LocationFinder:
|
|||||||
self.game = game
|
self.game = game
|
||||||
self.control_point = control_point
|
self.control_point = control_point
|
||||||
self.miz_data = MizDataLocationFinder.compute_possible_locations(
|
self.miz_data = MizDataLocationFinder.compute_possible_locations(
|
||||||
game.theater.terrain.name, control_point.full_name)
|
game.theater.terrain.name, control_point.full_name
|
||||||
|
)
|
||||||
|
|
||||||
def location_for(self, location_type: LocationType) -> Optional[Point]:
|
def location_for(self, location_type: LocationType) -> Optional[PointWithHeading]:
|
||||||
position = self.control_point.preset_locations.random_for(location_type)
|
position = self.control_point.preset_locations.random_for(location_type)
|
||||||
if position is not None:
|
if position is not None:
|
||||||
return position
|
return position
|
||||||
|
|
||||||
logging.warning(f"No campaign location for %s at %s",
|
logging.warning(
|
||||||
location_type.value, self.control_point)
|
f"No campaign location for %s Mat %s",
|
||||||
|
location_type.value,
|
||||||
|
self.control_point,
|
||||||
|
)
|
||||||
position = self.random_from_miz_data(
|
position = self.random_from_miz_data(
|
||||||
location_type == LocationType.OffshoreStrikeTarget)
|
location_type == LocationType.OffshoreStrikeTarget
|
||||||
|
)
|
||||||
if position is not None:
|
if position is not None:
|
||||||
return position
|
return position
|
||||||
|
|
||||||
logging.debug(f"No mizdata location for %s at %s", location_type.value,
|
logging.debug(
|
||||||
self.control_point)
|
f"No mizdata location for %s at %s", location_type.value, self.control_point
|
||||||
|
)
|
||||||
position = self.random_position(location_type)
|
position = self.random_position(location_type)
|
||||||
if position is not None:
|
if position is not None:
|
||||||
return position
|
return position
|
||||||
|
|
||||||
logging.error(f"Could not find position for %s at %s",
|
logging.error(
|
||||||
location_type.value, self.control_point)
|
f"Could not find position for %s at %s",
|
||||||
|
location_type.value,
|
||||||
|
self.control_point,
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def random_from_miz_data(self, offshore: bool) -> Optional[Point]:
|
def random_from_miz_data(self, offshore: bool) -> Optional[PointWithHeading]:
|
||||||
if offshore:
|
if offshore:
|
||||||
locations = self.miz_data.offshore_locations
|
locations = self.miz_data.offshore_locations
|
||||||
else:
|
else:
|
||||||
@@ -172,13 +186,23 @@ class LocationFinder:
|
|||||||
if self.miz_data.offshore_locations:
|
if self.miz_data.offshore_locations:
|
||||||
preset = random.choice(locations)
|
preset = random.choice(locations)
|
||||||
locations.remove(preset)
|
locations.remove(preset)
|
||||||
return preset.position
|
return PointWithHeading.from_point(preset.position, preset.heading)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def random_position(self, location_type: LocationType) -> Optional[Point]:
|
def random_position(
|
||||||
|
self, location_type: LocationType
|
||||||
|
) -> Optional[PointWithHeading]:
|
||||||
# TODO: Flesh out preset locations so we never hit this case.
|
# TODO: Flesh out preset locations so we never hit this case.
|
||||||
logging.warning("Falling back to random location for %s at %s",
|
|
||||||
location_type.value, self.control_point)
|
if location_type == LocationType.Coastal:
|
||||||
|
# No coastal locations generated randomly
|
||||||
|
return None
|
||||||
|
|
||||||
|
logging.warning(
|
||||||
|
"Falling back to random location for %s at %s",
|
||||||
|
location_type.value,
|
||||||
|
self.control_point,
|
||||||
|
)
|
||||||
|
|
||||||
is_base_defense = location_type in {
|
is_base_defense = location_type in {
|
||||||
LocationType.BaseAirDefense,
|
LocationType.BaseAirDefense,
|
||||||
@@ -212,23 +236,28 @@ class LocationFinder:
|
|||||||
min_range = 10000
|
min_range = 10000
|
||||||
max_range = 40000
|
max_range = 40000
|
||||||
|
|
||||||
position = self._find_random_position(min_range, max_range,
|
position = self._find_random_position(
|
||||||
on_land, is_base_defense,
|
min_range, max_range, on_land, is_base_defense, avoid_others
|
||||||
avoid_others)
|
)
|
||||||
|
|
||||||
# Retry once, searching a bit further (On some big airbases, 3200 is too
|
# Retry once, searching a bit further (On some big airbases, 3200 is too
|
||||||
# short (Ex : Incirlik)), but searching farther on every base would be
|
# short (Ex : Incirlik)), but searching farther on every base would be
|
||||||
# problematic, as some base defense units would end up very far away
|
# problematic, as some base defense units would end up very far away
|
||||||
# from small airfields.
|
# from small airfields.
|
||||||
if position is None and is_base_defense:
|
if position is None and is_base_defense:
|
||||||
position = self._find_random_position(3200, 4800,
|
position = self._find_random_position(
|
||||||
on_land, is_base_defense,
|
3200, 4800, on_land, is_base_defense, avoid_others
|
||||||
avoid_others)
|
)
|
||||||
return position
|
return position
|
||||||
|
|
||||||
def _find_random_position(self, min_range: int, max_range: int,
|
def _find_random_position(
|
||||||
on_ground: bool, is_base_defense: bool,
|
self,
|
||||||
avoid_others: bool) -> Optional[Point]:
|
min_range: int,
|
||||||
|
max_range: int,
|
||||||
|
on_ground: bool,
|
||||||
|
is_base_defense: bool,
|
||||||
|
avoid_others: bool,
|
||||||
|
) -> Optional[PointWithHeading]:
|
||||||
"""
|
"""
|
||||||
Find a valid ground object location
|
Find a valid ground object location
|
||||||
:param on_ground: Whether it should be on ground or on sea (True = on
|
:param on_ground: Whether it should be on ground or on sea (True = on
|
||||||
@@ -241,7 +270,7 @@ class LocationFinder:
|
|||||||
near = self.control_point.position
|
near = self.control_point.position
|
||||||
others = self.control_point.ground_objects
|
others = self.control_point.ground_objects
|
||||||
|
|
||||||
def is_valid(point: Optional[Point]) -> bool:
|
def is_valid(point: Optional[PointWithHeading]) -> bool:
|
||||||
if point is None:
|
if point is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -272,15 +301,21 @@ class LocationFinder:
|
|||||||
|
|
||||||
for _ in range(300):
|
for _ in range(300):
|
||||||
# Check if on land or sea
|
# Check if on land or sea
|
||||||
p = near.random_point_within(max_range, min_range)
|
p = PointWithHeading.from_point(
|
||||||
|
near.random_point_within(max_range, min_range), random.randint(0, 360)
|
||||||
|
)
|
||||||
if is_valid(p):
|
if is_valid(p):
|
||||||
return p
|
return p
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class ControlPointGroundObjectGenerator:
|
class ControlPointGroundObjectGenerator:
|
||||||
def __init__(self, game: Game, generator_settings: GeneratorSettings,
|
def __init__(
|
||||||
control_point: ControlPoint) -> None:
|
self,
|
||||||
|
game: Game,
|
||||||
|
generator_settings: GeneratorSettings,
|
||||||
|
control_point: ControlPoint,
|
||||||
|
) -> None:
|
||||||
self.game = game
|
self.game = game
|
||||||
self.generator_settings = generator_settings
|
self.generator_settings = generator_settings
|
||||||
self.control_point = control_point
|
self.control_point = control_point
|
||||||
@@ -321,15 +356,15 @@ class ControlPointGroundObjectGenerator:
|
|||||||
self.generate_ship()
|
self.generate_ship()
|
||||||
|
|
||||||
def generate_ship(self) -> None:
|
def generate_ship(self) -> None:
|
||||||
point = self.location_finder.location_for(
|
point = self.location_finder.location_for(LocationType.OffshoreStrikeTarget)
|
||||||
LocationType.OffshoreStrikeTarget)
|
|
||||||
if point is None:
|
if point is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
group_id = self.game.next_group_id()
|
group_id = self.game.next_group_id()
|
||||||
|
|
||||||
g = ShipGroundObject(namegen.random_objective_name(), group_id, point,
|
g = ShipGroundObject(
|
||||||
self.control_point)
|
namegen.random_objective_name(), group_id, point, self.control_point
|
||||||
|
)
|
||||||
|
|
||||||
group = generate_ship_group(self.game, g, self.faction_name)
|
group = generate_ship_group(self.game, g, self.faction_name)
|
||||||
g.groups = []
|
g.groups = []
|
||||||
@@ -352,13 +387,15 @@ class CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
|||||||
if not carrier_names:
|
if not carrier_names:
|
||||||
logging.info(
|
logging.info(
|
||||||
f"Skipping generation of {self.control_point.name} because "
|
f"Skipping generation of {self.control_point.name} because "
|
||||||
f"{self.faction_name} has no carriers")
|
f"{self.faction_name} has no carriers"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Create ground object group
|
# Create ground object group
|
||||||
group_id = self.game.next_group_id()
|
group_id = self.game.next_group_id()
|
||||||
g = CarrierGroundObject(namegen.random_objective_name(), group_id,
|
g = CarrierGroundObject(
|
||||||
self.control_point)
|
namegen.random_objective_name(), group_id, self.control_point
|
||||||
|
)
|
||||||
group = generate_carrier_group(self.faction_name, self.game, g)
|
group = generate_carrier_group(self.faction_name, self.game, g)
|
||||||
g.groups = []
|
g.groups = []
|
||||||
if group is not None:
|
if group is not None:
|
||||||
@@ -377,13 +414,15 @@ class LhaGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
|||||||
if not lha_names:
|
if not lha_names:
|
||||||
logging.info(
|
logging.info(
|
||||||
f"Skipping generation of {self.control_point.name} because "
|
f"Skipping generation of {self.control_point.name} because "
|
||||||
f"{self.faction_name} has no LHAs")
|
f"{self.faction_name} has no LHAs"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Create ground object group
|
# Create ground object group
|
||||||
group_id = self.game.next_group_id()
|
group_id = self.game.next_group_id()
|
||||||
g = LhaGroundObject(namegen.random_objective_name(), group_id,
|
g = LhaGroundObject(
|
||||||
self.control_point)
|
namegen.random_objective_name(), group_id, self.control_point
|
||||||
|
)
|
||||||
group = generate_lha_group(self.faction_name, self.game, g)
|
group = generate_lha_group(self.faction_name, self.game, g)
|
||||||
g.groups = []
|
g.groups = []
|
||||||
if group is not None:
|
if group is not None:
|
||||||
@@ -416,14 +455,19 @@ class BaseDefenseGenerator:
|
|||||||
self.generate_base_defenses()
|
self.generate_base_defenses()
|
||||||
|
|
||||||
def generate_ewr(self) -> None:
|
def generate_ewr(self) -> None:
|
||||||
position = self.location_finder.location_for(LocationType.Ewr)
|
position = self.location_finder.location_for(LocationType.BaseEwr)
|
||||||
if position is None:
|
if position is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
group_id = self.game.next_group_id()
|
group_id = self.game.next_group_id()
|
||||||
|
|
||||||
g = EwrGroundObject(namegen.random_objective_name(), group_id,
|
g = EwrGroundObject(
|
||||||
position, self.control_point)
|
namegen.random_objective_name(),
|
||||||
|
group_id,
|
||||||
|
position,
|
||||||
|
self.control_point,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
group = generate_ewr_group(self.game, g, self.faction)
|
group = generate_ewr_group(self.game, g, self.faction)
|
||||||
if group is None:
|
if group is None:
|
||||||
@@ -454,28 +498,35 @@ class BaseDefenseGenerator:
|
|||||||
|
|
||||||
group_id = self.game.next_group_id()
|
group_id = self.game.next_group_id()
|
||||||
|
|
||||||
g = VehicleGroupGroundObject(namegen.random_objective_name(), group_id,
|
g = VehicleGroupGroundObject(
|
||||||
position, self.control_point,
|
namegen.random_objective_name(),
|
||||||
for_airbase=True)
|
group_id,
|
||||||
|
position,
|
||||||
|
self.control_point,
|
||||||
|
for_airbase=True,
|
||||||
|
)
|
||||||
|
|
||||||
group = generate_armor_group(self.faction_name, self.game, g)
|
group = generate_armor_group(self.faction_name, self.game, g)
|
||||||
if group is None:
|
if group is None:
|
||||||
logging.error(
|
logging.error(f"Could not generate garrison at {self.control_point}")
|
||||||
f"Could not generate garrison at {self.control_point}")
|
|
||||||
return
|
return
|
||||||
g.groups.append(group)
|
g.groups.append(group)
|
||||||
self.control_point.base_defenses.append(g)
|
self.control_point.base_defenses.append(g)
|
||||||
|
|
||||||
def generate_sam(self) -> None:
|
def generate_sam(self) -> None:
|
||||||
position = self.location_finder.location_for(
|
position = self.location_finder.location_for(LocationType.BaseAirDefense)
|
||||||
LocationType.BaseAirDefense)
|
|
||||||
if position is None:
|
if position is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
group_id = self.game.next_group_id()
|
group_id = self.game.next_group_id()
|
||||||
|
|
||||||
g = SamGroundObject(namegen.random_objective_name(), group_id,
|
g = SamGroundObject(
|
||||||
position, self.control_point, for_airbase=True)
|
namegen.random_objective_name(),
|
||||||
|
group_id,
|
||||||
|
position,
|
||||||
|
self.control_point,
|
||||||
|
for_airbase=True,
|
||||||
|
)
|
||||||
|
|
||||||
groups = generate_anti_air_group(self.game, g, self.faction)
|
groups = generate_anti_air_group(self.game, g, self.faction)
|
||||||
if not groups:
|
if not groups:
|
||||||
@@ -485,21 +536,25 @@ class BaseDefenseGenerator:
|
|||||||
self.control_point.base_defenses.append(g)
|
self.control_point.base_defenses.append(g)
|
||||||
|
|
||||||
def generate_shorad(self) -> None:
|
def generate_shorad(self) -> None:
|
||||||
position = self.location_finder.location_for(
|
position = self.location_finder.location_for(LocationType.BaseAirDefense)
|
||||||
LocationType.BaseAirDefense)
|
|
||||||
if position is None:
|
if position is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
group_id = self.game.next_group_id()
|
group_id = self.game.next_group_id()
|
||||||
|
|
||||||
g = SamGroundObject(namegen.random_objective_name(), group_id,
|
g = SamGroundObject(
|
||||||
position, self.control_point, for_airbase=True)
|
namegen.random_objective_name(),
|
||||||
|
group_id,
|
||||||
|
position,
|
||||||
|
self.control_point,
|
||||||
|
for_airbase=True,
|
||||||
|
)
|
||||||
|
|
||||||
groups = generate_anti_air_group(self.game, g, self.faction,
|
groups = generate_anti_air_group(
|
||||||
ranges=[{AirDefenseRange.Short}])
|
self.game, g, self.faction, ranges=[{AirDefenseRange.Short}]
|
||||||
|
)
|
||||||
if not groups:
|
if not groups:
|
||||||
logging.error(
|
logging.error(f"Could not generate SHORAD group at {self.control_point}")
|
||||||
f"Could not generate SHORAD group at {self.control_point}")
|
|
||||||
return
|
return
|
||||||
g.groups = groups
|
g.groups = groups
|
||||||
self.control_point.base_defenses.append(g)
|
self.control_point.base_defenses.append(g)
|
||||||
@@ -509,7 +564,7 @@ class FobDefenseGenerator(BaseDefenseGenerator):
|
|||||||
def generate(self) -> None:
|
def generate(self) -> None:
|
||||||
self.generate_garrison()
|
self.generate_garrison()
|
||||||
self.generate_fob_defenses()
|
self.generate_fob_defenses()
|
||||||
|
|
||||||
def generate_fob_defenses(self):
|
def generate_fob_defenses(self):
|
||||||
# First group has a 1/2 chance of being a SHORAD,
|
# First group has a 1/2 chance of being a SHORAD,
|
||||||
# and a 1/2 chance of a garrison.
|
# and a 1/2 chance of a garrison.
|
||||||
@@ -528,9 +583,13 @@ class FobDefenseGenerator(BaseDefenseGenerator):
|
|||||||
|
|
||||||
|
|
||||||
class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||||
def __init__(self, game: Game, generator_settings: GeneratorSettings,
|
def __init__(
|
||||||
control_point: ControlPoint,
|
self,
|
||||||
templates: GroundObjectTemplates) -> None:
|
game: Game,
|
||||||
|
generator_settings: GeneratorSettings,
|
||||||
|
control_point: ControlPoint,
|
||||||
|
templates: GroundObjectTemplates,
|
||||||
|
) -> None:
|
||||||
super().__init__(game, generator_settings, control_point)
|
super().__init__(game, generator_settings, control_point)
|
||||||
self.templates = templates
|
self.templates = templates
|
||||||
|
|
||||||
@@ -544,11 +603,15 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
|||||||
if self.faction.missiles:
|
if self.faction.missiles:
|
||||||
self.generate_missile_sites()
|
self.generate_missile_sites()
|
||||||
|
|
||||||
|
if self.faction.coastal_defenses:
|
||||||
|
self.generate_coastal_sites()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def generate_ground_points(self) -> None:
|
def generate_ground_points(self) -> None:
|
||||||
"""Generate ground objects and AA sites for the control point."""
|
"""Generate ground objects and AA sites for the control point."""
|
||||||
skip_sams = self.generate_required_aa()
|
skip_sams = self.generate_required_aa()
|
||||||
|
skip_ewrs = self.generate_required_ewr()
|
||||||
|
|
||||||
if self.control_point.is_global:
|
if self.control_point.is_global:
|
||||||
return
|
return
|
||||||
@@ -565,6 +628,12 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
|||||||
skip_sams -= 1
|
skip_sams -= 1
|
||||||
else:
|
else:
|
||||||
self.generate_aa_site()
|
self.generate_aa_site()
|
||||||
|
# 1 in 4 additional objectives are EWR.
|
||||||
|
elif random.randint(0, 3) == 0:
|
||||||
|
if skip_ewrs > 0:
|
||||||
|
skip_ewrs -= 1
|
||||||
|
else:
|
||||||
|
self.generate_ewr_site()
|
||||||
else:
|
else:
|
||||||
self.generate_ground_point()
|
self.generate_ground_point()
|
||||||
|
|
||||||
@@ -576,18 +645,36 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
|||||||
"""
|
"""
|
||||||
presets = self.control_point.preset_locations
|
presets = self.control_point.preset_locations
|
||||||
for position in presets.required_long_range_sams:
|
for position in presets.required_long_range_sams:
|
||||||
self.generate_aa_at(position, ranges=[
|
self.generate_aa_at(
|
||||||
{AirDefenseRange.Long},
|
position,
|
||||||
{AirDefenseRange.Medium},
|
ranges=[
|
||||||
{AirDefenseRange.Short},
|
{AirDefenseRange.Long},
|
||||||
])
|
{AirDefenseRange.Medium},
|
||||||
|
{AirDefenseRange.Short},
|
||||||
|
],
|
||||||
|
)
|
||||||
for position in presets.required_medium_range_sams:
|
for position in presets.required_medium_range_sams:
|
||||||
self.generate_aa_at(position, ranges=[
|
self.generate_aa_at(
|
||||||
{AirDefenseRange.Medium},
|
position,
|
||||||
{AirDefenseRange.Short},
|
ranges=[
|
||||||
])
|
{AirDefenseRange.Medium},
|
||||||
return (len(presets.required_long_range_sams) +
|
{AirDefenseRange.Short},
|
||||||
len(presets.required_medium_range_sams))
|
],
|
||||||
|
)
|
||||||
|
return len(presets.required_long_range_sams) + len(
|
||||||
|
presets.required_medium_range_sams
|
||||||
|
)
|
||||||
|
|
||||||
|
def generate_required_ewr(self) -> int:
|
||||||
|
"""Generates the EWR sites that are required by the campaign.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The number of EWR sites that were generated.
|
||||||
|
"""
|
||||||
|
presets = self.control_point.preset_locations
|
||||||
|
for position in presets.required_ewrs:
|
||||||
|
self.generate_ewr_at(position)
|
||||||
|
return len(presets.required_ewrs)
|
||||||
|
|
||||||
def generate_ground_point(self) -> None:
|
def generate_ground_point(self) -> None:
|
||||||
try:
|
try:
|
||||||
@@ -618,8 +705,15 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
|||||||
|
|
||||||
template_point = Point(unit["offset"].x, unit["offset"].y)
|
template_point = Point(unit["offset"].x, unit["offset"].y)
|
||||||
g = BuildingGroundObject(
|
g = BuildingGroundObject(
|
||||||
obj_name, category, group_id, object_id, point + template_point,
|
obj_name,
|
||||||
unit["heading"], self.control_point, unit["type"])
|
category,
|
||||||
|
group_id,
|
||||||
|
object_id,
|
||||||
|
point + template_point,
|
||||||
|
unit["heading"],
|
||||||
|
self.control_point,
|
||||||
|
unit["type"],
|
||||||
|
)
|
||||||
|
|
||||||
self.control_point.connected_objectives.append(g)
|
self.control_point.connected_objectives.append(g)
|
||||||
|
|
||||||
@@ -627,27 +721,65 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
|||||||
position = self.location_finder.location_for(LocationType.Sam)
|
position = self.location_finder.location_for(LocationType.Sam)
|
||||||
if position is None:
|
if position is None:
|
||||||
return
|
return
|
||||||
self.generate_aa_at(position, ranges=[
|
self.generate_aa_at(
|
||||||
# Prefer to use proper SAMs, but fall back to SHORADs if needed.
|
position,
|
||||||
{AirDefenseRange.Long, AirDefenseRange.Medium},
|
ranges=[
|
||||||
{AirDefenseRange.Short},
|
# Prefer to use proper SAMs, but fall back to SHORADs if needed.
|
||||||
])
|
{AirDefenseRange.Long, AirDefenseRange.Medium},
|
||||||
|
{AirDefenseRange.Short},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def generate_aa_at(
|
def generate_aa_at(
|
||||||
self, position: Point,
|
self, position: Point, ranges: Iterable[Set[AirDefenseRange]]
|
||||||
ranges: Iterable[Set[AirDefenseRange]]) -> None:
|
) -> None:
|
||||||
group_id = self.game.next_group_id()
|
group_id = self.game.next_group_id()
|
||||||
|
|
||||||
g = SamGroundObject(namegen.random_objective_name(), group_id,
|
g = SamGroundObject(
|
||||||
position, self.control_point, for_airbase=False)
|
namegen.random_objective_name(),
|
||||||
|
group_id,
|
||||||
|
position,
|
||||||
|
self.control_point,
|
||||||
|
for_airbase=False,
|
||||||
|
)
|
||||||
groups = generate_anti_air_group(self.game, g, self.faction, ranges)
|
groups = generate_anti_air_group(self.game, g, self.faction, ranges)
|
||||||
if not groups:
|
if not groups:
|
||||||
logging.error("Could not generate air defense group for %s at %s",
|
logging.error(
|
||||||
g.name, self.control_point)
|
"Could not generate air defense group for %s at %s",
|
||||||
|
g.name,
|
||||||
|
self.control_point,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
g.groups = groups
|
g.groups = groups
|
||||||
self.control_point.connected_objectives.append(g)
|
self.control_point.connected_objectives.append(g)
|
||||||
|
|
||||||
|
def generate_ewr_site(self) -> None:
|
||||||
|
position = self.location_finder.location_for(LocationType.Ewr)
|
||||||
|
if position is None:
|
||||||
|
return
|
||||||
|
self.generate_ewr_at(position)
|
||||||
|
|
||||||
|
def generate_ewr_at(self, position: Point) -> None:
|
||||||
|
group_id = self.game.next_group_id()
|
||||||
|
|
||||||
|
g = EwrGroundObject(
|
||||||
|
namegen.random_objective_name(),
|
||||||
|
group_id,
|
||||||
|
position,
|
||||||
|
self.control_point,
|
||||||
|
for_airbase=False,
|
||||||
|
)
|
||||||
|
group = generate_ewr_group(self.game, g, self.faction)
|
||||||
|
if group is None:
|
||||||
|
logging.error(
|
||||||
|
"Could not generate ewr group for %s at %s",
|
||||||
|
g.name,
|
||||||
|
self.control_point,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
g.groups = [group]
|
||||||
|
self.control_point.connected_objectives.append(g)
|
||||||
|
|
||||||
def generate_missile_sites(self) -> None:
|
def generate_missile_sites(self) -> None:
|
||||||
for i in range(self.faction.missiles_group_count):
|
for i in range(self.faction.missiles_group_count):
|
||||||
self.generate_missile_site()
|
self.generate_missile_site()
|
||||||
@@ -659,8 +791,9 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
|||||||
|
|
||||||
group_id = self.game.next_group_id()
|
group_id = self.game.next_group_id()
|
||||||
|
|
||||||
g = MissileSiteGroundObject(namegen.random_objective_name(), group_id,
|
g = MissileSiteGroundObject(
|
||||||
position, self.control_point)
|
namegen.random_objective_name(), group_id, position, self.control_point
|
||||||
|
)
|
||||||
group = generate_missile_group(self.game, g, self.faction_name)
|
group = generate_missile_group(self.game, g, self.faction_name)
|
||||||
g.groups = []
|
g.groups = []
|
||||||
if group is not None:
|
if group is not None:
|
||||||
@@ -668,6 +801,31 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
|||||||
self.control_point.connected_objectives.append(g)
|
self.control_point.connected_objectives.append(g)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def generate_coastal_sites(self) -> None:
|
||||||
|
for i in range(self.faction.coastal_group_count):
|
||||||
|
self.generate_coastal_site()
|
||||||
|
|
||||||
|
def generate_coastal_site(self) -> None:
|
||||||
|
position = self.location_finder.location_for(LocationType.Coastal)
|
||||||
|
if position is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
group_id = self.game.next_group_id()
|
||||||
|
|
||||||
|
g = CoastalSiteGroundObject(
|
||||||
|
namegen.random_objective_name(),
|
||||||
|
group_id,
|
||||||
|
position,
|
||||||
|
self.control_point,
|
||||||
|
position.heading,
|
||||||
|
)
|
||||||
|
group = generate_coastal_group(self.game, g, self.faction_name)
|
||||||
|
g.groups = []
|
||||||
|
if group is not None:
|
||||||
|
g.groups.append(group)
|
||||||
|
self.control_point.connected_objectives.append(g)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
|
class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
|
||||||
def generate(self) -> bool:
|
def generate(self) -> bool:
|
||||||
@@ -678,7 +836,7 @@ class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
|
|||||||
|
|
||||||
def generate_fob(self) -> None:
|
def generate_fob(self) -> None:
|
||||||
try:
|
try:
|
||||||
category = self.faction.building_set[self.faction.building_set.index('fob')]
|
category = self.faction.building_set[self.faction.building_set.index("fob")]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
logging.exception("Faction has no fob buildings defined")
|
logging.exception("Faction has no fob buildings defined")
|
||||||
return
|
return
|
||||||
@@ -696,14 +854,21 @@ class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
|
|||||||
|
|
||||||
template_point = Point(unit["offset"].x, unit["offset"].y)
|
template_point = Point(unit["offset"].x, unit["offset"].y)
|
||||||
g = BuildingGroundObject(
|
g = BuildingGroundObject(
|
||||||
obj_name, category, group_id, object_id, point + template_point,
|
obj_name,
|
||||||
unit["heading"], self.control_point, unit["type"], airbase_group=True)
|
category,
|
||||||
|
group_id,
|
||||||
|
object_id,
|
||||||
|
point + template_point,
|
||||||
|
unit["heading"],
|
||||||
|
self.control_point,
|
||||||
|
unit["type"],
|
||||||
|
airbase_group=True,
|
||||||
|
)
|
||||||
self.control_point.connected_objectives.append(g)
|
self.control_point.connected_objectives.append(g)
|
||||||
|
|
||||||
|
|
||||||
class GroundObjectGenerator:
|
class GroundObjectGenerator:
|
||||||
def __init__(self, game: Game,
|
def __init__(self, game: Game, generator_settings: GeneratorSettings) -> None:
|
||||||
generator_settings: GeneratorSettings) -> None:
|
|
||||||
self.game = game
|
self.game = game
|
||||||
self.generator_settings = generator_settings
|
self.generator_settings = generator_settings
|
||||||
with open("resources/groundobject_templates.p", "rb") as f:
|
with open("resources/groundobject_templates.p", "rb") as f:
|
||||||
@@ -721,19 +886,22 @@ class GroundObjectGenerator:
|
|||||||
generator: ControlPointGroundObjectGenerator
|
generator: ControlPointGroundObjectGenerator
|
||||||
if control_point.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP:
|
if control_point.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP:
|
||||||
generator = CarrierGroundObjectGenerator(
|
generator = CarrierGroundObjectGenerator(
|
||||||
self.game, self.generator_settings, control_point)
|
self.game, self.generator_settings, control_point
|
||||||
|
)
|
||||||
elif control_point.cptype == ControlPointType.LHA_GROUP:
|
elif control_point.cptype == ControlPointType.LHA_GROUP:
|
||||||
generator = LhaGroundObjectGenerator(
|
generator = LhaGroundObjectGenerator(
|
||||||
self.game, self.generator_settings, control_point)
|
self.game, self.generator_settings, control_point
|
||||||
|
)
|
||||||
elif isinstance(control_point, OffMapSpawn):
|
elif isinstance(control_point, OffMapSpawn):
|
||||||
generator = NoOpGroundObjectGenerator(
|
generator = NoOpGroundObjectGenerator(
|
||||||
self.game, self.generator_settings, control_point)
|
self.game, self.generator_settings, control_point
|
||||||
|
)
|
||||||
elif isinstance(control_point, Fob):
|
elif isinstance(control_point, Fob):
|
||||||
generator = FobGroundObjectGenerator(
|
generator = FobGroundObjectGenerator(
|
||||||
self.game, self.generator_settings, control_point,
|
self.game, self.generator_settings, control_point, self.templates
|
||||||
self.templates)
|
)
|
||||||
else:
|
else:
|
||||||
generator = AirbaseGroundObjectGenerator(
|
generator = AirbaseGroundObjectGenerator(
|
||||||
self.game, self.generator_settings, control_point,
|
self.game, self.generator_settings, control_point, self.templates
|
||||||
self.templates)
|
)
|
||||||
return generator.generate()
|
return generator.generate()
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ NAME_BY_CATEGORY = {
|
|||||||
"ww2bunker": "Bunker",
|
"ww2bunker": "Bunker",
|
||||||
"village": "Village",
|
"village": "Village",
|
||||||
"allycamp": "Camp",
|
"allycamp": "Camp",
|
||||||
"EWR":"EWR",
|
"EWR": "EWR",
|
||||||
}
|
}
|
||||||
|
|
||||||
ABBREV_NAME = {
|
ABBREV_NAME = {
|
||||||
@@ -54,34 +54,59 @@ ABBREV_NAME = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CATEGORY_MAP = {
|
CATEGORY_MAP = {
|
||||||
|
|
||||||
# Special cases
|
# Special cases
|
||||||
"CARRIER": ["CARRIER"],
|
"CARRIER": ["CARRIER"],
|
||||||
"LHA": ["LHA"],
|
"LHA": ["LHA"],
|
||||||
"aa": ["AA"],
|
"aa": ["AA"],
|
||||||
|
|
||||||
# Buildings
|
# Buildings
|
||||||
"power": ["Workshop A", "Electric power box", "Garage small A", "Farm B", "Repair workshop", "Garage B"],
|
"power": [
|
||||||
|
"Workshop A",
|
||||||
|
"Electric power box",
|
||||||
|
"Garage small A",
|
||||||
|
"Farm B",
|
||||||
|
"Repair workshop",
|
||||||
|
"Garage B",
|
||||||
|
],
|
||||||
"ware": ["Warehouse", "Hangar A"],
|
"ware": ["Warehouse", "Hangar A"],
|
||||||
"fuel": ["Tank", "Tank 2", "Tank 3", "Fuel tank"],
|
"fuel": ["Tank", "Tank 2", "Tank 3", "Fuel tank"],
|
||||||
"ammo": [".Ammunition depot", "Hangar B"],
|
"ammo": [".Ammunition depot", "Hangar B"],
|
||||||
"farp": ["FARP Tent", "FARP Ammo Dump Coating", "FARP Fuel Depot", "FARP Command Post", "FARP CP Blindage"],
|
"farp": [
|
||||||
|
"FARP Tent",
|
||||||
|
"FARP Ammo Dump Coating",
|
||||||
|
"FARP Fuel Depot",
|
||||||
|
"FARP Command Post",
|
||||||
|
"FARP CP Blindage",
|
||||||
|
],
|
||||||
"fob": ["Bunker 2", "Bunker 1", "Garage small B", ".Command Center", "Barracks 2"],
|
"fob": ["Bunker 2", "Bunker 1", "Garage small B", ".Command Center", "Barracks 2"],
|
||||||
"factory": ["Tech combine", "Tech hangar A"],
|
"factory": ["Tech combine", "Tech hangar A"],
|
||||||
"comms": ["TV tower", "Comms tower M"],
|
"comms": ["TV tower", "Comms tower M"],
|
||||||
"oil": ["Oil platform"],
|
"oil": ["Oil platform"],
|
||||||
"derrick": ["Oil derrick", "Pump station", "Subsidiary structure 2"],
|
"derrick": ["Oil derrick", "Pump station", "Subsidiary structure 2"],
|
||||||
"ww2bunker": ["Siegfried Line", "Fire Control Bunker", "SK_C_28_naval_gun", "Concertina Wire", "Czech hedgehogs 1"],
|
"ww2bunker": [
|
||||||
|
"Siegfried Line",
|
||||||
|
"Fire Control Bunker",
|
||||||
|
"SK_C_28_naval_gun",
|
||||||
|
"Concertina Wire",
|
||||||
|
"Czech hedgehogs 1",
|
||||||
|
],
|
||||||
"village": ["Small house 1B", "Small House 1A", "Small warehouse 1"],
|
"village": ["Small house 1B", "Small House 1A", "Small warehouse 1"],
|
||||||
"allycamp": [],
|
"allycamp": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class TheaterGroundObject(MissionTarget):
|
class TheaterGroundObject(MissionTarget):
|
||||||
|
def __init__(
|
||||||
def __init__(self, name: str, category: str, group_id: int, position: Point,
|
self,
|
||||||
heading: int, control_point: ControlPoint, dcs_identifier: str,
|
name: str,
|
||||||
airbase_group: bool, sea_object: bool) -> None:
|
category: str,
|
||||||
|
group_id: int,
|
||||||
|
position: Point,
|
||||||
|
heading: int,
|
||||||
|
control_point: ControlPoint,
|
||||||
|
dcs_identifier: str,
|
||||||
|
airbase_group: bool,
|
||||||
|
sea_object: bool,
|
||||||
|
) -> None:
|
||||||
super().__init__(name, position)
|
super().__init__(name, position)
|
||||||
self.category = category
|
self.category = category
|
||||||
self.group_id = group_id
|
self.group_id = group_id
|
||||||
@@ -131,6 +156,7 @@ class TheaterGroundObject(MissionTarget):
|
|||||||
|
|
||||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||||
from gen.flights.flight import FlightType
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
if self.is_friendly(for_player):
|
if self.is_friendly(for_player):
|
||||||
yield from [
|
yield from [
|
||||||
# TODO: FlightType.LOGISTICS
|
# TODO: FlightType.LOGISTICS
|
||||||
@@ -193,9 +219,18 @@ class TheaterGroundObject(MissionTarget):
|
|||||||
|
|
||||||
|
|
||||||
class BuildingGroundObject(TheaterGroundObject):
|
class BuildingGroundObject(TheaterGroundObject):
|
||||||
def __init__(self, name: str, category: str, group_id: int, object_id: int,
|
def __init__(
|
||||||
position: Point, heading: int, control_point: ControlPoint,
|
self,
|
||||||
dcs_identifier: str, airbase_group=False) -> None:
|
name: str,
|
||||||
|
category: str,
|
||||||
|
group_id: int,
|
||||||
|
object_id: int,
|
||||||
|
position: Point,
|
||||||
|
heading: int,
|
||||||
|
control_point: ControlPoint,
|
||||||
|
dcs_identifier: str,
|
||||||
|
airbase_group=False,
|
||||||
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
name=name,
|
name=name,
|
||||||
category=category,
|
category=category,
|
||||||
@@ -205,7 +240,7 @@ class BuildingGroundObject(TheaterGroundObject):
|
|||||||
control_point=control_point,
|
control_point=control_point,
|
||||||
dcs_identifier=dcs_identifier,
|
dcs_identifier=dcs_identifier,
|
||||||
airbase_group=airbase_group,
|
airbase_group=airbase_group,
|
||||||
sea_object=False
|
sea_object=False,
|
||||||
)
|
)
|
||||||
self.object_id = object_id
|
self.object_id = object_id
|
||||||
# Other TGOs track deadness based on the number of alive units, but
|
# Other TGOs track deadness based on the number of alive units, but
|
||||||
@@ -234,6 +269,7 @@ class BuildingGroundObject(TheaterGroundObject):
|
|||||||
class NavalGroundObject(TheaterGroundObject):
|
class NavalGroundObject(TheaterGroundObject):
|
||||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||||
from gen.flights.flight import FlightType
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
if not self.is_friendly(for_player):
|
if not self.is_friendly(for_player):
|
||||||
yield FlightType.ANTISHIP
|
yield FlightType.ANTISHIP
|
||||||
yield from super().mission_types(for_player)
|
yield from super().mission_types(for_player)
|
||||||
@@ -249,8 +285,7 @@ class GenericCarrierGroundObject(NavalGroundObject):
|
|||||||
|
|
||||||
# TODO: Why is this both a CP and a TGO?
|
# TODO: Why is this both a CP and a TGO?
|
||||||
class CarrierGroundObject(GenericCarrierGroundObject):
|
class CarrierGroundObject(GenericCarrierGroundObject):
|
||||||
def __init__(self, name: str, group_id: int,
|
def __init__(self, name: str, group_id: int, control_point: ControlPoint) -> None:
|
||||||
control_point: ControlPoint) -> None:
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
name=name,
|
name=name,
|
||||||
category="CARRIER",
|
category="CARRIER",
|
||||||
@@ -260,7 +295,7 @@ class CarrierGroundObject(GenericCarrierGroundObject):
|
|||||||
control_point=control_point,
|
control_point=control_point,
|
||||||
dcs_identifier="CARRIER",
|
dcs_identifier="CARRIER",
|
||||||
airbase_group=True,
|
airbase_group=True,
|
||||||
sea_object=True
|
sea_object=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -272,8 +307,7 @@ class CarrierGroundObject(GenericCarrierGroundObject):
|
|||||||
|
|
||||||
# TODO: Why is this both a CP and a TGO?
|
# TODO: Why is this both a CP and a TGO?
|
||||||
class LhaGroundObject(GenericCarrierGroundObject):
|
class LhaGroundObject(GenericCarrierGroundObject):
|
||||||
def __init__(self, name: str, group_id: int,
|
def __init__(self, name: str, group_id: int, control_point: ControlPoint) -> None:
|
||||||
control_point: ControlPoint) -> None:
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
name=name,
|
name=name,
|
||||||
category="LHA",
|
category="LHA",
|
||||||
@@ -283,7 +317,7 @@ class LhaGroundObject(GenericCarrierGroundObject):
|
|||||||
control_point=control_point,
|
control_point=control_point,
|
||||||
dcs_identifier="LHA",
|
dcs_identifier="LHA",
|
||||||
airbase_group=True,
|
airbase_group=True,
|
||||||
sea_object=True
|
sea_object=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -294,8 +328,9 @@ class LhaGroundObject(GenericCarrierGroundObject):
|
|||||||
|
|
||||||
|
|
||||||
class MissileSiteGroundObject(TheaterGroundObject):
|
class MissileSiteGroundObject(TheaterGroundObject):
|
||||||
def __init__(self, name: str, group_id: int, position: Point,
|
def __init__(
|
||||||
control_point: ControlPoint) -> None:
|
self, name: str, group_id: int, position: Point, control_point: ControlPoint
|
||||||
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
name=name,
|
name=name,
|
||||||
category="aa",
|
category="aa",
|
||||||
@@ -305,7 +340,29 @@ class MissileSiteGroundObject(TheaterGroundObject):
|
|||||||
control_point=control_point,
|
control_point=control_point,
|
||||||
dcs_identifier="AA",
|
dcs_identifier="AA",
|
||||||
airbase_group=False,
|
airbase_group=False,
|
||||||
sea_object=False
|
sea_object=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CoastalSiteGroundObject(TheaterGroundObject):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
group_id: int,
|
||||||
|
position: Point,
|
||||||
|
control_point: ControlPoint,
|
||||||
|
heading,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
category="aa",
|
||||||
|
group_id=group_id,
|
||||||
|
position=position,
|
||||||
|
heading=heading,
|
||||||
|
control_point=control_point,
|
||||||
|
dcs_identifier="AA",
|
||||||
|
airbase_group=False,
|
||||||
|
sea_object=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -317,8 +374,14 @@ class BaseDefenseGroundObject(TheaterGroundObject):
|
|||||||
# This type gets used both for AA sites (SAM, AAA, or SHORAD). These should each
|
# This type gets used both for AA sites (SAM, AAA, or SHORAD). These should each
|
||||||
# be split into their own types.
|
# be split into their own types.
|
||||||
class SamGroundObject(BaseDefenseGroundObject):
|
class SamGroundObject(BaseDefenseGroundObject):
|
||||||
def __init__(self, name: str, group_id: int, position: Point,
|
def __init__(
|
||||||
control_point: ControlPoint, for_airbase: bool) -> None:
|
self,
|
||||||
|
name: str,
|
||||||
|
group_id: int,
|
||||||
|
position: Point,
|
||||||
|
control_point: ControlPoint,
|
||||||
|
for_airbase: bool,
|
||||||
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
name=name,
|
name=name,
|
||||||
category="aa",
|
category="aa",
|
||||||
@@ -328,7 +391,7 @@ class SamGroundObject(BaseDefenseGroundObject):
|
|||||||
control_point=control_point,
|
control_point=control_point,
|
||||||
dcs_identifier="AA",
|
dcs_identifier="AA",
|
||||||
airbase_group=for_airbase,
|
airbase_group=for_airbase,
|
||||||
sea_object=False
|
sea_object=False,
|
||||||
)
|
)
|
||||||
# Set by the SAM unit generator if the generated group is compatible
|
# Set by the SAM unit generator if the generated group is compatible
|
||||||
# with Skynet.
|
# with Skynet.
|
||||||
@@ -345,6 +408,7 @@ class SamGroundObject(BaseDefenseGroundObject):
|
|||||||
|
|
||||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||||
from gen.flights.flight import FlightType
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
if not self.is_friendly(for_player):
|
if not self.is_friendly(for_player):
|
||||||
yield FlightType.DEAD
|
yield FlightType.DEAD
|
||||||
yield from super().mission_types(for_player)
|
yield from super().mission_types(for_player)
|
||||||
@@ -355,8 +419,14 @@ class SamGroundObject(BaseDefenseGroundObject):
|
|||||||
|
|
||||||
|
|
||||||
class VehicleGroupGroundObject(BaseDefenseGroundObject):
|
class VehicleGroupGroundObject(BaseDefenseGroundObject):
|
||||||
def __init__(self, name: str, group_id: int, position: Point,
|
def __init__(
|
||||||
control_point: ControlPoint, for_airbase: bool) -> None:
|
self,
|
||||||
|
name: str,
|
||||||
|
group_id: int,
|
||||||
|
position: Point,
|
||||||
|
control_point: ControlPoint,
|
||||||
|
for_airbase: bool,
|
||||||
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
name=name,
|
name=name,
|
||||||
category="aa",
|
category="aa",
|
||||||
@@ -366,13 +436,19 @@ class VehicleGroupGroundObject(BaseDefenseGroundObject):
|
|||||||
control_point=control_point,
|
control_point=control_point,
|
||||||
dcs_identifier="AA",
|
dcs_identifier="AA",
|
||||||
airbase_group=for_airbase,
|
airbase_group=for_airbase,
|
||||||
sea_object=False
|
sea_object=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class EwrGroundObject(BaseDefenseGroundObject):
|
class EwrGroundObject(BaseDefenseGroundObject):
|
||||||
def __init__(self, name: str, group_id: int, position: Point,
|
def __init__(
|
||||||
control_point: ControlPoint) -> None:
|
self,
|
||||||
|
name: str,
|
||||||
|
group_id: int,
|
||||||
|
position: Point,
|
||||||
|
control_point: ControlPoint,
|
||||||
|
for_airbase: bool,
|
||||||
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
name=name,
|
name=name,
|
||||||
category="EWR",
|
category="EWR",
|
||||||
@@ -381,8 +457,8 @@ class EwrGroundObject(BaseDefenseGroundObject):
|
|||||||
heading=0,
|
heading=0,
|
||||||
control_point=control_point,
|
control_point=control_point,
|
||||||
dcs_identifier="EWR",
|
dcs_identifier="EWR",
|
||||||
airbase_group=True,
|
airbase_group=for_airbase,
|
||||||
sea_object=False
|
sea_object=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -392,6 +468,7 @@ class EwrGroundObject(BaseDefenseGroundObject):
|
|||||||
|
|
||||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||||
from gen.flights.flight import FlightType
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
if not self.is_friendly(for_player):
|
if not self.is_friendly(for_player):
|
||||||
yield FlightType.DEAD
|
yield FlightType.DEAD
|
||||||
yield from super().mission_types(for_player)
|
yield from super().mission_types(for_player)
|
||||||
@@ -402,8 +479,9 @@ class EwrGroundObject(BaseDefenseGroundObject):
|
|||||||
|
|
||||||
|
|
||||||
class ShipGroundObject(NavalGroundObject):
|
class ShipGroundObject(NavalGroundObject):
|
||||||
def __init__(self, name: str, group_id: int, position: Point,
|
def __init__(
|
||||||
control_point: ControlPoint) -> None:
|
self, name: str, group_id: int, position: Point, control_point: ControlPoint
|
||||||
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
name=name,
|
name=name,
|
||||||
category="aa",
|
category="aa",
|
||||||
@@ -413,7 +491,7 @@ class ShipGroundObject(NavalGroundObject):
|
|||||||
control_point=control_point,
|
control_point=control_point,
|
||||||
dcs_identifier="AA",
|
dcs_identifier="AA",
|
||||||
airbase_group=False,
|
airbase_group=False,
|
||||||
sea_object=True
|
sea_object=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from shapely.ops import nearest_points, unary_union
|
|||||||
|
|
||||||
from game.theater import ControlPoint
|
from game.theater import ControlPoint
|
||||||
from game.utils import Distance, meters, nautical_miles
|
from game.utils import Distance, meters, nautical_miles
|
||||||
|
from gen import Conflict
|
||||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||||
from gen.flights.flight import Flight
|
from gen.flights.flight import Flight
|
||||||
|
|
||||||
@@ -32,8 +33,9 @@ class ThreatZones:
|
|||||||
self.all = unary_union([airbases, air_defenses])
|
self.all = unary_union([airbases, air_defenses])
|
||||||
|
|
||||||
def closest_boundary(self, point: DcsPoint) -> DcsPoint:
|
def closest_boundary(self, point: DcsPoint) -> DcsPoint:
|
||||||
boundary, _ = nearest_points(self.all.boundary,
|
boundary, _ = nearest_points(
|
||||||
self.dcs_to_shapely_point(point))
|
self.all.boundary, self.dcs_to_shapely_point(point)
|
||||||
|
)
|
||||||
return DcsPoint(boundary.x, boundary.y)
|
return DcsPoint(boundary.x, boundary.y)
|
||||||
|
|
||||||
@singledispatchmethod
|
@singledispatchmethod
|
||||||
@@ -49,8 +51,9 @@ class ThreatZones:
|
|||||||
return self.all.intersects(self.dcs_to_shapely_point(position))
|
return self.all.intersects(self.dcs_to_shapely_point(position))
|
||||||
|
|
||||||
def path_threatened(self, a: DcsPoint, b: DcsPoint) -> bool:
|
def path_threatened(self, a: DcsPoint, b: DcsPoint) -> bool:
|
||||||
return self.threatened(LineString(
|
return self.threatened(
|
||||||
[self.dcs_to_shapely_point(a), self.dcs_to_shapely_point(b)]))
|
LineString([self.dcs_to_shapely_point(a), self.dcs_to_shapely_point(b)])
|
||||||
|
)
|
||||||
|
|
||||||
@singledispatchmethod
|
@singledispatchmethod
|
||||||
def threatened_by_aircraft(self, target) -> bool:
|
def threatened_by_aircraft(self, target) -> bool:
|
||||||
@@ -62,9 +65,9 @@ class ThreatZones:
|
|||||||
|
|
||||||
@threatened_by_aircraft.register
|
@threatened_by_aircraft.register
|
||||||
def _threatened_by_aircraft_flight(self, flight: Flight) -> bool:
|
def _threatened_by_aircraft_flight(self, flight: Flight) -> bool:
|
||||||
return self.threatened_by_aircraft(LineString((
|
return self.threatened_by_aircraft(
|
||||||
self.dcs_to_shapely_point(p.position) for p in flight.points
|
LineString((self.dcs_to_shapely_point(p.position) for p in flight.points))
|
||||||
)))
|
)
|
||||||
|
|
||||||
@singledispatchmethod
|
@singledispatchmethod
|
||||||
def threatened_by_air_defense(self, target) -> bool:
|
def threatened_by_air_defense(self, target) -> bool:
|
||||||
@@ -76,13 +79,14 @@ class ThreatZones:
|
|||||||
|
|
||||||
@threatened_by_air_defense.register
|
@threatened_by_air_defense.register
|
||||||
def _threatened_by_air_defense_flight(self, flight: Flight) -> bool:
|
def _threatened_by_air_defense_flight(self, flight: Flight) -> bool:
|
||||||
return self.threatened_by_air_defense(LineString((
|
return self.threatened_by_air_defense(
|
||||||
self.dcs_to_shapely_point(p.position) for p in flight.points
|
LineString((self.dcs_to_shapely_point(p.position) for p in flight.points))
|
||||||
)))
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def closest_enemy_airbase(cls, location: ControlPoint,
|
def closest_enemy_airbase(
|
||||||
max_distance: Distance) -> Optional[ControlPoint]:
|
cls, location: ControlPoint, max_distance: Distance
|
||||||
|
) -> Optional[ControlPoint]:
|
||||||
airfields = ObjectiveDistanceCache.get_closest_airfields(location)
|
airfields = ObjectiveDistanceCache.get_closest_airfields(location)
|
||||||
for airfield in airfields.airfields_within(max_distance):
|
for airfield in airfields.airfields_within(max_distance):
|
||||||
if airfield.captured != location.captured:
|
if airfield.captured != location.captured:
|
||||||
@@ -90,13 +94,14 @@ class ThreatZones:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def barcap_threat_range(cls, game: Game,
|
def barcap_threat_range(cls, game: Game, control_point: ControlPoint) -> Distance:
|
||||||
control_point: ControlPoint) -> Distance:
|
|
||||||
doctrine = game.faction_for(control_point.captured).doctrine
|
doctrine = game.faction_for(control_point.captured).doctrine
|
||||||
cap_threat_range = (doctrine.cap_max_distance_from_cp +
|
cap_threat_range = (
|
||||||
doctrine.cap_engagement_range)
|
doctrine.cap_max_distance_from_cp + doctrine.cap_engagement_range
|
||||||
opposing_airfield = cls.closest_enemy_airbase(control_point,
|
)
|
||||||
cap_threat_range * 2)
|
opposing_airfield = cls.closest_enemy_airbase(
|
||||||
|
control_point, cap_threat_range * 2
|
||||||
|
)
|
||||||
if opposing_airfield is None:
|
if opposing_airfield is None:
|
||||||
return cap_threat_range
|
return cap_threat_range
|
||||||
|
|
||||||
@@ -127,16 +132,15 @@ class ThreatZones:
|
|||||||
zone belongs to the player, it is the zone that will be avoided by
|
zone belongs to the player, it is the zone that will be avoided by
|
||||||
the enemy and vice versa.
|
the enemy and vice versa.
|
||||||
"""
|
"""
|
||||||
airbases = []
|
air_threats = []
|
||||||
air_defenses = []
|
air_defenses = []
|
||||||
for control_point in game.theater.controlpoints:
|
for control_point in game.theater.controlpoints:
|
||||||
if control_point.captured != player:
|
if control_point.captured != player:
|
||||||
continue
|
continue
|
||||||
if control_point.runway_is_operational():
|
if control_point.runway_is_operational():
|
||||||
point = ShapelyPoint(control_point.position.x,
|
point = ShapelyPoint(control_point.position.x, control_point.position.y)
|
||||||
control_point.position.y)
|
|
||||||
cap_threat_range = cls.barcap_threat_range(game, control_point)
|
cap_threat_range = cls.barcap_threat_range(game, control_point)
|
||||||
airbases.append(point.buffer(cap_threat_range.meters))
|
air_threats.append(point.buffer(cap_threat_range.meters))
|
||||||
|
|
||||||
for tgo in control_point.ground_objects:
|
for tgo in control_point.ground_objects:
|
||||||
for group in tgo.groups:
|
for group in tgo.groups:
|
||||||
@@ -149,10 +153,9 @@ class ThreatZones:
|
|||||||
air_defenses.append(threat_zone)
|
air_defenses.append(threat_zone)
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
airbases=unary_union(airbases),
|
airbases=unary_union(air_threats), air_defenses=unary_union(air_defenses)
|
||||||
air_defenses=unary_union(air_defenses)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def dcs_to_shapely_point(point: DcsPoint) -> ShapelyPoint:
|
def dcs_to_shapely_point(point: DcsPoint) -> ShapelyPoint:
|
||||||
return ShapelyPoint(point.x, point.y)
|
return ShapelyPoint(point.x, point.y)
|
||||||
|
|||||||
@@ -70,15 +70,19 @@ class UnitMap:
|
|||||||
raise RuntimeError(f"Unknown unit type: {unit.type}")
|
raise RuntimeError(f"Unknown unit type: {unit.type}")
|
||||||
if not issubclass(unit_type, VehicleType):
|
if not issubclass(unit_type, VehicleType):
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"{name} is a {unit_type.__name__}, expected a VehicleType")
|
f"{name} is a {unit_type.__name__}, expected a VehicleType"
|
||||||
|
)
|
||||||
self.front_line_units[name] = FrontLineUnit(unit_type, origin)
|
self.front_line_units[name] = FrontLineUnit(unit_type, origin)
|
||||||
|
|
||||||
def front_line_unit(self, name: str) -> Optional[FrontLineUnit]:
|
def front_line_unit(self, name: str) -> Optional[FrontLineUnit]:
|
||||||
return self.front_line_units.get(name, None)
|
return self.front_line_units.get(name, None)
|
||||||
|
|
||||||
def add_ground_object_units(self, ground_object: TheaterGroundObject,
|
def add_ground_object_units(
|
||||||
persistence_group: Group,
|
self,
|
||||||
miz_group: Group) -> None:
|
ground_object: TheaterGroundObject,
|
||||||
|
persistence_group: Group,
|
||||||
|
miz_group: Group,
|
||||||
|
) -> None:
|
||||||
"""Adds a group associated with a TGO to the unit map.
|
"""Adds a group associated with a TGO to the unit map.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -103,13 +107,13 @@ class UnitMap:
|
|||||||
if name in self.ground_object_units:
|
if name in self.ground_object_units:
|
||||||
raise RuntimeError(f"Duplicate TGO unit: {name}")
|
raise RuntimeError(f"Duplicate TGO unit: {name}")
|
||||||
self.ground_object_units[name] = GroundObjectUnit(
|
self.ground_object_units[name] = GroundObjectUnit(
|
||||||
ground_object, persistence_group, persistent_unit)
|
ground_object, persistence_group, persistent_unit
|
||||||
|
)
|
||||||
|
|
||||||
def ground_object_unit(self, name: str) -> Optional[GroundObjectUnit]:
|
def ground_object_unit(self, name: str) -> Optional[GroundObjectUnit]:
|
||||||
return self.ground_object_units.get(name, None)
|
return self.ground_object_units.get(name, None)
|
||||||
|
|
||||||
def add_building(self, ground_object: BuildingGroundObject,
|
def add_building(self, ground_object: BuildingGroundObject, group: Group) -> None:
|
||||||
group: Group) -> None:
|
|
||||||
# The actual name is a String (the pydcs translatable string), which
|
# The actual name is a String (the pydcs translatable string), which
|
||||||
# doesn't define __eq__.
|
# doesn't define __eq__.
|
||||||
# The name of the initiator in the DCS dead event will have " object"
|
# The name of the initiator in the DCS dead event will have " object"
|
||||||
@@ -119,8 +123,9 @@ class UnitMap:
|
|||||||
raise RuntimeError(f"Duplicate TGO unit: {name}")
|
raise RuntimeError(f"Duplicate TGO unit: {name}")
|
||||||
self.buildings[name] = Building(ground_object)
|
self.buildings[name] = Building(ground_object)
|
||||||
|
|
||||||
def add_fortification(self, ground_object: BuildingGroundObject,
|
def add_fortification(
|
||||||
group: VehicleGroup) -> None:
|
self, ground_object: BuildingGroundObject, group: VehicleGroup
|
||||||
|
) -> None:
|
||||||
if len(group.units) != 1:
|
if len(group.units) != 1:
|
||||||
raise ValueError("Fortification groups must have exactly one unit.")
|
raise ValueError("Fortification groups must have exactly one unit.")
|
||||||
unit = group.units[0]
|
unit = group.units[0]
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
|
|
||||||
def _build_version_string() -> str:
|
def _build_version_string() -> str:
|
||||||
components = ["2.4"]
|
components = ["2.5"]
|
||||||
build_number_path = Path("resources/buildnumber")
|
build_number_path = Path("resources/buildnumber")
|
||||||
if build_number_path.exists():
|
if build_number_path.exists():
|
||||||
with build_number_path.open("r") as build_number_file:
|
with build_number_path.open("r") as build_number_file:
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ from __future__ import annotations
|
|||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional, TYPE_CHECKING
|
from typing import Optional, TYPE_CHECKING
|
||||||
|
|
||||||
from dcs.weather import Weather as PydcsWeather, Wind
|
from dcs.cloud_presets import Clouds as PydcsClouds
|
||||||
|
from dcs.weather import CloudPreset, Weather as PydcsWeather, Wind
|
||||||
|
|
||||||
from game.settings import Settings
|
from game.settings import Settings
|
||||||
from game.utils import Distance, meters
|
from game.utils import Distance, meters
|
||||||
@@ -36,6 +37,23 @@ class Clouds:
|
|||||||
density: int
|
density: int
|
||||||
thickness: int
|
thickness: int
|
||||||
precipitation: PydcsWeather.Preceptions
|
precipitation: PydcsWeather.Preceptions
|
||||||
|
preset: Optional[CloudPreset] = field(default=None)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def random_preset(cls, rain: bool) -> Clouds:
|
||||||
|
clouds = (p.value for p in PydcsClouds)
|
||||||
|
if rain:
|
||||||
|
presets = [p for p in clouds if "Rain" in p.name]
|
||||||
|
else:
|
||||||
|
presets = [p for p in clouds if "Rain" not in p.name]
|
||||||
|
preset = random.choice(presets)
|
||||||
|
return Clouds(
|
||||||
|
base=random.randint(preset.min_base, preset.max_base),
|
||||||
|
density=0,
|
||||||
|
thickness=0,
|
||||||
|
precipitation=PydcsWeather.Preceptions.None_,
|
||||||
|
preset=preset,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -58,7 +76,7 @@ class Weather:
|
|||||||
return None
|
return None
|
||||||
return Fog(
|
return Fog(
|
||||||
visibility=meters(random.randint(2500, 5000)),
|
visibility=meters(random.randint(2500, 5000)),
|
||||||
thickness=random.randint(100, 500)
|
thickness=random.randint(100, 500),
|
||||||
)
|
)
|
||||||
|
|
||||||
def generate_wind(self) -> WindConditions:
|
def generate_wind(self) -> WindConditions:
|
||||||
@@ -76,7 +94,7 @@ class Weather:
|
|||||||
# Always some wind to make the smoke move a bit.
|
# Always some wind to make the smoke move a bit.
|
||||||
at_0m=Wind(wind_direction, max(1, base_wind * at_0m_factor)),
|
at_0m=Wind(wind_direction, max(1, base_wind * at_0m_factor)),
|
||||||
at_2000m=Wind(wind_direction, base_wind * at_2000m_factor),
|
at_2000m=Wind(wind_direction, base_wind * at_2000m_factor),
|
||||||
at_8000m=Wind(wind_direction, base_wind * at_8000m_factor)
|
at_8000m=Wind(wind_direction, base_wind * at_8000m_factor),
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -101,12 +119,11 @@ class ClearSkies(Weather):
|
|||||||
|
|
||||||
class Cloudy(Weather):
|
class Cloudy(Weather):
|
||||||
def generate_clouds(self) -> Optional[Clouds]:
|
def generate_clouds(self) -> Optional[Clouds]:
|
||||||
return Clouds(
|
return Clouds.random_preset(rain=False)
|
||||||
base=self.random_cloud_base(),
|
|
||||||
density=random.randint(1, 8),
|
def generate_fog(self) -> Optional[Fog]:
|
||||||
thickness=self.random_cloud_thickness(),
|
# DCS 2.7 says to not use fog with the cloud presets.
|
||||||
precipitation=PydcsWeather.Preceptions.None_
|
return None
|
||||||
)
|
|
||||||
|
|
||||||
def generate_wind(self) -> WindConditions:
|
def generate_wind(self) -> WindConditions:
|
||||||
return self.random_wind(0, 4)
|
return self.random_wind(0, 4)
|
||||||
@@ -114,12 +131,11 @@ class Cloudy(Weather):
|
|||||||
|
|
||||||
class Raining(Weather):
|
class Raining(Weather):
|
||||||
def generate_clouds(self) -> Optional[Clouds]:
|
def generate_clouds(self) -> Optional[Clouds]:
|
||||||
return Clouds(
|
return Clouds.random_preset(rain=True)
|
||||||
base=self.random_cloud_base(),
|
|
||||||
density=random.randint(5, 8),
|
def generate_fog(self) -> Optional[Fog]:
|
||||||
thickness=self.random_cloud_thickness(),
|
# DCS 2.7 says to not use fog with the cloud presets.
|
||||||
precipitation=PydcsWeather.Preceptions.Rain
|
return None
|
||||||
)
|
|
||||||
|
|
||||||
def generate_wind(self) -> WindConditions:
|
def generate_wind(self) -> WindConditions:
|
||||||
return self.random_wind(0, 6)
|
return self.random_wind(0, 6)
|
||||||
@@ -131,7 +147,7 @@ class Thunderstorm(Weather):
|
|||||||
base=self.random_cloud_base(),
|
base=self.random_cloud_base(),
|
||||||
density=random.randint(9, 10),
|
density=random.randint(9, 10),
|
||||||
thickness=self.random_cloud_thickness(),
|
thickness=self.random_cloud_thickness(),
|
||||||
precipitation=PydcsWeather.Preceptions.Thunderstorm
|
precipitation=PydcsWeather.Preceptions.Thunderstorm,
|
||||||
)
|
)
|
||||||
|
|
||||||
def generate_wind(self) -> WindConditions:
|
def generate_wind(self) -> WindConditions:
|
||||||
@@ -145,20 +161,29 @@ class Conditions:
|
|||||||
weather: Weather
|
weather: Weather
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def generate(cls, theater: ConflictTheater, day: datetime.date,
|
def generate(
|
||||||
time_of_day: TimeOfDay, settings: Settings) -> Conditions:
|
cls,
|
||||||
|
theater: ConflictTheater,
|
||||||
|
day: datetime.date,
|
||||||
|
time_of_day: TimeOfDay,
|
||||||
|
settings: Settings,
|
||||||
|
) -> Conditions:
|
||||||
return cls(
|
return cls(
|
||||||
time_of_day=time_of_day,
|
time_of_day=time_of_day,
|
||||||
start_time=cls.generate_start_time(
|
start_time=cls.generate_start_time(
|
||||||
theater, day, time_of_day, settings.night_disabled
|
theater, day, time_of_day, settings.night_disabled
|
||||||
),
|
),
|
||||||
weather=cls.generate_weather()
|
weather=cls.generate_weather(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def generate_start_time(cls, theater: ConflictTheater, day: datetime.date,
|
def generate_start_time(
|
||||||
time_of_day: TimeOfDay,
|
cls,
|
||||||
night_disabled: bool) -> datetime.datetime:
|
theater: ConflictTheater,
|
||||||
|
day: datetime.date,
|
||||||
|
time_of_day: TimeOfDay,
|
||||||
|
night_disabled: bool,
|
||||||
|
) -> datetime.datetime:
|
||||||
if night_disabled:
|
if night_disabled:
|
||||||
logging.info("Skip Night mission due to user settings")
|
logging.info("Skip Night mission due to user settings")
|
||||||
time_range = {
|
time_range = {
|
||||||
@@ -181,6 +206,7 @@ class Conditions:
|
|||||||
Cloudy: 60,
|
Cloudy: 60,
|
||||||
ClearSkies: 20,
|
ClearSkies: 20,
|
||||||
}
|
}
|
||||||
weather_type = random.choices(list(chances.keys()),
|
weather_type = random.choices(
|
||||||
weights=list(chances.values()))[0]
|
list(chances.keys()), weights=list(chances.values())
|
||||||
|
)[0]
|
||||||
return weather_type()
|
return weather_type()
|
||||||
|
|||||||
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
|
import logging
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import List, Type, Tuple
|
from datetime import timedelta
|
||||||
|
from typing import List, Type, Tuple, Optional
|
||||||
|
|
||||||
from dcs.mission import Mission, StartType
|
from dcs.mission import Mission, StartType
|
||||||
from dcs.planes import IL_78M, KC130, KC135MPRS, KC_135
|
from dcs.planes import IL_78M, KC130, KC135MPRS, KC_135
|
||||||
@@ -21,6 +22,7 @@ from .conflictgen import Conflict
|
|||||||
from .radios import RadioFrequency, RadioRegistry
|
from .radios import RadioFrequency, RadioRegistry
|
||||||
from .tacan import TacanBand, TacanChannel, TacanRegistry
|
from .tacan import TacanBand, TacanChannel, TacanRegistry
|
||||||
|
|
||||||
|
|
||||||
TANKER_DISTANCE = 15000
|
TANKER_DISTANCE = 15000
|
||||||
TANKER_ALT = 4572
|
TANKER_ALT = 4572
|
||||||
TANKER_HEADING_OFFSET = 45
|
TANKER_HEADING_OFFSET = 45
|
||||||
@@ -32,14 +34,19 @@ AWACS_ALT = 13000
|
|||||||
@dataclass
|
@dataclass
|
||||||
class AwacsInfo:
|
class AwacsInfo:
|
||||||
"""AWACS information for the kneeboard."""
|
"""AWACS information for the kneeboard."""
|
||||||
|
|
||||||
dcsGroupName: str
|
dcsGroupName: str
|
||||||
callsign: str
|
callsign: str
|
||||||
freq: RadioFrequency
|
freq: RadioFrequency
|
||||||
|
depature_location: Optional[str]
|
||||||
|
start_time: Optional[timedelta]
|
||||||
|
end_time: Optional[timedelta]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class TankerInfo:
|
class TankerInfo:
|
||||||
"""Tanker information for the kneeboard."""
|
"""Tanker information for the kneeboard."""
|
||||||
|
|
||||||
dcsGroupName: str
|
dcsGroupName: str
|
||||||
callsign: str
|
callsign: str
|
||||||
variant: str
|
variant: str
|
||||||
@@ -54,10 +61,14 @@ class AirSupport:
|
|||||||
|
|
||||||
|
|
||||||
class AirSupportConflictGenerator:
|
class AirSupportConflictGenerator:
|
||||||
|
def __init__(
|
||||||
def __init__(self, mission: Mission, conflict: Conflict, game,
|
self,
|
||||||
radio_registry: RadioRegistry,
|
mission: Mission,
|
||||||
tacan_registry: TacanRegistry) -> None:
|
conflict: Conflict,
|
||||||
|
game,
|
||||||
|
radio_registry: RadioRegistry,
|
||||||
|
tacan_registry: TacanRegistry,
|
||||||
|
) -> None:
|
||||||
self.mission = mission
|
self.mission = mission
|
||||||
self.conflict = conflict
|
self.conflict = conflict
|
||||||
self.game = game
|
self.game = game
|
||||||
@@ -78,22 +89,37 @@ class AirSupportConflictGenerator:
|
|||||||
elif unit_type is KC135MPRS:
|
elif unit_type is KC135MPRS:
|
||||||
return (TANKER_ALT + 500, 596)
|
return (TANKER_ALT + 500, 596)
|
||||||
return (TANKER_ALT, 574)
|
return (TANKER_ALT, 574)
|
||||||
|
|
||||||
def generate(self):
|
def generate(self):
|
||||||
player_cp = self.conflict.from_cp if self.conflict.from_cp.captured else self.conflict.to_cp
|
player_cp = (
|
||||||
|
self.conflict.from_cp
|
||||||
|
if self.conflict.from_cp.captured
|
||||||
|
else self.conflict.to_cp
|
||||||
|
)
|
||||||
|
|
||||||
fallback_tanker_number = 0
|
fallback_tanker_number = 0
|
||||||
|
|
||||||
for i, tanker_unit_type in enumerate(db.find_unittype(Refueling, self.conflict.attackers_side)):
|
for i, tanker_unit_type in enumerate(
|
||||||
|
db.find_unittype(Refueling, self.conflict.attackers_side)
|
||||||
|
):
|
||||||
alt, airspeed = self._get_tanker_params(tanker_unit_type)
|
alt, airspeed = self._get_tanker_params(tanker_unit_type)
|
||||||
variant = db.unit_type_name(tanker_unit_type)
|
variant = db.unit_type_name(tanker_unit_type)
|
||||||
freq = self.radio_registry.alloc_uhf()
|
freq = self.radio_registry.alloc_uhf()
|
||||||
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y)
|
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y)
|
||||||
tanker_heading = self.conflict.to_cp.position.heading_between_point(self.conflict.from_cp.position) + TANKER_HEADING_OFFSET * i
|
tanker_heading = (
|
||||||
tanker_position = player_cp.position.point_from_heading(tanker_heading, TANKER_DISTANCE)
|
self.conflict.to_cp.position.heading_between_point(
|
||||||
|
self.conflict.from_cp.position
|
||||||
|
)
|
||||||
|
+ TANKER_HEADING_OFFSET * i
|
||||||
|
)
|
||||||
|
tanker_position = player_cp.position.point_from_heading(
|
||||||
|
tanker_heading, TANKER_DISTANCE
|
||||||
|
)
|
||||||
tanker_group = self.mission.refuel_flight(
|
tanker_group = self.mission.refuel_flight(
|
||||||
country=self.mission.country(self.game.player_country),
|
country=self.mission.country(self.game.player_country),
|
||||||
name=namegen.next_tanker_name(self.mission.country(self.game.player_country), tanker_unit_type),
|
name=namegen.next_tanker_name(
|
||||||
|
self.mission.country(self.game.player_country), tanker_unit_type
|
||||||
|
),
|
||||||
airport=None,
|
airport=None,
|
||||||
plane_type=tanker_unit_type,
|
plane_type=tanker_unit_type,
|
||||||
position=tanker_position,
|
position=tanker_position,
|
||||||
@@ -124,37 +150,59 @@ class AirSupportConflictGenerator:
|
|||||||
if tanker_unit_type != IL_78M:
|
if tanker_unit_type != IL_78M:
|
||||||
# Override PyDCS tacan channel.
|
# Override PyDCS tacan channel.
|
||||||
tanker_group.points[0].tasks.pop()
|
tanker_group.points[0].tasks.pop()
|
||||||
tanker_group.points[0].tasks.append(ActivateBeaconCommand(
|
tanker_group.points[0].tasks.append(
|
||||||
tacan.number, tacan.band.value, tacan_callsign, True,
|
ActivateBeaconCommand(
|
||||||
tanker_group.units[0].id, True))
|
tacan.number,
|
||||||
|
tacan.band.value,
|
||||||
|
tacan_callsign,
|
||||||
|
True,
|
||||||
|
tanker_group.units[0].id,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
tanker_group.points[0].tasks.append(SetInvisibleCommand(True))
|
tanker_group.points[0].tasks.append(SetInvisibleCommand(True))
|
||||||
tanker_group.points[0].tasks.append(SetImmortalCommand(True))
|
tanker_group.points[0].tasks.append(SetImmortalCommand(True))
|
||||||
|
|
||||||
self.air_support.tankers.append(TankerInfo(str(tanker_group.name), callsign, variant, freq, tacan))
|
self.air_support.tankers.append(
|
||||||
|
TankerInfo(str(tanker_group.name), callsign, variant, freq, tacan)
|
||||||
possible_awacs = db.find_unittype(AWACS, self.conflict.attackers_side)
|
|
||||||
|
|
||||||
if len(possible_awacs) > 0:
|
|
||||||
awacs_unit = possible_awacs[0]
|
|
||||||
freq = self.radio_registry.alloc_uhf()
|
|
||||||
|
|
||||||
awacs_flight = self.mission.awacs_flight(
|
|
||||||
country=self.mission.country(self.game.player_country),
|
|
||||||
name=namegen.next_awacs_name(self.mission.country(self.game.player_country)),
|
|
||||||
plane_type=awacs_unit,
|
|
||||||
altitude=AWACS_ALT,
|
|
||||||
airport=None,
|
|
||||||
position=self.conflict.position.random_point_within(AWACS_DISTANCE, AWACS_DISTANCE),
|
|
||||||
frequency=freq.mhz,
|
|
||||||
start_type=StartType.Warm,
|
|
||||||
)
|
)
|
||||||
awacs_flight.set_frequency(freq.mhz)
|
|
||||||
|
|
||||||
awacs_flight.points[0].tasks.append(SetInvisibleCommand(True))
|
if not self.game.settings.disable_legacy_aewc:
|
||||||
awacs_flight.points[0].tasks.append(SetImmortalCommand(True))
|
possible_awacs = db.find_unittype(AWACS, self.conflict.attackers_side)
|
||||||
|
|
||||||
self.air_support.awacs.append(AwacsInfo(
|
if len(possible_awacs) > 0:
|
||||||
str(awacs_flight.name), callsign_for_support_unit(awacs_flight), freq))
|
awacs_unit = possible_awacs[0]
|
||||||
else:
|
freq = self.radio_registry.alloc_uhf()
|
||||||
logging.warning("No AWACS for faction")
|
|
||||||
|
awacs_flight = self.mission.awacs_flight(
|
||||||
|
country=self.mission.country(self.game.player_country),
|
||||||
|
name=namegen.next_awacs_name(
|
||||||
|
self.mission.country(self.game.player_country)
|
||||||
|
),
|
||||||
|
plane_type=awacs_unit,
|
||||||
|
altitude=AWACS_ALT,
|
||||||
|
airport=None,
|
||||||
|
position=self.conflict.position.random_point_within(
|
||||||
|
AWACS_DISTANCE, AWACS_DISTANCE
|
||||||
|
),
|
||||||
|
frequency=freq.mhz,
|
||||||
|
start_type=StartType.Warm,
|
||||||
|
)
|
||||||
|
awacs_flight.set_frequency(freq.mhz)
|
||||||
|
|
||||||
|
awacs_flight.points[0].tasks.append(SetInvisibleCommand(True))
|
||||||
|
awacs_flight.points[0].tasks.append(SetImmortalCommand(True))
|
||||||
|
|
||||||
|
self.air_support.awacs.append(
|
||||||
|
AwacsInfo(
|
||||||
|
dcsGroupName=str(awacs_flight.name),
|
||||||
|
callsign=callsign_for_support_unit(awacs_flight),
|
||||||
|
freq=freq,
|
||||||
|
depature_location=None,
|
||||||
|
start_time=None,
|
||||||
|
end_time=None,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logging.warning("No AWACS for faction")
|
||||||
|
|||||||
362
gen/armor.py
362
gen/armor.py
@@ -12,9 +12,17 @@ from dcs.country import Country
|
|||||||
from dcs.mapping import Point
|
from dcs.mapping import Point
|
||||||
from dcs.planes import MQ_9_Reaper
|
from dcs.planes import MQ_9_Reaper
|
||||||
from dcs.point import PointAction
|
from dcs.point import PointAction
|
||||||
from dcs.task import (EPLRS, AttackGroup, ControlledTask, FireAtPoint,
|
from dcs.task import (
|
||||||
GoToWaypoint, Hold, OrbitAction, SetImmortalCommand,
|
EPLRS,
|
||||||
SetInvisibleCommand)
|
AttackGroup,
|
||||||
|
ControlledTask,
|
||||||
|
FireAtPoint,
|
||||||
|
GoToWaypoint,
|
||||||
|
Hold,
|
||||||
|
OrbitAction,
|
||||||
|
SetImmortalCommand,
|
||||||
|
SetInvisibleCommand,
|
||||||
|
)
|
||||||
from dcs.triggers import Event, TriggerOnce
|
from dcs.triggers import Event, TriggerOnce
|
||||||
from dcs.unit import Vehicle
|
from dcs.unit import Vehicle
|
||||||
from dcs.unitgroup import VehicleGroup
|
from dcs.unitgroup import VehicleGroup
|
||||||
@@ -24,8 +32,11 @@ from game.unitmap import UnitMap
|
|||||||
from game.utils import heading_sum, opposite_heading
|
from game.utils import heading_sum, opposite_heading
|
||||||
from game.theater.controlpoint import ControlPoint
|
from game.theater.controlpoint import ControlPoint
|
||||||
|
|
||||||
from gen.ground_forces.ai_ground_planner import (DISTANCE_FROM_FRONTLINE,
|
from gen.ground_forces.ai_ground_planner import (
|
||||||
CombatGroup, CombatGroupRole)
|
DISTANCE_FROM_FRONTLINE,
|
||||||
|
CombatGroup,
|
||||||
|
CombatGroupRole,
|
||||||
|
)
|
||||||
|
|
||||||
from .callsigns import callsign_for_support_unit
|
from .callsigns import callsign_for_support_unit
|
||||||
from .conflictgen import Conflict
|
from .conflictgen import Conflict
|
||||||
@@ -56,6 +67,7 @@ INFANTRY_GROUP_SIZE = 5
|
|||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class JtacInfo:
|
class JtacInfo:
|
||||||
"""JTAC information."""
|
"""JTAC information."""
|
||||||
|
|
||||||
dcsGroupName: str
|
dcsGroupName: str
|
||||||
unit_name: str
|
unit_name: str
|
||||||
callsign: str
|
callsign: str
|
||||||
@@ -65,16 +77,16 @@ class JtacInfo:
|
|||||||
|
|
||||||
|
|
||||||
class GroundConflictGenerator:
|
class GroundConflictGenerator:
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
mission: Mission,
|
mission: Mission,
|
||||||
conflict: Conflict,
|
conflict: Conflict,
|
||||||
game: Game,
|
game: Game,
|
||||||
player_planned_combat_groups: List[CombatGroup],
|
player_planned_combat_groups: List[CombatGroup],
|
||||||
enemy_planned_combat_groups: List[CombatGroup],
|
enemy_planned_combat_groups: List[CombatGroup],
|
||||||
player_stance: CombatStance,
|
player_stance: CombatStance,
|
||||||
unit_map: UnitMap) -> None:
|
unit_map: UnitMap,
|
||||||
|
) -> None:
|
||||||
self.mission = mission
|
self.mission = mission
|
||||||
self.conflict = conflict
|
self.conflict = conflict
|
||||||
self.enemy_planned_combat_groups = enemy_planned_combat_groups
|
self.enemy_planned_combat_groups = enemy_planned_combat_groups
|
||||||
@@ -87,14 +99,16 @@ class GroundConflictGenerator:
|
|||||||
|
|
||||||
def _enemy_stance(self):
|
def _enemy_stance(self):
|
||||||
"""Picks the enemy stance according to the number of planned groups on the frontline for each side"""
|
"""Picks the enemy stance according to the number of planned groups on the frontline for each side"""
|
||||||
if len(self.enemy_planned_combat_groups) > len(self.player_planned_combat_groups):
|
if len(self.enemy_planned_combat_groups) > len(
|
||||||
|
self.player_planned_combat_groups
|
||||||
|
):
|
||||||
return random.choice(
|
return random.choice(
|
||||||
[
|
[
|
||||||
CombatStance.AGGRESSIVE,
|
CombatStance.AGGRESSIVE,
|
||||||
CombatStance.AGGRESSIVE,
|
CombatStance.AGGRESSIVE,
|
||||||
CombatStance.AGGRESSIVE,
|
CombatStance.AGGRESSIVE,
|
||||||
CombatStance.ELIMINATION,
|
CombatStance.ELIMINATION,
|
||||||
CombatStance.BREAKTHROUGH
|
CombatStance.BREAKTHROUGH,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -104,31 +118,37 @@ class GroundConflictGenerator:
|
|||||||
CombatStance.DEFENSIVE,
|
CombatStance.DEFENSIVE,
|
||||||
CombatStance.DEFENSIVE,
|
CombatStance.DEFENSIVE,
|
||||||
CombatStance.AMBUSH,
|
CombatStance.AMBUSH,
|
||||||
CombatStance.AGGRESSIVE
|
CombatStance.AGGRESSIVE,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _group_point(point: Point, base_distance) -> Point:
|
def _group_point(point: Point, base_distance) -> Point:
|
||||||
distance = random.randint(
|
distance = random.randint(
|
||||||
int(base_distance * SPREAD_DISTANCE_FACTOR[0]),
|
int(base_distance * SPREAD_DISTANCE_FACTOR[0]),
|
||||||
int(base_distance * SPREAD_DISTANCE_FACTOR[1]),
|
int(base_distance * SPREAD_DISTANCE_FACTOR[1]),
|
||||||
)
|
)
|
||||||
return point.random_point_within(distance, base_distance * SPREAD_DISTANCE_SIZE_FACTOR)
|
return point.random_point_within(
|
||||||
|
distance, base_distance * SPREAD_DISTANCE_SIZE_FACTOR
|
||||||
|
)
|
||||||
|
|
||||||
def generate(self):
|
def generate(self):
|
||||||
position = Conflict.frontline_position(self.conflict.from_cp, self.conflict.to_cp, self.game.theater)
|
position = Conflict.frontline_position(
|
||||||
|
self.conflict.from_cp, self.conflict.to_cp, self.game.theater
|
||||||
|
)
|
||||||
frontline_vector = Conflict.frontline_vector(
|
frontline_vector = Conflict.frontline_vector(
|
||||||
self.conflict.from_cp,
|
self.conflict.from_cp, self.conflict.to_cp, self.game.theater
|
||||||
self.conflict.to_cp,
|
)
|
||||||
self.game.theater
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create player groups at random position
|
# Create player groups at random position
|
||||||
player_groups = self._generate_groups(self.player_planned_combat_groups, frontline_vector, True)
|
player_groups = self._generate_groups(
|
||||||
|
self.player_planned_combat_groups, frontline_vector, True
|
||||||
|
)
|
||||||
|
|
||||||
# Create enemy groups at random position
|
# Create enemy groups at random position
|
||||||
enemy_groups = self._generate_groups(self.enemy_planned_combat_groups, frontline_vector, False)
|
enemy_groups = self._generate_groups(
|
||||||
|
self.enemy_planned_combat_groups, frontline_vector, False
|
||||||
|
)
|
||||||
|
|
||||||
# Plan combat actions for groups
|
# Plan combat actions for groups
|
||||||
self.plan_action_for_groups(
|
self.plan_action_for_groups(
|
||||||
@@ -137,7 +157,7 @@ class GroundConflictGenerator:
|
|||||||
enemy_groups,
|
enemy_groups,
|
||||||
self.conflict.heading + 90,
|
self.conflict.heading + 90,
|
||||||
self.conflict.from_cp,
|
self.conflict.from_cp,
|
||||||
self.conflict.to_cp
|
self.conflict.to_cp,
|
||||||
)
|
)
|
||||||
self.plan_action_for_groups(
|
self.plan_action_for_groups(
|
||||||
self.enemy_stance,
|
self.enemy_stance,
|
||||||
@@ -145,7 +165,7 @@ class GroundConflictGenerator:
|
|||||||
player_groups,
|
player_groups,
|
||||||
self.conflict.heading - 90,
|
self.conflict.heading - 90,
|
||||||
self.conflict.to_cp,
|
self.conflict.to_cp,
|
||||||
self.conflict.from_cp
|
self.conflict.from_cp,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add JTAC
|
# Add JTAC
|
||||||
@@ -157,34 +177,38 @@ class GroundConflictGenerator:
|
|||||||
if self.game.player_faction.jtac_unit is not None:
|
if self.game.player_faction.jtac_unit is not None:
|
||||||
utype = self.game.player_faction.jtac_unit
|
utype = self.game.player_faction.jtac_unit
|
||||||
|
|
||||||
jtac = self.mission.flight_group(country=self.mission.country(self.game.player_country),
|
jtac = self.mission.flight_group(
|
||||||
name=n,
|
country=self.mission.country(self.game.player_country),
|
||||||
aircraft_type=utype,
|
name=n,
|
||||||
position=position[0],
|
aircraft_type=utype,
|
||||||
airport=None,
|
position=position[0],
|
||||||
altitude=5000)
|
airport=None,
|
||||||
|
altitude=5000,
|
||||||
|
)
|
||||||
jtac.points[0].tasks.append(SetInvisibleCommand(True))
|
jtac.points[0].tasks.append(SetInvisibleCommand(True))
|
||||||
jtac.points[0].tasks.append(SetImmortalCommand(True))
|
jtac.points[0].tasks.append(SetImmortalCommand(True))
|
||||||
jtac.points[0].tasks.append(OrbitAction(5000, 300, OrbitAction.OrbitPattern.Circle))
|
jtac.points[0].tasks.append(
|
||||||
frontline = f"Frontline {self.conflict.from_cp.name}/{self.conflict.to_cp.name}"
|
OrbitAction(5000, 300, OrbitAction.OrbitPattern.Circle)
|
||||||
|
)
|
||||||
|
frontline = (
|
||||||
|
f"Frontline {self.conflict.from_cp.name}/{self.conflict.to_cp.name}"
|
||||||
|
)
|
||||||
# Note: Will need to change if we ever add ground based JTAC.
|
# Note: Will need to change if we ever add ground based JTAC.
|
||||||
callsign = callsign_for_support_unit(jtac)
|
callsign = callsign_for_support_unit(jtac)
|
||||||
self.jtacs.append(JtacInfo(str(jtac.name), n, callsign, frontline, str(code)))
|
self.jtacs.append(
|
||||||
|
JtacInfo(str(jtac.name), n, callsign, frontline, str(code))
|
||||||
|
)
|
||||||
|
|
||||||
def gen_infantry_group_for_group(
|
def gen_infantry_group_for_group(
|
||||||
self,
|
self, group: VehicleGroup, is_player: bool, side: Country, forward_heading: int
|
||||||
group: VehicleGroup,
|
|
||||||
is_player: bool,
|
|
||||||
side: Country,
|
|
||||||
forward_heading: int
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
infantry_position = self.conflict.find_ground_position(
|
infantry_position = self.conflict.find_ground_position(
|
||||||
group.points[0].position.random_point_within(250, 50),
|
group.points[0].position.random_point_within(250, 50),
|
||||||
500,
|
500,
|
||||||
forward_heading,
|
forward_heading,
|
||||||
self.conflict.theater
|
self.conflict.theater,
|
||||||
)
|
)
|
||||||
if not infantry_position:
|
if not infantry_position:
|
||||||
logging.warning("Could not find infantry position")
|
logging.warning("Could not find infantry position")
|
||||||
return
|
return
|
||||||
@@ -208,44 +232,50 @@ class GroundConflictGenerator:
|
|||||||
u = random.choice(manpads)
|
u = random.choice(manpads)
|
||||||
self.mission.vehicle_group(
|
self.mission.vehicle_group(
|
||||||
side,
|
side,
|
||||||
namegen.next_infantry_name(side, cp.id, u), u,
|
namegen.next_infantry_name(side, cp.id, u),
|
||||||
|
u,
|
||||||
position=infantry_position,
|
position=infantry_position,
|
||||||
group_size=1,
|
group_size=1,
|
||||||
heading=forward_heading,
|
heading=forward_heading,
|
||||||
move_formation=PointAction.OffRoad)
|
move_formation=PointAction.OffRoad,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
possible_infantry_units = db.find_infantry(faction, allow_manpad=self.game.settings.manpads)
|
possible_infantry_units = db.find_infantry(
|
||||||
|
faction, allow_manpad=self.game.settings.manpads
|
||||||
|
)
|
||||||
if len(possible_infantry_units) == 0:
|
if len(possible_infantry_units) == 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
u = random.choice(possible_infantry_units)
|
u = random.choice(possible_infantry_units)
|
||||||
self.mission.vehicle_group(
|
self.mission.vehicle_group(
|
||||||
side,
|
side,
|
||||||
namegen.next_infantry_name(side, cp.id, u), u,
|
namegen.next_infantry_name(side, cp.id, u),
|
||||||
position=infantry_position,
|
u,
|
||||||
group_size=1,
|
position=infantry_position,
|
||||||
heading=forward_heading,
|
group_size=1,
|
||||||
move_formation=PointAction.OffRoad)
|
heading=forward_heading,
|
||||||
|
move_formation=PointAction.OffRoad,
|
||||||
|
)
|
||||||
|
|
||||||
for i in range(INFANTRY_GROUP_SIZE):
|
for i in range(INFANTRY_GROUP_SIZE):
|
||||||
u = random.choice(possible_infantry_units)
|
u = random.choice(possible_infantry_units)
|
||||||
position = infantry_position.random_point_within(55, 5)
|
position = infantry_position.random_point_within(55, 5)
|
||||||
self.mission.vehicle_group(
|
self.mission.vehicle_group(
|
||||||
side,
|
side,
|
||||||
namegen.next_infantry_name(side, cp.id, u), u,
|
namegen.next_infantry_name(side, cp.id, u),
|
||||||
|
u,
|
||||||
position=position,
|
position=position,
|
||||||
group_size=1,
|
group_size=1,
|
||||||
heading=forward_heading,
|
heading=forward_heading,
|
||||||
move_formation=PointAction.OffRoad)
|
move_formation=PointAction.OffRoad,
|
||||||
|
)
|
||||||
|
|
||||||
def _set_reform_waypoint(
|
def _set_reform_waypoint(
|
||||||
self,
|
self, dcs_group: VehicleGroup, forward_heading: int
|
||||||
dcs_group: VehicleGroup,
|
|
||||||
forward_heading: int
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Setting a waypoint close to the spawn position allows the group to reform gracefully
|
"""Setting a waypoint close to the spawn position allows the group to reform gracefully
|
||||||
rather than spin
|
rather than spin
|
||||||
"""
|
"""
|
||||||
reform_point = dcs_group.position.point_from_heading(forward_heading, 50)
|
reform_point = dcs_group.position.point_from_heading(forward_heading, 50)
|
||||||
dcs_group.add_waypoint(reform_point)
|
dcs_group.add_waypoint(reform_point)
|
||||||
@@ -256,7 +286,7 @@ class GroundConflictGenerator:
|
|||||||
gen_group: CombatGroup,
|
gen_group: CombatGroup,
|
||||||
dcs_group: VehicleGroup,
|
dcs_group: VehicleGroup,
|
||||||
forward_heading: int,
|
forward_heading: int,
|
||||||
target: Point
|
target: Point,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Handles adding the DCS tasks for artillery groups for all combat stances.
|
Handles adding the DCS tasks for artillery groups for all combat stances.
|
||||||
@@ -269,7 +299,9 @@ class GroundConflictGenerator:
|
|||||||
dcs_group.add_trigger_action(hold_task)
|
dcs_group.add_trigger_action(hold_task)
|
||||||
|
|
||||||
# Artillery strike random start
|
# Artillery strike random start
|
||||||
artillery_trigger = TriggerOnce(Event.NoEvent, "ArtilleryFireTask #" + str(dcs_group.id))
|
artillery_trigger = TriggerOnce(
|
||||||
|
Event.NoEvent, "ArtilleryFireTask #" + str(dcs_group.id)
|
||||||
|
)
|
||||||
artillery_trigger.add_condition(TimeAfter(seconds=random.randint(1, 45) * 60))
|
artillery_trigger.add_condition(TimeAfter(seconds=random.randint(1, 45) * 60))
|
||||||
# TODO: Update to fire at group instead of point
|
# TODO: Update to fire at group instead of point
|
||||||
fire_task = FireAtPoint(target, len(gen_group.units) * 10, 100)
|
fire_task = FireAtPoint(target, len(gen_group.units) * 10, 100)
|
||||||
@@ -283,12 +315,19 @@ class GroundConflictGenerator:
|
|||||||
|
|
||||||
# Hold position
|
# Hold position
|
||||||
dcs_group.points[1].tasks.append(Hold())
|
dcs_group.points[1].tasks.append(Hold())
|
||||||
retreat = self.find_retreat_point(dcs_group, forward_heading, (int)(RETREAT_DISTANCE/3))
|
retreat = self.find_retreat_point(
|
||||||
dcs_group.add_waypoint(dcs_group.position.point_from_heading(forward_heading, 1), PointAction.OffRoad)
|
dcs_group, forward_heading, (int)(RETREAT_DISTANCE / 3)
|
||||||
|
)
|
||||||
|
dcs_group.add_waypoint(
|
||||||
|
dcs_group.position.point_from_heading(forward_heading, 1),
|
||||||
|
PointAction.OffRoad,
|
||||||
|
)
|
||||||
dcs_group.points[2].tasks.append(Hold())
|
dcs_group.points[2].tasks.append(Hold())
|
||||||
dcs_group.add_waypoint(retreat, PointAction.OffRoad)
|
dcs_group.add_waypoint(retreat, PointAction.OffRoad)
|
||||||
|
|
||||||
artillery_fallback = TriggerOnce(Event.NoEvent, "ArtilleryRetreat #" + str(dcs_group.id))
|
artillery_fallback = TriggerOnce(
|
||||||
|
Event.NoEvent, "ArtilleryRetreat #" + str(dcs_group.id)
|
||||||
|
)
|
||||||
for i, u in enumerate(dcs_group.units):
|
for i, u in enumerate(dcs_group.units):
|
||||||
artillery_fallback.add_condition(UnitDamaged(u.id))
|
artillery_fallback.add_condition(UnitDamaged(u.id))
|
||||||
if i < len(dcs_group.units) - 1:
|
if i < len(dcs_group.units) - 1:
|
||||||
@@ -302,7 +341,9 @@ class GroundConflictGenerator:
|
|||||||
retreat_task.number = 4
|
retreat_task.number = 4
|
||||||
dcs_group.add_trigger_action(retreat_task)
|
dcs_group.add_trigger_action(retreat_task)
|
||||||
|
|
||||||
artillery_fallback.add_action(AITaskPush(dcs_group.id, len(dcs_group.tasks)))
|
artillery_fallback.add_action(
|
||||||
|
AITaskPush(dcs_group.id, len(dcs_group.tasks))
|
||||||
|
)
|
||||||
self.mission.triggerrules.triggers.append(artillery_fallback)
|
self.mission.triggerrules.triggers.append(artillery_fallback)
|
||||||
|
|
||||||
for u in dcs_group.units:
|
for u in dcs_group.units:
|
||||||
@@ -330,12 +371,8 @@ class GroundConflictGenerator:
|
|||||||
target = self.find_nearest_enemy_group(dcs_group, enemy_groups)
|
target = self.find_nearest_enemy_group(dcs_group, enemy_groups)
|
||||||
if target is not None:
|
if target is not None:
|
||||||
rand_offset = Point(
|
rand_offset = Point(
|
||||||
random.randint(
|
random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK),
|
||||||
-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK
|
random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK),
|
||||||
),
|
|
||||||
random.randint(
|
|
||||||
-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
target_point = self.conflict.theater.nearest_land_pos(
|
target_point = self.conflict.theater.nearest_land_pos(
|
||||||
target.points[0].position + rand_offset
|
target.points[0].position + rand_offset
|
||||||
@@ -345,8 +382,7 @@ class GroundConflictGenerator:
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
to_cp.position.distance_to_point(dcs_group.points[0].position)
|
to_cp.position.distance_to_point(dcs_group.points[0].position)
|
||||||
<=
|
<= AGGRESIVE_MOVE_DISTANCE
|
||||||
AGGRESIVE_MOVE_DISTANCE
|
|
||||||
):
|
):
|
||||||
attack_point = self.conflict.theater.nearest_land_pos(
|
attack_point = self.conflict.theater.nearest_land_pos(
|
||||||
to_cp.position.random_point_within(500, 0)
|
to_cp.position.random_point_within(500, 0)
|
||||||
@@ -358,16 +394,16 @@ class GroundConflictGenerator:
|
|||||||
if offset_heading < 0:
|
if offset_heading < 0:
|
||||||
offset_heading = 358
|
offset_heading = 358
|
||||||
attack_point = self.find_offensive_point(
|
attack_point = self.find_offensive_point(
|
||||||
dcs_group,
|
dcs_group, offset_heading, AGGRESIVE_MOVE_DISTANCE
|
||||||
offset_heading,
|
|
||||||
AGGRESIVE_MOVE_DISTANCE
|
|
||||||
)
|
)
|
||||||
dcs_group.add_waypoint(attack_point, PointAction.OffRoad)
|
dcs_group.add_waypoint(attack_point, PointAction.OffRoad)
|
||||||
elif stance == CombatStance.BREAKTHROUGH:
|
elif stance == CombatStance.BREAKTHROUGH:
|
||||||
# In breakthrough mode, the units will move forward
|
# In breakthrough mode, the units will move forward
|
||||||
# If the enemy base is close enough, the units will attack the base
|
# If the enemy base is close enough, the units will attack the base
|
||||||
if to_cp.position.distance_to_point(
|
if (
|
||||||
dcs_group.points[0].position) <= BREAKTHROUGH_OFFENSIVE_DISTANCE:
|
to_cp.position.distance_to_point(dcs_group.points[0].position)
|
||||||
|
<= BREAKTHROUGH_OFFENSIVE_DISTANCE
|
||||||
|
):
|
||||||
attack_point = self.conflict.theater.nearest_land_pos(
|
attack_point = self.conflict.theater.nearest_land_pos(
|
||||||
to_cp.position.random_point_within(500, 0)
|
to_cp.position.random_point_within(500, 0)
|
||||||
)
|
)
|
||||||
@@ -377,27 +413,27 @@ class GroundConflictGenerator:
|
|||||||
offset_heading = forward_heading - 1
|
offset_heading = forward_heading - 1
|
||||||
if offset_heading < 0:
|
if offset_heading < 0:
|
||||||
offset_heading = 359
|
offset_heading = 359
|
||||||
attack_point = self.find_offensive_point(dcs_group, offset_heading, BREAKTHROUGH_OFFENSIVE_DISTANCE)
|
attack_point = self.find_offensive_point(
|
||||||
|
dcs_group, offset_heading, BREAKTHROUGH_OFFENSIVE_DISTANCE
|
||||||
|
)
|
||||||
dcs_group.add_waypoint(attack_point, PointAction.OffRoad)
|
dcs_group.add_waypoint(attack_point, PointAction.OffRoad)
|
||||||
elif stance == CombatStance.ELIMINATION:
|
elif stance == CombatStance.ELIMINATION:
|
||||||
# In elimination mode, the units focus on destroying as much enemy groups as possible
|
# In elimination mode, the units focus on destroying as much enemy groups as possible
|
||||||
targets = self.find_n_nearest_enemy_groups(dcs_group, enemy_groups, 3)
|
targets = self.find_n_nearest_enemy_groups(dcs_group, enemy_groups, 3)
|
||||||
for i, target in enumerate(targets, start=1):
|
for i, target in enumerate(targets, start=1):
|
||||||
rand_offset = Point(
|
rand_offset = Point(
|
||||||
random.randint(
|
random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK),
|
||||||
-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK
|
random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK),
|
||||||
),
|
|
||||||
random.randint(
|
|
||||||
-RANDOM_OFFSET_ATTACK,
|
|
||||||
RANDOM_OFFSET_ATTACK
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
target_point = self.conflict.theater.nearest_land_pos(
|
target_point = self.conflict.theater.nearest_land_pos(
|
||||||
target.points[0].position+rand_offset
|
target.points[0].position + rand_offset
|
||||||
)
|
)
|
||||||
dcs_group.add_waypoint(target_point, PointAction.OffRoad)
|
dcs_group.add_waypoint(target_point, PointAction.OffRoad)
|
||||||
dcs_group.points[i + 1].tasks.append(AttackGroup(target.id))
|
dcs_group.points[i + 1].tasks.append(AttackGroup(target.id))
|
||||||
if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE:
|
if (
|
||||||
|
to_cp.position.distance_to_point(dcs_group.points[0].position)
|
||||||
|
<= AGGRESIVE_MOVE_DISTANCE
|
||||||
|
):
|
||||||
attack_point = self.conflict.theater.nearest_land_pos(
|
attack_point = self.conflict.theater.nearest_land_pos(
|
||||||
to_cp.position.random_point_within(500, 0)
|
to_cp.position.random_point_within(500, 0)
|
||||||
)
|
)
|
||||||
@@ -420,12 +456,23 @@ class GroundConflictGenerator:
|
|||||||
Returns True if tasking was added, returns False if the stance was not a combat stance.
|
Returns True if tasking was added, returns False if the stance was not a combat stance.
|
||||||
"""
|
"""
|
||||||
self._set_reform_waypoint(dcs_group, forward_heading)
|
self._set_reform_waypoint(dcs_group, forward_heading)
|
||||||
if stance in [CombatStance.AGGRESSIVE, CombatStance.BREAKTHROUGH, CombatStance.ELIMINATION]:
|
if stance in [
|
||||||
|
CombatStance.AGGRESSIVE,
|
||||||
|
CombatStance.BREAKTHROUGH,
|
||||||
|
CombatStance.ELIMINATION,
|
||||||
|
]:
|
||||||
# APC & ATGM will never move too much forward, but will follow along any offensive
|
# APC & ATGM will never move too much forward, but will follow along any offensive
|
||||||
if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE:
|
if (
|
||||||
attack_point = self.conflict.theater.nearest_land_pos(to_cp.position.random_point_within(500, 0))
|
to_cp.position.distance_to_point(dcs_group.points[0].position)
|
||||||
|
<= AGGRESIVE_MOVE_DISTANCE
|
||||||
|
):
|
||||||
|
attack_point = self.conflict.theater.nearest_land_pos(
|
||||||
|
to_cp.position.random_point_within(500, 0)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
attack_point = self.find_offensive_point(dcs_group, forward_heading, AGGRESIVE_MOVE_DISTANCE)
|
attack_point = self.find_offensive_point(
|
||||||
|
dcs_group, forward_heading, AGGRESIVE_MOVE_DISTANCE
|
||||||
|
)
|
||||||
dcs_group.add_waypoint(attack_point, PointAction.OffRoad)
|
dcs_group.add_waypoint(attack_point, PointAction.OffRoad)
|
||||||
|
|
||||||
if stance != CombatStance.RETREAT:
|
if stance != CombatStance.RETREAT:
|
||||||
@@ -434,29 +481,36 @@ class GroundConflictGenerator:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def plan_action_for_groups(
|
def plan_action_for_groups(
|
||||||
self, stance: CombatStance,
|
self,
|
||||||
|
stance: CombatStance,
|
||||||
ally_groups: List[Tuple[VehicleGroup, CombatGroup]],
|
ally_groups: List[Tuple[VehicleGroup, CombatGroup]],
|
||||||
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]],
|
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]],
|
||||||
forward_heading: int,
|
forward_heading: int,
|
||||||
from_cp: ControlPoint,
|
from_cp: ControlPoint,
|
||||||
to_cp: ControlPoint
|
to_cp: ControlPoint,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
if not self.game.settings.perf_moving_units:
|
if not self.game.settings.perf_moving_units:
|
||||||
return
|
return
|
||||||
|
|
||||||
for dcs_group, group in ally_groups:
|
for dcs_group, group in ally_groups:
|
||||||
if hasattr(group.units[0], 'eplrs') and group.units[0].eplrs:
|
if hasattr(group.units[0], "eplrs") and group.units[0].eplrs:
|
||||||
dcs_group.points[0].tasks.append(EPLRS(dcs_group.id))
|
dcs_group.points[0].tasks.append(EPLRS(dcs_group.id))
|
||||||
|
|
||||||
if group.role == CombatGroupRole.ARTILLERY:
|
if group.role == CombatGroupRole.ARTILLERY:
|
||||||
if self.game.settings.perf_artillery:
|
if self.game.settings.perf_artillery:
|
||||||
target = self.get_artillery_target_in_range(dcs_group, group, enemy_groups)
|
target = self.get_artillery_target_in_range(
|
||||||
|
dcs_group, group, enemy_groups
|
||||||
|
)
|
||||||
if target is not None:
|
if target is not None:
|
||||||
self._plan_artillery_action(stance, group, dcs_group, forward_heading, target)
|
self._plan_artillery_action(
|
||||||
|
stance, group, dcs_group, forward_heading, target
|
||||||
|
)
|
||||||
|
|
||||||
elif group.role in [CombatGroupRole.TANK, CombatGroupRole.IFV]:
|
elif group.role in [CombatGroupRole.TANK, CombatGroupRole.IFV]:
|
||||||
self._plan_tank_ifv_action(stance, enemy_groups, dcs_group, forward_heading, to_cp)
|
self._plan_tank_ifv_action(
|
||||||
|
stance, enemy_groups, dcs_group, forward_heading, to_cp
|
||||||
|
)
|
||||||
|
|
||||||
elif group.role in [CombatGroupRole.APC, CombatGroupRole.ATGM]:
|
elif group.role in [CombatGroupRole.APC, CombatGroupRole.ATGM]:
|
||||||
self._plan_apc_atgm_action(stance, dcs_group, forward_heading, to_cp)
|
self._plan_apc_atgm_action(stance, dcs_group, forward_heading, to_cp)
|
||||||
@@ -464,11 +518,16 @@ class GroundConflictGenerator:
|
|||||||
if stance == CombatStance.RETREAT:
|
if stance == CombatStance.RETREAT:
|
||||||
# In retreat mode, the units will fall back
|
# In retreat mode, the units will fall back
|
||||||
# If the ally base is close enough, the units will even regroup there
|
# If the ally base is close enough, the units will even regroup there
|
||||||
if from_cp.position.distance_to_point(dcs_group.points[0].position) <= RETREAT_DISTANCE:
|
if (
|
||||||
|
from_cp.position.distance_to_point(dcs_group.points[0].position)
|
||||||
|
<= RETREAT_DISTANCE
|
||||||
|
):
|
||||||
retreat_point = from_cp.position.random_point_within(500, 250)
|
retreat_point = from_cp.position.random_point_within(500, 250)
|
||||||
else:
|
else:
|
||||||
retreat_point = self.find_retreat_point(dcs_group, forward_heading)
|
retreat_point = self.find_retreat_point(dcs_group, forward_heading)
|
||||||
reposition_point = retreat_point.point_from_heading(forward_heading, 10) # Another point to make the unit face the enemy
|
reposition_point = retreat_point.point_from_heading(
|
||||||
|
forward_heading, 10
|
||||||
|
) # Another point to make the unit face the enemy
|
||||||
dcs_group.add_waypoint(retreat_point, PointAction.OffRoad)
|
dcs_group.add_waypoint(retreat_point, PointAction.OffRoad)
|
||||||
dcs_group.add_waypoint(reposition_point, PointAction.OffRoad)
|
dcs_group.add_waypoint(reposition_point, PointAction.OffRoad)
|
||||||
|
|
||||||
@@ -490,8 +549,10 @@ class GroundConflictGenerator:
|
|||||||
|
|
||||||
# We add a new retreat waypoint
|
# We add a new retreat waypoint
|
||||||
dcs_group.add_waypoint(
|
dcs_group.add_waypoint(
|
||||||
self.find_retreat_point(dcs_group, forward_heading, (int)(RETREAT_DISTANCE / 8)),
|
self.find_retreat_point(
|
||||||
PointAction.OffRoad
|
dcs_group, forward_heading, (int)(RETREAT_DISTANCE / 8)
|
||||||
|
),
|
||||||
|
PointAction.OffRoad,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Fallback task
|
# Fallback task
|
||||||
@@ -515,7 +576,7 @@ class GroundConflictGenerator:
|
|||||||
self,
|
self,
|
||||||
dcs_group: VehicleGroup,
|
dcs_group: VehicleGroup,
|
||||||
frontline_heading: int,
|
frontline_heading: int,
|
||||||
distance: int = RETREAT_DISTANCE
|
distance: int = RETREAT_DISTANCE,
|
||||||
) -> Point:
|
) -> Point:
|
||||||
"""
|
"""
|
||||||
Find a point to retreat to
|
Find a point to retreat to
|
||||||
@@ -523,17 +584,15 @@ class GroundConflictGenerator:
|
|||||||
:param frontline_heading: Heading of the frontline
|
:param frontline_heading: Heading of the frontline
|
||||||
:return: dcs.mapping.Point object with the desired position
|
:return: dcs.mapping.Point object with the desired position
|
||||||
"""
|
"""
|
||||||
desired_point = dcs_group.points[0].position.point_from_heading(heading_sum(frontline_heading, +180), distance)
|
desired_point = dcs_group.points[0].position.point_from_heading(
|
||||||
|
heading_sum(frontline_heading, +180), distance
|
||||||
|
)
|
||||||
if self.conflict.theater.is_on_land(desired_point):
|
if self.conflict.theater.is_on_land(desired_point):
|
||||||
return desired_point
|
return desired_point
|
||||||
return self.conflict.theater.nearest_land_pos(desired_point)
|
return self.conflict.theater.nearest_land_pos(desired_point)
|
||||||
|
|
||||||
|
|
||||||
def find_offensive_point(
|
def find_offensive_point(
|
||||||
self,
|
self, dcs_group: VehicleGroup, frontline_heading: int, distance: int
|
||||||
dcs_group: VehicleGroup,
|
|
||||||
frontline_heading: int,
|
|
||||||
distance: int
|
|
||||||
) -> Point:
|
) -> Point:
|
||||||
"""
|
"""
|
||||||
Find a point to attack
|
Find a point to attack
|
||||||
@@ -542,7 +601,9 @@ class GroundConflictGenerator:
|
|||||||
:param distance: Distance of the offensive (how far unit should move)
|
:param distance: Distance of the offensive (how far unit should move)
|
||||||
:return: dcs.mapping.Point object with the desired position
|
:return: dcs.mapping.Point object with the desired position
|
||||||
"""
|
"""
|
||||||
desired_point = dcs_group.points[0].position.point_from_heading(frontline_heading, distance)
|
desired_point = dcs_group.points[0].position.point_from_heading(
|
||||||
|
frontline_heading, distance
|
||||||
|
)
|
||||||
if self.conflict.theater.is_on_land(desired_point):
|
if self.conflict.theater.is_on_land(desired_point):
|
||||||
return desired_point
|
return desired_point
|
||||||
return self.conflict.theater.nearest_land_pos(desired_point)
|
return self.conflict.theater.nearest_land_pos(desired_point)
|
||||||
@@ -551,7 +612,7 @@ class GroundConflictGenerator:
|
|||||||
def find_n_nearest_enemy_groups(
|
def find_n_nearest_enemy_groups(
|
||||||
player_group: VehicleGroup,
|
player_group: VehicleGroup,
|
||||||
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]],
|
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]],
|
||||||
n: int
|
n: int,
|
||||||
) -> List[VehicleGroup]:
|
) -> List[VehicleGroup]:
|
||||||
"""
|
"""
|
||||||
Return the nearest enemy group for the player group
|
Return the nearest enemy group for the player group
|
||||||
@@ -562,7 +623,9 @@ class GroundConflictGenerator:
|
|||||||
targets = [] # type: List[Optional[VehicleGroup]]
|
targets = [] # type: List[Optional[VehicleGroup]]
|
||||||
sorted_list = sorted(
|
sorted_list = sorted(
|
||||||
enemy_groups,
|
enemy_groups,
|
||||||
key=lambda group: player_group.points[0].position.distance_to_point(group[0].points[0].position)
|
key=lambda group: player_group.points[0].position.distance_to_point(
|
||||||
|
group[0].points[0].position
|
||||||
|
),
|
||||||
)
|
)
|
||||||
for i in range(n):
|
for i in range(n):
|
||||||
# TODO: Is this supposed to return no groups if enemy_groups is less than n?
|
# TODO: Is this supposed to return no groups if enemy_groups is less than n?
|
||||||
@@ -574,8 +637,7 @@ class GroundConflictGenerator:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find_nearest_enemy_group(
|
def find_nearest_enemy_group(
|
||||||
player_group: VehicleGroup,
|
player_group: VehicleGroup, enemy_groups: List[Tuple[VehicleGroup, CombatGroup]]
|
||||||
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]]
|
|
||||||
) -> Optional[VehicleGroup]:
|
) -> Optional[VehicleGroup]:
|
||||||
"""
|
"""
|
||||||
Search the enemy groups for a potential target suitable to armored assault
|
Search the enemy groups for a potential target suitable to armored assault
|
||||||
@@ -585,7 +647,9 @@ class GroundConflictGenerator:
|
|||||||
min_distance = 99999999
|
min_distance = 99999999
|
||||||
target = None
|
target = None
|
||||||
for dcs_group, _ in enemy_groups:
|
for dcs_group, _ in enemy_groups:
|
||||||
dist = player_group.points[0].position.distance_to_point(dcs_group.points[0].position)
|
dist = player_group.points[0].position.distance_to_point(
|
||||||
|
dcs_group.points[0].position
|
||||||
|
)
|
||||||
if dist < min_distance:
|
if dist < min_distance:
|
||||||
min_distance = dist
|
min_distance = dist
|
||||||
target = dcs_group
|
target = dcs_group
|
||||||
@@ -595,7 +659,7 @@ class GroundConflictGenerator:
|
|||||||
def get_artillery_target_in_range(
|
def get_artillery_target_in_range(
|
||||||
dcs_group: VehicleGroup,
|
dcs_group: VehicleGroup,
|
||||||
group: CombatGroup,
|
group: CombatGroup,
|
||||||
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]]
|
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]],
|
||||||
) -> Optional[Point]:
|
) -> Optional[Point]:
|
||||||
"""
|
"""
|
||||||
Search the enemy groups for a potential target suitable to an artillery unit
|
Search the enemy groups for a potential target suitable to an artillery unit
|
||||||
@@ -606,7 +670,9 @@ class GroundConflictGenerator:
|
|||||||
return None
|
return None
|
||||||
for _ in range(10):
|
for _ in range(10):
|
||||||
potential_target = random.choice(enemy_groups)[0]
|
potential_target = random.choice(enemy_groups)[0]
|
||||||
distance_to_target = dcs_group.points[0].position.distance_to_point(potential_target.points[0].position)
|
distance_to_target = dcs_group.points[0].position.distance_to_point(
|
||||||
|
potential_target.points[0].position
|
||||||
|
)
|
||||||
if distance_to_target < rng:
|
if distance_to_target < rng:
|
||||||
return potential_target.points[0].position
|
return potential_target.points[0].position
|
||||||
return None
|
return None
|
||||||
@@ -620,12 +686,12 @@ class GroundConflictGenerator:
|
|||||||
if rg > DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]:
|
if rg > DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]:
|
||||||
rg = random.randint(
|
rg = random.randint(
|
||||||
DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][0],
|
DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][0],
|
||||||
DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]
|
DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1],
|
||||||
)
|
)
|
||||||
elif rg < DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]:
|
elif rg < DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]:
|
||||||
rg = random.randint(
|
rg = random.randint(
|
||||||
DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK][0],
|
DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK][0],
|
||||||
DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK][1]
|
DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK][1],
|
||||||
)
|
)
|
||||||
return rg
|
return rg
|
||||||
|
|
||||||
@@ -635,42 +701,46 @@ class GroundConflictGenerator:
|
|||||||
combat_width: int,
|
combat_width: int,
|
||||||
distance_from_frontline: int,
|
distance_from_frontline: int,
|
||||||
heading: int,
|
heading: int,
|
||||||
spawn_heading: int
|
spawn_heading: int,
|
||||||
):
|
):
|
||||||
shifted = conflict_position.point_from_heading(heading, random.randint(0, combat_width))
|
shifted = conflict_position.point_from_heading(
|
||||||
desired_point = shifted.point_from_heading(
|
heading, random.randint(0, combat_width)
|
||||||
spawn_heading,
|
)
|
||||||
distance_from_frontline
|
desired_point = shifted.point_from_heading(
|
||||||
|
spawn_heading, distance_from_frontline
|
||||||
|
)
|
||||||
|
return Conflict.find_ground_position(
|
||||||
|
desired_point, combat_width, heading, self.conflict.theater
|
||||||
)
|
)
|
||||||
return Conflict.find_ground_position(desired_point, combat_width, heading, self.conflict.theater)
|
|
||||||
|
|
||||||
|
|
||||||
def _generate_groups(
|
def _generate_groups(
|
||||||
self,
|
self,
|
||||||
groups: List[CombatGroup],
|
groups: List[CombatGroup],
|
||||||
frontline_vector: Tuple[Point, int, int],
|
frontline_vector: Tuple[Point, int, int],
|
||||||
is_player: bool
|
is_player: bool,
|
||||||
) -> List[Tuple[VehicleGroup, CombatGroup]]:
|
) -> List[Tuple[VehicleGroup, CombatGroup]]:
|
||||||
"""Finds valid positions for planned groups and generates a pydcs group for them"""
|
"""Finds valid positions for planned groups and generates a pydcs group for them"""
|
||||||
positioned_groups = []
|
positioned_groups = []
|
||||||
position, heading, combat_width = frontline_vector
|
position, heading, combat_width = frontline_vector
|
||||||
spawn_heading = int(heading_sum(heading, -90)) if is_player else int(heading_sum(heading, 90))
|
spawn_heading = (
|
||||||
|
int(heading_sum(heading, -90))
|
||||||
|
if is_player
|
||||||
|
else int(heading_sum(heading, 90))
|
||||||
|
)
|
||||||
country = self.game.player_country if is_player else self.game.enemy_country
|
country = self.game.player_country if is_player else self.game.enemy_country
|
||||||
for group in groups:
|
for group in groups:
|
||||||
if group.role == CombatGroupRole.ARTILLERY:
|
if group.role == CombatGroupRole.ARTILLERY:
|
||||||
distance_from_frontline = self.get_artilery_group_distance_from_frontline(group)
|
distance_from_frontline = (
|
||||||
|
self.get_artilery_group_distance_from_frontline(group)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
distance_from_frontline = random.randint(
|
distance_from_frontline = random.randint(
|
||||||
DISTANCE_FROM_FRONTLINE[group.role][0],
|
DISTANCE_FROM_FRONTLINE[group.role][0],
|
||||||
DISTANCE_FROM_FRONTLINE[group.role][1]
|
DISTANCE_FROM_FRONTLINE[group.role][1],
|
||||||
)
|
)
|
||||||
|
|
||||||
final_position = self.get_valid_position_for_group(
|
final_position = self.get_valid_position_for_group(
|
||||||
position,
|
position, combat_width, distance_from_frontline, heading, spawn_heading
|
||||||
combat_width,
|
|
||||||
distance_from_frontline,
|
|
||||||
heading,
|
|
||||||
spawn_heading
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if final_position is not None:
|
if final_position is not None:
|
||||||
@@ -693,7 +763,7 @@ class GroundConflictGenerator:
|
|||||||
g,
|
g,
|
||||||
is_player,
|
is_player,
|
||||||
self.mission.country(country),
|
self.mission.country(country),
|
||||||
opposite_heading(spawn_heading)
|
opposite_heading(spawn_heading),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logging.warning(f"Unable to get valid position for {group}")
|
logging.warning(f"Unable to get valid position for {group}")
|
||||||
@@ -718,12 +788,14 @@ class GroundConflictGenerator:
|
|||||||
|
|
||||||
logging.info("armorgen: {} for {}".format(unit, side.id))
|
logging.info("armorgen: {} for {}".format(unit, side.id))
|
||||||
group = self.mission.vehicle_group(
|
group = self.mission.vehicle_group(
|
||||||
side,
|
side,
|
||||||
namegen.next_unit_name(side, cp.id, unit), unit,
|
namegen.next_unit_name(side, cp.id, unit),
|
||||||
position=at,
|
unit,
|
||||||
group_size=count,
|
position=at,
|
||||||
heading=heading,
|
group_size=count,
|
||||||
move_formation=move_formation)
|
heading=heading,
|
||||||
|
move_formation=move_formation,
|
||||||
|
)
|
||||||
|
|
||||||
self.unit_map.add_front_line_units(group, cp)
|
self.unit_map.add_front_line_units(group, cp)
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,8 @@ class Package:
|
|||||||
if tot is None:
|
if tot is None:
|
||||||
logging.error(
|
logging.error(
|
||||||
f"{flight} requested escort at {waypoint} but that "
|
f"{flight} requested escort at {waypoint} but that "
|
||||||
"waypoint has no TOT. It may not be escorted.")
|
"waypoint has no TOT. It may not be escorted."
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
times.append(tot)
|
times.append(tot)
|
||||||
if times:
|
if times:
|
||||||
@@ -117,7 +118,8 @@ class Package:
|
|||||||
logging.error(
|
logging.error(
|
||||||
f"{flight} dismissed escort at {waypoint} but that "
|
f"{flight} dismissed escort at {waypoint} but that "
|
||||||
"waypoint has no TOT or departure time. It may not be "
|
"waypoint has no TOT or departure time. It may not be "
|
||||||
"escorted.")
|
"escorted."
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
times.append(tot)
|
times.append(tot)
|
||||||
if times:
|
if times:
|
||||||
@@ -173,6 +175,7 @@ class Package:
|
|||||||
FlightType.SEAD,
|
FlightType.SEAD,
|
||||||
FlightType.TARCAP,
|
FlightType.TARCAP,
|
||||||
FlightType.BARCAP,
|
FlightType.BARCAP,
|
||||||
|
FlightType.AEWC,
|
||||||
FlightType.SWEEP,
|
FlightType.SWEEP,
|
||||||
FlightType.ESCORT,
|
FlightType.ESCORT,
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -20,12 +20,15 @@ from .ground_forces.combat_stance import CombatStance
|
|||||||
from .radios import RadioFrequency
|
from .radios import RadioFrequency
|
||||||
from .runways import RunwayData
|
from .runways import RunwayData
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game import Game
|
from game import Game
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class CommInfo:
|
class CommInfo:
|
||||||
"""Communications information for the kneeboard."""
|
"""Communications information for the kneeboard."""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
freq: RadioFrequency
|
freq: RadioFrequency
|
||||||
|
|
||||||
@@ -37,10 +40,13 @@ class FrontLineInfo:
|
|||||||
self.enemy_base: ControlPoint = front_line.control_point_b
|
self.enemy_base: ControlPoint = front_line.control_point_b
|
||||||
self.player_zero: bool = self.player_base.base.total_armor == 0
|
self.player_zero: bool = self.player_base.base.total_armor == 0
|
||||||
self.enemy_zero: bool = self.enemy_base.base.total_armor == 0
|
self.enemy_zero: bool = self.enemy_base.base.total_armor == 0
|
||||||
self.advantage: bool = self.player_base.base.total_armor > self.enemy_base.base.total_armor
|
self.advantage: bool = (
|
||||||
|
self.player_base.base.total_armor > self.enemy_base.base.total_armor
|
||||||
|
)
|
||||||
self.stance: CombatStance = self.player_base.stances[self.enemy_base.id]
|
self.stance: CombatStance = self.player_base.stances[self.enemy_base.id]
|
||||||
self.combat_stances = CombatStance
|
self.combat_stances = CombatStance
|
||||||
|
|
||||||
|
|
||||||
class MissionInfoGenerator:
|
class MissionInfoGenerator:
|
||||||
"""Base type for generators of mission information for the player.
|
"""Base type for generators of mission information for the player.
|
||||||
|
|
||||||
@@ -131,7 +137,6 @@ def format_waypoint_time(waypoint: FlightWaypoint, depart_prefix: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
class BriefingGenerator(MissionInfoGenerator):
|
class BriefingGenerator(MissionInfoGenerator):
|
||||||
|
|
||||||
def __init__(self, mission: Mission, game: Game):
|
def __init__(self, mission: Mission, game: Game):
|
||||||
super().__init__(mission, game)
|
super().__init__(mission, game)
|
||||||
self.allied_flights_by_departure: Dict[str, List[FlightData]] = {}
|
self.allied_flights_by_departure: Dict[str, List[FlightData]] = {}
|
||||||
@@ -141,36 +146,36 @@ class BriefingGenerator(MissionInfoGenerator):
|
|||||||
disabled_extensions=("",),
|
disabled_extensions=("",),
|
||||||
default_for_string=True,
|
default_for_string=True,
|
||||||
default=True,
|
default=True,
|
||||||
),
|
),
|
||||||
trim_blocks=True,
|
trim_blocks=True,
|
||||||
lstrip_blocks=True,
|
lstrip_blocks=True,
|
||||||
)
|
)
|
||||||
env.filters["waypoint_timing"] = format_waypoint_time
|
env.filters["waypoint_timing"] = format_waypoint_time
|
||||||
self.template = env.get_template("briefingtemplate_EN.j2")
|
self.template = env.get_template("briefingtemplate_EN.j2")
|
||||||
|
|
||||||
def generate(self) -> None:
|
def generate(self) -> None:
|
||||||
"""Generate the mission briefing
|
"""Generate the mission briefing"""
|
||||||
"""
|
|
||||||
self._generate_frontline_info()
|
self._generate_frontline_info()
|
||||||
self.generate_allied_flights_by_departure()
|
self.generate_allied_flights_by_departure()
|
||||||
self.mission.set_description_text(self.template.render(vars(self)))
|
self.mission.set_description_text(self.template.render(vars(self)))
|
||||||
self.mission.add_picture_blue(os.path.abspath(
|
self.mission.add_picture_blue(
|
||||||
"./resources/ui/splash_screen.png"))
|
os.path.abspath("./resources/ui/splash_screen.png")
|
||||||
|
)
|
||||||
|
|
||||||
def _generate_frontline_info(self) -> None:
|
def _generate_frontline_info(self) -> None:
|
||||||
"""Build FrontLineInfo objects from FrontLine type and append to briefing.
|
"""Build FrontLineInfo objects from FrontLine type and append to briefing."""
|
||||||
"""
|
|
||||||
for front_line in self.game.theater.conflicts(from_player=True):
|
for front_line in self.game.theater.conflicts(from_player=True):
|
||||||
self.add_frontline(FrontLineInfo(front_line))
|
self.add_frontline(FrontLineInfo(front_line))
|
||||||
|
|
||||||
# TODO: This should determine if runway is friendly through a method more robust than the existing string match
|
# TODO: This should determine if runway is friendly through a method more robust than the existing string match
|
||||||
def generate_allied_flights_by_departure(self) -> None:
|
def generate_allied_flights_by_departure(self) -> None:
|
||||||
"""Create iterable to display allied flights grouped by departure airfield.
|
"""Create iterable to display allied flights grouped by departure airfield."""
|
||||||
"""
|
|
||||||
for flight in self.flights:
|
for flight in self.flights:
|
||||||
if not flight.client_units and flight.friendly:
|
if not flight.client_units and flight.friendly:
|
||||||
name = flight.departure.airfield_name
|
name = flight.departure.airfield_name
|
||||||
if name in self.allied_flights_by_departure: # where else can we get this?
|
if (
|
||||||
|
name in self.allied_flights_by_departure
|
||||||
|
): # where else can we get this?
|
||||||
self.allied_flights_by_departure[name].append(flight)
|
self.allied_flights_by_departure[name].append(flight)
|
||||||
else:
|
else:
|
||||||
self.allied_flights_by_departure[name] = [flight]
|
self.allied_flights_by_departure[name] = [flight]
|
||||||
|
|||||||
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
|
FRONTLINE_LENGTH = 80000
|
||||||
|
|
||||||
|
|
||||||
class Conflict:
|
class Conflict:
|
||||||
def __init__(self,
|
def __init__(
|
||||||
theater: ConflictTheater,
|
self,
|
||||||
from_cp: ControlPoint,
|
theater: ConflictTheater,
|
||||||
to_cp: ControlPoint,
|
from_cp: ControlPoint,
|
||||||
attackers_side: str,
|
to_cp: ControlPoint,
|
||||||
defenders_side: str,
|
attackers_side: str,
|
||||||
attackers_country: Country,
|
defenders_side: str,
|
||||||
defenders_country: Country,
|
attackers_country: Country,
|
||||||
position: Point,
|
defenders_country: Country,
|
||||||
heading: Optional[int] = None,
|
position: Point,
|
||||||
size: Optional[int] = None
|
heading: Optional[int] = None,
|
||||||
):
|
size: Optional[int] = None,
|
||||||
|
):
|
||||||
|
|
||||||
self.attackers_side = attackers_side
|
self.attackers_side = attackers_side
|
||||||
self.defenders_side = defenders_side
|
self.defenders_side = defenders_side
|
||||||
@@ -43,27 +45,49 @@ class Conflict:
|
|||||||
return from_cp.has_frontline and to_cp.has_frontline
|
return from_cp.has_frontline and to_cp.has_frontline
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def frontline_position(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int]:
|
def frontline_position(
|
||||||
|
cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater
|
||||||
|
) -> Tuple[Point, int]:
|
||||||
frontline = FrontLine(from_cp, to_cp, theater)
|
frontline = FrontLine(from_cp, to_cp, theater)
|
||||||
attack_heading = frontline.attack_heading
|
attack_heading = frontline.attack_heading
|
||||||
position = cls.find_ground_position(frontline.position, FRONTLINE_LENGTH, heading_sum(attack_heading, 90), theater)
|
position = cls.find_ground_position(
|
||||||
|
frontline.position,
|
||||||
|
FRONTLINE_LENGTH,
|
||||||
|
heading_sum(attack_heading, 90),
|
||||||
|
theater,
|
||||||
|
)
|
||||||
return position, opposite_heading(attack_heading)
|
return position, opposite_heading(attack_heading)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def frontline_vector(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int, int]:
|
def frontline_vector(
|
||||||
|
cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater
|
||||||
|
) -> Tuple[Point, int, int]:
|
||||||
"""
|
"""
|
||||||
Returns a vector for a valid frontline location avoiding exclusion zones.
|
Returns a vector for a valid frontline location avoiding exclusion zones.
|
||||||
"""
|
"""
|
||||||
center_position, heading = cls.frontline_position(from_cp, to_cp, theater)
|
center_position, heading = cls.frontline_position(from_cp, to_cp, theater)
|
||||||
left_heading = heading_sum(heading, -90)
|
left_heading = heading_sum(heading, -90)
|
||||||
right_heading = heading_sum(heading, 90)
|
right_heading = heading_sum(heading, 90)
|
||||||
left_position = cls.extend_ground_position(center_position, int(FRONTLINE_LENGTH / 2), left_heading, theater)
|
left_position = cls.extend_ground_position(
|
||||||
right_position = cls.extend_ground_position(center_position, int(FRONTLINE_LENGTH / 2), right_heading, theater)
|
center_position, int(FRONTLINE_LENGTH / 2), left_heading, theater
|
||||||
|
)
|
||||||
|
right_position = cls.extend_ground_position(
|
||||||
|
center_position, int(FRONTLINE_LENGTH / 2), right_heading, theater
|
||||||
|
)
|
||||||
distance = int(left_position.distance_to_point(right_position))
|
distance = int(left_position.distance_to_point(right_position))
|
||||||
return left_position, right_heading, distance
|
return left_position, right_heading, distance
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def frontline_cas_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
|
def frontline_cas_conflict(
|
||||||
|
cls,
|
||||||
|
attacker_name: str,
|
||||||
|
defender_name: str,
|
||||||
|
attacker: Country,
|
||||||
|
defender: Country,
|
||||||
|
from_cp: ControlPoint,
|
||||||
|
to_cp: ControlPoint,
|
||||||
|
theater: ConflictTheater,
|
||||||
|
):
|
||||||
assert cls.has_frontline_between(from_cp, to_cp)
|
assert cls.has_frontline_between(from_cp, to_cp)
|
||||||
position, heading, distance = cls.frontline_vector(from_cp, to_cp, theater)
|
position, heading, distance = cls.frontline_vector(from_cp, to_cp, theater)
|
||||||
conflict = cls(
|
conflict = cls(
|
||||||
@@ -76,12 +100,14 @@ class Conflict:
|
|||||||
defenders_side=defender_name,
|
defenders_side=defender_name,
|
||||||
attackers_country=attacker,
|
attackers_country=attacker,
|
||||||
defenders_country=defender,
|
defenders_country=defender,
|
||||||
size=distance
|
size=distance,
|
||||||
)
|
)
|
||||||
return conflict
|
return conflict
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def extend_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point:
|
def extend_ground_position(
|
||||||
|
cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater
|
||||||
|
) -> Point:
|
||||||
"""Finds the first intersection with an exclusion zone in one heading from an initial point up to max_distance"""
|
"""Finds the first intersection with an exclusion zone in one heading from an initial point up to max_distance"""
|
||||||
extended = initial.point_from_heading(heading, max_distance)
|
extended = initial.point_from_heading(heading, max_distance)
|
||||||
if theater.landmap is None:
|
if theater.landmap is None:
|
||||||
@@ -92,8 +118,7 @@ class Conflict:
|
|||||||
p1 = ShapelyPoint(extended.x, extended.y)
|
p1 = ShapelyPoint(extended.x, extended.y)
|
||||||
line = LineString([p0, p1])
|
line = LineString([p0, p1])
|
||||||
|
|
||||||
intersection = line.intersection(
|
intersection = line.intersection(theater.landmap.inclusion_zone_only.boundary)
|
||||||
theater.landmap.inclusion_zone_only.boundary)
|
|
||||||
if intersection.is_empty:
|
if intersection.is_empty:
|
||||||
# Max extent does not intersect with the boundary of the inclusion
|
# Max extent does not intersect with the boundary of the inclusion
|
||||||
# zone, so the full front line is usable. This does assume that the
|
# zone, so the full front line is usable. This does assume that the
|
||||||
@@ -104,7 +129,14 @@ class Conflict:
|
|||||||
return initial.point_from_heading(heading, p0.distance(intersection))
|
return initial.point_from_heading(heading, p0.distance(intersection))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater, coerce=True) -> Optional[Point]:
|
def find_ground_position(
|
||||||
|
cls,
|
||||||
|
initial: Point,
|
||||||
|
max_distance: int,
|
||||||
|
heading: int,
|
||||||
|
theater: ConflictTheater,
|
||||||
|
coerce=True,
|
||||||
|
) -> Optional[Point]:
|
||||||
"""
|
"""
|
||||||
Finds the nearest valid ground position along a provided heading and it's inverse up to max_distance.
|
Finds the nearest valid ground position along a provided heading and it's inverse up to max_distance.
|
||||||
`coerce=True` will return the closest land position to `initial` regardless of heading or distance
|
`coerce=True` will return the closest land position to `initial` regardless of heading or distance
|
||||||
@@ -123,4 +155,3 @@ class Conflict:
|
|||||||
return pos
|
return pos
|
||||||
logging.error("Didn't find ground position ({})!".format(initial))
|
logging.error("Didn't find ground position ({})!".format(initial))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,20 @@ import random
|
|||||||
from dcs.vehicles import Armor
|
from dcs.vehicles import Armor
|
||||||
|
|
||||||
from game import db
|
from game import db
|
||||||
from gen.defenses.armored_group_generator import ArmoredGroupGenerator, FixedSizeArmorGroupGenerator
|
from gen.defenses.armored_group_generator import (
|
||||||
|
ArmoredGroupGenerator,
|
||||||
|
FixedSizeArmorGroupGenerator,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def generate_armor_group(faction:str, game, ground_object):
|
def generate_armor_group(faction: str, game, ground_object):
|
||||||
"""
|
"""
|
||||||
This generate a group of ground units
|
This generate a group of ground units
|
||||||
:return: Generated group
|
:return: Generated group
|
||||||
"""
|
"""
|
||||||
possible_unit = [u for u in db.FACTIONS[faction].frontline_units if u in Armor.__dict__.values()]
|
possible_unit = [
|
||||||
|
u for u in db.FACTIONS[faction].frontline_units if u in Armor.__dict__.values()
|
||||||
|
]
|
||||||
if len(possible_unit) > 0:
|
if len(possible_unit) > 0:
|
||||||
unit_type = random.choice(possible_unit)
|
unit_type = random.choice(possible_unit)
|
||||||
return generate_armor_group_of_type(game, ground_object, unit_type)
|
return generate_armor_group_of_type(game, ground_object, unit_type)
|
||||||
@@ -36,4 +41,3 @@ def generate_armor_group_of_type_and_size(game, ground_object, unit_type, size:
|
|||||||
generator = FixedSizeArmorGroupGenerator(game, ground_object, unit_type, size)
|
generator = FixedSizeArmorGroupGenerator(game, ground_object, unit_type, size)
|
||||||
generator.generate()
|
generator.generate()
|
||||||
return generator.get_generated_group()
|
return generator.get_generated_group()
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ from gen.sam.group_generator import GroupGenerator
|
|||||||
|
|
||||||
|
|
||||||
class ArmoredGroupGenerator(GroupGenerator):
|
class ArmoredGroupGenerator(GroupGenerator):
|
||||||
|
|
||||||
def __init__(self, game, ground_object, unit_type):
|
def __init__(self, game, ground_object, unit_type):
|
||||||
super(ArmoredGroupGenerator, self).__init__(game, ground_object)
|
super(ArmoredGroupGenerator, self).__init__(game, ground_object)
|
||||||
self.unit_type = unit_type
|
self.unit_type = unit_type
|
||||||
@@ -20,13 +19,16 @@ class ArmoredGroupGenerator(GroupGenerator):
|
|||||||
for i in range(grid_x):
|
for i in range(grid_x):
|
||||||
for j in range(grid_y):
|
for j in range(grid_y):
|
||||||
index = index + 1
|
index = index + 1
|
||||||
self.add_unit(self.unit_type, "Armor#" + str(index),
|
self.add_unit(
|
||||||
self.position.x + spacing * i,
|
self.unit_type,
|
||||||
self.position.y + spacing * j, self.heading)
|
"Armor#" + str(index),
|
||||||
|
self.position.x + spacing * i,
|
||||||
|
self.position.y + spacing * j,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FixedSizeArmorGroupGenerator(GroupGenerator):
|
class FixedSizeArmorGroupGenerator(GroupGenerator):
|
||||||
|
|
||||||
def __init__(self, game, ground_object, unit_type, size):
|
def __init__(self, game, ground_object, unit_type, size):
|
||||||
super(FixedSizeArmorGroupGenerator, self).__init__(game, ground_object)
|
super(FixedSizeArmorGroupGenerator, self).__init__(game, ground_object)
|
||||||
self.unit_type = unit_type
|
self.unit_type = unit_type
|
||||||
@@ -38,7 +40,10 @@ class FixedSizeArmorGroupGenerator(GroupGenerator):
|
|||||||
index = 0
|
index = 0
|
||||||
for i in range(self.size):
|
for i in range(self.size):
|
||||||
index = index + 1
|
index = index + 1
|
||||||
self.add_unit(self.unit_type, "Armor#" + str(index),
|
self.add_unit(
|
||||||
self.position.x + spacing * i,
|
self.unit_type,
|
||||||
self.position.y, self.heading)
|
"Armor#" + str(index),
|
||||||
|
self.position.x + spacing * i,
|
||||||
|
self.position.y,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class EnvironmentGenerator:
|
|||||||
self.mission.weather.clouds_thickness = clouds.thickness
|
self.mission.weather.clouds_thickness = clouds.thickness
|
||||||
self.mission.weather.clouds_density = clouds.density
|
self.mission.weather.clouds_density = clouds.density
|
||||||
self.mission.weather.clouds_iprecptns = clouds.precipitation
|
self.mission.weather.clouds_iprecptns = clouds.precipitation
|
||||||
|
self.mission.weather.clouds_preset = clouds.preset
|
||||||
|
|
||||||
def set_fog(self, fog: Optional[Fog]) -> None:
|
def set_fog(self, fog: Optional[Fog]) -> None:
|
||||||
if fog is None:
|
if fog is None:
|
||||||
|
|||||||
@@ -2,25 +2,122 @@ import random
|
|||||||
|
|
||||||
from gen.sam.group_generator import ShipGroupGenerator
|
from gen.sam.group_generator import ShipGroupGenerator
|
||||||
|
|
||||||
|
from dcs.ships import DDG_Arleigh_Burke_IIa, CG_Ticonderoga
|
||||||
|
|
||||||
|
|
||||||
class CarrierGroupGenerator(ShipGroupGenerator):
|
class CarrierGroupGenerator(ShipGroupGenerator):
|
||||||
|
|
||||||
def generate(self):
|
def generate(self):
|
||||||
|
|
||||||
# Add carrier
|
# Carrier Strike Group 8
|
||||||
if len(self.faction.aircraft_carrier) > 0:
|
if self.faction.carrier_names[0] == "Carrier Strike Group 8":
|
||||||
carrier_type = random.choice(self.faction.aircraft_carrier)
|
carrier_type = random.choice(self.faction.aircraft_carrier)
|
||||||
self.add_unit(carrier_type, "Carrier", self.position.x, self.position.y, self.heading)
|
|
||||||
|
self.add_unit(
|
||||||
|
carrier_type,
|
||||||
|
"CVN-75 Harry S. Truman",
|
||||||
|
self.position.x,
|
||||||
|
self.position.y,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add Arleigh Burke escort
|
||||||
|
self.add_unit(
|
||||||
|
DDG_Arleigh_Burke_IIa,
|
||||||
|
"USS Ramage",
|
||||||
|
self.position.x + 6482,
|
||||||
|
self.position.y + 6667,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.add_unit(
|
||||||
|
DDG_Arleigh_Burke_IIa,
|
||||||
|
"USS Mitscher",
|
||||||
|
self.position.x - 7963,
|
||||||
|
self.position.y + 7037,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.add_unit(
|
||||||
|
DDG_Arleigh_Burke_IIa,
|
||||||
|
"USS Forrest Sherman",
|
||||||
|
self.position.x - 7408,
|
||||||
|
self.position.y - 7408,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.add_unit(
|
||||||
|
DDG_Arleigh_Burke_IIa,
|
||||||
|
"USS Lassen",
|
||||||
|
self.position.x + 8704,
|
||||||
|
self.position.y - 6296,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add Ticonderoga escort
|
||||||
|
if self.heading >= 180:
|
||||||
|
self.add_unit(
|
||||||
|
CG_Ticonderoga,
|
||||||
|
"USS Hué City",
|
||||||
|
self.position.x + 2222,
|
||||||
|
self.position.y - 3333,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.add_unit(
|
||||||
|
CG_Ticonderoga,
|
||||||
|
"USS Hué City",
|
||||||
|
self.position.x - 3333,
|
||||||
|
self.position.y + 2222,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.get_generated_group().points[0].speed = 20
|
||||||
|
##################################################################################################
|
||||||
|
# Add carrier for normal generation
|
||||||
else:
|
else:
|
||||||
return
|
if len(self.faction.aircraft_carrier) > 0:
|
||||||
|
carrier_type = random.choice(self.faction.aircraft_carrier)
|
||||||
|
self.add_unit(
|
||||||
|
carrier_type,
|
||||||
|
"Carrier",
|
||||||
|
self.position.x,
|
||||||
|
self.position.y,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
# Add destroyers escort
|
# Add destroyers escort
|
||||||
if len(self.faction.destroyers) > 0:
|
if len(self.faction.destroyers) > 0:
|
||||||
dd_type = random.choice(self.faction.destroyers)
|
dd_type = random.choice(self.faction.destroyers)
|
||||||
self.add_unit(dd_type, "DD1", self.position.x + 2500, self.position.y + 4500, self.heading)
|
self.add_unit(
|
||||||
self.add_unit(dd_type, "DD2", self.position.x + 2500, self.position.y - 4500, self.heading)
|
dd_type,
|
||||||
|
"DD1",
|
||||||
|
self.position.x + 2500,
|
||||||
|
self.position.y + 4500,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
self.add_unit(
|
||||||
|
dd_type,
|
||||||
|
"DD2",
|
||||||
|
self.position.x + 2500,
|
||||||
|
self.position.y - 4500,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
self.add_unit(dd_type, "DD3", self.position.x + 4500, self.position.y + 8500, self.heading)
|
self.add_unit(
|
||||||
self.add_unit(dd_type, "DD4", self.position.x + 4500, self.position.y - 8500, self.heading)
|
dd_type,
|
||||||
|
"DD3",
|
||||||
|
self.position.x + 4500,
|
||||||
|
self.position.y + 8500,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
self.add_unit(
|
||||||
|
dd_type,
|
||||||
|
"DD4",
|
||||||
|
self.position.x + 4500,
|
||||||
|
self.position.y - 8500,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
self.get_generated_group().points[0].speed = 20
|
self.get_generated_group().points[0].speed = 20
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
class ChineseNavyGroupGenerator(ShipGroupGenerator):
|
class ChineseNavyGroupGenerator(ShipGroupGenerator):
|
||||||
|
|
||||||
def generate(self):
|
def generate(self):
|
||||||
|
|
||||||
include_frigate = random.choice([True, True, False])
|
include_frigate = random.choice([True, True, False])
|
||||||
@@ -30,17 +29,45 @@ class ChineseNavyGroupGenerator(ShipGroupGenerator):
|
|||||||
include_frigate = True
|
include_frigate = True
|
||||||
|
|
||||||
if include_frigate:
|
if include_frigate:
|
||||||
self.add_unit(Type_054A_Frigate, "FF1", self.position.x + 1200, self.position.y + 900, self.heading)
|
self.add_unit(
|
||||||
self.add_unit(Type_054A_Frigate, "FF2", self.position.x + 1200, self.position.y - 900, self.heading)
|
Type_054A_Frigate,
|
||||||
|
"FF1",
|
||||||
|
self.position.x + 1200,
|
||||||
|
self.position.y + 900,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
self.add_unit(
|
||||||
|
Type_054A_Frigate,
|
||||||
|
"FF2",
|
||||||
|
self.position.x + 1200,
|
||||||
|
self.position.y - 900,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
if include_dd:
|
if include_dd:
|
||||||
dd_type = random.choice([Type_052C_Destroyer, Type_052B_Destroyer])
|
dd_type = random.choice([Type_052C_Destroyer, Type_052B_Destroyer])
|
||||||
self.add_unit(dd_type, "DD1", self.position.x + 2400, self.position.y + 900, self.heading)
|
self.add_unit(
|
||||||
self.add_unit(dd_type, "DD2", self.position.x + 2400, self.position.y - 900, self.heading)
|
dd_type,
|
||||||
|
"DD1",
|
||||||
|
self.position.x + 2400,
|
||||||
|
self.position.y + 900,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
self.add_unit(
|
||||||
|
dd_type,
|
||||||
|
"DD2",
|
||||||
|
self.position.x + 2400,
|
||||||
|
self.position.y - 900,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
self.get_generated_group().points[0].speed = 20
|
self.get_generated_group().points[0].speed = 20
|
||||||
|
|
||||||
|
|
||||||
class Type54GroupGenerator(DDGroupGenerator):
|
class Type54GroupGenerator(DDGroupGenerator):
|
||||||
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
|
def __init__(
|
||||||
super(Type54GroupGenerator, self).__init__(game, ground_object, faction, Type_054A_Frigate)
|
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
||||||
|
):
|
||||||
|
super(Type54GroupGenerator, self).__init__(
|
||||||
|
game, ground_object, faction, Type_054A_Frigate
|
||||||
|
)
|
||||||
|
|||||||
@@ -6,29 +6,54 @@ from game.theater.theatergroundobject import TheaterGroundObject
|
|||||||
|
|
||||||
from gen.sam.group_generator import ShipGroupGenerator
|
from gen.sam.group_generator import ShipGroupGenerator
|
||||||
from dcs.unittype import ShipType
|
from dcs.unittype import ShipType
|
||||||
from dcs.ships import Oliver_Hazzard_Perry_class, USS_Arleigh_Burke_IIa
|
from dcs.ships import FFG_Oliver_Hazzard_Perry, DDG_Arleigh_Burke_IIa
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game.game import Game
|
from game.game import Game
|
||||||
|
|
||||||
|
|
||||||
class DDGroupGenerator(ShipGroupGenerator):
|
class DDGroupGenerator(ShipGroupGenerator):
|
||||||
|
def __init__(
|
||||||
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction, ddtype: ShipType):
|
self,
|
||||||
|
game: Game,
|
||||||
|
ground_object: TheaterGroundObject,
|
||||||
|
faction: Faction,
|
||||||
|
ddtype: ShipType,
|
||||||
|
):
|
||||||
super(DDGroupGenerator, self).__init__(game, ground_object, faction)
|
super(DDGroupGenerator, self).__init__(game, ground_object, faction)
|
||||||
self.ddtype = ddtype
|
self.ddtype = ddtype
|
||||||
|
|
||||||
def generate(self):
|
def generate(self):
|
||||||
self.add_unit(self.ddtype, "DD1", self.position.x + 500, self.position.y + 900, self.heading)
|
self.add_unit(
|
||||||
self.add_unit(self.ddtype, "DD2", self.position.x + 500, self.position.y - 900, self.heading)
|
self.ddtype,
|
||||||
|
"DD1",
|
||||||
|
self.position.x + 500,
|
||||||
|
self.position.y + 900,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
self.add_unit(
|
||||||
|
self.ddtype,
|
||||||
|
"DD2",
|
||||||
|
self.position.x + 500,
|
||||||
|
self.position.y - 900,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
self.get_generated_group().points[0].speed = 20
|
self.get_generated_group().points[0].speed = 20
|
||||||
|
|
||||||
|
|
||||||
class OliverHazardPerryGroupGenerator(DDGroupGenerator):
|
class OliverHazardPerryGroupGenerator(DDGroupGenerator):
|
||||||
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
|
def __init__(
|
||||||
super(OliverHazardPerryGroupGenerator, self).__init__(game, ground_object, faction, Oliver_Hazzard_Perry_class)
|
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
||||||
|
):
|
||||||
|
super(OliverHazardPerryGroupGenerator, self).__init__(
|
||||||
|
game, ground_object, faction, FFG_Oliver_Hazzard_Perry
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ArleighBurkeGroupGenerator(DDGroupGenerator):
|
class ArleighBurkeGroupGenerator(DDGroupGenerator):
|
||||||
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
|
def __init__(
|
||||||
super(ArleighBurkeGroupGenerator, self).__init__(game, ground_object, faction, USS_Arleigh_Burke_IIa)
|
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
||||||
|
):
|
||||||
|
super(ArleighBurkeGroupGenerator, self).__init__(
|
||||||
|
game, ground_object, faction, DDG_Arleigh_Burke_IIa
|
||||||
|
)
|
||||||
|
|||||||
@@ -4,18 +4,31 @@ from gen.sam.group_generator import ShipGroupGenerator
|
|||||||
|
|
||||||
|
|
||||||
class LHAGroupGenerator(ShipGroupGenerator):
|
class LHAGroupGenerator(ShipGroupGenerator):
|
||||||
|
|
||||||
def generate(self):
|
def generate(self):
|
||||||
|
|
||||||
# Add carrier
|
# Add carrier
|
||||||
if len(self.faction.helicopter_carrier) > 0:
|
if len(self.faction.helicopter_carrier) > 0:
|
||||||
carrier_type = random.choice(self.faction.helicopter_carrier)
|
carrier_type = random.choice(self.faction.helicopter_carrier)
|
||||||
self.add_unit(carrier_type, "LHA", self.position.x, self.position.y, self.heading)
|
self.add_unit(
|
||||||
|
carrier_type, "LHA", self.position.x, self.position.y, self.heading
|
||||||
|
)
|
||||||
|
|
||||||
# Add destroyers escort
|
# Add destroyers escort
|
||||||
if len(self.faction.destroyers) > 0:
|
if len(self.faction.destroyers) > 0:
|
||||||
dd_type = random.choice(self.faction.destroyers)
|
dd_type = random.choice(self.faction.destroyers)
|
||||||
self.add_unit(dd_type, "DD1", self.position.x + 1250, self.position.y + 1450, self.heading)
|
self.add_unit(
|
||||||
self.add_unit(dd_type, "DD2", self.position.x + 1250, self.position.y - 1450, self.heading)
|
dd_type,
|
||||||
|
"DD1",
|
||||||
|
self.position.x + 1250,
|
||||||
|
self.position.y + 1450,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
self.add_unit(
|
||||||
|
dd_type,
|
||||||
|
"DD2",
|
||||||
|
self.position.x + 1250,
|
||||||
|
self.position.y - 1450,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
self.get_generated_group().points[0].speed = 20
|
self.get_generated_group().points[0].speed = 20
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import random
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from dcs.ships import (
|
from dcs.ships import (
|
||||||
FFL_1124_4_Grisha,
|
Corvette_1124_4_Grisha,
|
||||||
FSG_1241_1MP_Molniya,
|
Corvette_1241_1_Molniya,
|
||||||
FFG_11540_Neustrashimy,
|
Frigate_11540_Neustrashimy,
|
||||||
FF_1135M_Rezky,
|
Frigate_1135M_Rezky,
|
||||||
CG_1164_Moskva,
|
Cruiser_1164_Moskva,
|
||||||
SSK_877,
|
SSK_877V_Kilo,
|
||||||
SSK_641B
|
SSK_641B_Tango,
|
||||||
)
|
)
|
||||||
|
|
||||||
from gen.fleet.dd_group import DDGroupGenerator
|
from gen.fleet.dd_group import DDGroupGenerator
|
||||||
@@ -23,7 +23,6 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
class RussianNavyGroupGenerator(ShipGroupGenerator):
|
class RussianNavyGroupGenerator(ShipGroupGenerator):
|
||||||
|
|
||||||
def generate(self):
|
def generate(self):
|
||||||
|
|
||||||
include_frigate = random.choice([True, True, False])
|
include_frigate = random.choice([True, True, False])
|
||||||
@@ -38,38 +37,86 @@ class RussianNavyGroupGenerator(ShipGroupGenerator):
|
|||||||
include_frigate = True
|
include_frigate = True
|
||||||
|
|
||||||
if include_frigate:
|
if include_frigate:
|
||||||
frigate_type = random.choice([FFL_1124_4_Grisha, FSG_1241_1MP_Molniya])
|
frigate_type = random.choice(
|
||||||
self.add_unit(frigate_type, "FF1", self.position.x + 1200, self.position.y + 900, self.heading)
|
[Corvette_1124_4_Grisha, Corvette_1241_1_Molniya]
|
||||||
self.add_unit(frigate_type, "FF2", self.position.x + 1200, self.position.y - 900, self.heading)
|
)
|
||||||
|
self.add_unit(
|
||||||
|
frigate_type,
|
||||||
|
"FF1",
|
||||||
|
self.position.x + 1200,
|
||||||
|
self.position.y + 900,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
self.add_unit(
|
||||||
|
frigate_type,
|
||||||
|
"FF2",
|
||||||
|
self.position.x + 1200,
|
||||||
|
self.position.y - 900,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
if include_dd:
|
if include_dd:
|
||||||
dd_type = random.choice([FFG_11540_Neustrashimy, FF_1135M_Rezky])
|
dd_type = random.choice([Frigate_11540_Neustrashimy, Frigate_1135M_Rezky])
|
||||||
self.add_unit(dd_type, "DD1", self.position.x + 2400, self.position.y + 900, self.heading)
|
self.add_unit(
|
||||||
self.add_unit(dd_type, "DD2", self.position.x + 2400, self.position.y - 900, self.heading)
|
dd_type,
|
||||||
|
"DD1",
|
||||||
|
self.position.x + 2400,
|
||||||
|
self.position.y + 900,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
self.add_unit(
|
||||||
|
dd_type,
|
||||||
|
"DD2",
|
||||||
|
self.position.x + 2400,
|
||||||
|
self.position.y - 900,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
if include_cc:
|
if include_cc:
|
||||||
# Only include the Moskva for now, the Pyotry Velikiy is an unkillable monster.
|
# Only include the Moskva for now, the Pyotry Velikiy is an unkillable monster.
|
||||||
# See https://github.com/Khopa/dcs_liberation/issues/567
|
# See https://github.com/Khopa/dcs_liberation/issues/567
|
||||||
self.add_unit(CG_1164_Moskva, "CC1", self.position.x, self.position.y, self.heading)
|
self.add_unit(
|
||||||
|
Cruiser_1164_Moskva,
|
||||||
|
"CC1",
|
||||||
|
self.position.x,
|
||||||
|
self.position.y,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
self.get_generated_group().points[0].speed = 20
|
self.get_generated_group().points[0].speed = 20
|
||||||
|
|
||||||
|
|
||||||
class GrishaGroupGenerator(DDGroupGenerator):
|
class GrishaGroupGenerator(DDGroupGenerator):
|
||||||
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
|
def __init__(
|
||||||
super(GrishaGroupGenerator, self).__init__(game, ground_object, faction, FFL_1124_4_Grisha)
|
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
||||||
|
):
|
||||||
|
super(GrishaGroupGenerator, self).__init__(
|
||||||
|
game, ground_object, faction, Corvette_1124_4_Grisha
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MolniyaGroupGenerator(DDGroupGenerator):
|
class MolniyaGroupGenerator(DDGroupGenerator):
|
||||||
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
|
def __init__(
|
||||||
super(MolniyaGroupGenerator, self).__init__(game, ground_object, faction, FSG_1241_1MP_Molniya)
|
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
||||||
|
):
|
||||||
|
super(MolniyaGroupGenerator, self).__init__(
|
||||||
|
game, ground_object, faction, Corvette_1241_1_Molniya
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class KiloSubGroupGenerator(DDGroupGenerator):
|
class KiloSubGroupGenerator(DDGroupGenerator):
|
||||||
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
|
def __init__(
|
||||||
super(KiloSubGroupGenerator, self).__init__(game, ground_object, faction, SSK_877)
|
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
||||||
|
):
|
||||||
|
super(KiloSubGroupGenerator, self).__init__(
|
||||||
|
game, ground_object, faction, SSK_877V_Kilo
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TangoSubGroupGenerator(DDGroupGenerator):
|
class TangoSubGroupGenerator(DDGroupGenerator):
|
||||||
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
|
def __init__(
|
||||||
super(TangoSubGroupGenerator, self).__init__(game, ground_object, faction, SSK_641B)
|
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
||||||
|
):
|
||||||
|
super(TangoSubGroupGenerator, self).__init__(
|
||||||
|
game, ground_object, faction, SSK_641B_Tango
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
import random
|
import random
|
||||||
|
|
||||||
from dcs.ships import Schnellboot_type_S130
|
from dcs.ships import Boat_Schnellboot_type_S130
|
||||||
|
|
||||||
from gen.sam.group_generator import ShipGroupGenerator
|
from gen.sam.group_generator import ShipGroupGenerator
|
||||||
|
|
||||||
|
|
||||||
class SchnellbootGroupGenerator(ShipGroupGenerator):
|
class SchnellbootGroupGenerator(ShipGroupGenerator):
|
||||||
|
|
||||||
def generate(self):
|
def generate(self):
|
||||||
|
|
||||||
for i in range(random.randint(2, 4)):
|
for i in range(random.randint(2, 4)):
|
||||||
self.add_unit(Schnellboot_type_S130, "Schnellboot" + str(i), self.position.x + i * random.randint(100, 250), self.position.y + (random.randint(100, 200)-100), self.heading)
|
self.add_unit(
|
||||||
|
Boat_Schnellboot_type_S130,
|
||||||
|
"Schnellboot" + str(i),
|
||||||
|
self.position.x + i * random.randint(100, 250),
|
||||||
|
self.position.y + (random.randint(100, 200) - 100),
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
self.get_generated_group().points[0].speed = 20
|
self.get_generated_group().points[0].speed = 20
|
||||||
|
|||||||
@@ -4,10 +4,18 @@ import random
|
|||||||
from game import db
|
from game import db
|
||||||
from gen.fleet.carrier_group import CarrierGroupGenerator
|
from gen.fleet.carrier_group import CarrierGroupGenerator
|
||||||
from gen.fleet.cn_dd_group import ChineseNavyGroupGenerator, Type54GroupGenerator
|
from gen.fleet.cn_dd_group import ChineseNavyGroupGenerator, Type54GroupGenerator
|
||||||
from gen.fleet.dd_group import ArleighBurkeGroupGenerator, OliverHazardPerryGroupGenerator
|
from gen.fleet.dd_group import (
|
||||||
|
ArleighBurkeGroupGenerator,
|
||||||
|
OliverHazardPerryGroupGenerator,
|
||||||
|
)
|
||||||
from gen.fleet.lha_group import LHAGroupGenerator
|
from gen.fleet.lha_group import LHAGroupGenerator
|
||||||
from gen.fleet.ru_dd_group import RussianNavyGroupGenerator, GrishaGroupGenerator, MolniyaGroupGenerator, \
|
from gen.fleet.ru_dd_group import (
|
||||||
KiloSubGroupGenerator, TangoSubGroupGenerator
|
RussianNavyGroupGenerator,
|
||||||
|
GrishaGroupGenerator,
|
||||||
|
MolniyaGroupGenerator,
|
||||||
|
KiloSubGroupGenerator,
|
||||||
|
TangoSubGroupGenerator,
|
||||||
|
)
|
||||||
from gen.fleet.schnellboot import SchnellbootGroupGenerator
|
from gen.fleet.schnellboot import SchnellbootGroupGenerator
|
||||||
from gen.fleet.uboat import UBoatGroupGenerator
|
from gen.fleet.uboat import UBoatGroupGenerator
|
||||||
from gen.fleet.ww2lst import WW2LSTGroupGenerator
|
from gen.fleet.ww2lst import WW2LSTGroupGenerator
|
||||||
@@ -25,7 +33,7 @@ SHIP_MAP = {
|
|||||||
"MolniyaGroupGenerator": MolniyaGroupGenerator,
|
"MolniyaGroupGenerator": MolniyaGroupGenerator,
|
||||||
"KiloSubGroupGenerator": KiloSubGroupGenerator,
|
"KiloSubGroupGenerator": KiloSubGroupGenerator,
|
||||||
"TangoSubGroupGenerator": TangoSubGroupGenerator,
|
"TangoSubGroupGenerator": TangoSubGroupGenerator,
|
||||||
"Type54GroupGenerator": Type54GroupGenerator
|
"Type54GroupGenerator": Type54GroupGenerator,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -39,10 +47,15 @@ def generate_ship_group(game, ground_object, faction_name: str):
|
|||||||
gen = random.choice(faction.navy_generators)
|
gen = random.choice(faction.navy_generators)
|
||||||
if gen in SHIP_MAP.keys():
|
if gen in SHIP_MAP.keys():
|
||||||
generator = SHIP_MAP[gen](game, ground_object, faction)
|
generator = SHIP_MAP[gen](game, ground_object, faction)
|
||||||
|
print(generator.position)
|
||||||
generator.generate()
|
generator.generate()
|
||||||
return generator.get_generated_group()
|
return generator.get_generated_group()
|
||||||
else:
|
else:
|
||||||
logging.info("Unable to generate ship group, generator : " + str(gen) + "does not exists")
|
logging.info(
|
||||||
|
"Unable to generate ship group, generator : "
|
||||||
|
+ str(gen)
|
||||||
|
+ "does not exists"
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
import random
|
import random
|
||||||
|
|
||||||
from dcs.ships import Uboat_VIIC_U_flak
|
from dcs.ships import U_boat_VIIC_U_flak
|
||||||
|
|
||||||
from gen.sam.group_generator import ShipGroupGenerator
|
from gen.sam.group_generator import ShipGroupGenerator
|
||||||
|
|
||||||
|
|
||||||
class UBoatGroupGenerator(ShipGroupGenerator):
|
class UBoatGroupGenerator(ShipGroupGenerator):
|
||||||
|
|
||||||
def generate(self):
|
def generate(self):
|
||||||
|
|
||||||
for i in range(random.randint(1, 4)):
|
for i in range(random.randint(1, 4)):
|
||||||
self.add_unit(Uboat_VIIC_U_flak, "Uboat" + str(i), self.position.x + i * random.randint(100, 250), self.position.y + (random.randint(100, 200)-100), self.heading)
|
self.add_unit(
|
||||||
|
U_boat_VIIC_U_flak,
|
||||||
|
"Uboat" + str(i),
|
||||||
|
self.position.x + i * random.randint(100, 250),
|
||||||
|
self.position.y + (random.randint(100, 200) - 100),
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
self.get_generated_group().points[0].speed = 20
|
self.get_generated_group().points[0].speed = 20
|
||||||
|
|||||||
@@ -6,13 +6,24 @@ from gen.sam.group_generator import ShipGroupGenerator
|
|||||||
|
|
||||||
|
|
||||||
class WW2LSTGroupGenerator(ShipGroupGenerator):
|
class WW2LSTGroupGenerator(ShipGroupGenerator):
|
||||||
|
|
||||||
def generate(self):
|
def generate(self):
|
||||||
|
|
||||||
# Add LS Samuel Chase
|
# Add LS Samuel Chase
|
||||||
self.add_unit(LS_Samuel_Chase, "SamuelChase", self.position.x, self.position.y, self.heading)
|
self.add_unit(
|
||||||
|
LS_Samuel_Chase,
|
||||||
|
"SamuelChase",
|
||||||
|
self.position.x,
|
||||||
|
self.position.y,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
for i in range(1, random.randint(3, 4)):
|
for i in range(1, random.randint(3, 4)):
|
||||||
self.add_unit(LST_Mk_II, "LST" + str(i), self.position.x + i * random.randint(800, 1200), self.position.y, self.heading)
|
self.add_unit(
|
||||||
|
LST_Mk_II,
|
||||||
|
"LST" + str(i),
|
||||||
|
self.position.x + i * random.randint(800, 1200),
|
||||||
|
self.position.y,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
self.get_generated_group().points[0].speed = 20
|
self.get_generated_group().points[0].speed = 20
|
||||||
|
|||||||
@@ -109,22 +109,25 @@ class ProposedMission:
|
|||||||
flights: List[ProposedFlight]
|
flights: List[ProposedFlight]
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
flights = ', '.join([str(f) for f in self.flights])
|
flights = ", ".join([str(f) for f in self.flights])
|
||||||
return f"{self.location.name}: {flights}"
|
return f"{self.location.name}: {flights}"
|
||||||
|
|
||||||
|
|
||||||
class AircraftAllocator:
|
class AircraftAllocator:
|
||||||
"""Finds suitable aircraft for proposed missions."""
|
"""Finds suitable aircraft for proposed missions."""
|
||||||
|
|
||||||
def __init__(self, closest_airfields: ClosestAirfields,
|
def __init__(
|
||||||
global_inventory: GlobalAircraftInventory,
|
self,
|
||||||
is_player: bool) -> None:
|
closest_airfields: ClosestAirfields,
|
||||||
|
global_inventory: GlobalAircraftInventory,
|
||||||
|
is_player: bool,
|
||||||
|
) -> None:
|
||||||
self.closest_airfields = closest_airfields
|
self.closest_airfields = closest_airfields
|
||||||
self.global_inventory = global_inventory
|
self.global_inventory = global_inventory
|
||||||
self.is_player = is_player
|
self.is_player = is_player
|
||||||
|
|
||||||
def find_aircraft_for_flight(
|
def find_aircraft_for_flight(
|
||||||
self, flight: ProposedFlight
|
self, flight: ProposedFlight
|
||||||
) -> Optional[Tuple[ControlPoint, Type[FlyingType]]]:
|
) -> Optional[Tuple[ControlPoint, Type[FlyingType]]]:
|
||||||
"""Finds aircraft suitable for the given mission.
|
"""Finds aircraft suitable for the given mission.
|
||||||
|
|
||||||
@@ -144,12 +147,12 @@ class AircraftAllocator:
|
|||||||
on subsequent calls. If the found aircraft are not used, the caller is
|
on subsequent calls. If the found aircraft are not used, the caller is
|
||||||
responsible for returning them to the inventory.
|
responsible for returning them to the inventory.
|
||||||
"""
|
"""
|
||||||
return self.find_aircraft_of_type(
|
return self.find_aircraft_of_type(flight, aircraft_for_task(flight.task))
|
||||||
flight, aircraft_for_task(flight.task)
|
|
||||||
)
|
|
||||||
|
|
||||||
def find_aircraft_of_type(
|
def find_aircraft_of_type(
|
||||||
self, flight: ProposedFlight, types: List[Type[FlyingType]],
|
self,
|
||||||
|
flight: ProposedFlight,
|
||||||
|
types: List[Type[FlyingType]],
|
||||||
) -> Optional[Tuple[ControlPoint, Type[FlyingType]]]:
|
) -> Optional[Tuple[ControlPoint, Type[FlyingType]]]:
|
||||||
airfields_in_range = self.closest_airfields.airfields_within(
|
airfields_in_range = self.closest_airfields.airfields_within(
|
||||||
flight.max_distance
|
flight.max_distance
|
||||||
@@ -171,18 +174,22 @@ class AircraftAllocator:
|
|||||||
class PackageBuilder:
|
class PackageBuilder:
|
||||||
"""Builds a Package for the flights it receives."""
|
"""Builds a Package for the flights it receives."""
|
||||||
|
|
||||||
def __init__(self, location: MissionTarget,
|
def __init__(
|
||||||
closest_airfields: ClosestAirfields,
|
self,
|
||||||
global_inventory: GlobalAircraftInventory,
|
location: MissionTarget,
|
||||||
is_player: bool,
|
closest_airfields: ClosestAirfields,
|
||||||
package_country: str,
|
global_inventory: GlobalAircraftInventory,
|
||||||
start_type: str) -> None:
|
is_player: bool,
|
||||||
|
package_country: str,
|
||||||
|
start_type: str,
|
||||||
|
) -> None:
|
||||||
self.closest_airfields = closest_airfields
|
self.closest_airfields = closest_airfields
|
||||||
self.is_player = is_player
|
self.is_player = is_player
|
||||||
self.package_country = package_country
|
self.package_country = package_country
|
||||||
self.package = Package(location)
|
self.package = Package(location)
|
||||||
self.allocator = AircraftAllocator(closest_airfields, global_inventory,
|
self.allocator = AircraftAllocator(
|
||||||
is_player)
|
closest_airfields, global_inventory, is_player
|
||||||
|
)
|
||||||
self.global_inventory = global_inventory
|
self.global_inventory = global_inventory
|
||||||
self.start_type = start_type
|
self.start_type = start_type
|
||||||
|
|
||||||
@@ -203,14 +210,23 @@ class PackageBuilder:
|
|||||||
else:
|
else:
|
||||||
start_type = self.start_type
|
start_type = self.start_type
|
||||||
|
|
||||||
flight = Flight(self.package, self.package_country, aircraft, plan.num_aircraft, plan.task,
|
flight = Flight(
|
||||||
start_type, departure=airfield, arrival=airfield,
|
self.package,
|
||||||
divert=self.find_divert_field(aircraft, airfield))
|
self.package_country,
|
||||||
|
aircraft,
|
||||||
|
plan.num_aircraft,
|
||||||
|
plan.task,
|
||||||
|
start_type,
|
||||||
|
departure=airfield,
|
||||||
|
arrival=airfield,
|
||||||
|
divert=self.find_divert_field(aircraft, airfield),
|
||||||
|
)
|
||||||
self.package.add_flight(flight)
|
self.package.add_flight(flight)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def find_divert_field(self, aircraft: Type[FlyingType],
|
def find_divert_field(
|
||||||
arrival: ControlPoint) -> Optional[ControlPoint]:
|
self, aircraft: Type[FlyingType], arrival: ControlPoint
|
||||||
|
) -> Optional[ControlPoint]:
|
||||||
divert_limit = nautical_miles(150)
|
divert_limit = nautical_miles(150)
|
||||||
for airfield in self.closest_airfields.airfields_within(divert_limit):
|
for airfield in self.closest_airfields.airfields_within(divert_limit):
|
||||||
if airfield.captured != self.is_player:
|
if airfield.captured != self.is_player:
|
||||||
@@ -323,8 +339,8 @@ class ObjectiveFinder:
|
|||||||
return self._targets_by_range(self.enemy_ships())
|
return self._targets_by_range(self.enemy_ships())
|
||||||
|
|
||||||
def _targets_by_range(
|
def _targets_by_range(
|
||||||
self,
|
self, targets: Iterable[MissionTarget]
|
||||||
targets: Iterable[MissionTarget]) -> Iterator[MissionTarget]:
|
) -> Iterator[MissionTarget]:
|
||||||
target_ranges: List[Tuple[MissionTarget, int]] = []
|
target_ranges: List[Tuple[MissionTarget, int]] = []
|
||||||
for target in targets:
|
for target in targets:
|
||||||
ranges: List[int] = []
|
ranges: List[int] = []
|
||||||
@@ -430,13 +446,43 @@ class ObjectiveFinder:
|
|||||||
|
|
||||||
def friendly_control_points(self) -> Iterator[ControlPoint]:
|
def friendly_control_points(self) -> Iterator[ControlPoint]:
|
||||||
"""Iterates over all friendly control points."""
|
"""Iterates over all friendly control points."""
|
||||||
return (c for c in self.game.theater.controlpoints if
|
return (
|
||||||
c.is_friendly(self.is_player))
|
c for c in self.game.theater.controlpoints if c.is_friendly(self.is_player)
|
||||||
|
)
|
||||||
|
|
||||||
|
def farthest_friendly_control_point(self) -> Optional[ControlPoint]:
|
||||||
|
"""
|
||||||
|
Iterates over all friendly control points and find the one farthest away from the frontline
|
||||||
|
BUT! prefer Cvs. Everybody likes CVs!
|
||||||
|
"""
|
||||||
|
from_frontline = 0
|
||||||
|
cp = None
|
||||||
|
first_friendly_cp = None
|
||||||
|
|
||||||
|
for c in self.game.theater.controlpoints:
|
||||||
|
if c.is_friendly(self.is_player):
|
||||||
|
if first_friendly_cp is None:
|
||||||
|
first_friendly_cp = c
|
||||||
|
if c.is_carrier:
|
||||||
|
return c
|
||||||
|
if c.has_active_frontline:
|
||||||
|
if c.distance_to(self.front_lines().__next__()) > from_frontline:
|
||||||
|
from_frontline = c.distance_to(self.front_lines().__next__())
|
||||||
|
cp = c
|
||||||
|
|
||||||
|
# If no frontlines on the map, return the first friendly cp
|
||||||
|
if cp is None:
|
||||||
|
return first_friendly_cp
|
||||||
|
else:
|
||||||
|
return cp
|
||||||
|
|
||||||
def enemy_control_points(self) -> Iterator[ControlPoint]:
|
def enemy_control_points(self) -> Iterator[ControlPoint]:
|
||||||
"""Iterates over all enemy control points."""
|
"""Iterates over all enemy control points."""
|
||||||
return (c for c in self.game.theater.controlpoints if
|
return (
|
||||||
not c.is_friendly(self.is_player))
|
c
|
||||||
|
for c in self.game.theater.controlpoints
|
||||||
|
if not c.is_friendly(self.is_player)
|
||||||
|
)
|
||||||
|
|
||||||
def all_possible_targets(self) -> Iterator[MissionTarget]:
|
def all_possible_targets(self) -> Iterator[MissionTarget]:
|
||||||
"""Iterates over all possible mission targets in the theater.
|
"""Iterates over all possible mission targets in the theater.
|
||||||
@@ -487,6 +533,7 @@ class CoalitionMissionPlanner:
|
|||||||
MAX_OCA_RANGE = nautical_miles(150)
|
MAX_OCA_RANGE = nautical_miles(150)
|
||||||
MAX_SEAD_RANGE = nautical_miles(150)
|
MAX_SEAD_RANGE = nautical_miles(150)
|
||||||
MAX_STRIKE_RANGE = nautical_miles(150)
|
MAX_STRIKE_RANGE = nautical_miles(150)
|
||||||
|
MAX_AWEC_RANGE = nautical_miles(200)
|
||||||
|
|
||||||
def __init__(self, game: Game, is_player: bool) -> None:
|
def __init__(self, game: Game, is_player: bool) -> None:
|
||||||
self.game = game
|
self.game = game
|
||||||
@@ -506,27 +553,62 @@ class CoalitionMissionPlanner:
|
|||||||
ensure that they can be planned again next turn even if all aircraft are
|
ensure that they can be planned again next turn even if all aircraft are
|
||||||
eliminated this turn.
|
eliminated this turn.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Find farthest, friendly CP for AEWC
|
||||||
|
cp = self.objective_finder.farthest_friendly_control_point()
|
||||||
|
if cp is not None:
|
||||||
|
yield ProposedMission(
|
||||||
|
cp, [ProposedFlight(FlightType.AEWC, 1, self.MAX_AWEC_RANGE)]
|
||||||
|
)
|
||||||
|
|
||||||
# Find friendly CPs within 100 nmi from an enemy airfield, plan CAP.
|
# Find friendly CPs within 100 nmi from an enemy airfield, plan CAP.
|
||||||
for cp in self.objective_finder.vulnerable_control_points():
|
for cp in self.objective_finder.vulnerable_control_points():
|
||||||
# Plan three rounds of CAP to give ~90 minutes coverage. Spacing
|
# Plan three rounds of CAP to give ~90 minutes coverage. Spacing
|
||||||
# these out appropriately is done in stagger_missions.
|
# these out appropriately is done in stagger_missions.
|
||||||
yield ProposedMission(cp, [
|
yield ProposedMission(
|
||||||
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
|
cp,
|
||||||
])
|
[
|
||||||
yield ProposedMission(cp, [
|
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
|
||||||
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
|
],
|
||||||
])
|
)
|
||||||
yield ProposedMission(cp, [
|
yield ProposedMission(
|
||||||
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
|
cp,
|
||||||
])
|
[
|
||||||
|
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
yield ProposedMission(
|
||||||
|
cp,
|
||||||
|
[
|
||||||
|
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
# Find front lines, plan CAS.
|
# Find front lines, plan CAS.
|
||||||
for front_line in self.objective_finder.front_lines():
|
for front_line in self.objective_finder.front_lines():
|
||||||
yield ProposedMission(front_line, [
|
yield ProposedMission(
|
||||||
ProposedFlight(FlightType.CAS, 2, self.MAX_CAS_RANGE),
|
front_line,
|
||||||
ProposedFlight(FlightType.TARCAP, 2, self.MAX_CAP_RANGE,
|
[
|
||||||
EscortType.AirToAir),
|
ProposedFlight(FlightType.CAS, 2, self.MAX_CAS_RANGE),
|
||||||
])
|
# This is *not* an escort because front lines don't create a threat
|
||||||
|
# zone. Generating threat zones from front lines causes the front
|
||||||
|
# line to push back BARCAPs as it gets closer to the base. While
|
||||||
|
# front lines do have the same problem of potentially pulling
|
||||||
|
# BARCAPs off bases to engage a front line TARCAP, that's probably
|
||||||
|
# the one time where we do want that.
|
||||||
|
#
|
||||||
|
# TODO: Use intercepts and extra TARCAPs to cover bases near fronts.
|
||||||
|
# We don't have intercept missions yet so this isn't something we
|
||||||
|
# can do today, but we should probably return to having the front
|
||||||
|
# line project a threat zone (so that strike missions will route
|
||||||
|
# around it) and instead *not plan* a BARCAP at bases near the
|
||||||
|
# front, since there isn't a place to put a barrier. Instead, the
|
||||||
|
# aircraft that would have been a BARCAP could be used as additional
|
||||||
|
# interceptors and TARCAPs which will defend the base but won't be
|
||||||
|
# trying to avoid front line contacts.
|
||||||
|
ProposedFlight(FlightType.TARCAP, 2, self.MAX_CAP_RANGE),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def propose_missions(self) -> Iterator[ProposedMission]:
|
def propose_missions(self) -> Iterator[ProposedMission]:
|
||||||
"""Identifies and iterates over potential mission in priority order."""
|
"""Identifies and iterates over potential mission in priority order."""
|
||||||
@@ -537,30 +619,46 @@ class CoalitionMissionPlanner:
|
|||||||
# Find enemy SAM sites with ranges that extend to within 50 nmi of
|
# Find enemy SAM sites with ranges that extend to within 50 nmi of
|
||||||
# friendly CPs, front, lines, or objects, plan DEAD.
|
# friendly CPs, front, lines, or objects, plan DEAD.
|
||||||
for sam in self.objective_finder.threatening_sams():
|
for sam in self.objective_finder.threatening_sams():
|
||||||
yield ProposedMission(sam, [
|
yield ProposedMission(
|
||||||
ProposedFlight(FlightType.DEAD, 2, self.MAX_SEAD_RANGE),
|
sam,
|
||||||
# TODO: Max escort range.
|
[
|
||||||
ProposedFlight(FlightType.ESCORT, 2, self.MAX_SEAD_RANGE,
|
ProposedFlight(FlightType.DEAD, 2, self.MAX_SEAD_RANGE),
|
||||||
EscortType.AirToAir),
|
# TODO: Max escort range.
|
||||||
])
|
ProposedFlight(
|
||||||
|
FlightType.ESCORT, 2, self.MAX_SEAD_RANGE, EscortType.AirToAir
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
for group in self.objective_finder.threatening_ships():
|
for group in self.objective_finder.threatening_ships():
|
||||||
yield ProposedMission(group, [
|
yield ProposedMission(
|
||||||
ProposedFlight(FlightType.ANTISHIP, 2, self.MAX_ANTISHIP_RANGE),
|
group,
|
||||||
# TODO: Max escort range.
|
[
|
||||||
ProposedFlight(FlightType.ESCORT, 2, self.MAX_ANTISHIP_RANGE,
|
ProposedFlight(FlightType.ANTISHIP, 2, self.MAX_ANTISHIP_RANGE),
|
||||||
EscortType.AirToAir),
|
# TODO: Max escort range.
|
||||||
])
|
ProposedFlight(
|
||||||
|
FlightType.ESCORT,
|
||||||
|
2,
|
||||||
|
self.MAX_ANTISHIP_RANGE,
|
||||||
|
EscortType.AirToAir,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
for group in self.objective_finder.threatening_vehicle_groups():
|
for group in self.objective_finder.threatening_vehicle_groups():
|
||||||
yield ProposedMission(group, [
|
yield ProposedMission(
|
||||||
ProposedFlight(FlightType.BAI, 2, self.MAX_BAI_RANGE),
|
group,
|
||||||
# TODO: Max escort range.
|
[
|
||||||
ProposedFlight(FlightType.ESCORT, 2, self.MAX_BAI_RANGE,
|
ProposedFlight(FlightType.BAI, 2, self.MAX_BAI_RANGE),
|
||||||
EscortType.AirToAir),
|
# TODO: Max escort range.
|
||||||
ProposedFlight(FlightType.SEAD, 2, self.MAX_OCA_RANGE,
|
ProposedFlight(
|
||||||
EscortType.Sead),
|
FlightType.ESCORT, 2, self.MAX_BAI_RANGE, EscortType.AirToAir
|
||||||
])
|
),
|
||||||
|
ProposedFlight(
|
||||||
|
FlightType.SEAD, 2, self.MAX_OCA_RANGE, EscortType.Sead
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
for target in self.objective_finder.oca_targets(min_aircraft=20):
|
for target in self.objective_finder.oca_targets(min_aircraft=20):
|
||||||
flights = [
|
flights = [
|
||||||
@@ -569,27 +667,37 @@ class CoalitionMissionPlanner:
|
|||||||
if self.game.settings.default_start_type == "Cold":
|
if self.game.settings.default_start_type == "Cold":
|
||||||
# Only schedule if the default start type is Cold. If the player
|
# Only schedule if the default start type is Cold. If the player
|
||||||
# has set anything else there are no targets to hit.
|
# has set anything else there are no targets to hit.
|
||||||
flights.append(ProposedFlight(FlightType.OCA_AIRCRAFT, 2,
|
flights.append(
|
||||||
self.MAX_OCA_RANGE))
|
ProposedFlight(FlightType.OCA_AIRCRAFT, 2, self.MAX_OCA_RANGE)
|
||||||
flights.extend([
|
)
|
||||||
# TODO: Max escort range.
|
flights.extend(
|
||||||
ProposedFlight(FlightType.ESCORT, 2, self.MAX_OCA_RANGE,
|
[
|
||||||
EscortType.AirToAir),
|
# TODO: Max escort range.
|
||||||
ProposedFlight(FlightType.SEAD, 2, self.MAX_OCA_RANGE,
|
ProposedFlight(
|
||||||
EscortType.Sead),
|
FlightType.ESCORT, 2, self.MAX_OCA_RANGE, EscortType.AirToAir
|
||||||
])
|
),
|
||||||
|
ProposedFlight(
|
||||||
|
FlightType.SEAD, 2, self.MAX_OCA_RANGE, EscortType.Sead
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
yield ProposedMission(target, flights)
|
yield ProposedMission(target, flights)
|
||||||
|
|
||||||
# Plan strike missions.
|
# Plan strike missions.
|
||||||
for target in self.objective_finder.strike_targets():
|
for target in self.objective_finder.strike_targets():
|
||||||
yield ProposedMission(target, [
|
yield ProposedMission(
|
||||||
ProposedFlight(FlightType.STRIKE, 2, self.MAX_STRIKE_RANGE),
|
target,
|
||||||
# TODO: Max escort range.
|
[
|
||||||
ProposedFlight(FlightType.ESCORT, 2, self.MAX_STRIKE_RANGE,
|
ProposedFlight(FlightType.STRIKE, 2, self.MAX_STRIKE_RANGE),
|
||||||
EscortType.AirToAir),
|
# TODO: Max escort range.
|
||||||
ProposedFlight(FlightType.SEAD, 2, self.MAX_STRIKE_RANGE,
|
ProposedFlight(
|
||||||
EscortType.Sead),
|
FlightType.ESCORT, 2, self.MAX_STRIKE_RANGE, EscortType.AirToAir
|
||||||
])
|
),
|
||||||
|
ProposedFlight(
|
||||||
|
FlightType.SEAD, 2, self.MAX_STRIKE_RANGE, EscortType.Sead
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def plan_missions(self) -> None:
|
def plan_missions(self) -> None:
|
||||||
"""Identifies and plans mission for the turn."""
|
"""Identifies and plans mission for the turn."""
|
||||||
@@ -604,19 +712,23 @@ class CoalitionMissionPlanner:
|
|||||||
for cp in self.objective_finder.friendly_control_points():
|
for cp in self.objective_finder.friendly_control_points():
|
||||||
inventory = self.game.aircraft_inventory.for_control_point(cp)
|
inventory = self.game.aircraft_inventory.for_control_point(cp)
|
||||||
for aircraft, available in inventory.all_aircraft:
|
for aircraft, available in inventory.all_aircraft:
|
||||||
self.message("Unused aircraft",
|
self.message("Unused aircraft", f"{available} {aircraft.id} from {cp}")
|
||||||
f"{available} {aircraft.id} from {cp}")
|
|
||||||
|
|
||||||
def plan_flight(self, mission: ProposedMission, flight: ProposedFlight,
|
def plan_flight(
|
||||||
builder: PackageBuilder, missing_types: Set[FlightType],
|
self,
|
||||||
for_reserves: bool) -> None:
|
mission: ProposedMission,
|
||||||
|
flight: ProposedFlight,
|
||||||
|
builder: PackageBuilder,
|
||||||
|
missing_types: Set[FlightType],
|
||||||
|
for_reserves: bool,
|
||||||
|
) -> None:
|
||||||
if not builder.plan_flight(flight):
|
if not builder.plan_flight(flight):
|
||||||
missing_types.add(flight.task)
|
missing_types.add(flight.task)
|
||||||
purchase_order = AircraftProcurementRequest(
|
purchase_order = AircraftProcurementRequest(
|
||||||
near=mission.location,
|
near=mission.location,
|
||||||
range=flight.max_distance,
|
range=flight.max_distance,
|
||||||
task_capability=flight.task,
|
task_capability=flight.task,
|
||||||
number=flight.num_aircraft
|
number=flight.num_aircraft,
|
||||||
)
|
)
|
||||||
if for_reserves:
|
if for_reserves:
|
||||||
# Reserves are planned for critical missions, so prioritize
|
# Reserves are planned for critical missions, so prioritize
|
||||||
@@ -626,26 +738,28 @@ class CoalitionMissionPlanner:
|
|||||||
self.procurement_requests.append(purchase_order)
|
self.procurement_requests.append(purchase_order)
|
||||||
|
|
||||||
def scrub_mission_missing_aircraft(
|
def scrub_mission_missing_aircraft(
|
||||||
self, mission: ProposedMission, builder: PackageBuilder,
|
self,
|
||||||
missing_types: Set[FlightType],
|
mission: ProposedMission,
|
||||||
not_attempted: Iterable[ProposedFlight],
|
builder: PackageBuilder,
|
||||||
reserves: bool) -> None:
|
missing_types: Set[FlightType],
|
||||||
|
not_attempted: Iterable[ProposedFlight],
|
||||||
|
reserves: bool,
|
||||||
|
) -> None:
|
||||||
# Try to plan the rest of the mission just so we can count the missing
|
# Try to plan the rest of the mission just so we can count the missing
|
||||||
# types to buy.
|
# types to buy.
|
||||||
for flight in not_attempted:
|
for flight in not_attempted:
|
||||||
self.plan_flight(mission, flight, builder, missing_types, reserves)
|
self.plan_flight(mission, flight, builder, missing_types, reserves)
|
||||||
|
|
||||||
missing_types_str = ", ".join(
|
missing_types_str = ", ".join(sorted([t.name for t in missing_types]))
|
||||||
sorted([t.name for t in missing_types]))
|
|
||||||
builder.release_planned_aircraft()
|
builder.release_planned_aircraft()
|
||||||
desc = "reserve aircraft" if reserves else "aircraft"
|
desc = "reserve aircraft" if reserves else "aircraft"
|
||||||
self.message(
|
self.message(
|
||||||
"Insufficient aircraft",
|
"Insufficient aircraft",
|
||||||
f"Not enough {desc} in range for {mission.location.name} "
|
f"Not enough {desc} in range for {mission.location.name} "
|
||||||
f"capable of: {missing_types_str}")
|
f"capable of: {missing_types_str}",
|
||||||
|
)
|
||||||
|
|
||||||
def check_needed_escorts(
|
def check_needed_escorts(self, builder: PackageBuilder) -> Dict[EscortType, bool]:
|
||||||
self, builder: PackageBuilder) -> Dict[EscortType, bool]:
|
|
||||||
threats = defaultdict(bool)
|
threats = defaultdict(bool)
|
||||||
for flight in builder.package.flights:
|
for flight in builder.package.flights:
|
||||||
if self.threat_zones.threatened_by_aircraft(flight):
|
if self.threat_zones.threatened_by_aircraft(flight):
|
||||||
@@ -654,8 +768,7 @@ class CoalitionMissionPlanner:
|
|||||||
threats[EscortType.Sead] = True
|
threats[EscortType.Sead] = True
|
||||||
return threats
|
return threats
|
||||||
|
|
||||||
def plan_mission(self, mission: ProposedMission,
|
def plan_mission(self, mission: ProposedMission, reserves: bool = False) -> None:
|
||||||
reserves: bool = False) -> None:
|
|
||||||
"""Allocates aircraft for a proposed mission and adds it to the ATO."""
|
"""Allocates aircraft for a proposed mission and adds it to the ATO."""
|
||||||
|
|
||||||
if self.is_player:
|
if self.is_player:
|
||||||
@@ -669,7 +782,7 @@ class CoalitionMissionPlanner:
|
|||||||
self.game.aircraft_inventory,
|
self.game.aircraft_inventory,
|
||||||
self.is_player,
|
self.is_player,
|
||||||
package_country,
|
package_country,
|
||||||
self.game.settings.default_start_type
|
self.game.settings.default_start_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Attempt to plan all the main elements of the mission first. Escorts
|
# Attempt to plan all the main elements of the mission first. Escorts
|
||||||
@@ -683,12 +796,12 @@ class CoalitionMissionPlanner:
|
|||||||
# If the package does not need escorts they may be pruned.
|
# If the package does not need escorts they may be pruned.
|
||||||
escorts.append(proposed_flight)
|
escorts.append(proposed_flight)
|
||||||
continue
|
continue
|
||||||
self.plan_flight(mission, proposed_flight, builder, missing_types,
|
self.plan_flight(mission, proposed_flight, builder, missing_types, reserves)
|
||||||
reserves)
|
|
||||||
|
|
||||||
if missing_types:
|
if missing_types:
|
||||||
self.scrub_mission_missing_aircraft(mission, builder, missing_types,
|
self.scrub_mission_missing_aircraft(
|
||||||
escorts, reserves)
|
mission, builder, missing_types, escorts, reserves
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Create flight plans for the main flights of the package so we can
|
# Create flight plans for the main flights of the package so we can
|
||||||
@@ -697,8 +810,9 @@ class CoalitionMissionPlanner:
|
|||||||
# flights that will rendezvous with their package will be affected by
|
# flights that will rendezvous with their package will be affected by
|
||||||
# the other flights in the package. Escorts will not be able to
|
# the other flights in the package. Escorts will not be able to
|
||||||
# contribute to this.
|
# contribute to this.
|
||||||
flight_plan_builder = FlightPlanBuilder(self.game, builder.package,
|
flight_plan_builder = FlightPlanBuilder(
|
||||||
self.is_player)
|
self.game, builder.package, self.is_player
|
||||||
|
)
|
||||||
for flight in builder.package.flights:
|
for flight in builder.package.flights:
|
||||||
flight_plan_builder.populate_flight_plan(flight)
|
flight_plan_builder.populate_flight_plan(flight)
|
||||||
|
|
||||||
@@ -708,14 +822,14 @@ class CoalitionMissionPlanner:
|
|||||||
# impossible.
|
# impossible.
|
||||||
assert escort.escort_type is not None
|
assert escort.escort_type is not None
|
||||||
if needed_escorts[escort.escort_type]:
|
if needed_escorts[escort.escort_type]:
|
||||||
self.plan_flight(mission, escort, builder, missing_types,
|
self.plan_flight(mission, escort, builder, missing_types, reserves)
|
||||||
reserves)
|
|
||||||
|
|
||||||
# Check again for unavailable aircraft. If the escort was required and
|
# Check again for unavailable aircraft. If the escort was required and
|
||||||
# none were found, scrub the mission.
|
# none were found, scrub the mission.
|
||||||
if missing_types:
|
if missing_types:
|
||||||
self.scrub_mission_missing_aircraft(mission, builder, missing_types,
|
self.scrub_mission_missing_aircraft(
|
||||||
escorts, reserves)
|
mission, builder, missing_types, escorts, reserves
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if reserves:
|
if reserves:
|
||||||
@@ -732,8 +846,9 @@ class CoalitionMissionPlanner:
|
|||||||
self.ato.add_package(package)
|
self.ato.add_package(package)
|
||||||
|
|
||||||
def stagger_missions(self) -> None:
|
def stagger_missions(self) -> None:
|
||||||
def start_time_generator(count: int, earliest: int, latest: int,
|
def start_time_generator(
|
||||||
margin: int) -> Iterator[timedelta]:
|
count: int, earliest: int, latest: int, margin: int
|
||||||
|
) -> Iterator[timedelta]:
|
||||||
interval = (latest - earliest) // count
|
interval = (latest - earliest) // count
|
||||||
for time in range(earliest, latest, interval):
|
for time in range(earliest, latest, interval):
|
||||||
error = random.randint(-margin, margin)
|
error = random.randint(-margin, margin)
|
||||||
@@ -744,17 +859,13 @@ class CoalitionMissionPlanner:
|
|||||||
FlightType.TARCAP,
|
FlightType.TARCAP,
|
||||||
}
|
}
|
||||||
|
|
||||||
previous_cap_end_time: Dict[MissionTarget, timedelta] = defaultdict(
|
previous_cap_end_time: Dict[MissionTarget, timedelta] = defaultdict(timedelta)
|
||||||
timedelta
|
non_dca_packages = [
|
||||||
)
|
p for p in self.ato.packages if p.primary_task not in dca_types
|
||||||
non_dca_packages = [p for p in self.ato.packages if
|
]
|
||||||
p.primary_task not in dca_types]
|
|
||||||
|
|
||||||
start_time = start_time_generator(
|
start_time = start_time_generator(
|
||||||
count=len(non_dca_packages),
|
count=len(non_dca_packages), earliest=5, latest=90, margin=5
|
||||||
earliest=5,
|
|
||||||
latest=90,
|
|
||||||
margin=5
|
|
||||||
)
|
)
|
||||||
for package in self.ato.packages:
|
for package in self.ato.packages:
|
||||||
tot = TotEstimator(package).earliest_tot()
|
tot = TotEstimator(package).earliest_tot()
|
||||||
@@ -771,8 +882,7 @@ class CoalitionMissionPlanner:
|
|||||||
departure_time = package.mission_departure_time
|
departure_time = package.mission_departure_time
|
||||||
# Should be impossible for CAPs
|
# Should be impossible for CAPs
|
||||||
if departure_time is None:
|
if departure_time is None:
|
||||||
logging.error(
|
logging.error(f"Could not determine mission end time for {package}")
|
||||||
f"Could not determine mission end time for {package}")
|
|
||||||
continue
|
continue
|
||||||
previous_cap_end_time[package.target] = departure_time
|
previous_cap_end_time[package.target] = departure_time
|
||||||
else:
|
else:
|
||||||
@@ -791,8 +901,6 @@ class CoalitionMissionPlanner:
|
|||||||
message to the info panel.
|
message to the info panel.
|
||||||
"""
|
"""
|
||||||
if self.is_player:
|
if self.is_player:
|
||||||
self.game.informations.append(
|
self.game.informations.append(Information(title, text, self.game.turn))
|
||||||
Information(title, text, self.game.turn)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
logging.info(f"{title}: {text}")
|
logging.info(f"{title}: {text}")
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ from dcs.helicopters import (
|
|||||||
OH_58D,
|
OH_58D,
|
||||||
SA342L,
|
SA342L,
|
||||||
SA342M,
|
SA342M,
|
||||||
|
SH_60B,
|
||||||
UH_1H,
|
UH_1H,
|
||||||
SH_60B
|
|
||||||
)
|
)
|
||||||
from dcs.planes import (
|
from dcs.planes import (
|
||||||
AJS37,
|
AJS37,
|
||||||
@@ -22,11 +22,14 @@ from dcs.planes import (
|
|||||||
A_10C,
|
A_10C,
|
||||||
A_10C_2,
|
A_10C_2,
|
||||||
A_20G,
|
A_20G,
|
||||||
|
A_50,
|
||||||
B_17G,
|
B_17G,
|
||||||
B_1B,
|
B_1B,
|
||||||
B_52H,
|
B_52H,
|
||||||
Bf_109K_4,
|
Bf_109K_4,
|
||||||
C_101CC,
|
C_101CC,
|
||||||
|
E_2C,
|
||||||
|
E_3A,
|
||||||
FA_18C_hornet,
|
FA_18C_hornet,
|
||||||
FW_190A8,
|
FW_190A8,
|
||||||
FW_190D9,
|
FW_190D9,
|
||||||
@@ -40,9 +43,11 @@ from dcs.planes import (
|
|||||||
F_4E,
|
F_4E,
|
||||||
F_5E_3,
|
F_5E_3,
|
||||||
F_86F_Sabre,
|
F_86F_Sabre,
|
||||||
|
I_16,
|
||||||
JF_17,
|
JF_17,
|
||||||
J_11A,
|
J_11A,
|
||||||
Ju_88A4,
|
Ju_88A4,
|
||||||
|
KJ_2000,
|
||||||
L_39ZA,
|
L_39ZA,
|
||||||
MQ_9_Reaper,
|
MQ_9_Reaper,
|
||||||
M_2000C,
|
M_2000C,
|
||||||
@@ -54,7 +59,6 @@ from dcs.planes import (
|
|||||||
MiG_27K,
|
MiG_27K,
|
||||||
MiG_29A,
|
MiG_29A,
|
||||||
MiG_29G,
|
MiG_29G,
|
||||||
MiG_29K,
|
|
||||||
MiG_29S,
|
MiG_29S,
|
||||||
MiG_31,
|
MiG_31,
|
||||||
Mirage_2000_5,
|
Mirage_2000_5,
|
||||||
@@ -83,18 +87,16 @@ from dcs.planes import (
|
|||||||
Tu_22M3,
|
Tu_22M3,
|
||||||
Tu_95MS,
|
Tu_95MS,
|
||||||
WingLoong_I,
|
WingLoong_I,
|
||||||
I_16
|
I_16,
|
||||||
)
|
)
|
||||||
from dcs.unittype import FlyingType
|
from dcs.unittype import FlyingType
|
||||||
|
|
||||||
from gen.flights.flight import FlightType
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
from pydcs_extensions.a4ec.a4ec import A_4E_C
|
from pydcs_extensions.a4ec.a4ec import A_4E_C
|
||||||
from pydcs_extensions.f22a.f22a import F_22A
|
from pydcs_extensions.f22a.f22a import F_22A
|
||||||
from pydcs_extensions.mb339.mb339 import MB_339PAN
|
|
||||||
from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_M, Rafale_B
|
|
||||||
from pydcs_extensions.su57.su57 import Su_57
|
|
||||||
from pydcs_extensions.hercules.hercules import Hercules
|
from pydcs_extensions.hercules.hercules import Hercules
|
||||||
|
from pydcs_extensions.mb339.mb339 import MB_339PAN
|
||||||
|
from pydcs_extensions.su57.su57 import Su_57
|
||||||
|
|
||||||
# All aircraft lists are in priority order. Aircraft higher in the list will be
|
# All aircraft lists are in priority order. Aircraft higher in the list will be
|
||||||
# preferred over those lower in the list.
|
# preferred over those lower in the list.
|
||||||
@@ -111,14 +113,12 @@ CAP_CAPABLE = [
|
|||||||
F_14B,
|
F_14B,
|
||||||
F_14A_135_GR,
|
F_14A_135_GR,
|
||||||
MiG_25PD,
|
MiG_25PD,
|
||||||
Rafale_M,
|
|
||||||
Su_33,
|
Su_33,
|
||||||
Su_30,
|
Su_30,
|
||||||
Su_27,
|
Su_27,
|
||||||
J_11A,
|
J_11A,
|
||||||
F_15C,
|
F_15C,
|
||||||
MiG_29S,
|
MiG_29S,
|
||||||
MiG_29K,
|
|
||||||
MiG_29G,
|
MiG_29G,
|
||||||
MiG_29A,
|
MiG_29A,
|
||||||
F_16C_50,
|
F_16C_50,
|
||||||
@@ -155,8 +155,8 @@ CAP_CAPABLE = [
|
|||||||
# Used for CAS (Close air support) and BAI (Battlefield Interdiction)
|
# Used for CAS (Close air support) and BAI (Battlefield Interdiction)
|
||||||
CAS_CAPABLE = [
|
CAS_CAPABLE = [
|
||||||
A_10C_2,
|
A_10C_2,
|
||||||
A_10C,
|
|
||||||
B_1B,
|
B_1B,
|
||||||
|
A_10C,
|
||||||
F_14B,
|
F_14B,
|
||||||
F_14A_135_GR,
|
F_14A_135_GR,
|
||||||
Su_25TM,
|
Su_25TM,
|
||||||
@@ -165,8 +165,6 @@ CAS_CAPABLE = [
|
|||||||
F_15E,
|
F_15E,
|
||||||
F_16C_50,
|
F_16C_50,
|
||||||
FA_18C_hornet,
|
FA_18C_hornet,
|
||||||
Rafale_A_S,
|
|
||||||
Rafale_B,
|
|
||||||
Tornado_GR4,
|
Tornado_GR4,
|
||||||
Tornado_IDS,
|
Tornado_IDS,
|
||||||
JF_17,
|
JF_17,
|
||||||
@@ -180,6 +178,7 @@ CAS_CAPABLE = [
|
|||||||
S_3B,
|
S_3B,
|
||||||
Su_34,
|
Su_34,
|
||||||
Su_30,
|
Su_30,
|
||||||
|
MiG_19P,
|
||||||
MiG_29S,
|
MiG_29S,
|
||||||
MiG_27K,
|
MiG_27K,
|
||||||
MiG_29A,
|
MiG_29A,
|
||||||
@@ -227,8 +226,6 @@ SEAD_CAPABLE = [
|
|||||||
Tornado_IDS,
|
Tornado_IDS,
|
||||||
Su_25T,
|
Su_25T,
|
||||||
Su_25TM,
|
Su_25TM,
|
||||||
Rafale_A_S,
|
|
||||||
Rafale_B,
|
|
||||||
F_4E,
|
F_4E,
|
||||||
A_4E_C,
|
A_4E_C,
|
||||||
AV8BNA,
|
AV8BNA,
|
||||||
@@ -276,8 +273,6 @@ STRIKE_CAPABLE = [
|
|||||||
Tu_22M3,
|
Tu_22M3,
|
||||||
F_15E,
|
F_15E,
|
||||||
AJS37,
|
AJS37,
|
||||||
Rafale_A_S,
|
|
||||||
Rafale_B,
|
|
||||||
Tornado_GR4,
|
Tornado_GR4,
|
||||||
F_16C_50,
|
F_16C_50,
|
||||||
FA_18C_hornet,
|
FA_18C_hornet,
|
||||||
@@ -296,7 +291,6 @@ STRIKE_CAPABLE = [
|
|||||||
Su_30,
|
Su_30,
|
||||||
Su_27,
|
Su_27,
|
||||||
MiG_29S,
|
MiG_29S,
|
||||||
MiG_29K,
|
|
||||||
MiG_29G,
|
MiG_29G,
|
||||||
MiG_29A,
|
MiG_29A,
|
||||||
JF_17,
|
JF_17,
|
||||||
@@ -333,8 +327,6 @@ ANTISHIP_CAPABLE = [
|
|||||||
AJS37,
|
AJS37,
|
||||||
Tu_22M3,
|
Tu_22M3,
|
||||||
FA_18C_hornet,
|
FA_18C_hornet,
|
||||||
Rafale_A_S,
|
|
||||||
Rafale_B,
|
|
||||||
Su_24M,
|
Su_24M,
|
||||||
Su_17M4,
|
Su_17M4,
|
||||||
JF_17,
|
JF_17,
|
||||||
@@ -367,10 +359,13 @@ TRANSPORT_CAPABLE = [
|
|||||||
UH_1H,
|
UH_1H,
|
||||||
]
|
]
|
||||||
|
|
||||||
DRONES = [
|
DRONES = [MQ_9_Reaper, RQ_1A_Predator, WingLoong_I]
|
||||||
MQ_9_Reaper,
|
|
||||||
RQ_1A_Predator,
|
AEWC_CAPABLE = [
|
||||||
WingLoong_I
|
E_3A,
|
||||||
|
E_2C,
|
||||||
|
A_50,
|
||||||
|
KJ_2000,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -396,6 +391,8 @@ def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
|
|||||||
return STRIKE_CAPABLE
|
return STRIKE_CAPABLE
|
||||||
elif task == FlightType.ESCORT:
|
elif task == FlightType.ESCORT:
|
||||||
return CAP_CAPABLE
|
return CAP_CAPABLE
|
||||||
|
elif task == FlightType.AEWC:
|
||||||
|
return AEWC_CAPABLE
|
||||||
else:
|
else:
|
||||||
logging.error(f"Unplannable flight type: {task}")
|
logging.error(f"Unplannable flight type: {task}")
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ if TYPE_CHECKING:
|
|||||||
class ClosestAirfields:
|
class ClosestAirfields:
|
||||||
"""Precalculates which control points are closes to the given target."""
|
"""Precalculates which control points are closes to the given target."""
|
||||||
|
|
||||||
def __init__(self, target: MissionTarget,
|
def __init__(
|
||||||
all_control_points: List[ControlPoint]) -> None:
|
self, target: MissionTarget, all_control_points: List[ControlPoint]
|
||||||
|
) -> None:
|
||||||
self.target = target
|
self.target = target
|
||||||
# This cache is configured once on load, so it's important that it is
|
# This cache is configured once on load, so it's important that it is
|
||||||
# complete and deterministic to avoid different behaviors across loads.
|
# complete and deterministic to avoid different behaviors across loads.
|
||||||
@@ -52,9 +53,7 @@ class ObjectiveDistanceCache:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def get_closest_airfields(cls, location: MissionTarget) -> ClosestAirfields:
|
def get_closest_airfields(cls, location: MissionTarget) -> ClosestAirfields:
|
||||||
if cls.theater is None:
|
if cls.theater is None:
|
||||||
raise RuntimeError(
|
raise RuntimeError("Call ObjectiveDistanceCache.set_theater before using")
|
||||||
"Call ObjectiveDistanceCache.set_theater before using"
|
|
||||||
)
|
|
||||||
|
|
||||||
if location.name not in cls.closest_airfields:
|
if location.name not in cls.closest_airfields:
|
||||||
cls.closest_airfields[location.name] = ClosestAirfields(
|
cls.closest_airfields[location.name] = ClosestAirfields(
|
||||||
|
|||||||
@@ -20,6 +20,15 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
class FlightType(Enum):
|
class FlightType(Enum):
|
||||||
|
"""Enumeration of mission types.
|
||||||
|
|
||||||
|
The value of each enumeration is the name that will be shown in the UI.
|
||||||
|
|
||||||
|
These values are persisted to the save game as well since they are a part of
|
||||||
|
each flight and thus a part of the ATO, so changing these values will break
|
||||||
|
save compat.
|
||||||
|
"""
|
||||||
|
|
||||||
TARCAP = "TARCAP"
|
TARCAP = "TARCAP"
|
||||||
BARCAP = "BARCAP"
|
BARCAP = "BARCAP"
|
||||||
CAS = "CAS"
|
CAS = "CAS"
|
||||||
@@ -33,28 +42,29 @@ class FlightType(Enum):
|
|||||||
SWEEP = "Fighter sweep"
|
SWEEP = "Fighter sweep"
|
||||||
OCA_RUNWAY = "OCA/Runway"
|
OCA_RUNWAY = "OCA/Runway"
|
||||||
OCA_AIRCRAFT = "OCA/Aircraft"
|
OCA_AIRCRAFT = "OCA/Aircraft"
|
||||||
|
AEWC = "AEW&C"
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
class FlightWaypointType(Enum):
|
class FlightWaypointType(Enum):
|
||||||
TAKEOFF = 0 # Take off point
|
TAKEOFF = 0 # Take off point
|
||||||
ASCEND_POINT = 1 # Ascension point after take off
|
ASCEND_POINT = 1 # Ascension point after take off
|
||||||
PATROL = 2 # Patrol point
|
PATROL = 2 # Patrol point
|
||||||
PATROL_TRACK = 3 # Patrol race track
|
PATROL_TRACK = 3 # Patrol race track
|
||||||
NAV = 4 # Nav point
|
NAV = 4 # Nav point
|
||||||
INGRESS_STRIKE = 5 # Ingress strike (For generator, means that this should have bombing on next TARGET_POINT points)
|
INGRESS_STRIKE = 5 # Ingress strike (For generator, means that this should have bombing on next TARGET_POINT points)
|
||||||
INGRESS_SEAD = 6 # Ingress sead (For generator, means that this should attack groups on TARGET_GROUP_LOC points)
|
INGRESS_SEAD = 6 # Ingress sead (For generator, means that this should attack groups on TARGET_GROUP_LOC points)
|
||||||
INGRESS_CAS = 7 # Ingress cas (should start CAS task)
|
INGRESS_CAS = 7 # Ingress cas (should start CAS task)
|
||||||
CAS = 8 # Should do CAS there
|
CAS = 8 # Should do CAS there
|
||||||
EGRESS = 9 # Should stop attack
|
EGRESS = 9 # Should stop attack
|
||||||
DESCENT_POINT = 10 # Should start descending to pattern alt
|
DESCENT_POINT = 10 # Should start descending to pattern alt
|
||||||
LANDING_POINT = 11 # Should land there
|
LANDING_POINT = 11 # Should land there
|
||||||
TARGET_POINT = 12 # A target building or static object, position
|
TARGET_POINT = 12 # A target building or static object, position
|
||||||
TARGET_GROUP_LOC = 13 # A target group approximate location
|
TARGET_GROUP_LOC = 13 # A target group approximate location
|
||||||
TARGET_SHIP = 14 # A target ship known location
|
TARGET_SHIP = 14 # A target ship known location
|
||||||
CUSTOM = 15 # User waypoint (no specific behaviour)
|
CUSTOM = 15 # User waypoint (no specific behaviour)
|
||||||
JOIN = 16
|
JOIN = 16
|
||||||
SPLIT = 17
|
SPLIT = 17
|
||||||
LOITER = 18
|
LOITER = 18
|
||||||
@@ -68,9 +78,13 @@ class FlightWaypointType(Enum):
|
|||||||
|
|
||||||
|
|
||||||
class FlightWaypoint:
|
class FlightWaypoint:
|
||||||
|
def __init__(
|
||||||
def __init__(self, waypoint_type: FlightWaypointType, x: float, y: float,
|
self,
|
||||||
alt: Distance = meters(0)) -> None:
|
waypoint_type: FlightWaypointType,
|
||||||
|
x: float,
|
||||||
|
y: float,
|
||||||
|
alt: Distance = meters(0),
|
||||||
|
) -> None:
|
||||||
"""Creates a flight waypoint.
|
"""Creates a flight waypoint.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -108,10 +122,13 @@ class FlightWaypoint:
|
|||||||
return Point(self.x, self.y)
|
return Point(self.x, self.y)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_pydcs(cls, point: MovingPoint,
|
def from_pydcs(cls, point: MovingPoint, from_cp: ControlPoint) -> "FlightWaypoint":
|
||||||
from_cp: ControlPoint) -> "FlightWaypoint":
|
waypoint = FlightWaypoint(
|
||||||
waypoint = FlightWaypoint(FlightWaypointType.NAV, point.position.x,
|
FlightWaypointType.NAV,
|
||||||
point.position.y, meters(point.alt))
|
point.position.x,
|
||||||
|
point.position.y,
|
||||||
|
meters(point.alt),
|
||||||
|
)
|
||||||
waypoint.alt_type = point.alt_type
|
waypoint.alt_type = point.alt_type
|
||||||
# Other actions exist... but none of them *should* be the first
|
# Other actions exist... but none of them *should* be the first
|
||||||
# waypoint for a flight.
|
# waypoint for a flight.
|
||||||
@@ -135,12 +152,19 @@ class FlightWaypoint:
|
|||||||
|
|
||||||
|
|
||||||
class Flight:
|
class Flight:
|
||||||
|
def __init__(
|
||||||
def __init__(self, package: Package, country: str, unit_type: Type[FlyingType],
|
self,
|
||||||
count: int, flight_type: FlightType, start_type: str,
|
package: Package,
|
||||||
departure: ControlPoint, arrival: ControlPoint,
|
country: str,
|
||||||
divert: Optional[ControlPoint],
|
unit_type: Type[FlyingType],
|
||||||
custom_name: Optional[str] = None) -> None:
|
count: int,
|
||||||
|
flight_type: FlightType,
|
||||||
|
start_type: str,
|
||||||
|
departure: ControlPoint,
|
||||||
|
arrival: ControlPoint,
|
||||||
|
divert: Optional[ControlPoint],
|
||||||
|
custom_name: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
self.package = package
|
self.package = package
|
||||||
self.country = country
|
self.country = country
|
||||||
self.unit_type = unit_type
|
self.unit_type = unit_type
|
||||||
@@ -161,10 +185,9 @@ class Flight:
|
|||||||
# FlightPlanBuilder, but an empty flight plan the flight begins with an
|
# FlightPlanBuilder, but an empty flight plan the flight begins with an
|
||||||
# empty flight plan.
|
# empty flight plan.
|
||||||
from gen.flights.flightplan import CustomFlightPlan
|
from gen.flights.flightplan import CustomFlightPlan
|
||||||
|
|
||||||
self.flight_plan: FlightPlan = CustomFlightPlan(
|
self.flight_plan: FlightPlan = CustomFlightPlan(
|
||||||
package=package,
|
package=package, flight=self, custom_waypoints=[]
|
||||||
flight=self,
|
|
||||||
custom_waypoints=[]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -182,7 +205,7 @@ class Flight:
|
|||||||
return f"[{self.flight_type}] {self.count} x {name}"
|
return f"[{self.flight_type}] {self.count} x {name}"
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
name = db.unit_get_expanded_info(self.country, self.unit_type, 'name')
|
name = db.unit_get_expanded_info(self.country, self.unit_type, "name")
|
||||||
if self.custom_name:
|
if self.custom_name:
|
||||||
return f"{self.custom_name} {self.count} x {name}"
|
return f"{self.custom_name} {self.count} x {name}"
|
||||||
return f"[{self.flight_type}] {self.count} x {name}"
|
return f"[{self.flight_type}] {self.count} x {name}"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -23,7 +23,6 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
class GroundSpeed:
|
class GroundSpeed:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def for_flight(cls, flight: Flight, altitude: Distance) -> Speed:
|
def for_flight(cls, flight: Flight, altitude: Distance) -> Speed:
|
||||||
if not issubclass(flight.unit_type, FlyingType):
|
if not issubclass(flight.unit_type, FlyingType):
|
||||||
@@ -55,13 +54,11 @@ class TravelTime:
|
|||||||
def between_points(a: Point, b: Point, speed: Speed) -> timedelta:
|
def between_points(a: Point, b: Point, speed: Speed) -> timedelta:
|
||||||
error_factor = 1.1
|
error_factor = 1.1
|
||||||
distance = meters(a.distance_to_point(b))
|
distance = meters(a.distance_to_point(b))
|
||||||
return timedelta(
|
return timedelta(hours=distance.nautical_miles / speed.knots * error_factor)
|
||||||
hours=distance.nautical_miles / speed.knots * error_factor)
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: Most if not all of this should move into FlightPlan.
|
# TODO: Most if not all of this should move into FlightPlan.
|
||||||
class TotEstimator:
|
class TotEstimator:
|
||||||
|
|
||||||
def __init__(self, package: Package) -> None:
|
def __init__(self, package: Package) -> None:
|
||||||
self.package = package
|
self.package = package
|
||||||
|
|
||||||
@@ -75,9 +72,9 @@ class TotEstimator:
|
|||||||
return startup_time
|
return startup_time
|
||||||
|
|
||||||
def earliest_tot(self) -> timedelta:
|
def earliest_tot(self) -> timedelta:
|
||||||
earliest_tot = max((
|
earliest_tot = max(
|
||||||
self.earliest_tot_for_flight(f) for f in self.package.flights
|
(self.earliest_tot_for_flight(f) for f in self.package.flights)
|
||||||
))
|
)
|
||||||
|
|
||||||
# Trim microseconds. DCS doesn't handle sub-second resolution for tasks,
|
# Trim microseconds. DCS doesn't handle sub-second resolution for tasks,
|
||||||
# and they're not interesting from a mission planning perspective so we
|
# and they're not interesting from a mission planning perspective so we
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from typing import (
|
|||||||
|
|
||||||
from dcs.mapping import Point
|
from dcs.mapping import Point
|
||||||
from dcs.unit import Unit
|
from dcs.unit import Unit
|
||||||
from dcs.unitgroup import VehicleGroup
|
from dcs.unitgroup import Group, VehicleGroup
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game import Game
|
from game import Game
|
||||||
@@ -32,12 +32,17 @@ from .flight import Flight, FlightWaypoint, FlightWaypointType
|
|||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class StrikeTarget:
|
class StrikeTarget:
|
||||||
name: str
|
name: str
|
||||||
target: Union[VehicleGroup, TheaterGroundObject, Unit]
|
target: Union[VehicleGroup, TheaterGroundObject, Unit, Group]
|
||||||
|
|
||||||
|
|
||||||
class WaypointBuilder:
|
class WaypointBuilder:
|
||||||
def __init__(self, flight: Flight, game: Game, player: bool,
|
def __init__(
|
||||||
targets: Optional[List[StrikeTarget]] = None) -> None:
|
self,
|
||||||
|
flight: Flight,
|
||||||
|
game: Game,
|
||||||
|
player: bool,
|
||||||
|
targets: Optional[List[StrikeTarget]] = None,
|
||||||
|
) -> None:
|
||||||
self.flight = flight
|
self.flight = flight
|
||||||
self.conditions = game.conditions
|
self.conditions = game.conditions
|
||||||
self.doctrine = game.faction_for(player).doctrine
|
self.doctrine = game.faction_for(player).doctrine
|
||||||
@@ -65,9 +70,7 @@ class WaypointBuilder:
|
|||||||
FlightWaypointType.NAV,
|
FlightWaypointType.NAV,
|
||||||
position.x,
|
position.x,
|
||||||
position.y,
|
position.y,
|
||||||
meters(
|
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
|
||||||
500
|
|
||||||
) if self.is_helo else self.doctrine.rendezvous_altitude
|
|
||||||
)
|
)
|
||||||
waypoint.name = "NAV"
|
waypoint.name = "NAV"
|
||||||
waypoint.alt_type = "BARO"
|
waypoint.alt_type = "BARO"
|
||||||
@@ -75,10 +78,7 @@ class WaypointBuilder:
|
|||||||
waypoint.pretty_name = "Enter theater"
|
waypoint.pretty_name = "Enter theater"
|
||||||
else:
|
else:
|
||||||
waypoint = FlightWaypoint(
|
waypoint = FlightWaypoint(
|
||||||
FlightWaypointType.TAKEOFF,
|
FlightWaypointType.TAKEOFF, position.x, position.y, meters(0)
|
||||||
position.x,
|
|
||||||
position.y,
|
|
||||||
meters(0)
|
|
||||||
)
|
)
|
||||||
waypoint.name = "TAKEOFF"
|
waypoint.name = "TAKEOFF"
|
||||||
waypoint.alt_type = "RADIO"
|
waypoint.alt_type = "RADIO"
|
||||||
@@ -98,9 +98,7 @@ class WaypointBuilder:
|
|||||||
FlightWaypointType.NAV,
|
FlightWaypointType.NAV,
|
||||||
position.x,
|
position.x,
|
||||||
position.y,
|
position.y,
|
||||||
meters(
|
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
|
||||||
500
|
|
||||||
) if self.is_helo else self.doctrine.rendezvous_altitude
|
|
||||||
)
|
)
|
||||||
waypoint.name = "NAV"
|
waypoint.name = "NAV"
|
||||||
waypoint.alt_type = "BARO"
|
waypoint.alt_type = "BARO"
|
||||||
@@ -108,10 +106,7 @@ class WaypointBuilder:
|
|||||||
waypoint.pretty_name = "Exit theater"
|
waypoint.pretty_name = "Exit theater"
|
||||||
else:
|
else:
|
||||||
waypoint = FlightWaypoint(
|
waypoint = FlightWaypoint(
|
||||||
FlightWaypointType.LANDING_POINT,
|
FlightWaypointType.LANDING_POINT, position.x, position.y, meters(0)
|
||||||
position.x,
|
|
||||||
position.y,
|
|
||||||
meters(0)
|
|
||||||
)
|
)
|
||||||
waypoint.name = "LANDING"
|
waypoint.name = "LANDING"
|
||||||
waypoint.alt_type = "RADIO"
|
waypoint.alt_type = "RADIO"
|
||||||
@@ -119,8 +114,7 @@ class WaypointBuilder:
|
|||||||
waypoint.pretty_name = "Land"
|
waypoint.pretty_name = "Land"
|
||||||
return waypoint
|
return waypoint
|
||||||
|
|
||||||
def divert(self,
|
def divert(self, divert: Optional[ControlPoint]) -> Optional[FlightWaypoint]:
|
||||||
divert: Optional[ControlPoint]) -> Optional[FlightWaypoint]:
|
|
||||||
"""Create divert waypoint for the given arrival airfield or carrier.
|
"""Create divert waypoint for the given arrival airfield or carrier.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -141,10 +135,7 @@ class WaypointBuilder:
|
|||||||
altitude_type = "RADIO"
|
altitude_type = "RADIO"
|
||||||
|
|
||||||
waypoint = FlightWaypoint(
|
waypoint = FlightWaypoint(
|
||||||
FlightWaypointType.DIVERT,
|
FlightWaypointType.DIVERT, position.x, position.y, altitude
|
||||||
position.x,
|
|
||||||
position.y,
|
|
||||||
altitude
|
|
||||||
)
|
)
|
||||||
waypoint.alt_type = altitude_type
|
waypoint.alt_type = altitude_type
|
||||||
waypoint.name = "DIVERT"
|
waypoint.name = "DIVERT"
|
||||||
@@ -158,9 +149,7 @@ class WaypointBuilder:
|
|||||||
FlightWaypointType.LOITER,
|
FlightWaypointType.LOITER,
|
||||||
position.x,
|
position.x,
|
||||||
position.y,
|
position.y,
|
||||||
meters(
|
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
|
||||||
500
|
|
||||||
) if self.is_helo else self.doctrine.rendezvous_altitude
|
|
||||||
)
|
)
|
||||||
waypoint.pretty_name = "Hold"
|
waypoint.pretty_name = "Hold"
|
||||||
waypoint.description = "Wait until push time"
|
waypoint.description = "Wait until push time"
|
||||||
@@ -172,10 +161,10 @@ class WaypointBuilder:
|
|||||||
FlightWaypointType.JOIN,
|
FlightWaypointType.JOIN,
|
||||||
position.x,
|
position.x,
|
||||||
position.y,
|
position.y,
|
||||||
meters(
|
meters(80) if self.is_helo else self.doctrine.ingress_altitude,
|
||||||
500
|
|
||||||
) if self.is_helo else self.doctrine.ingress_altitude
|
|
||||||
)
|
)
|
||||||
|
if self.is_helo:
|
||||||
|
waypoint.alt_type = "RADIO"
|
||||||
waypoint.pretty_name = "Join"
|
waypoint.pretty_name = "Join"
|
||||||
waypoint.description = "Rendezvous with package"
|
waypoint.description = "Rendezvous with package"
|
||||||
waypoint.name = "JOIN"
|
waypoint.name = "JOIN"
|
||||||
@@ -186,25 +175,29 @@ class WaypointBuilder:
|
|||||||
FlightWaypointType.SPLIT,
|
FlightWaypointType.SPLIT,
|
||||||
position.x,
|
position.x,
|
||||||
position.y,
|
position.y,
|
||||||
meters(
|
meters(80) if self.is_helo else self.doctrine.ingress_altitude,
|
||||||
500
|
|
||||||
) if self.is_helo else self.doctrine.ingress_altitude
|
|
||||||
)
|
)
|
||||||
|
if self.is_helo:
|
||||||
|
waypoint.alt_type = "RADIO"
|
||||||
waypoint.pretty_name = "Split"
|
waypoint.pretty_name = "Split"
|
||||||
waypoint.description = "Depart from package"
|
waypoint.description = "Depart from package"
|
||||||
waypoint.name = "SPLIT"
|
waypoint.name = "SPLIT"
|
||||||
return waypoint
|
return waypoint
|
||||||
|
|
||||||
def ingress(self, ingress_type: FlightWaypointType, position: Point,
|
def ingress(
|
||||||
objective: MissionTarget) -> FlightWaypoint:
|
self,
|
||||||
|
ingress_type: FlightWaypointType,
|
||||||
|
position: Point,
|
||||||
|
objective: MissionTarget,
|
||||||
|
) -> FlightWaypoint:
|
||||||
waypoint = FlightWaypoint(
|
waypoint = FlightWaypoint(
|
||||||
ingress_type,
|
ingress_type,
|
||||||
position.x,
|
position.x,
|
||||||
position.y,
|
position.y,
|
||||||
meters(
|
meters(50) if self.is_helo else self.doctrine.ingress_altitude,
|
||||||
500
|
|
||||||
) if self.is_helo else self.doctrine.ingress_altitude
|
|
||||||
)
|
)
|
||||||
|
if self.is_helo:
|
||||||
|
waypoint.alt_type = "RADIO"
|
||||||
waypoint.pretty_name = "INGRESS on " + objective.name
|
waypoint.pretty_name = "INGRESS on " + objective.name
|
||||||
waypoint.description = "INGRESS on " + objective.name
|
waypoint.description = "INGRESS on " + objective.name
|
||||||
waypoint.name = "INGRESS"
|
waypoint.name = "INGRESS"
|
||||||
@@ -217,10 +210,10 @@ class WaypointBuilder:
|
|||||||
FlightWaypointType.EGRESS,
|
FlightWaypointType.EGRESS,
|
||||||
position.x,
|
position.x,
|
||||||
position.y,
|
position.y,
|
||||||
meters(
|
meters(50) if self.is_helo else self.doctrine.ingress_altitude,
|
||||||
500
|
|
||||||
) if self.is_helo else self.doctrine.ingress_altitude
|
|
||||||
)
|
)
|
||||||
|
if self.is_helo:
|
||||||
|
waypoint.alt_type = "RADIO"
|
||||||
waypoint.pretty_name = "EGRESS from " + target.name
|
waypoint.pretty_name = "EGRESS from " + target.name
|
||||||
waypoint.description = "EGRESS from " + target.name
|
waypoint.description = "EGRESS from " + target.name
|
||||||
waypoint.name = "EGRESS"
|
waypoint.name = "EGRESS"
|
||||||
@@ -244,7 +237,7 @@ class WaypointBuilder:
|
|||||||
FlightWaypointType.TARGET_POINT,
|
FlightWaypointType.TARGET_POINT,
|
||||||
target.target.position.x,
|
target.target.position.x,
|
||||||
target.target.position.y,
|
target.target.position.y,
|
||||||
meters(0)
|
meters(0),
|
||||||
)
|
)
|
||||||
waypoint.description = description
|
waypoint.description = description
|
||||||
waypoint.pretty_name = description
|
waypoint.pretty_name = description
|
||||||
@@ -269,13 +262,14 @@ class WaypointBuilder:
|
|||||||
return self._target_area(f"ATTACK {target.name}", target, flyover=True)
|
return self._target_area(f"ATTACK {target.name}", target, flyover=True)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _target_area(name: str, location: MissionTarget,
|
def _target_area(
|
||||||
flyover: bool = False) -> FlightWaypoint:
|
name: str, location: MissionTarget, flyover: bool = False
|
||||||
|
) -> FlightWaypoint:
|
||||||
waypoint = FlightWaypoint(
|
waypoint = FlightWaypoint(
|
||||||
FlightWaypointType.TARGET_GROUP_LOC,
|
FlightWaypointType.TARGET_GROUP_LOC,
|
||||||
location.position.x,
|
location.position.x,
|
||||||
location.position.y,
|
location.position.y,
|
||||||
meters(0)
|
meters(0),
|
||||||
)
|
)
|
||||||
waypoint.description = name
|
waypoint.description = name
|
||||||
waypoint.pretty_name = name
|
waypoint.pretty_name = name
|
||||||
@@ -300,7 +294,7 @@ class WaypointBuilder:
|
|||||||
FlightWaypointType.CAS,
|
FlightWaypointType.CAS,
|
||||||
position.x,
|
position.x,
|
||||||
position.y,
|
position.y,
|
||||||
meters(500) if self.is_helo else meters(1000)
|
meters(50) if self.is_helo else meters(1000),
|
||||||
)
|
)
|
||||||
waypoint.alt_type = "RADIO"
|
waypoint.alt_type = "RADIO"
|
||||||
waypoint.description = "Provide CAS"
|
waypoint.description = "Provide CAS"
|
||||||
@@ -317,10 +311,7 @@ class WaypointBuilder:
|
|||||||
altitude: Altitude of the racetrack.
|
altitude: Altitude of the racetrack.
|
||||||
"""
|
"""
|
||||||
waypoint = FlightWaypoint(
|
waypoint = FlightWaypoint(
|
||||||
FlightWaypointType.PATROL_TRACK,
|
FlightWaypointType.PATROL_TRACK, position.x, position.y, altitude
|
||||||
position.x,
|
|
||||||
position.y,
|
|
||||||
altitude
|
|
||||||
)
|
)
|
||||||
waypoint.name = "RACETRACK START"
|
waypoint.name = "RACETRACK START"
|
||||||
waypoint.description = "Orbit between this point and the next point"
|
waypoint.description = "Orbit between this point and the next point"
|
||||||
@@ -336,18 +327,16 @@ class WaypointBuilder:
|
|||||||
altitude: Altitude of the racetrack.
|
altitude: Altitude of the racetrack.
|
||||||
"""
|
"""
|
||||||
waypoint = FlightWaypoint(
|
waypoint = FlightWaypoint(
|
||||||
FlightWaypointType.PATROL,
|
FlightWaypointType.PATROL, position.x, position.y, altitude
|
||||||
position.x,
|
|
||||||
position.y,
|
|
||||||
altitude
|
|
||||||
)
|
)
|
||||||
waypoint.name = "RACETRACK END"
|
waypoint.name = "RACETRACK END"
|
||||||
waypoint.description = "Orbit between this point and the previous point"
|
waypoint.description = "Orbit between this point and the previous point"
|
||||||
waypoint.pretty_name = "Race-track end"
|
waypoint.pretty_name = "Race-track end"
|
||||||
return waypoint
|
return waypoint
|
||||||
|
|
||||||
def race_track(self, start: Point, end: Point,
|
def race_track(
|
||||||
altitude: Distance) -> Tuple[FlightWaypoint, FlightWaypoint]:
|
self, start: Point, end: Point, altitude: Distance
|
||||||
|
) -> Tuple[FlightWaypoint, FlightWaypoint]:
|
||||||
"""Creates two waypoint for a racetrack orbit.
|
"""Creates two waypoint for a racetrack orbit.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -355,8 +344,25 @@ class WaypointBuilder:
|
|||||||
end: The ending racetrack waypoint.
|
end: The ending racetrack waypoint.
|
||||||
altitude: The racetrack altitude.
|
altitude: The racetrack altitude.
|
||||||
"""
|
"""
|
||||||
return (self.race_track_start(start, altitude),
|
return (
|
||||||
self.race_track_end(end, altitude))
|
self.race_track_start(start, altitude),
|
||||||
|
self.race_track_end(end, altitude),
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def orbit(start: Point, altitude: Distance) -> FlightWaypoint:
|
||||||
|
"""Creates an circular orbit point.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
start: Position of the waypoint.
|
||||||
|
altitude: Altitude of the racetrack.
|
||||||
|
"""
|
||||||
|
|
||||||
|
waypoint = FlightWaypoint(FlightWaypointType.LOITER, start.x, start.y, altitude)
|
||||||
|
waypoint.name = "ORBIT"
|
||||||
|
waypoint.description = "Anchor and hold at this point"
|
||||||
|
waypoint.pretty_name = "Orbit"
|
||||||
|
return waypoint
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def sweep_start(position: Point, altitude: Distance) -> FlightWaypoint:
|
def sweep_start(position: Point, altitude: Distance) -> FlightWaypoint:
|
||||||
@@ -367,10 +373,7 @@ class WaypointBuilder:
|
|||||||
altitude: Altitude of the sweep in meters.
|
altitude: Altitude of the sweep in meters.
|
||||||
"""
|
"""
|
||||||
waypoint = FlightWaypoint(
|
waypoint = FlightWaypoint(
|
||||||
FlightWaypointType.INGRESS_SWEEP,
|
FlightWaypointType.INGRESS_SWEEP, position.x, position.y, altitude
|
||||||
position.x,
|
|
||||||
position.y,
|
|
||||||
altitude
|
|
||||||
)
|
)
|
||||||
waypoint.name = "SWEEP START"
|
waypoint.name = "SWEEP START"
|
||||||
waypoint.description = "Proceed to the target and engage enemy aircraft"
|
waypoint.description = "Proceed to the target and engage enemy aircraft"
|
||||||
@@ -386,18 +389,16 @@ class WaypointBuilder:
|
|||||||
altitude: Altitude of the sweep in meters.
|
altitude: Altitude of the sweep in meters.
|
||||||
"""
|
"""
|
||||||
waypoint = FlightWaypoint(
|
waypoint = FlightWaypoint(
|
||||||
FlightWaypointType.EGRESS,
|
FlightWaypointType.EGRESS, position.x, position.y, altitude
|
||||||
position.x,
|
|
||||||
position.y,
|
|
||||||
altitude
|
|
||||||
)
|
)
|
||||||
waypoint.name = "SWEEP END"
|
waypoint.name = "SWEEP END"
|
||||||
waypoint.description = "End of sweep"
|
waypoint.description = "End of sweep"
|
||||||
waypoint.pretty_name = "Sweep end"
|
waypoint.pretty_name = "Sweep end"
|
||||||
return waypoint
|
return waypoint
|
||||||
|
|
||||||
def sweep(self, start: Point, end: Point,
|
def sweep(
|
||||||
altitude: Distance) -> Tuple[FlightWaypoint, FlightWaypoint]:
|
self, start: Point, end: Point, altitude: Distance
|
||||||
|
) -> Tuple[FlightWaypoint, FlightWaypoint]:
|
||||||
"""Creates two waypoint for a racetrack orbit.
|
"""Creates two waypoint for a racetrack orbit.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -405,11 +406,11 @@ class WaypointBuilder:
|
|||||||
end: The end of the sweep.
|
end: The end of the sweep.
|
||||||
altitude: The sweep altitude.
|
altitude: The sweep altitude.
|
||||||
"""
|
"""
|
||||||
return (self.sweep_start(start, altitude),
|
return (self.sweep_start(start, altitude), self.sweep_end(end, altitude))
|
||||||
self.sweep_end(end, altitude))
|
|
||||||
|
|
||||||
def escort(self, ingress: Point, target: MissionTarget, egress: Point) -> \
|
def escort(
|
||||||
Tuple[FlightWaypoint, FlightWaypoint, FlightWaypoint]:
|
self, ingress: Point, target: MissionTarget, egress: Point
|
||||||
|
) -> Tuple[FlightWaypoint, FlightWaypoint, FlightWaypoint]:
|
||||||
"""Creates the waypoints needed to escort the package.
|
"""Creates the waypoints needed to escort the package.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -423,17 +424,16 @@ class WaypointBuilder:
|
|||||||
# description in gen.aircraft.JoinPointBuilder), so instead we give
|
# description in gen.aircraft.JoinPointBuilder), so instead we give
|
||||||
# the escort flights a flight plan including the ingress point, target
|
# the escort flights a flight plan including the ingress point, target
|
||||||
# area, and egress point.
|
# area, and egress point.
|
||||||
ingress = self.ingress(FlightWaypointType.INGRESS_ESCORT, ingress,
|
ingress = self.ingress(FlightWaypointType.INGRESS_ESCORT, ingress, target)
|
||||||
target)
|
|
||||||
|
|
||||||
waypoint = FlightWaypoint(
|
waypoint = FlightWaypoint(
|
||||||
FlightWaypointType.TARGET_GROUP_LOC,
|
FlightWaypointType.TARGET_GROUP_LOC,
|
||||||
target.position.x,
|
target.position.x,
|
||||||
target.position.y,
|
target.position.y,
|
||||||
meters(
|
meters(50) if self.is_helo else self.doctrine.ingress_altitude,
|
||||||
500
|
|
||||||
) if self.is_helo else self.doctrine.ingress_altitude
|
|
||||||
)
|
)
|
||||||
|
if self.is_helo:
|
||||||
|
waypoint.alt_type = "RADIO"
|
||||||
waypoint.name = "TARGET"
|
waypoint.name = "TARGET"
|
||||||
waypoint.description = "Escort the package"
|
waypoint.description = "Escort the package"
|
||||||
waypoint.pretty_name = "Target area"
|
waypoint.pretty_name = "Target area"
|
||||||
@@ -450,18 +450,14 @@ class WaypointBuilder:
|
|||||||
altitude: Altitude of the waypoint.
|
altitude: Altitude of the waypoint.
|
||||||
"""
|
"""
|
||||||
waypoint = FlightWaypoint(
|
waypoint = FlightWaypoint(
|
||||||
FlightWaypointType.NAV,
|
FlightWaypointType.NAV, position.x, position.y, altitude
|
||||||
position.x,
|
|
||||||
position.y,
|
|
||||||
altitude
|
|
||||||
)
|
)
|
||||||
waypoint.name = "NAV"
|
waypoint.name = "NAV"
|
||||||
waypoint.description = "NAV"
|
waypoint.description = "NAV"
|
||||||
waypoint.pretty_name = "Nav"
|
waypoint.pretty_name = "Nav"
|
||||||
return waypoint
|
return waypoint
|
||||||
|
|
||||||
def nav_path(self, a: Point, b: Point,
|
def nav_path(self, a: Point, b: Point, altitude: Distance) -> List[FlightWaypoint]:
|
||||||
altitude: Distance) -> List[FlightWaypoint]:
|
|
||||||
path = self.clean_nav_points(self.navmesh.shortest_path(a, b))
|
path = self.clean_nav_points(self.navmesh.shortest_path(a, b))
|
||||||
return [self.nav(self.perturb(p), altitude) for p in path]
|
return [self.nav(self.perturb(p), altitude) for p in path]
|
||||||
|
|
||||||
@@ -488,10 +484,8 @@ class WaypointBuilder:
|
|||||||
previous = current
|
previous = current
|
||||||
current = nxt
|
current = nxt
|
||||||
|
|
||||||
def nav_point_prunable(self, previous: Point, current: Point,
|
def nav_point_prunable(self, previous: Point, current: Point, nxt: Point) -> bool:
|
||||||
nxt: Point) -> bool:
|
previous_threatened = self.threat_zones.path_threatened(previous, current)
|
||||||
previous_threatened = self.threat_zones.path_threatened(previous,
|
|
||||||
current)
|
|
||||||
next_threatened = self.threat_zones.path_threatened(current, nxt)
|
next_threatened = self.threat_zones.path_threatened(current, nxt)
|
||||||
pruned_threatened = self.threat_zones.path_threatened(previous, nxt)
|
pruned_threatened = self.threat_zones.path_threatened(previous, nxt)
|
||||||
previous_distance = meters(previous.distance_to_point(current))
|
previous_distance = meters(previous.distance_to_point(current))
|
||||||
|
|||||||
@@ -15,11 +15,15 @@ class ForcedOptionsGenerator:
|
|||||||
self.game = game
|
self.game = game
|
||||||
|
|
||||||
def _set_options_view(self) -> None:
|
def _set_options_view(self) -> None:
|
||||||
self.mission.forced_options.options_view = self.game.settings.map_coalition_visibility
|
self.mission.forced_options.options_view = (
|
||||||
|
self.game.settings.map_coalition_visibility
|
||||||
|
)
|
||||||
|
|
||||||
def _set_external_views(self) -> None:
|
def _set_external_views(self) -> None:
|
||||||
if not self.game.settings.external_views_allowed:
|
if not self.game.settings.external_views_allowed:
|
||||||
self.mission.forced_options.external_views = self.game.settings.external_views_allowed
|
self.mission.forced_options.external_views = (
|
||||||
|
self.game.settings.external_views_allowed
|
||||||
|
)
|
||||||
|
|
||||||
def _set_labels(self) -> None:
|
def _set_labels(self) -> None:
|
||||||
# TODO: Fix settings to use the real type.
|
# TODO: Fix settings to use the real type.
|
||||||
|
|||||||
@@ -39,12 +39,11 @@ GROUP_SIZES_BY_COMBAT_STANCE = {
|
|||||||
CombatStance.RETREAT: [2, 4, 6, 8],
|
CombatStance.RETREAT: [2, 4, 6, 8],
|
||||||
CombatStance.BREAKTHROUGH: [4, 6, 6, 8],
|
CombatStance.BREAKTHROUGH: [4, 6, 6, 8],
|
||||||
CombatStance.ELIMINATION: [2, 4, 4, 4, 6],
|
CombatStance.ELIMINATION: [2, 4, 4, 4, 6],
|
||||||
CombatStance.AMBUSH: [1, 1, 2, 2, 2, 2, 4]
|
CombatStance.AMBUSH: [1, 1, 2, 2, 2, 2, 4],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class CombatGroup:
|
class CombatGroup:
|
||||||
|
|
||||||
def __init__(self, role: CombatGroupRole):
|
def __init__(self, role: CombatGroupRole):
|
||||||
self.units: List[VehicleType] = []
|
self.units: List[VehicleType] = []
|
||||||
self.role = role
|
self.role = role
|
||||||
@@ -60,11 +59,12 @@ class CombatGroup:
|
|||||||
|
|
||||||
|
|
||||||
class GroundPlanner:
|
class GroundPlanner:
|
||||||
|
def __init__(self, cp: ControlPoint, game):
|
||||||
def __init__(self, cp:ControlPoint, game):
|
|
||||||
self.cp = cp
|
self.cp = cp
|
||||||
self.game = game
|
self.game = game
|
||||||
self.connected_enemy_cp = [cp for cp in self.cp.connected_points if cp.captured != self.cp.captured]
|
self.connected_enemy_cp = [
|
||||||
|
cp for cp in self.cp.connected_points if cp.captured != self.cp.captured
|
||||||
|
]
|
||||||
self.tank_groups: List[CombatGroup] = []
|
self.tank_groups: List[CombatGroup] = []
|
||||||
self.apc_group: List[CombatGroup] = []
|
self.apc_group: List[CombatGroup] = []
|
||||||
self.ifv_group: List[CombatGroup] = []
|
self.ifv_group: List[CombatGroup] = []
|
||||||
@@ -80,7 +80,7 @@ class GroundPlanner:
|
|||||||
|
|
||||||
def plan_groundwar(self):
|
def plan_groundwar(self):
|
||||||
|
|
||||||
if hasattr(self.cp, 'stance'):
|
if hasattr(self.cp, "stance"):
|
||||||
group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[self.cp.stance]
|
group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[self.cp.stance]
|
||||||
else:
|
else:
|
||||||
self.cp.stance = CombatStance.DEFENSIVE
|
self.cp.stance = CombatStance.DEFENSIVE
|
||||||
@@ -152,12 +152,3 @@ class GroundPlanner:
|
|||||||
print("For : #" + str(key))
|
print("For : #" + str(key))
|
||||||
for group in self.units_per_cp[key]:
|
for group in self.units_per_cp[key]:
|
||||||
print(str(group))
|
print(str(group))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,51 +14,44 @@ TYPE_TANKS = [
|
|||||||
Armor.MBT_Challenger_II,
|
Armor.MBT_Challenger_II,
|
||||||
Armor.MBT_M1A2_Abrams,
|
Armor.MBT_M1A2_Abrams,
|
||||||
Armor.MBT_M60A3_Patton,
|
Armor.MBT_M60A3_Patton,
|
||||||
Armor.MBT_Merkava_Mk__4,
|
Armor.MBT_Merkava_IV,
|
||||||
Armor.ZTZ_96B,
|
Armor.ZTZ_96B,
|
||||||
|
|
||||||
# WW2
|
# WW2
|
||||||
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
|
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
|
||||||
Armor.MT_Pz_Kpfw_IV_Ausf_H,
|
Armor.MT_PzIV_H,
|
||||||
Armor.HT_Pz_Kpfw_VI_Tiger_I,
|
Armor.HT_Pz_Kpfw_VI_Tiger_I,
|
||||||
Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II,
|
Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II,
|
||||||
Armor.MT_M4_Sherman,
|
Armor.MT_M4_Sherman,
|
||||||
Armor.MT_M4A4_Sherman_Firefly,
|
Armor.MT_M4A4_Sherman_Firefly,
|
||||||
Armor.StuG_IV,
|
Armor.SPG_StuG_IV,
|
||||||
Armor.CT_Centaur_IV,
|
Armor.CT_Centaur_IV,
|
||||||
Armor.CT_Cromwell_IV,
|
Armor.CT_Cromwell_IV,
|
||||||
Armor.HIT_Churchill_VII,
|
Armor.HIT_Churchill_VII,
|
||||||
Armor.LT_Mk_VII_Tetrarch,
|
Armor.LT_Mk_VII_Tetrarch,
|
||||||
|
|
||||||
# Mods
|
# Mods
|
||||||
frenchpack.DIM__TOYOTA_BLUE,
|
frenchpack.DIM__TOYOTA_BLUE,
|
||||||
frenchpack.DIM__TOYOTA_GREEN,
|
frenchpack.DIM__TOYOTA_GREEN,
|
||||||
frenchpack.DIM__TOYOTA_DESERT,
|
frenchpack.DIM__TOYOTA_DESERT,
|
||||||
frenchpack.DIM__KAMIKAZE,
|
frenchpack.DIM__KAMIKAZE,
|
||||||
|
|
||||||
frenchpack.AMX_10RCR,
|
frenchpack.AMX_10RCR,
|
||||||
frenchpack.AMX_10RCR_SEPAR,
|
frenchpack.AMX_10RCR_SEPAR,
|
||||||
frenchpack.AMX_30B2,
|
frenchpack.AMX_30B2,
|
||||||
frenchpack.Leclerc_Serie_XXI,
|
frenchpack.Leclerc_Serie_XXI,
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
TYPE_ATGM = [
|
TYPE_ATGM = [
|
||||||
Armor.ATGM_M1045_HMMWV_TOW,
|
Armor.ATGM_HMMWV,
|
||||||
Armor.ATGM_M1134_Stryker,
|
Armor.ATGM_Stryker,
|
||||||
Armor.IFV_BMP_2,
|
Armor.IFV_BMP_2,
|
||||||
|
|
||||||
# WW2 (Tank Destroyers)
|
# WW2 (Tank Destroyers)
|
||||||
Armor.M30_Cargo_Carrier,
|
Unarmed.Carrier_M30_Cargo,
|
||||||
Armor.TD_Jagdpanzer_IV,
|
Armor.SPG_Jagdpanzer_IV,
|
||||||
Armor.TD_Jagdpanther_G1,
|
Armor.SPG_Jagdpanther_G1,
|
||||||
Armor.TD_M10_GMC,
|
Armor.SPG_M10_GMC,
|
||||||
|
|
||||||
# Mods
|
# Mods
|
||||||
frenchpack.VBAE_CRAB_MMP,
|
frenchpack.VBAE_CRAB_MMP,
|
||||||
frenchpack.VAB_MEPHISTO,
|
frenchpack.VAB_MEPHISTO,
|
||||||
frenchpack.TRM_2000_PAMELA,
|
frenchpack.TRM_2000_PAMELA,
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
TYPE_IFV = [
|
TYPE_IFV = [
|
||||||
@@ -66,134 +59,124 @@ TYPE_IFV = [
|
|||||||
Armor.IFV_BMP_2,
|
Armor.IFV_BMP_2,
|
||||||
Armor.IFV_BMP_1,
|
Armor.IFV_BMP_1,
|
||||||
Armor.IFV_Marder,
|
Armor.IFV_Marder,
|
||||||
Armor.IFV_MCV_80,
|
Armor.IFV_Warrior,
|
||||||
Armor.IFV_LAV_25,
|
Armor.IFV_LAV_25,
|
||||||
Armor.SPG_M1128_Stryker_MGS,
|
Armor.SPG_Stryker_MGS,
|
||||||
Armor.AC_Sd_Kfz_234_2_Puma,
|
Armor.IFV_Sd_Kfz_234_2_Puma,
|
||||||
Armor.IFV_M2A2_Bradley,
|
Armor.IFV_M2A2_Bradley,
|
||||||
Armor.IFV_BMD_1,
|
Armor.IFV_BMD_1,
|
||||||
Armor.ZBD_04A,
|
Armor.ZBD_04A,
|
||||||
|
|
||||||
# WW2
|
# WW2
|
||||||
Armor.AC_Sd_Kfz_234_2_Puma,
|
Armor.IFV_Sd_Kfz_234_2_Puma,
|
||||||
Armor.LAC_M8_Greyhound,
|
Armor.Car_M8_Greyhound_Armored,
|
||||||
Armor.Daimler_Armoured_Car,
|
Armor.Car_Daimler_Armored,
|
||||||
|
|
||||||
# Mods
|
# Mods
|
||||||
frenchpack.ERC_90,
|
frenchpack.ERC_90,
|
||||||
frenchpack.VBAE_CRAB,
|
frenchpack.VBAE_CRAB,
|
||||||
frenchpack.VAB_T20_13
|
frenchpack.VAB_T20_13,
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
TYPE_APC = [
|
TYPE_APC = [
|
||||||
Armor.APC_M1043_HMMWV_Armament,
|
Armor.APC_HMMWV__Scout,
|
||||||
Armor.APC_M1126_Stryker_ICV,
|
Armor.IFV_M1126_Stryker_ICV,
|
||||||
Armor.APC_M113,
|
Armor.APC_M113,
|
||||||
Armor.APC_BTR_80,
|
Armor.APC_BTR_80,
|
||||||
Armor.APC_BTR_82A,
|
Armor.APC_BTR_82A,
|
||||||
Armor.APC_MTLB,
|
Armor.APC_MTLB,
|
||||||
Armor.APC_M2A1,
|
Armor.APC_M2A1_Halftrack,
|
||||||
Armor.APC_Cobra,
|
Armor.APC_Cobra__Scout,
|
||||||
Armor.APC_Sd_Kfz_251,
|
Armor.APC_Sd_Kfz_251_Halftrack,
|
||||||
Armor.APC_AAV_7,
|
Armor.APC_AAV_7_Amphibious,
|
||||||
Armor.TPz_Fuchs,
|
Armor.APC_TPz_Fuchs,
|
||||||
Armor.ARV_BRDM_2,
|
Armor.IFV_BRDM_2,
|
||||||
Armor.ARV_BTR_RD,
|
Armor.APC_BTR_RD,
|
||||||
Armor.FDDM_Grad,
|
Artillery.Grad_MRL_FDDM__FC,
|
||||||
|
|
||||||
# WW2
|
# WW2
|
||||||
Armor.APC_M2A1,
|
Armor.APC_M2A1_Halftrack,
|
||||||
Armor.APC_Sd_Kfz_251,
|
Armor.APC_Sd_Kfz_251_Halftrack,
|
||||||
|
|
||||||
# Mods
|
# Mods
|
||||||
frenchpack.VAB__50,
|
frenchpack.VAB__50,
|
||||||
frenchpack.VBL__50,
|
frenchpack.VBL__50,
|
||||||
frenchpack.VBL_AANF1,
|
frenchpack.VBL_AANF1,
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
TYPE_ARTILLERY = [
|
TYPE_ARTILLERY = [
|
||||||
Artillery.MLRS_9A52_Smerch,
|
Artillery.MLRS_9A52_Smerch_HE_300mm,
|
||||||
Artillery.SPH_2S1_Gvozdika,
|
Artillery.SPH_2S1_Gvozdika_122mm,
|
||||||
Artillery.SPH_2S3_Akatsia,
|
Artillery.SPH_2S3_Akatsia_152mm,
|
||||||
Artillery.MLRS_BM_21_Grad,
|
Artillery.MLRS_BM_21_Grad_122mm,
|
||||||
Artillery.MLRS_9K57_Uragan_BM_27,
|
Artillery.MLRS_BM_27_Uragan_220mm,
|
||||||
Artillery.SPH_M109_Paladin,
|
Artillery.SPH_M109_Paladin_155mm,
|
||||||
Artillery.MLRS_M270,
|
Artillery.MLRS_M270_227mm,
|
||||||
Artillery.SPH_2S9_Nona,
|
Artillery.SPH_2S9_Nona_120mm_M,
|
||||||
Artillery.SpGH_Dana,
|
Artillery.SPH_Dana_vz77_152mm,
|
||||||
Artillery.SPH_2S19_Msta,
|
Artillery.PLZ_05,
|
||||||
Artillery.MLRS_FDDM,
|
Artillery.SPH_2S19_Msta_152mm,
|
||||||
|
Artillery.MLRS_9A52_Smerch_CM_300mm,
|
||||||
# WW2
|
# WW2
|
||||||
Artillery.Sturmpanzer_IV_Brummbär,
|
Artillery.SPG_Sturmpanzer_IV_Brummbar,
|
||||||
Artillery.M12_GMC
|
Artillery.SPG_M12_GMC_155mm,
|
||||||
]
|
]
|
||||||
|
|
||||||
TYPE_LOGI = [
|
TYPE_LOGI = [
|
||||||
Unarmed.Transport_M818,
|
Unarmed.Truck_M818_6x6,
|
||||||
Unarmed.Transport_KAMAZ_43101,
|
Unarmed.Truck_KAMAZ_43101,
|
||||||
Unarmed.Transport_Ural_375,
|
Unarmed.Truck_Ural_375,
|
||||||
Unarmed.Transport_GAZ_66,
|
Unarmed.Truck_GAZ_66,
|
||||||
Unarmed.Transport_GAZ_3307,
|
Unarmed.Truck_GAZ_3307,
|
||||||
Unarmed.Transport_GAZ_3308,
|
Unarmed.Truck_GAZ_3308,
|
||||||
Unarmed.Transport_Ural_4320_31_Armored,
|
Unarmed.Truck_Ural_4320_31_Arm_d,
|
||||||
Unarmed.Transport_Ural_4320T,
|
Unarmed.Truck_Ural_4320T,
|
||||||
Unarmed.Blitz_3_6_6700A,
|
Unarmed.Truck_Opel_Blitz,
|
||||||
Unarmed.Kübelwagen_82,
|
Unarmed.LUV_Kubelwagen_82,
|
||||||
Unarmed.Sd_Kfz_7,
|
Unarmed.Carrier_Sd_Kfz_7_Tractor,
|
||||||
Unarmed.Sd_Kfz_2,
|
Unarmed.LUV_Kettenrad,
|
||||||
Unarmed.Willys_MB,
|
Unarmed.Car_Willys_Jeep,
|
||||||
Unarmed.Land_Rover_109_S3,
|
Unarmed.LUV_Land_Rover_109,
|
||||||
Unarmed.Land_Rover_101_FC,
|
Unarmed.Truck_Land_Rover_101_FC,
|
||||||
|
|
||||||
# Mods
|
# Mods
|
||||||
frenchpack.VBL,
|
frenchpack.VBL,
|
||||||
frenchpack.VAB,
|
frenchpack.VAB,
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
TYPE_INFANTRY = [
|
TYPE_INFANTRY = [
|
||||||
Infantry.Infantry_Soldier_Insurgents,
|
Infantry.Insurgent_AK_74,
|
||||||
Infantry.Soldier_AK,
|
Infantry.Infantry_AK_74,
|
||||||
Infantry.Infantry_M1_Garand,
|
Infantry.Infantry_M1_Garand,
|
||||||
Infantry.Infantry_Mauser_98,
|
Infantry.Infantry_Mauser_98,
|
||||||
Infantry.Infantry_SMLE_No_4_Mk_1,
|
Infantry.Infantry_SMLE_No_4_Mk_1,
|
||||||
Infantry.Georgian_soldier_with_M4,
|
Infantry.Infantry_M4_Georgia,
|
||||||
Infantry.Infantry_Soldier_Rus,
|
Infantry.Infantry_AK_74_Rus,
|
||||||
Infantry.Paratrooper_AKS,
|
Infantry.Paratrooper_AKS,
|
||||||
Infantry.Paratrooper_RPG_16,
|
Infantry.Paratrooper_RPG_16,
|
||||||
Infantry.Soldier_M249,
|
Infantry.Infantry_M249,
|
||||||
Infantry.Infantry_M4,
|
Infantry.Infantry_M4,
|
||||||
Infantry.Soldier_RPG,
|
Infantry.Infantry_RPG,
|
||||||
]
|
]
|
||||||
|
|
||||||
TYPE_SHORAD = [
|
TYPE_SHORAD = [
|
||||||
AirDefence.AAA_ZU_23_on_Ural_375,
|
AirDefence.SPAAA_ZU_23_2_Mounted_Ural_375,
|
||||||
AirDefence.AAA_ZU_23_Insurgent_on_Ural_375,
|
AirDefence.SPAAA_ZU_23_2_Insurgent_Mounted_Ural_375,
|
||||||
AirDefence.AAA_ZSU_57_2,
|
AirDefence.SPAAA_ZSU_57_2,
|
||||||
AirDefence.SPAAA_ZSU_23_4_Shilka,
|
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
|
||||||
AirDefence.SAM_SA_8_Osa_9A33,
|
AirDefence.SAM_SA_8_Osa_Gecko_TEL,
|
||||||
AirDefence.SAM_SA_9_Strela_1_9P31,
|
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL,
|
||||||
AirDefence.SAM_SA_13_Strela_10M3_9A35M3,
|
AirDefence.SAM_SA_13_Strela_10M3_Gopher_TEL,
|
||||||
AirDefence.SAM_SA_15_Tor_9A331,
|
AirDefence.SAM_SA_15_Tor_Gauntlet,
|
||||||
AirDefence.SAM_SA_19_Tunguska_2S6,
|
AirDefence.SAM_SA_19_Tunguska_Grison,
|
||||||
|
|
||||||
AirDefence.SPAAA_Gepard,
|
AirDefence.SPAAA_Gepard,
|
||||||
AirDefence.AAA_Vulcan_M163,
|
AirDefence.SPAAA_Vulcan_M163,
|
||||||
AirDefence.SAM_Linebacker_M6,
|
AirDefence.SAM_Linebacker___Bradley_M6,
|
||||||
AirDefence.SAM_Chaparral_M48,
|
AirDefence.SAM_Chaparral_M48,
|
||||||
AirDefence.SAM_Avenger_M1097,
|
AirDefence.SAM_Avenger__Stinger,
|
||||||
AirDefence.SAM_Roland_ADS,
|
AirDefence.SAM_Roland_ADS,
|
||||||
AirDefence.HQ_7_Self_Propelled_LN,
|
AirDefence.HQ_7_Self_Propelled_LN,
|
||||||
|
|
||||||
AirDefence.AAA_8_8cm_Flak_18,
|
AirDefence.AAA_8_8cm_Flak_18,
|
||||||
AirDefence.AAA_8_8cm_Flak_36,
|
AirDefence.AAA_8_8cm_Flak_36,
|
||||||
AirDefence.AAA_8_8cm_Flak_37,
|
AirDefence.AAA_8_8cm_Flak_37,
|
||||||
AirDefence.AAA_8_8cm_Flak_41,
|
AirDefence.AAA_8_8cm_Flak_41,
|
||||||
AirDefence.AAA_Bofors_40mm,
|
AirDefence.AAA_40mm_Bofors,
|
||||||
|
AirDefence.AAA_S_60_57mm,
|
||||||
AirDefence.AAA_M1_37mm,
|
AirDefence.AAA_M1_37mm,
|
||||||
AirDefence.AA_gun_QF_3_7,
|
AirDefence.AAA_QF_3_7,
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ from enum import Enum
|
|||||||
|
|
||||||
|
|
||||||
class CombatStance(Enum):
|
class CombatStance(Enum):
|
||||||
DEFENSIVE = 0 # Unit will adopt defensive stance with medium group of units
|
DEFENSIVE = 0 # Unit will adopt defensive stance with medium group of units
|
||||||
AGGRESSIVE = 1 # Unit will attempt to make progress with medium sized group of units
|
AGGRESSIVE = (
|
||||||
RETREAT = 2 # Unit will retreat
|
1 # Unit will attempt to make progress with medium sized group of units
|
||||||
BREAKTHROUGH = 3 # Unit will attempt a breakthrough, rushing forward very aggresively with big group of armored units, and even less armored units will move aggresively
|
)
|
||||||
|
RETREAT = 2 # Unit will retreat
|
||||||
|
BREAKTHROUGH = 3 # Unit will attempt a breakthrough, rushing forward very aggresively with big group of armored units, and even less armored units will move aggresively
|
||||||
ELIMINATION = 4 # Unit will progress aggresively toward anemy units, attempting to eliminate the ennemy force
|
ELIMINATION = 4 # Unit will progress aggresively toward anemy units, attempting to eliminate the ennemy force
|
||||||
AMBUSH = 5 # Units will adopt a defensive stance a bit different from 'DEFENSIVE', ATGM & INFANTRY with RPG will be located on frontline with the armored units. (The groups of units will be smaller)
|
AMBUSH = 5 # Units will adopt a defensive stance a bit different from 'DEFENSIVE', ATGM & INFANTRY with RPG will be located on frontline with the armored units. (The groups of units will be smaller)
|
||||||
|
|
||||||
|
|||||||
@@ -11,16 +11,18 @@ import logging
|
|||||||
import random
|
import random
|
||||||
from typing import Dict, Iterator, Optional, TYPE_CHECKING, Type, List
|
from typing import Dict, Iterator, Optional, TYPE_CHECKING, Type, List
|
||||||
|
|
||||||
from dcs import Mission, Point
|
from dcs import Mission, Point, unitgroup
|
||||||
from dcs.country import Country
|
from dcs.country import Country
|
||||||
from dcs.statics import fortification_map, warehouse_map
|
from dcs.point import StaticPoint
|
||||||
|
from dcs.statics import fortification_map, warehouse_map, Warehouse
|
||||||
from dcs.task import (
|
from dcs.task import (
|
||||||
ActivateBeaconCommand,
|
ActivateBeaconCommand,
|
||||||
ActivateICLSCommand,
|
ActivateICLSCommand,
|
||||||
EPLRS,
|
EPLRS,
|
||||||
OptAlarmState, FireAtPoint,
|
OptAlarmState,
|
||||||
|
FireAtPoint,
|
||||||
)
|
)
|
||||||
from dcs.unit import Ship, Unit, Vehicle
|
from dcs.unit import Ship, Unit, Vehicle, SingleHeliPad, Static
|
||||||
from dcs.unitgroup import Group, ShipGroup, StaticGroup, VehicleGroup
|
from dcs.unitgroup import Group, ShipGroup, StaticGroup, VehicleGroup
|
||||||
from dcs.unittype import StaticType, UnitType
|
from dcs.unittype import StaticType, UnitType
|
||||||
from dcs.vehicles import vehicle_map
|
from dcs.vehicles import vehicle_map
|
||||||
@@ -30,9 +32,12 @@ from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID
|
|||||||
from game.db import unit_type_from_name
|
from game.db import unit_type_from_name
|
||||||
from game.theater import ControlPoint, TheaterGroundObject
|
from game.theater import ControlPoint, TheaterGroundObject
|
||||||
from game.theater.theatergroundobject import (
|
from game.theater.theatergroundobject import (
|
||||||
BuildingGroundObject, CarrierGroundObject,
|
BuildingGroundObject,
|
||||||
|
CarrierGroundObject,
|
||||||
GenericCarrierGroundObject,
|
GenericCarrierGroundObject,
|
||||||
LhaGroundObject, ShipGroundObject, MissileSiteGroundObject,
|
LhaGroundObject,
|
||||||
|
ShipGroundObject,
|
||||||
|
MissileSiteGroundObject,
|
||||||
)
|
)
|
||||||
from game.unitmap import UnitMap
|
from game.unitmap import UnitMap
|
||||||
from game.utils import knots, mps
|
from game.utils import knots, mps
|
||||||
@@ -43,7 +48,6 @@ from .tacan import TacanBand, TacanChannel, TacanRegistry
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game import Game
|
from game import Game
|
||||||
|
|
||||||
|
|
||||||
FARP_FRONTLINE_DISTANCE = 10000
|
FARP_FRONTLINE_DISTANCE = 10000
|
||||||
AA_CP_MIN_DISTANCE = 40000
|
AA_CP_MIN_DISTANCE = 40000
|
||||||
|
|
||||||
@@ -53,8 +57,15 @@ class GenericGroundObjectGenerator:
|
|||||||
|
|
||||||
Currently used only for SAM
|
Currently used only for SAM
|
||||||
"""
|
"""
|
||||||
def __init__(self, ground_object: TheaterGroundObject, country: Country,
|
|
||||||
game: Game, mission: Mission, unit_map: UnitMap) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
ground_object: TheaterGroundObject,
|
||||||
|
country: Country,
|
||||||
|
game: Game,
|
||||||
|
mission: Mission,
|
||||||
|
unit_map: UnitMap,
|
||||||
|
) -> None:
|
||||||
self.ground_object = ground_object
|
self.ground_object = ground_object
|
||||||
self.country = country
|
self.country = country
|
||||||
self.game = game
|
self.game = game
|
||||||
@@ -72,18 +83,22 @@ class GenericGroundObjectGenerator:
|
|||||||
|
|
||||||
unit_type = unit_type_from_name(group.units[0].type)
|
unit_type = unit_type_from_name(group.units[0].type)
|
||||||
if unit_type is None:
|
if unit_type is None:
|
||||||
raise RuntimeError(
|
raise RuntimeError(f"Unrecognized unit type: {group.units[0].type}")
|
||||||
f"Unrecognized unit type: {group.units[0].type}")
|
|
||||||
|
|
||||||
vg = self.m.vehicle_group(self.country, group.name, unit_type,
|
vg = self.m.vehicle_group(
|
||||||
position=group.position,
|
self.country,
|
||||||
heading=group.units[0].heading)
|
group.name,
|
||||||
|
unit_type,
|
||||||
|
position=group.position,
|
||||||
|
heading=group.units[0].heading,
|
||||||
|
)
|
||||||
vg.units[0].name = self.m.string(group.units[0].name)
|
vg.units[0].name = self.m.string(group.units[0].name)
|
||||||
vg.units[0].player_can_drive = True
|
vg.units[0].player_can_drive = True
|
||||||
for i, u in enumerate(group.units):
|
for i, u in enumerate(group.units):
|
||||||
if i > 0:
|
if i > 0:
|
||||||
vehicle = Vehicle(self.m.next_unit_id(),
|
vehicle = Vehicle(
|
||||||
self.m.string(u.name), u.type)
|
self.m.next_unit_id(), self.m.string(u.name), u.type
|
||||||
|
)
|
||||||
vehicle.position.x = u.position.x
|
vehicle.position.x = u.position.x
|
||||||
vehicle.position.y = u.position.y
|
vehicle.position.y = u.position.y
|
||||||
vehicle.heading = u.heading
|
vehicle.heading = u.heading
|
||||||
@@ -96,7 +111,7 @@ class GenericGroundObjectGenerator:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def enable_eplrs(group: Group, unit_type: Type[UnitType]) -> None:
|
def enable_eplrs(group: Group, unit_type: Type[UnitType]) -> None:
|
||||||
if hasattr(unit_type, 'eplrs'):
|
if hasattr(unit_type, "eplrs"):
|
||||||
if unit_type.eplrs:
|
if unit_type.eplrs:
|
||||||
group.points[0].tasks.append(EPLRS(group.id))
|
group.points[0].tasks.append(EPLRS(group.id))
|
||||||
|
|
||||||
@@ -106,14 +121,13 @@ class GenericGroundObjectGenerator:
|
|||||||
else:
|
else:
|
||||||
group.points[0].tasks.append(OptAlarmState(1))
|
group.points[0].tasks.append(OptAlarmState(1))
|
||||||
|
|
||||||
def _register_unit_group(self, persistence_group: Group,
|
def _register_unit_group(self, persistence_group: Group, miz_group: Group) -> None:
|
||||||
miz_group: Group) -> None:
|
self.unit_map.add_ground_object_units(
|
||||||
self.unit_map.add_ground_object_units(self.ground_object,
|
self.ground_object, persistence_group, miz_group
|
||||||
persistence_group, miz_group)
|
)
|
||||||
|
|
||||||
|
|
||||||
class MissileSiteGenerator(GenericGroundObjectGenerator):
|
class MissileSiteGenerator(GenericGroundObjectGenerator):
|
||||||
|
|
||||||
def generate(self) -> None:
|
def generate(self) -> None:
|
||||||
super(MissileSiteGenerator, self).generate()
|
super(MissileSiteGenerator, self).generate()
|
||||||
# Note : Only the SCUD missiles group can fire (V1 site cannot fire in game right now)
|
# Note : Only the SCUD missiles group can fire (V1 site cannot fire in game right now)
|
||||||
@@ -125,13 +139,19 @@ class MissileSiteGenerator(GenericGroundObjectGenerator):
|
|||||||
targets = self.possible_missile_targets(vg)
|
targets = self.possible_missile_targets(vg)
|
||||||
if targets:
|
if targets:
|
||||||
target = random.choice(targets)
|
target = random.choice(targets)
|
||||||
real_target = target.point_from_heading(random.randint(0, 360), random.randint(0, 2500))
|
real_target = target.point_from_heading(
|
||||||
|
random.randint(0, 360), random.randint(0, 2500)
|
||||||
|
)
|
||||||
vg.points[0].add_task(FireAtPoint(real_target))
|
vg.points[0].add_task(FireAtPoint(real_target))
|
||||||
logging.info("Set up fire task for missile group.")
|
logging.info("Set up fire task for missile group.")
|
||||||
else:
|
else:
|
||||||
logging.info("Couldn't setup missile site to fire, no valid target in range.")
|
logging.info(
|
||||||
|
"Couldn't setup missile site to fire, no valid target in range."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logging.info("Couldn't setup missile site to fire, group was not generated.")
|
logging.info(
|
||||||
|
"Couldn't setup missile site to fire, group was not generated."
|
||||||
|
)
|
||||||
|
|
||||||
def possible_missile_targets(self, vg: Group) -> List[Point]:
|
def possible_missile_targets(self, vg: Group) -> List[Point]:
|
||||||
"""
|
"""
|
||||||
@@ -190,7 +210,8 @@ class BuildingSiteGenerator(GenericGroundObjectGenerator):
|
|||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
logging.error(
|
logging.error(
|
||||||
f"{self.ground_object.dcs_identifier} not found in static maps")
|
f"{self.ground_object.dcs_identifier} not found in static maps"
|
||||||
|
)
|
||||||
|
|
||||||
def generate_vehicle_group(self, unit_type: UnitType) -> None:
|
def generate_vehicle_group(self, unit_type: UnitType) -> None:
|
||||||
if not self.ground_object.is_dead:
|
if not self.ground_object.is_dead:
|
||||||
@@ -228,11 +249,20 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
|
|||||||
|
|
||||||
Used by both CV(N) groups and LHA groups.
|
Used by both CV(N) groups and LHA groups.
|
||||||
"""
|
"""
|
||||||
def __init__(self, ground_object: GenericCarrierGroundObject,
|
|
||||||
control_point: ControlPoint, country: Country, game: Game,
|
def __init__(
|
||||||
mission: Mission, radio_registry: RadioRegistry,
|
self,
|
||||||
tacan_registry: TacanRegistry, icls_alloc: Iterator[int],
|
ground_object: GenericCarrierGroundObject,
|
||||||
runways: Dict[str, RunwayData], unit_map: UnitMap) -> None:
|
control_point: ControlPoint,
|
||||||
|
country: Country,
|
||||||
|
game: Game,
|
||||||
|
mission: Mission,
|
||||||
|
radio_registry: RadioRegistry,
|
||||||
|
tacan_registry: TacanRegistry,
|
||||||
|
icls_alloc: Iterator[int],
|
||||||
|
runways: Dict[str, RunwayData],
|
||||||
|
unit_map: UnitMap,
|
||||||
|
) -> None:
|
||||||
super().__init__(ground_object, country, game, mission, unit_map)
|
super().__init__(ground_object, country, game, mission, unit_map)
|
||||||
self.ground_object = ground_object
|
self.ground_object = ground_object
|
||||||
self.control_point = control_point
|
self.control_point = control_point
|
||||||
@@ -245,8 +275,7 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
|
|||||||
# TODO: Require single group?
|
# TODO: Require single group?
|
||||||
for group in self.ground_object.groups:
|
for group in self.ground_object.groups:
|
||||||
if not group.units:
|
if not group.units:
|
||||||
logging.warning(
|
logging.warning(f"Found empty carrier group in {self.control_point}")
|
||||||
f"Found empty carrier group in {self.control_point}")
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
atc = self.radio_registry.alloc_uhf()
|
atc = self.radio_registry.alloc_uhf()
|
||||||
@@ -270,25 +299,29 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
|
|||||||
def get_carrier_type(self, group: Group) -> Type[UnitType]:
|
def get_carrier_type(self, group: Group) -> Type[UnitType]:
|
||||||
unit_type = unit_type_from_name(group.units[0].type)
|
unit_type = unit_type_from_name(group.units[0].type)
|
||||||
if unit_type is None:
|
if unit_type is None:
|
||||||
raise RuntimeError(
|
raise RuntimeError(f"Unrecognized carrier name: {group.units[0].type}")
|
||||||
f"Unrecognized carrier name: {group.units[0].type}")
|
|
||||||
return unit_type
|
return unit_type
|
||||||
|
|
||||||
def configure_carrier(self, group: Group,
|
def configure_carrier(self, group: Group, atc_channel: RadioFrequency) -> ShipGroup:
|
||||||
atc_channel: RadioFrequency) -> ShipGroup:
|
|
||||||
unit_type = self.get_carrier_type(group)
|
unit_type = self.get_carrier_type(group)
|
||||||
|
|
||||||
ship_group = self.m.ship_group(self.country, group.name, unit_type,
|
ship_group = self.m.ship_group(
|
||||||
position=group.position,
|
self.country,
|
||||||
heading=group.units[0].heading)
|
group.name,
|
||||||
|
unit_type,
|
||||||
|
position=group.position,
|
||||||
|
heading=group.units[0].heading,
|
||||||
|
)
|
||||||
ship_group.set_frequency(atc_channel.hertz)
|
ship_group.set_frequency(atc_channel.hertz)
|
||||||
ship_group.units[0].name = self.m.string(group.units[0].name)
|
ship_group.units[0].name = self.m.string(group.units[0].name)
|
||||||
return ship_group
|
return ship_group
|
||||||
|
|
||||||
def create_ship(self, unit: Unit, atc_channel: RadioFrequency) -> Ship:
|
def create_ship(self, unit: Unit, atc_channel: RadioFrequency) -> Ship:
|
||||||
ship = Ship(self.m.next_unit_id(),
|
ship = Ship(
|
||||||
self.m.string(unit.name),
|
self.m.next_unit_id(),
|
||||||
unit_type_from_name(unit.type))
|
self.m.string(unit.name),
|
||||||
|
unit_type_from_name(unit.type),
|
||||||
|
)
|
||||||
ship.position.x = unit.position.x
|
ship.position.x = unit.position.x
|
||||||
ship.position.y = unit.position.y
|
ship.position.y = unit.position.y
|
||||||
ship.heading = unit.heading
|
ship.heading = unit.heading
|
||||||
@@ -303,7 +336,8 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
|
|||||||
carrier_speed = knots(25) - mps(wind.speed)
|
carrier_speed = knots(25) - mps(wind.speed)
|
||||||
for attempt in range(5):
|
for attempt in range(5):
|
||||||
point = group.points[0].position.point_from_heading(
|
point = group.points[0].position.point_from_heading(
|
||||||
brc, 100000 - attempt * 20000)
|
brc, 100000 - attempt * 20000
|
||||||
|
)
|
||||||
if self.game.theater.is_in_sea(point):
|
if self.game.theater.is_in_sea(point):
|
||||||
group.points[0].speed = carrier_speed.meters_per_second
|
group.points[0].speed = carrier_speed.meters_per_second
|
||||||
group.add_waypoint(point, carrier_speed.kph)
|
group.add_waypoint(point, carrier_speed.kph)
|
||||||
@@ -314,21 +348,30 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def activate_beacons(group: ShipGroup, tacan: TacanChannel,
|
def activate_beacons(
|
||||||
callsign: str, icls: int) -> None:
|
group: ShipGroup, tacan: TacanChannel, callsign: str, icls: int
|
||||||
group.points[0].tasks.append(ActivateBeaconCommand(
|
) -> None:
|
||||||
channel=tacan.number,
|
group.points[0].tasks.append(
|
||||||
modechannel=tacan.band.value,
|
ActivateBeaconCommand(
|
||||||
callsign=callsign,
|
channel=tacan.number,
|
||||||
unit_id=group.units[0].id,
|
modechannel=tacan.band.value,
|
||||||
aa=False
|
callsign=callsign,
|
||||||
))
|
unit_id=group.units[0].id,
|
||||||
group.points[0].tasks.append(ActivateICLSCommand(
|
aa=False,
|
||||||
icls, unit_id=group.units[0].id
|
)
|
||||||
))
|
)
|
||||||
|
group.points[0].tasks.append(
|
||||||
|
ActivateICLSCommand(icls, unit_id=group.units[0].id)
|
||||||
|
)
|
||||||
|
|
||||||
def add_runway_data(self, brc: int, atc: RadioFrequency,
|
def add_runway_data(
|
||||||
tacan: TacanChannel, callsign: str, icls: int) -> None:
|
self,
|
||||||
|
brc: int,
|
||||||
|
atc: RadioFrequency,
|
||||||
|
tacan: TacanChannel,
|
||||||
|
callsign: str,
|
||||||
|
icls: int,
|
||||||
|
) -> None:
|
||||||
# TODO: Make unit name usable.
|
# TODO: Make unit name usable.
|
||||||
# This relies on one control point mapping exactly
|
# This relies on one control point mapping exactly
|
||||||
# to one LHA, carrier, or other usable "runway".
|
# to one LHA, carrier, or other usable "runway".
|
||||||
@@ -354,24 +397,28 @@ class CarrierGenerator(GenericCarrierGenerator):
|
|||||||
def get_carrier_type(self, group: Group) -> UnitType:
|
def get_carrier_type(self, group: Group) -> UnitType:
|
||||||
unit_type = super().get_carrier_type(group)
|
unit_type = super().get_carrier_type(group)
|
||||||
if self.game.settings.supercarrier:
|
if self.game.settings.supercarrier:
|
||||||
unit_type = db.upgrade_to_supercarrier(unit_type,
|
unit_type = db.upgrade_to_supercarrier(unit_type, self.control_point.name)
|
||||||
self.control_point.name)
|
|
||||||
return unit_type
|
return unit_type
|
||||||
|
|
||||||
def tacan_callsign(self) -> str:
|
def tacan_callsign(self) -> str:
|
||||||
# TODO: Assign these properly.
|
# TODO: Assign these properly.
|
||||||
return random.choice([
|
if self.control_point.name == "Carrier Strike Group 8":
|
||||||
"STE",
|
return "TRU"
|
||||||
"CVN",
|
else:
|
||||||
"CVH",
|
return random.choice(
|
||||||
"CCV",
|
[
|
||||||
"ACC",
|
"STE",
|
||||||
"ARC",
|
"CVN",
|
||||||
"GER",
|
"CVH",
|
||||||
"ABR",
|
"CCV",
|
||||||
"LIN",
|
"ACC",
|
||||||
"TRU",
|
"ARC",
|
||||||
])
|
"GER",
|
||||||
|
"ABR",
|
||||||
|
"LIN",
|
||||||
|
"TRU",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class LhaGenerator(GenericCarrierGenerator):
|
class LhaGenerator(GenericCarrierGenerator):
|
||||||
@@ -379,14 +426,16 @@ class LhaGenerator(GenericCarrierGenerator):
|
|||||||
|
|
||||||
def tacan_callsign(self) -> str:
|
def tacan_callsign(self) -> str:
|
||||||
# TODO: Assign these properly.
|
# TODO: Assign these properly.
|
||||||
return random.choice([
|
return random.choice(
|
||||||
"LHD",
|
[
|
||||||
"LHA",
|
"LHD",
|
||||||
"LHB",
|
"LHA",
|
||||||
"LHC",
|
"LHB",
|
||||||
"LHD",
|
"LHC",
|
||||||
"LDS",
|
"LHD",
|
||||||
])
|
"LDS",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ShipObjectGenerator(GenericGroundObjectGenerator):
|
class ShipObjectGenerator(GenericGroundObjectGenerator):
|
||||||
@@ -403,22 +452,23 @@ class ShipObjectGenerator(GenericGroundObjectGenerator):
|
|||||||
|
|
||||||
unit_type = unit_type_from_name(group.units[0].type)
|
unit_type = unit_type_from_name(group.units[0].type)
|
||||||
if unit_type is None:
|
if unit_type is None:
|
||||||
raise RuntimeError(
|
raise RuntimeError(f"Unrecognized unit type: {group.units[0].type}")
|
||||||
f"Unrecognized unit type: {group.units[0].type}")
|
|
||||||
|
|
||||||
self.generate_group(group, unit_type)
|
self.generate_group(group, unit_type)
|
||||||
|
|
||||||
def generate_group(self, group_def: Group,
|
def generate_group(self, group_def: Group, first_unit_type: Type[UnitType]) -> None:
|
||||||
first_unit_type: Type[UnitType]) -> None:
|
group = self.m.ship_group(
|
||||||
group = self.m.ship_group(self.country, group_def.name, first_unit_type,
|
self.country,
|
||||||
position=group_def.position,
|
group_def.name,
|
||||||
heading=group_def.units[0].heading)
|
first_unit_type,
|
||||||
|
position=group_def.position,
|
||||||
|
heading=group_def.units[0].heading,
|
||||||
|
)
|
||||||
group.units[0].name = self.m.string(group_def.units[0].name)
|
group.units[0].name = self.m.string(group_def.units[0].name)
|
||||||
# TODO: Skipping the first unit looks like copy pasta from the carrier.
|
# TODO: Skipping the first unit looks like copy pasta from the carrier.
|
||||||
for unit in group_def.units[1:]:
|
for unit in group_def.units[1:]:
|
||||||
unit_type = unit_type_from_name(unit.type)
|
unit_type = unit_type_from_name(unit.type)
|
||||||
ship = Ship(self.m.next_unit_id(),
|
ship = Ship(self.m.next_unit_id(), self.m.string(unit.name), unit_type)
|
||||||
self.m.string(unit.name), unit_type)
|
|
||||||
ship.position.x = unit.position.x
|
ship.position.x = unit.position.x
|
||||||
ship.position.y = unit.position.y
|
ship.position.y = unit.position.y
|
||||||
ship.heading = unit.heading
|
ship.heading = unit.heading
|
||||||
@@ -427,6 +477,48 @@ class ShipObjectGenerator(GenericGroundObjectGenerator):
|
|||||||
self._register_unit_group(group_def, group)
|
self._register_unit_group(group_def, group)
|
||||||
|
|
||||||
|
|
||||||
|
class HelipadGenerator:
|
||||||
|
"""
|
||||||
|
Generates helipads for given control point
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
mission: Mission,
|
||||||
|
cp: ControlPoint,
|
||||||
|
game: Game,
|
||||||
|
radio_registry: RadioRegistry,
|
||||||
|
tacan_registry: TacanRegistry,
|
||||||
|
):
|
||||||
|
self.m = mission
|
||||||
|
self.cp = cp
|
||||||
|
self.game = game
|
||||||
|
self.radio_registry = radio_registry
|
||||||
|
self.tacan_registry = tacan_registry
|
||||||
|
|
||||||
|
def generate(self) -> None:
|
||||||
|
|
||||||
|
if self.cp.captured:
|
||||||
|
country_name = self.game.player_country
|
||||||
|
else:
|
||||||
|
country_name = self.game.enemy_country
|
||||||
|
country = self.m.country(country_name)
|
||||||
|
|
||||||
|
for i, helipad in enumerate(self.cp.helipads):
|
||||||
|
name = self.cp.name + "_helipad_" + str(i)
|
||||||
|
logging.info("Generating helipad : " + name)
|
||||||
|
pad = SingleHeliPad(name=self.m.string(name + "_unit"))
|
||||||
|
pad.position = Point(helipad.x, helipad.y)
|
||||||
|
pad.heading = helipad.heading
|
||||||
|
# pad.heliport_frequency = self.radio_registry.alloc_uhf() TODO : alloc radio & callsign
|
||||||
|
sg = unitgroup.StaticGroup(self.m.next_group_id(), self.m.string(name))
|
||||||
|
sg.add_unit(pad)
|
||||||
|
sp = StaticPoint()
|
||||||
|
sp.position = pad.position
|
||||||
|
sg.add_point(sp)
|
||||||
|
country.add_static_group(sg)
|
||||||
|
|
||||||
|
|
||||||
class GroundObjectsGenerator:
|
class GroundObjectsGenerator:
|
||||||
"""Creates DCS groups and statics for the theater during mission generation.
|
"""Creates DCS groups and statics for the theater during mission generation.
|
||||||
|
|
||||||
@@ -436,9 +528,14 @@ class GroundObjectsGenerator:
|
|||||||
the appropriate generators.
|
the appropriate generators.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, mission: Mission, game: Game,
|
def __init__(
|
||||||
radio_registry: RadioRegistry, tacan_registry: TacanRegistry,
|
self,
|
||||||
unit_map: UnitMap) -> None:
|
mission: Mission,
|
||||||
|
game: Game,
|
||||||
|
radio_registry: RadioRegistry,
|
||||||
|
tacan_registry: TacanRegistry,
|
||||||
|
unit_map: UnitMap,
|
||||||
|
) -> None:
|
||||||
self.m = mission
|
self.m = mission
|
||||||
self.game = game
|
self.game = game
|
||||||
self.radio_registry = radio_registry
|
self.radio_registry = radio_registry
|
||||||
@@ -455,31 +552,51 @@ class GroundObjectsGenerator:
|
|||||||
country_name = self.game.enemy_country
|
country_name = self.game.enemy_country
|
||||||
country = self.m.country(country_name)
|
country = self.m.country(country_name)
|
||||||
|
|
||||||
|
HelipadGenerator(
|
||||||
|
self.m, cp, self.game, self.radio_registry, self.tacan_registry
|
||||||
|
).generate()
|
||||||
|
|
||||||
for ground_object in cp.ground_objects:
|
for ground_object in cp.ground_objects:
|
||||||
if isinstance(ground_object, BuildingGroundObject):
|
if isinstance(ground_object, BuildingGroundObject):
|
||||||
generator = BuildingSiteGenerator(
|
generator = BuildingSiteGenerator(
|
||||||
ground_object, country, self.game, self.m,
|
ground_object, country, self.game, self.m, self.unit_map
|
||||||
self.unit_map)
|
)
|
||||||
elif isinstance(ground_object, CarrierGroundObject):
|
elif isinstance(ground_object, CarrierGroundObject):
|
||||||
generator = CarrierGenerator(
|
generator = CarrierGenerator(
|
||||||
ground_object, cp, country, self.game, self.m,
|
ground_object,
|
||||||
self.radio_registry, self.tacan_registry,
|
cp,
|
||||||
self.icls_alloc, self.runways, self.unit_map)
|
country,
|
||||||
|
self.game,
|
||||||
|
self.m,
|
||||||
|
self.radio_registry,
|
||||||
|
self.tacan_registry,
|
||||||
|
self.icls_alloc,
|
||||||
|
self.runways,
|
||||||
|
self.unit_map,
|
||||||
|
)
|
||||||
elif isinstance(ground_object, LhaGroundObject):
|
elif isinstance(ground_object, LhaGroundObject):
|
||||||
generator = CarrierGenerator(
|
generator = CarrierGenerator(
|
||||||
ground_object, cp, country, self.game, self.m,
|
ground_object,
|
||||||
self.radio_registry, self.tacan_registry,
|
cp,
|
||||||
self.icls_alloc, self.runways, self.unit_map)
|
country,
|
||||||
|
self.game,
|
||||||
|
self.m,
|
||||||
|
self.radio_registry,
|
||||||
|
self.tacan_registry,
|
||||||
|
self.icls_alloc,
|
||||||
|
self.runways,
|
||||||
|
self.unit_map,
|
||||||
|
)
|
||||||
elif isinstance(ground_object, ShipGroundObject):
|
elif isinstance(ground_object, ShipGroundObject):
|
||||||
generator = ShipObjectGenerator(
|
generator = ShipObjectGenerator(
|
||||||
ground_object, country, self.game, self.m,
|
ground_object, country, self.game, self.m, self.unit_map
|
||||||
self.unit_map)
|
)
|
||||||
elif isinstance(ground_object, MissileSiteGroundObject):
|
elif isinstance(ground_object, MissileSiteGroundObject):
|
||||||
generator = MissileSiteGenerator(
|
generator = MissileSiteGenerator(
|
||||||
ground_object, country, self.game, self.m,
|
ground_object, country, self.game, self.m, self.unit_map
|
||||||
self.unit_map)
|
)
|
||||||
else:
|
else:
|
||||||
generator = GenericGroundObjectGenerator(
|
generator = GenericGroundObjectGenerator(
|
||||||
ground_object, country, self.game, self.m,
|
ground_object, country, self.game, self.m, self.unit_map
|
||||||
self.unit_map)
|
)
|
||||||
generator.generate()
|
generator.generate()
|
||||||
|
|||||||
236
gen/kneeboard.py
236
gen/kneeboard.py
@@ -41,6 +41,7 @@ from .flights.flight import FlightWaypoint, FlightWaypointType
|
|||||||
from .radios import RadioFrequency
|
from .radios import RadioFrequency
|
||||||
from .runways import RunwayData
|
from .runways import RunwayData
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game import Game
|
from game import Game
|
||||||
|
|
||||||
@@ -48,18 +49,33 @@ if TYPE_CHECKING:
|
|||||||
class KneeboardPageWriter:
|
class KneeboardPageWriter:
|
||||||
"""Creates kneeboard images."""
|
"""Creates kneeboard images."""
|
||||||
|
|
||||||
def __init__(self, page_margin: int = 24, line_spacing: int = 12) -> None:
|
def __init__(
|
||||||
self.image = Image.new('RGB', (768, 1024), (0xff, 0xff, 0xff))
|
self, page_margin: int = 24, line_spacing: int = 12, dark_theme: bool = False
|
||||||
|
) -> None:
|
||||||
|
if dark_theme:
|
||||||
|
self.foreground_fill = (215, 200, 200)
|
||||||
|
self.background_fill = (10, 5, 5)
|
||||||
|
else:
|
||||||
|
self.foreground_fill = (15, 15, 15)
|
||||||
|
self.background_fill = (255, 252, 252)
|
||||||
|
self.image = Image.new("RGB", (768, 1024), self.background_fill)
|
||||||
# These font sizes create a relatively full page for current sorties. If
|
# These font sizes create a relatively full page for current sorties. If
|
||||||
# we start generating more complicated flight plans, or start including
|
# we start generating more complicated flight plans, or start including
|
||||||
# more information in the comm ladder (the latter of which we should
|
# more information in the comm ladder (the latter of which we should
|
||||||
# probably do), we'll need to split some of this information off into a
|
# probably do), we'll need to split some of this information off into a
|
||||||
# second page.
|
# second page.
|
||||||
self.title_font = ImageFont.truetype("arial.ttf", 32)
|
self.title_font = ImageFont.truetype(
|
||||||
self.heading_font = ImageFont.truetype("arial.ttf", 24)
|
"arial.ttf", 32, layout_engine=ImageFont.LAYOUT_BASIC
|
||||||
self.content_font = ImageFont.truetype("arial.ttf", 20)
|
)
|
||||||
|
self.heading_font = ImageFont.truetype(
|
||||||
|
"arial.ttf", 24, layout_engine=ImageFont.LAYOUT_BASIC
|
||||||
|
)
|
||||||
|
self.content_font = ImageFont.truetype(
|
||||||
|
"arial.ttf", 20, layout_engine=ImageFont.LAYOUT_BASIC
|
||||||
|
)
|
||||||
self.table_font = ImageFont.truetype(
|
self.table_font = ImageFont.truetype(
|
||||||
"resources/fonts/Inconsolata.otf", 20)
|
"resources/fonts/Inconsolata.otf", 20, layout_engine=ImageFont.LAYOUT_BASIC
|
||||||
|
)
|
||||||
self.draw = ImageDraw.Draw(self.image)
|
self.draw = ImageDraw.Draw(self.image)
|
||||||
self.x = page_margin
|
self.x = page_margin
|
||||||
self.y = page_margin
|
self.y = page_margin
|
||||||
@@ -69,8 +85,9 @@ class KneeboardPageWriter:
|
|||||||
def position(self) -> Tuple[int, int]:
|
def position(self) -> Tuple[int, int]:
|
||||||
return self.x, self.y
|
return self.x, self.y
|
||||||
|
|
||||||
def text(self, text: str, font=None,
|
def text(
|
||||||
fill: Tuple[int, int, int] = (0, 0, 0)) -> None:
|
self, text: str, font=None, fill: Tuple[int, int, int] = (0, 0, 0)
|
||||||
|
) -> None:
|
||||||
if font is None:
|
if font is None:
|
||||||
font = self.content_font
|
font = self.content_font
|
||||||
|
|
||||||
@@ -79,17 +96,18 @@ class KneeboardPageWriter:
|
|||||||
self.y += height + self.line_spacing
|
self.y += height + self.line_spacing
|
||||||
|
|
||||||
def title(self, title: str) -> None:
|
def title(self, title: str) -> None:
|
||||||
self.text(title, font=self.title_font)
|
self.text(title, font=self.title_font, fill=self.foreground_fill)
|
||||||
|
|
||||||
def heading(self, text: str) -> None:
|
def heading(self, text: str) -> None:
|
||||||
self.text(text, font=self.heading_font)
|
self.text(text, font=self.heading_font, fill=self.foreground_fill)
|
||||||
|
|
||||||
def table(self, cells: List[List[str]],
|
def table(
|
||||||
headers: Optional[List[str]] = None) -> None:
|
self, cells: List[List[str]], headers: Optional[List[str]] = None
|
||||||
|
) -> None:
|
||||||
if headers is None:
|
if headers is None:
|
||||||
headers = []
|
headers = []
|
||||||
table = tabulate(cells, headers=headers, numalign="right")
|
table = tabulate(cells, headers=headers, numalign="right")
|
||||||
self.text(table, font=self.table_font)
|
self.text(table, font=self.table_font, fill=self.foreground_fill)
|
||||||
|
|
||||||
def write(self, path: Path) -> None:
|
def write(self, path: Path) -> None:
|
||||||
self.image.save(path)
|
self.image.save(path)
|
||||||
@@ -157,29 +175,34 @@ class FlightPlanBuilder:
|
|||||||
first_waypoint_num = self.target_points[0].number
|
first_waypoint_num = self.target_points[0].number
|
||||||
last_waypoint_num = self.target_points[-1].number
|
last_waypoint_num = self.target_points[-1].number
|
||||||
|
|
||||||
self.rows.append([
|
self.rows.append(
|
||||||
f"{first_waypoint_num}-{last_waypoint_num}",
|
[
|
||||||
"Target points",
|
f"{first_waypoint_num}-{last_waypoint_num}",
|
||||||
"0",
|
"Target points",
|
||||||
self._waypoint_distance(self.target_points[0].waypoint),
|
"0",
|
||||||
self._ground_speed(self.target_points[0].waypoint),
|
self._waypoint_distance(self.target_points[0].waypoint),
|
||||||
self._format_time(self.target_points[0].waypoint.tot),
|
self._ground_speed(self.target_points[0].waypoint),
|
||||||
self._format_time(self.target_points[0].waypoint.departure_time),
|
self._format_time(self.target_points[0].waypoint.tot),
|
||||||
])
|
self._format_time(self.target_points[0].waypoint.departure_time),
|
||||||
|
]
|
||||||
|
)
|
||||||
self.last_waypoint = self.target_points[-1].waypoint
|
self.last_waypoint = self.target_points[-1].waypoint
|
||||||
|
|
||||||
def add_waypoint_row(self, waypoint: NumberedWaypoint) -> None:
|
def add_waypoint_row(self, waypoint: NumberedWaypoint) -> None:
|
||||||
self.rows.append([
|
self.rows.append(
|
||||||
str(waypoint.number),
|
[
|
||||||
KneeboardPageWriter.wrap_line(
|
str(waypoint.number),
|
||||||
waypoint.waypoint.pretty_name,
|
KneeboardPageWriter.wrap_line(
|
||||||
FlightPlanBuilder.WAYPOINT_DESC_MAX_LEN),
|
waypoint.waypoint.pretty_name,
|
||||||
str(int(waypoint.waypoint.alt.feet)),
|
FlightPlanBuilder.WAYPOINT_DESC_MAX_LEN,
|
||||||
self._waypoint_distance(waypoint.waypoint),
|
),
|
||||||
self._ground_speed(waypoint.waypoint),
|
str(int(waypoint.waypoint.alt.feet)),
|
||||||
self._format_time(waypoint.waypoint.tot),
|
self._waypoint_distance(waypoint.waypoint),
|
||||||
self._format_time(waypoint.waypoint.departure_time),
|
self._ground_speed(waypoint.waypoint),
|
||||||
])
|
self._format_time(waypoint.waypoint.tot),
|
||||||
|
self._format_time(waypoint.waypoint.departure_time),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
def _format_time(self, time: Optional[datetime.timedelta]) -> str:
|
def _format_time(self, time: Optional[datetime.timedelta]) -> str:
|
||||||
if time is None:
|
if time is None:
|
||||||
@@ -191,9 +214,9 @@ class FlightPlanBuilder:
|
|||||||
if self.last_waypoint is None:
|
if self.last_waypoint is None:
|
||||||
return "-"
|
return "-"
|
||||||
|
|
||||||
distance = meters(self.last_waypoint.position.distance_to_point(
|
distance = meters(
|
||||||
waypoint.position
|
self.last_waypoint.position.distance_to_point(waypoint.position)
|
||||||
))
|
)
|
||||||
return f"{distance.nautical_miles:.1f} NM"
|
return f"{distance.nautical_miles:.1f} NM"
|
||||||
|
|
||||||
def _ground_speed(self, waypoint: FlightWaypoint) -> str:
|
def _ground_speed(self, waypoint: FlightWaypoint) -> str:
|
||||||
@@ -210,9 +233,9 @@ class FlightPlanBuilder:
|
|||||||
else:
|
else:
|
||||||
return "-"
|
return "-"
|
||||||
|
|
||||||
distance = meters(self.last_waypoint.position.distance_to_point(
|
distance = meters(
|
||||||
waypoint.position
|
self.last_waypoint.position.distance_to_point(waypoint.position)
|
||||||
))
|
)
|
||||||
duration = (waypoint.tot - last_time).total_seconds() / 3600
|
duration = (waypoint.tot - last_time).total_seconds() / 3600
|
||||||
return f"{int(distance.nautical_miles / duration)} kt"
|
return f"{int(distance.nautical_miles / duration)} kt"
|
||||||
|
|
||||||
@@ -222,66 +245,113 @@ class FlightPlanBuilder:
|
|||||||
|
|
||||||
class BriefingPage(KneeboardPage):
|
class BriefingPage(KneeboardPage):
|
||||||
"""A kneeboard page containing briefing information."""
|
"""A kneeboard page containing briefing information."""
|
||||||
def __init__(self, flight: FlightData, comms: List[CommInfo],
|
|
||||||
awacs: List[AwacsInfo], tankers: List[TankerInfo],
|
def __init__(
|
||||||
jtacs: List[JtacInfo], start_time: datetime.datetime) -> None:
|
self,
|
||||||
|
flight: FlightData,
|
||||||
|
comms: List[CommInfo],
|
||||||
|
awacs: List[AwacsInfo],
|
||||||
|
tankers: List[TankerInfo],
|
||||||
|
jtacs: List[JtacInfo],
|
||||||
|
start_time: datetime.datetime,
|
||||||
|
dark_kneeboard: bool,
|
||||||
|
) -> None:
|
||||||
self.flight = flight
|
self.flight = flight
|
||||||
self.comms = list(comms)
|
self.comms = list(comms)
|
||||||
self.awacs = awacs
|
self.awacs = awacs
|
||||||
self.tankers = tankers
|
self.tankers = tankers
|
||||||
self.jtacs = jtacs
|
self.jtacs = jtacs
|
||||||
self.start_time = start_time
|
self.start_time = start_time
|
||||||
|
self.dark_kneeboard = dark_kneeboard
|
||||||
self.comms.append(CommInfo("Flight", self.flight.intra_flight_channel))
|
self.comms.append(CommInfo("Flight", self.flight.intra_flight_channel))
|
||||||
|
|
||||||
def write(self, path: Path) -> None:
|
def write(self, path: Path) -> None:
|
||||||
writer = KneeboardPageWriter()
|
writer = KneeboardPageWriter(dark_theme=self.dark_kneeboard)
|
||||||
writer.title(f"{self.flight.callsign} Mission Info")
|
if self.flight.custom_name is not None:
|
||||||
|
custom_name_title = ' ("{}")'.format(self.flight.custom_name)
|
||||||
|
else:
|
||||||
|
custom_name_title = ""
|
||||||
|
writer.title(f"{self.flight.callsign} Mission Info{custom_name_title}")
|
||||||
|
|
||||||
# TODO: Handle carriers.
|
# TODO: Handle carriers.
|
||||||
writer.heading("Airfield Info")
|
writer.heading("Airfield Info")
|
||||||
writer.table([
|
writer.table(
|
||||||
self.airfield_info_row("Departure", self.flight.departure),
|
[
|
||||||
self.airfield_info_row("Arrival", self.flight.arrival),
|
self.airfield_info_row("Departure", self.flight.departure),
|
||||||
self.airfield_info_row("Divert", self.flight.divert),
|
self.airfield_info_row("Arrival", self.flight.arrival),
|
||||||
], headers=["", "Airbase", "ATC", "TCN", "I(C)LS", "RWY"])
|
self.airfield_info_row("Divert", self.flight.divert),
|
||||||
|
],
|
||||||
|
headers=["", "Airbase", "ATC", "TCN", "I(C)LS", "RWY"],
|
||||||
|
)
|
||||||
|
|
||||||
writer.heading("Flight Plan")
|
writer.heading("Flight Plan")
|
||||||
flight_plan_builder = FlightPlanBuilder(self.start_time)
|
flight_plan_builder = FlightPlanBuilder(self.start_time)
|
||||||
for num, waypoint in enumerate(self.flight.waypoints):
|
for num, waypoint in enumerate(self.flight.waypoints):
|
||||||
flight_plan_builder.add_waypoint(num, waypoint)
|
flight_plan_builder.add_waypoint(num, waypoint)
|
||||||
writer.table(flight_plan_builder.build(), headers=[
|
writer.table(
|
||||||
"#", "Action", "Alt", "Dist", "GSPD", "Time", "Departure"
|
flight_plan_builder.build(),
|
||||||
])
|
headers=["#", "Action", "Alt", "Dist", "GSPD", "Time", "Departure"],
|
||||||
|
)
|
||||||
|
|
||||||
flight_plan_builder
|
flight_plan_builder
|
||||||
writer.table([
|
writer.table(
|
||||||
["{}lbs".format(self.flight.bingo_fuel), "{}lbs".format(self.flight.joker_fuel)]
|
[
|
||||||
], ['Bingo', 'Joker'])
|
[
|
||||||
|
"{}lbs".format(self.flight.bingo_fuel),
|
||||||
|
"{}lbs".format(self.flight.joker_fuel),
|
||||||
|
]
|
||||||
|
],
|
||||||
|
["Bingo", "Joker"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# AEW&C
|
||||||
|
writer.heading("AEW&C")
|
||||||
|
aewc_ladder = []
|
||||||
|
|
||||||
|
for single_aewc in self.awacs:
|
||||||
|
|
||||||
|
if single_aewc.depature_location is None:
|
||||||
|
dep = "-"
|
||||||
|
arr = "-"
|
||||||
|
else:
|
||||||
|
dep = self._format_time(single_aewc.start_time)
|
||||||
|
arr = self._format_time(single_aewc.end_time)
|
||||||
|
|
||||||
|
aewc_ladder.append(
|
||||||
|
[
|
||||||
|
str(single_aewc.callsign),
|
||||||
|
str(single_aewc.freq),
|
||||||
|
str(single_aewc.depature_location),
|
||||||
|
str(dep),
|
||||||
|
str(arr),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
writer.table(
|
||||||
|
aewc_ladder,
|
||||||
|
headers=["Callsign", "FREQ", "Depature", "ETD", "ETA"],
|
||||||
|
)
|
||||||
|
|
||||||
# Package Section
|
# Package Section
|
||||||
writer.heading("Comm ladder")
|
writer.heading("Comm ladder")
|
||||||
comm_ladder = []
|
comm_ladder = []
|
||||||
for comm in self.comms:
|
for comm in self.comms:
|
||||||
comm_ladder.append([comm.name, '', '', '', self.format_frequency(comm.freq)])
|
comm_ladder.append(
|
||||||
|
[comm.name, "", "", "", self.format_frequency(comm.freq)]
|
||||||
|
)
|
||||||
|
|
||||||
for a in self.awacs:
|
|
||||||
comm_ladder.append([
|
|
||||||
a.callsign,
|
|
||||||
'AWACS',
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
self.format_frequency(a.freq)
|
|
||||||
])
|
|
||||||
for tanker in self.tankers:
|
for tanker in self.tankers:
|
||||||
comm_ladder.append([
|
comm_ladder.append(
|
||||||
tanker.callsign,
|
[
|
||||||
"Tanker",
|
tanker.callsign,
|
||||||
tanker.variant,
|
"Tanker",
|
||||||
str(tanker.tacan),
|
tanker.variant,
|
||||||
self.format_frequency(tanker.freq),
|
str(tanker.tacan),
|
||||||
])
|
self.format_frequency(tanker.freq),
|
||||||
|
]
|
||||||
writer.table(comm_ladder, headers=["Callsign","Task", "Type", "TACAN", "FREQ"])
|
)
|
||||||
|
|
||||||
|
writer.table(comm_ladder, headers=["Callsign", "Task", "Type", "TACAN", "FREQ"])
|
||||||
|
|
||||||
writer.heading("JTAC")
|
writer.heading("JTAC")
|
||||||
jtacs = []
|
jtacs = []
|
||||||
@@ -291,8 +361,9 @@ class BriefingPage(KneeboardPage):
|
|||||||
|
|
||||||
writer.write(path)
|
writer.write(path)
|
||||||
|
|
||||||
def airfield_info_row(self, row_title: str,
|
def airfield_info_row(
|
||||||
runway: Optional[RunwayData]) -> List[str]:
|
self, row_title: str, runway: Optional[RunwayData]
|
||||||
|
) -> List[str]:
|
||||||
"""Creates a table row for a given airfield.
|
"""Creates a table row for a given airfield.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -337,12 +408,21 @@ class BriefingPage(KneeboardPage):
|
|||||||
channel_name = namer.channel_name(channel.radio_id, channel.channel)
|
channel_name = namer.channel_name(channel.radio_id, channel.channel)
|
||||||
return f"{channel_name} {frequency}"
|
return f"{channel_name} {frequency}"
|
||||||
|
|
||||||
|
def _format_time(self, time: Optional[datetime.timedelta]) -> str:
|
||||||
|
if time is None:
|
||||||
|
return ""
|
||||||
|
local_time = self.start_time + time
|
||||||
|
return local_time.strftime(f"%H:%M:%S")
|
||||||
|
|
||||||
|
|
||||||
class KneeboardGenerator(MissionInfoGenerator):
|
class KneeboardGenerator(MissionInfoGenerator):
|
||||||
"""Creates kneeboard pages for each client flight in the mission."""
|
"""Creates kneeboard pages for each client flight in the mission."""
|
||||||
|
|
||||||
def __init__(self, mission: Mission, game: "Game") -> None:
|
def __init__(self, mission: Mission, game: "Game") -> None:
|
||||||
super().__init__(mission, game)
|
super().__init__(mission, game)
|
||||||
|
self.dark_kneeboard = self.game.settings.generate_dark_kneeboard and (
|
||||||
|
self.mission.start_time.hour > 19 or self.mission.start_time.hour < 7
|
||||||
|
)
|
||||||
|
|
||||||
def generate(self) -> None:
|
def generate(self) -> None:
|
||||||
"""Generates a kneeboard per client flight."""
|
"""Generates a kneeboard per client flight."""
|
||||||
@@ -372,7 +452,8 @@ class KneeboardGenerator(MissionInfoGenerator):
|
|||||||
if not flight.client_units:
|
if not flight.client_units:
|
||||||
continue
|
continue
|
||||||
all_flights[flight.aircraft_type].extend(
|
all_flights[flight.aircraft_type].extend(
|
||||||
self.generate_flight_kneeboard(flight))
|
self.generate_flight_kneeboard(flight)
|
||||||
|
)
|
||||||
return all_flights
|
return all_flights
|
||||||
|
|
||||||
def generate_flight_kneeboard(self, flight: FlightData) -> List[KneeboardPage]:
|
def generate_flight_kneeboard(self, flight: FlightData) -> List[KneeboardPage]:
|
||||||
@@ -384,6 +465,7 @@ class KneeboardGenerator(MissionInfoGenerator):
|
|||||||
self.awacs,
|
self.awacs,
|
||||||
self.tankers,
|
self.tankers,
|
||||||
self.jtacs,
|
self.jtacs,
|
||||||
self.mission.start_time
|
self.mission.start_time,
|
||||||
|
self.dark_kneeboard,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from gen.locations.preset_locations import PresetLocation
|
|||||||
class PresetControlPointLocations:
|
class PresetControlPointLocations:
|
||||||
"""A repository of preset locations for a given control point"""
|
"""A repository of preset locations for a given control point"""
|
||||||
|
|
||||||
# List of possible ashore locations to generate objects (Represented in miz file by an APC_AAV_7)
|
# List of possible ashore locations to generate objects (Represented in miz file by an APC_AAV_7_Amphibious)
|
||||||
ashore_locations: List[PresetLocation] = field(default_factory=list)
|
ashore_locations: List[PresetLocation] = field(default_factory=list)
|
||||||
|
|
||||||
# List of possible offshore locations to generate ship groups (Represented in miz file by an Oliver Hazard Perry)
|
# List of possible offshore locations to generate ship groups (Represented in miz file by an Oliver Hazard Perry)
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ from gen.locations.preset_locations import PresetLocation
|
|||||||
|
|
||||||
|
|
||||||
class MizDataLocationFinder:
|
class MizDataLocationFinder:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def compute_possible_locations(terrain_name: str, cp_name: str) -> PresetControlPointLocations:
|
def compute_possible_locations(
|
||||||
|
terrain_name: str, cp_name: str
|
||||||
|
) -> PresetControlPointLocations:
|
||||||
"""
|
"""
|
||||||
Extract the list of preset locations from miz data
|
Extract the list of preset locations from miz data
|
||||||
:param terrain_name: Terrain/Map name
|
:param terrain_name: Terrain/Map name
|
||||||
@@ -32,28 +33,55 @@ class MizDataLocationFinder:
|
|||||||
|
|
||||||
for vehicle_group in m.country("USA").vehicle_group:
|
for vehicle_group in m.country("USA").vehicle_group:
|
||||||
if len(vehicle_group.units) > 0:
|
if len(vehicle_group.units) > 0:
|
||||||
ashore_locations.append(PresetLocation(vehicle_group.position,
|
ashore_locations.append(
|
||||||
vehicle_group.units[0].heading,
|
PresetLocation(
|
||||||
vehicle_group.name))
|
vehicle_group.position,
|
||||||
|
vehicle_group.units[0].heading,
|
||||||
|
vehicle_group.name,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
for ship_group in m.country("USA").ship_group:
|
for ship_group in m.country("USA").ship_group:
|
||||||
if len(ship_group.units) > 0 and ship_group.units[0].type == ships.Oliver_Hazzard_Perry_class.id:
|
if (
|
||||||
offshore_locations.append(PresetLocation(ship_group.position,
|
len(ship_group.units) > 0
|
||||||
ship_group.units[0].heading,
|
and ship_group.units[0].type == ships.FFG_Oliver_Hazzard_Perry.id
|
||||||
ship_group.name))
|
):
|
||||||
|
offshore_locations.append(
|
||||||
|
PresetLocation(
|
||||||
|
ship_group.position,
|
||||||
|
ship_group.units[0].heading,
|
||||||
|
ship_group.name,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
for static_group in m.country("USA").static_group:
|
for static_group in m.country("USA").static_group:
|
||||||
if len(static_group.units) > 0:
|
if len(static_group.units) > 0:
|
||||||
powerplants_locations.append(PresetLocation(static_group.position,
|
powerplants_locations.append(
|
||||||
static_group.units[0].heading,
|
PresetLocation(
|
||||||
static_group.name))
|
static_group.position,
|
||||||
|
static_group.units[0].heading,
|
||||||
|
static_group.name,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if m.country("Iran") is not None:
|
if m.country("Iran") is not None:
|
||||||
for vehicle_group in m.country("Iran").vehicle_group:
|
for vehicle_group in m.country("Iran").vehicle_group:
|
||||||
if len(vehicle_group.units) > 0 and vehicle_group.units[0].type == MissilesSS.SS_N_2_Silkworm.id:
|
if (
|
||||||
antiship_locations.append(PresetLocation(vehicle_group.position,
|
len(vehicle_group.units) > 0
|
||||||
vehicle_group.units[0].heading,
|
and vehicle_group.units[0].type
|
||||||
vehicle_group.name))
|
== MissilesSS.AShM_SS_N_2_Silkworm.id
|
||||||
|
):
|
||||||
|
antiship_locations.append(
|
||||||
|
PresetLocation(
|
||||||
|
vehicle_group.position,
|
||||||
|
vehicle_group.units[0].heading,
|
||||||
|
vehicle_group.name,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return PresetControlPointLocations(ashore_locations, offshore_locations,
|
return PresetControlPointLocations(
|
||||||
antiship_locations, powerplants_locations)
|
ashore_locations,
|
||||||
|
offshore_locations,
|
||||||
|
antiship_locations,
|
||||||
|
powerplants_locations,
|
||||||
|
)
|
||||||
|
|||||||
@@ -6,10 +6,16 @@ from dcs import Point
|
|||||||
@dataclass
|
@dataclass
|
||||||
class PresetLocation:
|
class PresetLocation:
|
||||||
"""A preset location"""
|
"""A preset location"""
|
||||||
|
|
||||||
position: Point
|
position: Point
|
||||||
heading: int
|
heading: int
|
||||||
id: str
|
id: str
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "-" * 10 + "X: {}\n Y: {}\nHdg: {}°\nId: {}".format(self.position.x, self.position.y, self.heading,
|
return (
|
||||||
self.id) + "-" * 10
|
"-" * 10
|
||||||
|
+ "X: {}\n Y: {}\nHdg: {}°\nId: {}".format(
|
||||||
|
self.position.x, self.position.y, self.heading, self.id
|
||||||
|
)
|
||||||
|
+ "-" * 10
|
||||||
|
)
|
||||||
|
|||||||
@@ -4,15 +4,12 @@ from game import db
|
|||||||
from gen.missiles.scud_site import ScudGenerator
|
from gen.missiles.scud_site import ScudGenerator
|
||||||
from gen.missiles.v1_group import V1GroupGenerator
|
from gen.missiles.v1_group import V1GroupGenerator
|
||||||
|
|
||||||
MISSILES_MAP = {
|
MISSILES_MAP = {"V1GroupGenerator": V1GroupGenerator, "ScudGenerator": ScudGenerator}
|
||||||
"V1GroupGenerator": V1GroupGenerator,
|
|
||||||
"ScudGenerator": ScudGenerator
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def generate_missile_group(game, ground_object, faction_name: str):
|
def generate_missile_group(game, ground_object, faction_name: str):
|
||||||
"""
|
"""
|
||||||
This generate a ship group
|
This generate a missiles group
|
||||||
:return: Nothing, but put the group reference inside the ground object
|
:return: Nothing, but put the group reference inside the ground object
|
||||||
"""
|
"""
|
||||||
faction = db.FACTIONS[faction_name]
|
faction = db.FACTIONS[faction_name]
|
||||||
@@ -25,5 +22,9 @@ def generate_missile_group(game, ground_object, faction_name: str):
|
|||||||
generator.generate()
|
generator.generate()
|
||||||
return generator.get_generated_group()
|
return generator.get_generated_group()
|
||||||
else:
|
else:
|
||||||
logging.info("Unable to generate missile group, generator : " + str(gen) + "does not exists")
|
logging.info(
|
||||||
return None
|
"Unable to generate missile group, generator : "
|
||||||
|
+ str(gen)
|
||||||
|
+ "does not exists"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from gen.sam.group_generator import GroupGenerator
|
|||||||
|
|
||||||
|
|
||||||
class ScudGenerator(GroupGenerator):
|
class ScudGenerator(GroupGenerator):
|
||||||
|
|
||||||
def __init__(self, game, ground_object, faction):
|
def __init__(self, game, ground_object, faction):
|
||||||
super(ScudGenerator, self).__init__(game, ground_object)
|
super(ScudGenerator, self).__init__(game, ground_object)
|
||||||
self.faction = faction
|
self.faction = faction
|
||||||
@@ -14,17 +13,50 @@ class ScudGenerator(GroupGenerator):
|
|||||||
def generate(self):
|
def generate(self):
|
||||||
|
|
||||||
# Scuds
|
# Scuds
|
||||||
self.add_unit(MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M, "V1#0", self.position.x, self.position.y + random.randint(1, 8), self.heading)
|
self.add_unit(
|
||||||
self.add_unit(MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M, "V1#1", self.position.x + 50, self.position.y + random.randint(1, 8), self.heading)
|
MissilesSS.SSM_SS_1C_Scud_B,
|
||||||
self.add_unit(MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M, "V1#2", self.position.x + 100, self.position.y + random.randint(1, 8), self.heading)
|
"V1#0",
|
||||||
|
self.position.x,
|
||||||
|
self.position.y + random.randint(1, 8),
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
self.add_unit(
|
||||||
|
MissilesSS.SSM_SS_1C_Scud_B,
|
||||||
|
"V1#1",
|
||||||
|
self.position.x + 50,
|
||||||
|
self.position.y + random.randint(1, 8),
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
self.add_unit(
|
||||||
|
MissilesSS.SSM_SS_1C_Scud_B,
|
||||||
|
"V1#2",
|
||||||
|
self.position.x + 100,
|
||||||
|
self.position.y + random.randint(1, 8),
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
# Commander
|
# Commander
|
||||||
self.add_unit(Unarmed.Transport_UAZ_469, "Kubel#0", self.position.x - 35, self.position.y - 20,
|
self.add_unit(
|
||||||
self.heading)
|
Unarmed.LUV_UAZ_469_Jeep,
|
||||||
|
"Kubel#0",
|
||||||
|
self.position.x - 35,
|
||||||
|
self.position.y - 20,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
# Shorad
|
# Shorad
|
||||||
self.add_unit(AirDefence.SPAAA_ZSU_23_4_Shilka, "SHILKA#0", self.position.x - 55, self.position.y - 38,
|
self.add_unit(
|
||||||
self.heading)
|
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
|
||||||
|
"SHILKA#0",
|
||||||
|
self.position.x - 55,
|
||||||
|
self.position.y - 38,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
self.add_unit(AirDefence.SAM_SA_9_Strela_1_9P31, "STRELA#0",
|
self.add_unit(
|
||||||
self.position.x + 200, self.position.y + 15, 90)
|
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL,
|
||||||
|
"STRELA#0",
|
||||||
|
self.position.x + 200,
|
||||||
|
self.position.y + 15,
|
||||||
|
90,
|
||||||
|
)
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from gen.sam.group_generator import GroupGenerator
|
|||||||
|
|
||||||
|
|
||||||
class V1GroupGenerator(GroupGenerator):
|
class V1GroupGenerator(GroupGenerator):
|
||||||
|
|
||||||
def __init__(self, game, ground_object, faction):
|
def __init__(self, game, ground_object, faction):
|
||||||
super(V1GroupGenerator, self).__init__(game, ground_object)
|
super(V1GroupGenerator, self).__init__(game, ground_object)
|
||||||
self.faction = faction
|
self.faction = faction
|
||||||
@@ -14,19 +13,54 @@ class V1GroupGenerator(GroupGenerator):
|
|||||||
def generate(self):
|
def generate(self):
|
||||||
|
|
||||||
# Ramps
|
# Ramps
|
||||||
self.add_unit(MissilesSS.V_1_ramp, "V1#0", self.position.x, self.position.y + random.randint(1, 8), self.heading)
|
self.add_unit(
|
||||||
self.add_unit(MissilesSS.V_1_ramp, "V1#1", self.position.x + 50, self.position.y + random.randint(1, 8), self.heading)
|
MissilesSS.SSM_V_1_Launcher,
|
||||||
self.add_unit(MissilesSS.V_1_ramp, "V1#2", self.position.x + 100, self.position.y + random.randint(1, 8), self.heading)
|
"V1#0",
|
||||||
|
self.position.x,
|
||||||
|
self.position.y + random.randint(1, 8),
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
self.add_unit(
|
||||||
|
MissilesSS.SSM_V_1_Launcher,
|
||||||
|
"V1#1",
|
||||||
|
self.position.x + 50,
|
||||||
|
self.position.y + random.randint(1, 8),
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
self.add_unit(
|
||||||
|
MissilesSS.SSM_V_1_Launcher,
|
||||||
|
"V1#2",
|
||||||
|
self.position.x + 100,
|
||||||
|
self.position.y + random.randint(1, 8),
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
# Commander
|
# Commander
|
||||||
self.add_unit(Unarmed.Kübelwagen_82, "Kubel#0", self.position.x - 35, self.position.y - 20,
|
self.add_unit(
|
||||||
self.heading)
|
Unarmed.LUV_Kubelwagen_82,
|
||||||
|
"Kubel#0",
|
||||||
|
self.position.x - 35,
|
||||||
|
self.position.y - 20,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
# Self defense flak
|
# Self defense flak
|
||||||
flak_unit = random.choice([AirDefence.AAA_Flak_Vierling_38, AirDefence.AAA_Flak_38])
|
flak_unit = random.choice(
|
||||||
|
[AirDefence.AAA_Flak_Vierling_38_Quad_20mm, AirDefence.AAA_Flak_38_20mm]
|
||||||
|
)
|
||||||
|
|
||||||
self.add_unit(flak_unit, "FLAK#0", self.position.x - 55, self.position.y - 38,
|
self.add_unit(
|
||||||
self.heading)
|
flak_unit,
|
||||||
|
"FLAK#0",
|
||||||
|
self.position.x - 55,
|
||||||
|
self.position.y - 38,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
self.add_unit(Unarmed.Blitz_3_6_6700A, "Blitz#0",
|
self.add_unit(
|
||||||
self.position.x + 200, self.position.y + 15, 90)
|
Unarmed.Truck_Opel_Blitz,
|
||||||
|
"Blitz#0",
|
||||||
|
self.position.x + 200,
|
||||||
|
self.position.y + 15,
|
||||||
|
90,
|
||||||
|
)
|
||||||
|
|||||||
304
gen/naming.py
304
gen/naming.py
@@ -9,39 +9,242 @@ from game import db
|
|||||||
|
|
||||||
from gen.flights.flight import Flight
|
from gen.flights.flight import Flight
|
||||||
|
|
||||||
ALPHA_MILITARY = ["Alpha", "Bravo", "Charlie", "Delta", "Echo", "Foxtrot",
|
ALPHA_MILITARY = [
|
||||||
"Golf", "Hotel", "India", "Juliet", "Kilo", "Lima", "Mike",
|
"Alpha",
|
||||||
"November", "Oscar", "Papa", "Quebec", "Romeo", "Sierra",
|
"Bravo",
|
||||||
"Tango", "Uniform", "Victor", "Whisky", "XRay", "Yankee",
|
"Charlie",
|
||||||
"Zulu", "Zero"]
|
"Delta",
|
||||||
|
"Echo",
|
||||||
|
"Foxtrot",
|
||||||
|
"Golf",
|
||||||
|
"Hotel",
|
||||||
|
"India",
|
||||||
|
"Juliet",
|
||||||
|
"Kilo",
|
||||||
|
"Lima",
|
||||||
|
"Mike",
|
||||||
|
"November",
|
||||||
|
"Oscar",
|
||||||
|
"Papa",
|
||||||
|
"Quebec",
|
||||||
|
"Romeo",
|
||||||
|
"Sierra",
|
||||||
|
"Tango",
|
||||||
|
"Uniform",
|
||||||
|
"Victor",
|
||||||
|
"Whisky",
|
||||||
|
"XRay",
|
||||||
|
"Yankee",
|
||||||
|
"Zulu",
|
||||||
|
"Zero",
|
||||||
|
]
|
||||||
|
|
||||||
ANIMALS = [
|
ANIMALS = [
|
||||||
"SHARK", "TORTOISE", "BAT", "PANGOLIN", "AARDWOLF",
|
"SHARK",
|
||||||
"MONKEY", "BUFFALO", "DOG", "BOBCAT", "LYNX", "PANTHER", "TIGER",
|
"TORTOISE",
|
||||||
"LION", "OWL", "BUTTERFLY", "BISON", "DUCK", "COBRA", "MAMBA",
|
"BAT",
|
||||||
"DOLPHIN", "PHEASANT", "ARMADILLLO", "RACOON", "ZEBRA", "COW", "COYOTE", "FOX",
|
"PANGOLIN",
|
||||||
"LIGHTFOOT", "COTTONMOUTH", "TAURUS", "VIPER", "CASTOR", "GIRAFFE", "SNAKE",
|
"AARDWOLF",
|
||||||
"MONSTER", "ALBATROSS", "HAWK", "DOVE", "MOCKINGBIRD", "GECKO", "ORYX", "GORILLA",
|
"MONKEY",
|
||||||
"HARAMBE", "GOOSE", "MAVERICK", "HARE", "JACKAL", "LEOPARD", "CAT", "MUSK", "ORCA",
|
"BUFFALO",
|
||||||
"OCELOT", "BEAR", "PANDA", "GULL", "PENGUIN", "PYTHON", "RAVEN", "DEER", "MOOSE",
|
"DOG",
|
||||||
"REINDEER", "SHEEP", "GAZELLE", "INSECT", "VULTURE", "WALLABY", "KANGAROO", "KOALA",
|
"BOBCAT",
|
||||||
"KIWI", "WHALE", "FISH", "RHINO", "HIPPO", "RAT", "WOODPECKER", "WORM", "BABOON",
|
"LYNX",
|
||||||
"YAK", "SCORPIO", "HORSE", "POODLE", "CENTIPEDE", "CHICKEN", "CHEETAH", "CHAMELEON",
|
"PANTHER",
|
||||||
"CATFISH", "CATERPILLAR", "CARACAL", "CAMEL", "CAIMAN", "BARRACUDA", "BANDICOOT",
|
"TIGER",
|
||||||
"ALLIGATOR", "BONGO", "CORAL", "ELEPHANT", "ANTELOPE", "CRAB", "DACHSHUND", "DODO",
|
"LION",
|
||||||
"FLAMINGO", "FERRET", "FALCON", "BULLDOG", "DONKEY", "IGUANA", "TAMARIN", "HARRIER",
|
"OWL",
|
||||||
"GRIZZLY", "GREYHOUND", "GRASSHOPPER", "JAGUAR", "LADYBUG", "KOMODO", "DRAGON", "LIZARD",
|
"BUTTERFLY",
|
||||||
"LLAMA", "LOBSTER", "OCTOPUS", "MANATEE", "MAGPIE", "MACAW", "OSTRICH", "OYSTER",
|
"BISON",
|
||||||
"MOLE", "MULE", "MOTH", "MONGOOSE", "MOLLY", "MEERKAT", "MOUSE", "PEACOCK", "PIKE", "ROBIN",
|
"DUCK",
|
||||||
"RAGDOLL", "PLATYPUS", "PELICAN", "PARROT", "PORCUPINE", "PIRANHA", "PUMA", "PUG", "TAPIR",
|
"COBRA",
|
||||||
"TERMITE", "URCHIN", "SHRIMP", "TURKEY", "TOUCAN", "TETRA", "HUSKY", "STARFISH", "SWAN",
|
"MAMBA",
|
||||||
"FROG", "SQUIRREL", "WALRUS", "WARTHOG", "CORGI", "WEASEL", "WOMBAT", "WOLVERINE", "MAMMOTH",
|
"DOLPHIN",
|
||||||
"TOAD", "WOLF", "ZEBU", "SEAL", "SKATE", "JELLYFISH", "MOSQUITO", "LOCUST", "SLUG", "SNAIL",
|
"PHEASANT",
|
||||||
"HEDGEHOG", "PIGLET", "FENNEC", "BADGER", "ALPACA", "DINGO", "COLT", "SKUNK", "BUNNY", "IMPALA",
|
"ARMADILLO",
|
||||||
"GUANACO", "CAPYBARA", "ELK", "MINK", "PRONGHORN", "CROW", "BUMBLEBEE", "FAWN", "OTTER", "WATERBUCK",
|
"RACOON",
|
||||||
"JERBOA", "KITTEN", "ARGALI", "OX", "MARE", "FINCH", "BASILISK", "GOPHER", "HAMSTER", "CANARY", "WOODCHUCK",
|
"ZEBRA",
|
||||||
"ANACONDA"
|
"COW",
|
||||||
]
|
"COYOTE",
|
||||||
|
"FOX",
|
||||||
|
"LIGHTFOOT",
|
||||||
|
"COTTONMOUTH",
|
||||||
|
"TAURUS",
|
||||||
|
"VIPER",
|
||||||
|
"CASTOR",
|
||||||
|
"GIRAFFE",
|
||||||
|
"SNAKE",
|
||||||
|
"MONSTER",
|
||||||
|
"ALBATROSS",
|
||||||
|
"HAWK",
|
||||||
|
"DOVE",
|
||||||
|
"MOCKINGBIRD",
|
||||||
|
"GECKO",
|
||||||
|
"ORYX",
|
||||||
|
"GORILLA",
|
||||||
|
"HARAMBE",
|
||||||
|
"GOOSE",
|
||||||
|
"MAVERICK",
|
||||||
|
"HARE",
|
||||||
|
"JACKAL",
|
||||||
|
"LEOPARD",
|
||||||
|
"CAT",
|
||||||
|
"MUSK",
|
||||||
|
"ORCA",
|
||||||
|
"OCELOT",
|
||||||
|
"BEAR",
|
||||||
|
"PANDA",
|
||||||
|
"GULL",
|
||||||
|
"PENGUIN",
|
||||||
|
"PYTHON",
|
||||||
|
"RAVEN",
|
||||||
|
"DEER",
|
||||||
|
"MOOSE",
|
||||||
|
"REINDEER",
|
||||||
|
"SHEEP",
|
||||||
|
"GAZELLE",
|
||||||
|
"INSECT",
|
||||||
|
"VULTURE",
|
||||||
|
"WALLABY",
|
||||||
|
"KANGAROO",
|
||||||
|
"KOALA",
|
||||||
|
"KIWI",
|
||||||
|
"WHALE",
|
||||||
|
"FISH",
|
||||||
|
"RHINO",
|
||||||
|
"HIPPO",
|
||||||
|
"RAT",
|
||||||
|
"WOODPECKER",
|
||||||
|
"WORM",
|
||||||
|
"BABOON",
|
||||||
|
"YAK",
|
||||||
|
"SCORPIO",
|
||||||
|
"HORSE",
|
||||||
|
"POODLE",
|
||||||
|
"CENTIPEDE",
|
||||||
|
"CHICKEN",
|
||||||
|
"CHEETAH",
|
||||||
|
"CHAMELEON",
|
||||||
|
"CATFISH",
|
||||||
|
"CATERPILLAR",
|
||||||
|
"CARACAL",
|
||||||
|
"CAMEL",
|
||||||
|
"CAIMAN",
|
||||||
|
"BARRACUDA",
|
||||||
|
"BANDICOOT",
|
||||||
|
"ALLIGATOR",
|
||||||
|
"BONGO",
|
||||||
|
"CORAL",
|
||||||
|
"ELEPHANT",
|
||||||
|
"ANTELOPE",
|
||||||
|
"CRAB",
|
||||||
|
"DACHSHUND",
|
||||||
|
"DODO",
|
||||||
|
"FLAMINGO",
|
||||||
|
"FERRET",
|
||||||
|
"FALCON",
|
||||||
|
"BULLDOG",
|
||||||
|
"DONKEY",
|
||||||
|
"IGUANA",
|
||||||
|
"TAMARIN",
|
||||||
|
"HARRIER",
|
||||||
|
"GRIZZLY",
|
||||||
|
"GREYHOUND",
|
||||||
|
"GRASSHOPPER",
|
||||||
|
"JAGUAR",
|
||||||
|
"LADYBUG",
|
||||||
|
"KOMODO",
|
||||||
|
"DRAGON",
|
||||||
|
"LIZARD",
|
||||||
|
"LLAMA",
|
||||||
|
"LOBSTER",
|
||||||
|
"OCTOPUS",
|
||||||
|
"MANATEE",
|
||||||
|
"MAGPIE",
|
||||||
|
"MACAW",
|
||||||
|
"OSTRICH",
|
||||||
|
"OYSTER",
|
||||||
|
"MOLE",
|
||||||
|
"MULE",
|
||||||
|
"MOTH",
|
||||||
|
"MONGOOSE",
|
||||||
|
"MOLLY",
|
||||||
|
"MEERKAT",
|
||||||
|
"MOUSE",
|
||||||
|
"PEACOCK",
|
||||||
|
"PIKE",
|
||||||
|
"ROBIN",
|
||||||
|
"RAGDOLL",
|
||||||
|
"PLATYPUS",
|
||||||
|
"PELICAN",
|
||||||
|
"PARROT",
|
||||||
|
"PORCUPINE",
|
||||||
|
"PIRANHA",
|
||||||
|
"PUMA",
|
||||||
|
"PUG",
|
||||||
|
"TAPIR",
|
||||||
|
"TERMITE",
|
||||||
|
"URCHIN",
|
||||||
|
"SHRIMP",
|
||||||
|
"TURKEY",
|
||||||
|
"TOUCAN",
|
||||||
|
"TETRA",
|
||||||
|
"HUSKY",
|
||||||
|
"STARFISH",
|
||||||
|
"SWAN",
|
||||||
|
"FROG",
|
||||||
|
"SQUIRREL",
|
||||||
|
"WALRUS",
|
||||||
|
"WARTHOG",
|
||||||
|
"CORGI",
|
||||||
|
"WEASEL",
|
||||||
|
"WOMBAT",
|
||||||
|
"WOLVERINE",
|
||||||
|
"MAMMOTH",
|
||||||
|
"TOAD",
|
||||||
|
"WOLF",
|
||||||
|
"ZEBU",
|
||||||
|
"SEAL",
|
||||||
|
"SKATE",
|
||||||
|
"JELLYFISH",
|
||||||
|
"MOSQUITO",
|
||||||
|
"LOCUST",
|
||||||
|
"SLUG",
|
||||||
|
"SNAIL",
|
||||||
|
"HEDGEHOG",
|
||||||
|
"PIGLET",
|
||||||
|
"FENNEC",
|
||||||
|
"BADGER",
|
||||||
|
"ALPACA",
|
||||||
|
"DINGO",
|
||||||
|
"COLT",
|
||||||
|
"SKUNK",
|
||||||
|
"BUNNY",
|
||||||
|
"IMPALA",
|
||||||
|
"GUANACO",
|
||||||
|
"CAPYBARA",
|
||||||
|
"ELK",
|
||||||
|
"MINK",
|
||||||
|
"PRONGHORN",
|
||||||
|
"CROW",
|
||||||
|
"BUMBLEBEE",
|
||||||
|
"FAWN",
|
||||||
|
"OTTER",
|
||||||
|
"WATERBUCK",
|
||||||
|
"JERBOA",
|
||||||
|
"KITTEN",
|
||||||
|
"ARGALI",
|
||||||
|
"OX",
|
||||||
|
"MARE",
|
||||||
|
"FINCH",
|
||||||
|
"BASILISK",
|
||||||
|
"GOPHER",
|
||||||
|
"HAMSTER",
|
||||||
|
"CANARY",
|
||||||
|
"WOODCHUCK",
|
||||||
|
"ANACONDA",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class NameGenerator:
|
class NameGenerator:
|
||||||
number = 0
|
number = 0
|
||||||
@@ -72,21 +275,36 @@ class NameGenerator:
|
|||||||
name_str = flight.custom_name
|
name_str = flight.custom_name
|
||||||
else:
|
else:
|
||||||
name_str = "{} {}".format(
|
name_str = "{} {}".format(
|
||||||
flight.package.target.name, flight.flight_type)
|
flight.package.target.name, flight.flight_type
|
||||||
|
)
|
||||||
except AttributeError: # Here to maintain save compatibility with 2.3
|
except AttributeError: # Here to maintain save compatibility with 2.3
|
||||||
name_str = "{} {}".format(
|
name_str = "{} {}".format(flight.package.target.name, flight.flight_type)
|
||||||
flight.package.target.name, flight.flight_type)
|
return "{}|{}|{}|{}|{}|".format(
|
||||||
return "{}|{}|{}|{}|{}|".format(name_str, country.id, cls.aircraft_number, parent_base_id, db.unit_type_name(flight.unit_type))
|
name_str,
|
||||||
|
country.id,
|
||||||
|
cls.aircraft_number,
|
||||||
|
parent_base_id,
|
||||||
|
db.unit_type_name(flight.unit_type),
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def next_unit_name(cls, country: Country, parent_base_id: int, unit_type: UnitType):
|
def next_unit_name(cls, country: Country, parent_base_id: int, unit_type: UnitType):
|
||||||
cls.number += 1
|
cls.number += 1
|
||||||
return "unit|{}|{}|{}|{}|".format(country.id, cls.number, parent_base_id, db.unit_type_name(unit_type))
|
return "unit|{}|{}|{}|{}|".format(
|
||||||
|
country.id, cls.number, parent_base_id, db.unit_type_name(unit_type)
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def next_infantry_name(cls, country: Country, parent_base_id: int, unit_type: UnitType):
|
def next_infantry_name(
|
||||||
|
cls, country: Country, parent_base_id: int, unit_type: UnitType
|
||||||
|
):
|
||||||
cls.infantry_number += 1
|
cls.infantry_number += 1
|
||||||
return "infantry|{}|{}|{}|{}|".format(country.id, cls.infantry_number, parent_base_id, db.unit_type_name(unit_type))
|
return "infantry|{}|{}|{}|{}|".format(
|
||||||
|
country.id,
|
||||||
|
cls.infantry_number,
|
||||||
|
parent_base_id,
|
||||||
|
db.unit_type_name(unit_type),
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def next_basedefense_name():
|
def next_basedefense_name():
|
||||||
@@ -100,7 +318,9 @@ class NameGenerator:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def next_tanker_name(cls, country: Country, unit_type: UnitType):
|
def next_tanker_name(cls, country: Country, unit_type: UnitType):
|
||||||
cls.number += 1
|
cls.number += 1
|
||||||
return "tanker|{}|{}|0|{}".format(country.id, cls.number, db.unit_type_name(unit_type))
|
return "tanker|{}|{}|0|{}".format(
|
||||||
|
country.id, cls.number, db.unit_type_name(unit_type)
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def next_carrier_name(cls, country: Country):
|
def next_carrier_name(cls, country: Country):
|
||||||
@@ -112,7 +332,11 @@ class NameGenerator:
|
|||||||
if len(cls.ANIMALS) == 0:
|
if len(cls.ANIMALS) == 0:
|
||||||
for i in range(10):
|
for i in range(10):
|
||||||
new_name_generated = True
|
new_name_generated = True
|
||||||
alpha_mil_name = random.choice(ALPHA_MILITARY).upper() + "#" + str(random.randint(0, 100))
|
alpha_mil_name = (
|
||||||
|
random.choice(ALPHA_MILITARY).upper()
|
||||||
|
+ "#"
|
||||||
|
+ str(random.randint(0, 100))
|
||||||
|
)
|
||||||
for existing_name in cls.existing_alphas:
|
for existing_name in cls.existing_alphas:
|
||||||
if existing_name == alpha_mil_name:
|
if existing_name == alpha_mil_name:
|
||||||
new_name_generated = False
|
new_name_generated = False
|
||||||
|
|||||||
@@ -68,9 +68,10 @@ class Radio:
|
|||||||
|
|
||||||
def range(self) -> Iterator[RadioFrequency]:
|
def range(self) -> Iterator[RadioFrequency]:
|
||||||
"""Returns an iterator over the usable frequencies of this radio."""
|
"""Returns an iterator over the usable frequencies of this radio."""
|
||||||
return (RadioFrequency(x) for x in range(
|
return (
|
||||||
self.minimum.hertz, self.maximum.hertz, self.step.hertz
|
RadioFrequency(x)
|
||||||
))
|
for x in range(self.minimum.hertz, self.maximum.hertz, self.step.hertz)
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def last_channel(self) -> RadioFrequency:
|
def last_channel(self) -> RadioFrequency:
|
||||||
@@ -99,14 +100,12 @@ RADIOS: List[Radio] = [
|
|||||||
Radio("SCR-522", MHz(100), MHz(156), step=MHz(1)),
|
Radio("SCR-522", MHz(100), MHz(156), step=MHz(1)),
|
||||||
Radio("A.R.I. 1063", MHz(100), MHz(156), step=MHz(1)),
|
Radio("A.R.I. 1063", MHz(100), MHz(156), step=MHz(1)),
|
||||||
Radio("BC-1206", kHz(200), kHz(400), step=kHz(10)),
|
Radio("BC-1206", kHz(200), kHz(400), step=kHz(10)),
|
||||||
|
|
||||||
# Note: The M2000C V/UHF can operate in both ranges, but has a gap between
|
# Note: The M2000C V/UHF can operate in both ranges, but has a gap between
|
||||||
# 150 MHz and 225 MHz. We can't allocate in that gap, and the current
|
# 150 MHz and 225 MHz. We can't allocate in that gap, and the current
|
||||||
# system doesn't model gaps, so just pretend it ends at 150 MHz for now. We
|
# system doesn't model gaps, so just pretend it ends at 150 MHz for now. We
|
||||||
# can model gaps later if needed.
|
# can model gaps later if needed.
|
||||||
Radio("TRT ERA 7000 V/UHF", MHz(118), MHz(150), step=MHz(1)),
|
Radio("TRT ERA 7000 V/UHF", MHz(118), MHz(150), step=MHz(1)),
|
||||||
Radio("TRT ERA 7200 UHF", MHz(225), MHz(400), step=MHz(1)),
|
Radio("TRT ERA 7200 UHF", MHz(225), MHz(400), step=MHz(1)),
|
||||||
|
|
||||||
# Tomcat radios
|
# Tomcat radios
|
||||||
# # https://www.heatblur.se/F-14Manual/general.html#an-arc-159-uhf-1-radio
|
# # https://www.heatblur.se/F-14Manual/general.html#an-arc-159-uhf-1-radio
|
||||||
Radio("AN/ARC-159", MHz(225), MHz(400), step=MHz(1)),
|
Radio("AN/ARC-159", MHz(225), MHz(400), step=MHz(1)),
|
||||||
@@ -114,31 +113,23 @@ RADIOS: List[Radio] = [
|
|||||||
# to 400 MHz range, but we can't model gaps with the current implementation.
|
# to 400 MHz range, but we can't model gaps with the current implementation.
|
||||||
# https://www.heatblur.se/F-14Manual/general.html#an-arc-182-v-uhf-2-radio
|
# https://www.heatblur.se/F-14Manual/general.html#an-arc-182-v-uhf-2-radio
|
||||||
Radio("AN/ARC-182", MHz(108), MHz(174), step=MHz(1)),
|
Radio("AN/ARC-182", MHz(108), MHz(174), step=MHz(1)),
|
||||||
|
|
||||||
# Also capable of [103, 156) at 25 kHz intervals, but we can't do gaps.
|
# Also capable of [103, 156) at 25 kHz intervals, but we can't do gaps.
|
||||||
Radio("FR 22", MHz(225), MHz(400), step=kHz(50)),
|
Radio("FR 22", MHz(225), MHz(400), step=kHz(50)),
|
||||||
|
|
||||||
# P-51 / P-47 Radio
|
# P-51 / P-47 Radio
|
||||||
# 4 preset channels (A/B/C/D)
|
# 4 preset channels (A/B/C/D)
|
||||||
Radio("SCR522", MHz(100), MHz(156), step=kHz(25)),
|
Radio("SCR522", MHz(100), MHz(156), step=kHz(25)),
|
||||||
|
|
||||||
Radio("R&S M3AR VHF", MHz(120), MHz(174), step=MHz(1)),
|
Radio("R&S M3AR VHF", MHz(120), MHz(174), step=MHz(1)),
|
||||||
Radio("R&S M3AR UHF", MHz(225), MHz(400), step=MHz(1)),
|
Radio("R&S M3AR UHF", MHz(225), MHz(400), step=MHz(1)),
|
||||||
|
|
||||||
# MiG-15bis
|
# MiG-15bis
|
||||||
Radio("RSI-6K HF", MHz(3, 750), MHz(5), step=kHz(25)),
|
Radio("RSI-6K HF", MHz(3, 750), MHz(5), step=kHz(25)),
|
||||||
|
|
||||||
# MiG-19P
|
# MiG-19P
|
||||||
Radio("RSIU-4V", MHz(100), MHz(150), step=MHz(1)),
|
Radio("RSIU-4V", MHz(100), MHz(150), step=MHz(1)),
|
||||||
|
|
||||||
# MiG-21bis
|
# MiG-21bis
|
||||||
Radio("RSIU-5V", MHz(118), MHz(140), step=MHz(1)),
|
Radio("RSIU-5V", MHz(118), MHz(140), step=MHz(1)),
|
||||||
|
|
||||||
# Ka-50
|
# Ka-50
|
||||||
# Note: Also capable of 100MHz-150MHz, but we can't model gaps.
|
# Note: Also capable of 100MHz-150MHz, but we can't model gaps.
|
||||||
Radio("R-800L1", MHz(220), MHz(400), step=kHz(25)),
|
Radio("R-800L1", MHz(220), MHz(400), step=kHz(25)),
|
||||||
Radio("R-828", MHz(20), MHz(60), step=kHz(25)),
|
Radio("R-828", MHz(20), MHz(60), step=kHz(25)),
|
||||||
|
|
||||||
# UH-1H
|
# UH-1H
|
||||||
Radio("AN/ARC-51BX", MHz(225), MHz(400), step=kHz(50)),
|
Radio("AN/ARC-51BX", MHz(225), MHz(400), step=kHz(50)),
|
||||||
Radio("AN/ARC-131", MHz(30), MHz(76), step=kHz(50)),
|
Radio("AN/ARC-131", MHz(30), MHz(76), step=kHz(50)),
|
||||||
@@ -218,7 +209,8 @@ class RadioRegistry:
|
|||||||
# https://github.com/Khopa/dcs_liberation/issues/598
|
# https://github.com/Khopa/dcs_liberation/issues/598
|
||||||
channel = radio.last_channel
|
channel = radio.last_channel
|
||||||
logging.warning(
|
logging.warning(
|
||||||
f"No more free channels for {radio.name}. Reusing {channel}.")
|
f"No more free channels for {radio.name}. Reusing {channel}."
|
||||||
|
)
|
||||||
return channel
|
return channel
|
||||||
|
|
||||||
def alloc_uhf(self) -> RadioFrequency:
|
def alloc_uhf(self) -> RadioFrequency:
|
||||||
|
|||||||
@@ -25,8 +25,9 @@ class RunwayData:
|
|||||||
icls: Optional[int] = None
|
icls: Optional[int] = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def for_airfield(cls, airport: Airport, runway_heading: int,
|
def for_airfield(
|
||||||
runway_name: str) -> RunwayData:
|
cls, airport: Airport, runway_heading: int, runway_name: str
|
||||||
|
) -> RunwayData:
|
||||||
"""Creates RunwayData for the given runway of an airfield.
|
"""Creates RunwayData for the given runway of an airfield.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -56,7 +57,7 @@ class RunwayData:
|
|||||||
atc=atc,
|
atc=atc,
|
||||||
tacan=tacan,
|
tacan=tacan,
|
||||||
tacan_callsign=tacan_callsign,
|
tacan_callsign=tacan_callsign,
|
||||||
ils=ils
|
ils=ils,
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -20,15 +20,19 @@ class BoforsGenerator(AirDefenseGroupGenerator):
|
|||||||
grid_x = random.randint(2, 3)
|
grid_x = random.randint(2, 3)
|
||||||
grid_y = random.randint(2, 3)
|
grid_y = random.randint(2, 3)
|
||||||
|
|
||||||
spacing = random.randint(10,40)
|
spacing = random.randint(10, 40)
|
||||||
|
|
||||||
index = 0
|
index = 0
|
||||||
for i in range(grid_x):
|
for i in range(grid_x):
|
||||||
for j in range(grid_y):
|
for j in range(grid_y):
|
||||||
index = index+1
|
index = index + 1
|
||||||
self.add_unit(AirDefence.AAA_Bofors_40mm, "AAA#" + str(index),
|
self.add_unit(
|
||||||
self.position.x + spacing*i,
|
AirDefence.AAA_40mm_Bofors,
|
||||||
self.position.y + spacing*j, self.heading)
|
"AAA#" + str(index),
|
||||||
|
self.position.x + spacing * i,
|
||||||
|
self.position.y + spacing * j,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def range(cls) -> AirDefenseRange:
|
def range(cls) -> AirDefenseRange:
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ from gen.sam.airdefensegroupgenerator import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
GFLAK = [
|
GFLAK = [
|
||||||
AirDefence.AAA_Flak_Vierling_38,
|
AirDefence.AAA_Flak_Vierling_38_Quad_20mm,
|
||||||
AirDefence.AAA_8_8cm_Flak_18,
|
AirDefence.AAA_8_8cm_Flak_18,
|
||||||
AirDefence.AAA_8_8cm_Flak_36,
|
AirDefence.AAA_8_8cm_Flak_36,
|
||||||
AirDefence.AAA_8_8cm_Flak_37,
|
AirDefence.AAA_8_8cm_Flak_37,
|
||||||
AirDefence.AAA_8_8cm_Flak_41,
|
AirDefence.AAA_8_8cm_Flak_41,
|
||||||
AirDefence.AAA_Flak_38,
|
AirDefence.AAA_Flak_38_20mm,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -37,34 +37,64 @@ class FlakGenerator(AirDefenseGroupGenerator):
|
|||||||
|
|
||||||
for i in range(grid_x):
|
for i in range(grid_x):
|
||||||
for j in range(grid_y):
|
for j in range(grid_y):
|
||||||
index = index+1
|
index = index + 1
|
||||||
self.add_unit(unit_type, "AAA#" + str(index),
|
self.add_unit(
|
||||||
self.position.x + spacing*i + random.randint(1,5),
|
unit_type,
|
||||||
self.position.y + spacing*j + random.randint(1,5), self.heading)
|
"AAA#" + str(index),
|
||||||
|
self.position.x + spacing * i + random.randint(1, 5),
|
||||||
|
self.position.y + spacing * j + random.randint(1, 5),
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
if(mixed):
|
if mixed:
|
||||||
unit_type = random.choice(GFLAK)
|
unit_type = random.choice(GFLAK)
|
||||||
|
|
||||||
# Search lights
|
# Search lights
|
||||||
search_pos = self.get_circular_position(random.randint(2,3), 80)
|
search_pos = self.get_circular_position(random.randint(2, 3), 80)
|
||||||
for index, pos in enumerate(search_pos):
|
for index, pos in enumerate(search_pos):
|
||||||
self.add_unit(AirDefence.Flak_Searchlight_37, "SearchLight#" + str(index), pos[0], pos[1], self.heading)
|
self.add_unit(
|
||||||
|
AirDefence.SL_Flakscheinwerfer_37,
|
||||||
|
"SearchLight#" + str(index),
|
||||||
|
pos[0],
|
||||||
|
pos[1],
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
# Support
|
# Support
|
||||||
self.add_unit(AirDefence.Maschinensatz_33, "MC33#", self.position.x-20, self.position.y-20, self.heading)
|
self.add_unit(
|
||||||
self.add_unit(AirDefence.AAA_Kdo_G_40, "KDO#", self.position.x - 25, self.position.y - 20,
|
AirDefence.PU_Maschinensatz_33,
|
||||||
self.heading)
|
"MC33#",
|
||||||
|
self.position.x - 20,
|
||||||
|
self.position.y - 20,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
self.add_unit(
|
||||||
|
AirDefence.AAA_SP_Kdo_G_40,
|
||||||
|
"KDO#",
|
||||||
|
self.position.x - 25,
|
||||||
|
self.position.y - 20,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
# Commander
|
# Commander
|
||||||
self.add_unit(Unarmed.Kübelwagen_82, "Kubel#", self.position.x - 35, self.position.y - 20,
|
self.add_unit(
|
||||||
self.heading)
|
Unarmed.LUV_Kubelwagen_82,
|
||||||
|
"Kubel#",
|
||||||
|
self.position.x - 35,
|
||||||
|
self.position.y - 20,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
# Some Opel Blitz trucks
|
# Some Opel Blitz trucks
|
||||||
for i in range(int(max(1,grid_x/2))):
|
for i in range(int(max(1, grid_x / 2))):
|
||||||
for j in range(int(max(1,grid_x/2))):
|
for j in range(int(max(1, grid_x / 2))):
|
||||||
self.add_unit(Unarmed.Blitz_3_6_6700A, "BLITZ#" + str(index),
|
self.add_unit(
|
||||||
self.position.x + 125 + 15*i + random.randint(1,5),
|
Unarmed.Truck_Opel_Blitz,
|
||||||
self.position.y + 15*j + random.randint(1,5), 75)
|
"BLITZ#" + str(index),
|
||||||
|
self.position.x + 125 + 15 * i + random.randint(1, 5),
|
||||||
|
self.position.y + 15 * j + random.randint(1, 5),
|
||||||
|
75,
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def range(cls) -> AirDefenseRange:
|
def range(cls) -> AirDefenseRange:
|
||||||
|
|||||||
@@ -24,12 +24,22 @@ class Flak18Generator(AirDefenseGroupGenerator):
|
|||||||
for i in range(3):
|
for i in range(3):
|
||||||
for j in range(2):
|
for j in range(2):
|
||||||
index = index + 1
|
index = index + 1
|
||||||
self.add_unit(AirDefence.AAA_8_8cm_Flak_18, "AAA#" + str(index),
|
self.add_unit(
|
||||||
self.position.x + spacing * i + random.randint(1, 5),
|
AirDefence.AAA_8_8cm_Flak_18,
|
||||||
self.position.y + spacing * j + random.randint(1, 5), self.heading)
|
"AAA#" + str(index),
|
||||||
|
self.position.x + spacing * i + random.randint(1, 5),
|
||||||
|
self.position.y + spacing * j + random.randint(1, 5),
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
# Add a commander truck
|
# Add a commander truck
|
||||||
self.add_unit(Unarmed.Blitz_3_6_6700A, "Blitz#", self.position.x - 35, self.position.y - 20, self.heading)
|
self.add_unit(
|
||||||
|
Unarmed.Truck_Opel_Blitz,
|
||||||
|
"Blitz#",
|
||||||
|
self.position.x - 35,
|
||||||
|
self.position.y - 20,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def range(cls) -> AirDefenseRange:
|
def range(cls) -> AirDefenseRange:
|
||||||
|
|||||||
@@ -19,15 +19,25 @@ class KS19Generator(AirDefenseGroupGenerator):
|
|||||||
|
|
||||||
spacing = random.randint(10, 40)
|
spacing = random.randint(10, 40)
|
||||||
|
|
||||||
self.add_unit(highdigitsams.AAA_SON_9_Fire_Can, "TR", self.position.x - 20, self.position.y - 20, self.heading)
|
self.add_unit(
|
||||||
|
highdigitsams.AAA_SON_9_Fire_Can,
|
||||||
|
"TR",
|
||||||
|
self.position.x - 20,
|
||||||
|
self.position.y - 20,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
index = 0
|
index = 0
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
for j in range(3):
|
for j in range(3):
|
||||||
index = index + 1
|
index = index + 1
|
||||||
self.add_unit(highdigitsams.AAA_100mm_KS_19, "AAA#" + str(index),
|
self.add_unit(
|
||||||
self.position.x + spacing * i,
|
highdigitsams.AAA_100mm_KS_19,
|
||||||
self.position.y + spacing * j, self.heading)
|
"AAA#" + str(index),
|
||||||
|
self.position.x + spacing * i,
|
||||||
|
self.position.y + spacing * j,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def range(cls) -> AirDefenseRange:
|
def range(cls) -> AirDefenseRange:
|
||||||
|
|||||||
@@ -20,21 +20,63 @@ class AllyWW2FlakGenerator(AirDefenseGroupGenerator):
|
|||||||
|
|
||||||
positions = self.get_circular_position(4, launcher_distance=30, coverage=360)
|
positions = self.get_circular_position(4, launcher_distance=30, coverage=360)
|
||||||
for i, position in enumerate(positions):
|
for i, position in enumerate(positions):
|
||||||
self.add_unit(AirDefence.AA_gun_QF_3_7, "AA#" + str(i), position[0], position[1], position[2])
|
self.add_unit(
|
||||||
|
AirDefence.AAA_QF_3_7,
|
||||||
|
"AA#" + str(i),
|
||||||
|
position[0],
|
||||||
|
position[1],
|
||||||
|
position[2],
|
||||||
|
)
|
||||||
|
|
||||||
positions = self.get_circular_position(8, launcher_distance=60, coverage=360)
|
positions = self.get_circular_position(8, launcher_distance=60, coverage=360)
|
||||||
for i, position in enumerate(positions):
|
for i, position in enumerate(positions):
|
||||||
self.add_unit(AirDefence.AAA_M1_37mm, "AA#" + str(4 + i), position[0], position[1], position[2])
|
self.add_unit(
|
||||||
|
AirDefence.AAA_M1_37mm,
|
||||||
|
"AA#" + str(4 + i),
|
||||||
|
position[0],
|
||||||
|
position[1],
|
||||||
|
position[2],
|
||||||
|
)
|
||||||
|
|
||||||
positions = self.get_circular_position(8, launcher_distance=90, coverage=360)
|
positions = self.get_circular_position(8, launcher_distance=90, coverage=360)
|
||||||
for i, position in enumerate(positions):
|
for i, position in enumerate(positions):
|
||||||
self.add_unit(AirDefence.AAA_M45_Quadmount, "AA#" + str(12 + i), position[0], position[1], position[2])
|
self.add_unit(
|
||||||
|
AirDefence.AAA_M45_Quadmount_HB_12_7mm,
|
||||||
|
"AA#" + str(12 + i),
|
||||||
|
position[0],
|
||||||
|
position[1],
|
||||||
|
position[2],
|
||||||
|
)
|
||||||
|
|
||||||
# Add a commander truck
|
# Add a commander truck
|
||||||
self.add_unit(Unarmed.Willys_MB, "CMD#1", self.position.x, self.position.y - 20, random.randint(0, 360))
|
self.add_unit(
|
||||||
self.add_unit(Armor.M30_Cargo_Carrier, "LOG#1", self.position.x, self.position.y + 20, random.randint(0, 360))
|
Unarmed.Car_Willys_Jeep,
|
||||||
self.add_unit(Armor.M4_Tractor, "LOG#2", self.position.x + 20, self.position.y, random.randint(0, 360))
|
"CMD#1",
|
||||||
self.add_unit(Unarmed.Bedford_MWD, "LOG#3", self.position.x - 20, self.position.y, random.randint(0, 360))
|
self.position.x,
|
||||||
|
self.position.y - 20,
|
||||||
|
random.randint(0, 360),
|
||||||
|
)
|
||||||
|
self.add_unit(
|
||||||
|
Unarmed.Carrier_M30_Cargo,
|
||||||
|
"LOG#1",
|
||||||
|
self.position.x,
|
||||||
|
self.position.y + 20,
|
||||||
|
random.randint(0, 360),
|
||||||
|
)
|
||||||
|
self.add_unit(
|
||||||
|
Unarmed.Tractor_M4_Hi_Speed,
|
||||||
|
"LOG#2",
|
||||||
|
self.position.x + 20,
|
||||||
|
self.position.y,
|
||||||
|
random.randint(0, 360),
|
||||||
|
)
|
||||||
|
self.add_unit(
|
||||||
|
Unarmed.Truck_Bedford,
|
||||||
|
"LOG#3",
|
||||||
|
self.position.x - 20,
|
||||||
|
self.position.y,
|
||||||
|
random.randint(0, 360),
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def range(cls) -> AirDefenseRange:
|
def range(cls) -> AirDefenseRange:
|
||||||
|
|||||||
@@ -16,9 +16,17 @@ class ZSU57Generator(AirDefenseGroupGenerator):
|
|||||||
|
|
||||||
def generate(self):
|
def generate(self):
|
||||||
num_launchers = 5
|
num_launchers = 5
|
||||||
positions = self.get_circular_position(num_launchers, launcher_distance=110, coverage=360)
|
positions = self.get_circular_position(
|
||||||
|
num_launchers, launcher_distance=110, coverage=360
|
||||||
|
)
|
||||||
for i, position in enumerate(positions):
|
for i, position in enumerate(positions):
|
||||||
self.add_unit(AirDefence.AAA_ZSU_57_2, "SPAA#" + str(i), position[0], position[1], position[2])
|
self.add_unit(
|
||||||
|
AirDefence.SPAAA_ZSU_57_2,
|
||||||
|
"SPAA#" + str(i),
|
||||||
|
position[0],
|
||||||
|
position[1],
|
||||||
|
position[2],
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def range(cls) -> AirDefenseRange:
|
def range(cls) -> AirDefenseRange:
|
||||||
|
|||||||
@@ -20,15 +20,19 @@ class ZU23InsurgentGenerator(AirDefenseGroupGenerator):
|
|||||||
grid_x = random.randint(2, 3)
|
grid_x = random.randint(2, 3)
|
||||||
grid_y = random.randint(2, 3)
|
grid_y = random.randint(2, 3)
|
||||||
|
|
||||||
spacing = random.randint(10,40)
|
spacing = random.randint(10, 40)
|
||||||
|
|
||||||
index = 0
|
index = 0
|
||||||
for i in range(grid_x):
|
for i in range(grid_x):
|
||||||
for j in range(grid_y):
|
for j in range(grid_y):
|
||||||
index = index+1
|
index = index + 1
|
||||||
self.add_unit(AirDefence.AAA_ZU_23_Insurgent_Closed, "AAA#" + str(index),
|
self.add_unit(
|
||||||
self.position.x + spacing*i,
|
AirDefence.AAA_ZU_23_Closed_Emplacement_Insurgent,
|
||||||
self.position.y + spacing*j, self.heading)
|
"AAA#" + str(index),
|
||||||
|
self.position.x + spacing * i,
|
||||||
|
self.position.y + spacing * j,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def range(cls) -> AirDefenseRange:
|
def range(cls) -> AirDefenseRange:
|
||||||
|
|||||||
@@ -28,8 +28,9 @@ class AirDefenseGroupGenerator(GroupGenerator, ABC):
|
|||||||
self.auxiliary_groups: List[VehicleGroup] = []
|
self.auxiliary_groups: List[VehicleGroup] = []
|
||||||
|
|
||||||
def add_auxiliary_group(self, name_suffix: str) -> VehicleGroup:
|
def add_auxiliary_group(self, name_suffix: str) -> VehicleGroup:
|
||||||
group = VehicleGroup(self.game.next_group_id(),
|
group = VehicleGroup(
|
||||||
"|".join([self.go.group_name, name_suffix]))
|
self.game.next_group_id(), "|".join([self.go.group_name, name_suffix])
|
||||||
|
)
|
||||||
self.auxiliary_groups.append(group)
|
self.auxiliary_groups.append(group)
|
||||||
return group
|
return group
|
||||||
|
|
||||||
@@ -37,7 +38,8 @@ class AirDefenseGroupGenerator(GroupGenerator, ABC):
|
|||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Deprecated call to AirDefenseGroupGenerator.get_generated_group "
|
"Deprecated call to AirDefenseGroupGenerator.get_generated_group "
|
||||||
"misses auxiliary groups. Use AirDefenseGroupGenerator.groups "
|
"misses auxiliary groups. Use AirDefenseGroupGenerator.groups "
|
||||||
"instead.")
|
"instead."
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def groups(self) -> Iterator[VehicleGroup]:
|
def groups(self) -> Iterator[VehicleGroup]:
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ from gen.sam.group_generator import GroupGenerator
|
|||||||
class EarlyColdWarFlakGenerator(AirDefenseGroupGenerator):
|
class EarlyColdWarFlakGenerator(AirDefenseGroupGenerator):
|
||||||
"""
|
"""
|
||||||
This generator attempt to mimic an early cold-war era flak AAA site.
|
This generator attempt to mimic an early cold-war era flak AAA site.
|
||||||
The Flak 18 88mm is used as the main long range gun and 2 Bofors 40mm guns provide short range protection.
|
The Flak 18 88mm is used as the main long range gun, S-60 is used as a mid range gun and 2 Bofors 40mm guns provide short range protection.
|
||||||
|
|
||||||
This does not include search lights and telemeter computer (Kdo.G 40) because these are paid units only available in WW2 asset pack
|
This does not include search lights and telemeter computer (Kdo.G 40) because these are paid units only available in WW2 asset pack
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "Early Cold War Flak Site"
|
name = "Early Cold War Flak Site"
|
||||||
price = 58
|
price = 74
|
||||||
|
|
||||||
def generate(self):
|
def generate(self):
|
||||||
|
|
||||||
@@ -29,18 +29,54 @@ class EarlyColdWarFlakGenerator(AirDefenseGroupGenerator):
|
|||||||
for i in range(3):
|
for i in range(3):
|
||||||
for j in range(2):
|
for j in range(2):
|
||||||
index = index + 1
|
index = index + 1
|
||||||
self.add_unit(AirDefence.AAA_8_8cm_Flak_18, "AAA#" + str(index),
|
self.add_unit(
|
||||||
self.position.x + spacing * i + random.randint(1, 5),
|
AirDefence.AAA_8_8cm_Flak_18,
|
||||||
self.position.y + spacing * j + random.randint(1, 5), self.heading)
|
"AAA#" + str(index),
|
||||||
|
self.position.x + spacing * i + random.randint(1, 5),
|
||||||
|
self.position.y + spacing * j + random.randint(1, 5),
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Medium range guns
|
||||||
|
self.add_unit(
|
||||||
|
AirDefence.AAA_S_60_57mm,
|
||||||
|
"SHO#1",
|
||||||
|
self.position.x - 40,
|
||||||
|
self.position.y - 40,
|
||||||
|
self.heading + 180,
|
||||||
|
),
|
||||||
|
self.add_unit(
|
||||||
|
AirDefence.AAA_S_60_57mm,
|
||||||
|
"SHO#2",
|
||||||
|
self.position.x + spacing * 2 + 40,
|
||||||
|
self.position.y + spacing + 40,
|
||||||
|
self.heading,
|
||||||
|
),
|
||||||
|
|
||||||
# Short range guns
|
# Short range guns
|
||||||
self.add_unit(AirDefence.AAA_Bofors_40mm, "SHO#1",
|
self.add_unit(
|
||||||
self.position.x - 40, self.position.y - 40, self.heading + 180),
|
AirDefence.AAA_ZU_23_Closed_Emplacement,
|
||||||
self.add_unit(AirDefence.AAA_Bofors_40mm, "SHO#2",
|
"SHO#3",
|
||||||
self.position.x + spacing * 2 + 40, self.position.y + spacing + 40, self.heading),
|
self.position.x - 80,
|
||||||
|
self.position.y - 40,
|
||||||
|
self.heading + 180,
|
||||||
|
),
|
||||||
|
self.add_unit(
|
||||||
|
AirDefence.AAA_ZU_23_Closed_Emplacement,
|
||||||
|
"SHO#4",
|
||||||
|
self.position.x + spacing * 2 + 80,
|
||||||
|
self.position.y + spacing + 40,
|
||||||
|
self.heading,
|
||||||
|
),
|
||||||
|
|
||||||
# Add a truck
|
# Add a truck
|
||||||
self.add_unit(Unarmed.Transport_KAMAZ_43101, "Truck#", self.position.x - 60, self.position.y - 20, self.heading)
|
self.add_unit(
|
||||||
|
Unarmed.Truck_KAMAZ_43101,
|
||||||
|
"Truck#",
|
||||||
|
self.position.x - 60,
|
||||||
|
self.position.y - 20,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def range(cls) -> AirDefenseRange:
|
def range(cls) -> AirDefenseRange:
|
||||||
@@ -50,7 +86,7 @@ class EarlyColdWarFlakGenerator(AirDefenseGroupGenerator):
|
|||||||
class ColdWarFlakGenerator(AirDefenseGroupGenerator):
|
class ColdWarFlakGenerator(AirDefenseGroupGenerator):
|
||||||
"""
|
"""
|
||||||
This generator attempt to mimic a cold-war era flak AAA site.
|
This generator attempt to mimic a cold-war era flak AAA site.
|
||||||
The Flak 18 88mm is used as the main long range gun while 2 Zu-23 guns provide short range protection.
|
The Flak 18 88mm is used as the main long range gun, 2 S-60 57mm gun improve mid range firepower, while 2 Zu-23 guns even provide short range protection.
|
||||||
The site is also fitted with a P-19 radar for early detection.
|
The site is also fitted with a P-19 radar for early detection.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -66,18 +102,54 @@ class ColdWarFlakGenerator(AirDefenseGroupGenerator):
|
|||||||
for i in range(3):
|
for i in range(3):
|
||||||
for j in range(2):
|
for j in range(2):
|
||||||
index = index + 1
|
index = index + 1
|
||||||
self.add_unit(AirDefence.AAA_8_8cm_Flak_18, "AAA#" + str(index),
|
self.add_unit(
|
||||||
self.position.x + spacing * i + random.randint(1, 5),
|
AirDefence.AAA_8_8cm_Flak_18,
|
||||||
self.position.y + spacing * j + random.randint(1, 5), self.heading)
|
"AAA#" + str(index),
|
||||||
|
self.position.x + spacing * i + random.randint(1, 5),
|
||||||
|
self.position.y + spacing * j + random.randint(1, 5),
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Medium range guns
|
||||||
|
self.add_unit(
|
||||||
|
AirDefence.AAA_S_60_57mm,
|
||||||
|
"SHO#1",
|
||||||
|
self.position.x - 40,
|
||||||
|
self.position.y - 40,
|
||||||
|
self.heading + 180,
|
||||||
|
),
|
||||||
|
self.add_unit(
|
||||||
|
AirDefence.AAA_S_60_57mm,
|
||||||
|
"SHO#2",
|
||||||
|
self.position.x + spacing * 2 + 40,
|
||||||
|
self.position.y + spacing + 40,
|
||||||
|
self.heading,
|
||||||
|
),
|
||||||
|
|
||||||
# Short range guns
|
# Short range guns
|
||||||
self.add_unit(AirDefence.AAA_ZU_23_Closed, "SHO#1",
|
self.add_unit(
|
||||||
self.position.x - 40, self.position.y - 40, self.heading + 180),
|
AirDefence.AAA_ZU_23_Closed_Emplacement,
|
||||||
self.add_unit(AirDefence.AAA_ZU_23_Closed, "SHO#2",
|
"SHO#3",
|
||||||
self.position.x + spacing * 2 + 40, self.position.y + spacing + 40, self.heading),
|
self.position.x - 80,
|
||||||
|
self.position.y - 40,
|
||||||
|
self.heading + 180,
|
||||||
|
),
|
||||||
|
self.add_unit(
|
||||||
|
AirDefence.AAA_ZU_23_Closed_Emplacement,
|
||||||
|
"SHO#4",
|
||||||
|
self.position.x + spacing * 2 + 80,
|
||||||
|
self.position.y + spacing + 40,
|
||||||
|
self.heading,
|
||||||
|
),
|
||||||
|
|
||||||
# Add a P19 Radar for EWR
|
# Add a P19 Radar for EWR
|
||||||
self.add_unit(AirDefence.SAM_SR_P_19, "SR#0", self.position.x - 60, self.position.y - 20, self.heading)
|
self.add_unit(
|
||||||
|
AirDefence.SAM_P19_Flat_Face_SR__SA_2_3,
|
||||||
|
"SR#0",
|
||||||
|
self.position.x - 60,
|
||||||
|
self.position.y - 20,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def range(cls) -> AirDefenseRange:
|
def range(cls) -> AirDefenseRange:
|
||||||
|
|||||||
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
|
raise NotImplementedError
|
||||||
|
|
||||||
def generate(self) -> None:
|
def generate(self) -> None:
|
||||||
self.add_unit(self.unit_type, "EWR", self.position.x, self.position.y,
|
self.add_unit(
|
||||||
self.heading)
|
self.unit_type, "EWR", self.position.x, self.position.y, self.heading
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BoxSpringGenerator(EwrGenerator):
|
class BoxSpringGenerator(EwrGenerator):
|
||||||
@@ -32,7 +33,7 @@ class DogEarGenerator(EwrGenerator):
|
|||||||
This is the SA-8 search radar, but used as an early warning radar.
|
This is the SA-8 search radar, but used as an early warning radar.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
unit_type = AirDefence.CP_9S80M1_Sborka
|
unit_type = AirDefence.MCC_SR_Sborka_Dog_Ear_SR
|
||||||
|
|
||||||
|
|
||||||
class RolandEwrGenerator(EwrGenerator):
|
class RolandEwrGenerator(EwrGenerator):
|
||||||
@@ -50,7 +51,7 @@ class FlatFaceGenerator(EwrGenerator):
|
|||||||
This is the SA-3 search radar, but used as an early warning radar.
|
This is the SA-3 search radar, but used as an early warning radar.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
unit_type = AirDefence.SAM_SR_P_19
|
unit_type = AirDefence.SAM_P19_Flat_Face_SR__SA_2_3
|
||||||
|
|
||||||
|
|
||||||
class PatriotEwrGenerator(EwrGenerator):
|
class PatriotEwrGenerator(EwrGenerator):
|
||||||
@@ -59,7 +60,7 @@ class PatriotEwrGenerator(EwrGenerator):
|
|||||||
This is the Patriot search/track radar, but used as an early warning radar.
|
This is the Patriot search/track radar, but used as an early warning radar.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
unit_type = AirDefence.SAM_Patriot_STR_AN_MPQ_53
|
unit_type = AirDefence.SAM_Patriot_STR
|
||||||
|
|
||||||
|
|
||||||
class BigBirdGenerator(EwrGenerator):
|
class BigBirdGenerator(EwrGenerator):
|
||||||
@@ -68,7 +69,7 @@ class BigBirdGenerator(EwrGenerator):
|
|||||||
This is the SA-10 track radar, but used as an early warning radar.
|
This is the SA-10 track radar, but used as an early warning radar.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
unit_type = AirDefence.SAM_SA_10_S_300PS_SR_64H6E
|
unit_type = AirDefence.SAM_SA_10_S_300_Grumble_Big_Bird_SR
|
||||||
|
|
||||||
|
|
||||||
class SnowDriftGenerator(EwrGenerator):
|
class SnowDriftGenerator(EwrGenerator):
|
||||||
@@ -77,7 +78,7 @@ class SnowDriftGenerator(EwrGenerator):
|
|||||||
This is the SA-11 search radar, but used as an early warning radar.
|
This is the SA-11 search radar, but used as an early warning radar.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
unit_type = AirDefence.SAM_SA_11_Buk_SR_9S18M1
|
unit_type = AirDefence.SAM_SA_11_Buk_Gadfly_Snow_Drift_SR
|
||||||
|
|
||||||
|
|
||||||
class StraightFlushGenerator(EwrGenerator):
|
class StraightFlushGenerator(EwrGenerator):
|
||||||
@@ -86,7 +87,7 @@ class StraightFlushGenerator(EwrGenerator):
|
|||||||
This is the SA-6 search/track radar, but used as an early warning radar.
|
This is the SA-6 search/track radar, but used as an early warning radar.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
unit_type = AirDefence.SAM_SA_6_Kub_STR_9S91
|
unit_type = AirDefence.SAM_SA_6_Kub_Long_Track_STR
|
||||||
|
|
||||||
|
|
||||||
class HawkEwrGenerator(EwrGenerator):
|
class HawkEwrGenerator(EwrGenerator):
|
||||||
@@ -95,4 +96,4 @@ class HawkEwrGenerator(EwrGenerator):
|
|||||||
This is the Hawk search radar, but used as an early warning radar.
|
This is the Hawk search radar, but used as an early warning radar.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
unit_type = AirDefence.SAM_Hawk_SR_AN_MPQ_50
|
unit_type = AirDefence.SAM_Hawk_SR__AN_MPQ_50
|
||||||
|
|||||||
@@ -17,27 +17,93 @@ class FreyaGenerator(AirDefenseGroupGenerator):
|
|||||||
def generate(self):
|
def generate(self):
|
||||||
|
|
||||||
# TODO : would be better with the Concrete structure that is supposed to protect it
|
# TODO : would be better with the Concrete structure that is supposed to protect it
|
||||||
self.add_unit(AirDefence.EWR_FuMG_401_Freya_LZ, "EWR#1", self.position.x, self.position.y, self.heading)
|
self.add_unit(
|
||||||
|
AirDefence.EWR_FuMG_401_Freya_LZ,
|
||||||
|
"EWR#1",
|
||||||
|
self.position.x,
|
||||||
|
self.position.y,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
positions = self.get_circular_position(4, launcher_distance=50, coverage=360)
|
positions = self.get_circular_position(4, launcher_distance=50, coverage=360)
|
||||||
for i, position in enumerate(positions):
|
for i, position in enumerate(positions):
|
||||||
self.add_unit(AirDefence.AAA_Flak_Vierling_38, "AA#" + str(i), position[0], position[1], position[2])
|
self.add_unit(
|
||||||
|
AirDefence.AAA_Flak_Vierling_38_Quad_20mm,
|
||||||
|
"AA#" + str(i),
|
||||||
|
position[0],
|
||||||
|
position[1],
|
||||||
|
position[2],
|
||||||
|
)
|
||||||
|
|
||||||
positions = self.get_circular_position(4, launcher_distance=100, coverage=360)
|
positions = self.get_circular_position(4, launcher_distance=100, coverage=360)
|
||||||
for i, position in enumerate(positions):
|
for i, position in enumerate(positions):
|
||||||
self.add_unit(AirDefence.AAA_8_8cm_Flak_18, "AA#" + str(4+i), position[0], position[1], position[2])
|
self.add_unit(
|
||||||
|
AirDefence.AAA_8_8cm_Flak_18,
|
||||||
|
"AA#" + str(4 + i),
|
||||||
|
position[0],
|
||||||
|
position[1],
|
||||||
|
position[2],
|
||||||
|
)
|
||||||
|
|
||||||
# Command/Logi
|
# Command/Logi
|
||||||
self.add_unit(Unarmed.Kübelwagen_82, "Kubel#1", self.position.x - 20, self.position.y - 20, self.heading)
|
self.add_unit(
|
||||||
self.add_unit(Unarmed.Sd_Kfz_7, "Sdkfz#1", self.position.x + 20, self.position.y + 22, self.heading)
|
Unarmed.LUV_Kubelwagen_82,
|
||||||
self.add_unit(Unarmed.Sd_Kfz_2, "Sdkfz#2", self.position.x - 22, self.position.y + 20, self.heading)
|
"Kubel#1",
|
||||||
|
self.position.x - 20,
|
||||||
|
self.position.y - 20,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
self.add_unit(
|
||||||
|
Unarmed.Carrier_Sd_Kfz_7_Tractor,
|
||||||
|
"Sdkfz#1",
|
||||||
|
self.position.x + 20,
|
||||||
|
self.position.y + 22,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
self.add_unit(
|
||||||
|
Unarmed.LUV_Kettenrad,
|
||||||
|
"Sdkfz#2",
|
||||||
|
self.position.x - 22,
|
||||||
|
self.position.y + 20,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
# Maschinensatz_33 and Kdo.g 40 Telemeter
|
# PU_Maschinensatz_33 and Kdo.g 40 Telemeter
|
||||||
self.add_unit(AirDefence.Maschinensatz_33, "Energy#1", self.position.x + 20, self.position.y - 20, self.heading)
|
self.add_unit(
|
||||||
self.add_unit(AirDefence.AAA_Kdo_G_40, "Telemeter#1", self.position.x + 20, self.position.y - 10, self.heading)
|
AirDefence.PU_Maschinensatz_33,
|
||||||
self.add_unit(Infantry.Infantry_Mauser_98, "Inf#1", self.position.x + 20, self.position.y - 14, self.heading)
|
"Energy#1",
|
||||||
self.add_unit(Infantry.Infantry_Mauser_98, "Inf#2", self.position.x + 20, self.position.y - 22, self.heading)
|
self.position.x + 20,
|
||||||
self.add_unit(Infantry.Infantry_Mauser_98, "Inf#3", self.position.x + 20, self.position.y - 24, self.heading + 45)
|
self.position.y - 20,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
self.add_unit(
|
||||||
|
AirDefence.AAA_SP_Kdo_G_40,
|
||||||
|
"Telemeter#1",
|
||||||
|
self.position.x + 20,
|
||||||
|
self.position.y - 10,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
self.add_unit(
|
||||||
|
Infantry.Infantry_Mauser_98,
|
||||||
|
"Inf#1",
|
||||||
|
self.position.x + 20,
|
||||||
|
self.position.y - 14,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
self.add_unit(
|
||||||
|
Infantry.Infantry_Mauser_98,
|
||||||
|
"Inf#2",
|
||||||
|
self.position.x + 20,
|
||||||
|
self.position.y - 22,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
self.add_unit(
|
||||||
|
Infantry.Infantry_Mauser_98,
|
||||||
|
"Inf#3",
|
||||||
|
self.position.x + 20,
|
||||||
|
self.position.y - 24,
|
||||||
|
self.heading + 45,
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def range(cls) -> AirDefenseRange:
|
def range(cls) -> AirDefenseRange:
|
||||||
|
|||||||
@@ -23,14 +23,12 @@ if TYPE_CHECKING:
|
|||||||
# care about in the format we want if we just generate our own group description
|
# care about in the format we want if we just generate our own group description
|
||||||
# types rather than pydcs groups.
|
# types rather than pydcs groups.
|
||||||
class GroupGenerator:
|
class GroupGenerator:
|
||||||
|
|
||||||
def __init__(self, game: Game, ground_object: TheaterGroundObject) -> None:
|
def __init__(self, game: Game, ground_object: TheaterGroundObject) -> None:
|
||||||
self.game = game
|
self.game = game
|
||||||
self.go = ground_object
|
self.go = ground_object
|
||||||
self.position = ground_object.position
|
self.position = ground_object.position
|
||||||
self.heading = random.randint(0, 359)
|
self.heading = random.randint(0, 359)
|
||||||
self.vg = unitgroup.VehicleGroup(self.game.next_group_id(),
|
self.vg = unitgroup.VehicleGroup(self.game.next_group_id(), self.go.group_name)
|
||||||
self.go.group_name)
|
|
||||||
wp = self.vg.add_waypoint(self.position, PointAction.OffRoad, 0)
|
wp = self.vg.add_waypoint(self.position, PointAction.OffRoad, 0)
|
||||||
wp.ETA_locked = True
|
wp.ETA_locked = True
|
||||||
|
|
||||||
@@ -40,16 +38,27 @@ class GroupGenerator:
|
|||||||
def get_generated_group(self) -> unitgroup.VehicleGroup:
|
def get_generated_group(self) -> unitgroup.VehicleGroup:
|
||||||
return self.vg
|
return self.vg
|
||||||
|
|
||||||
def add_unit(self, unit_type: Type[VehicleType], name: str, pos_x: float,
|
def add_unit(
|
||||||
pos_y: float, heading: int) -> Vehicle:
|
self,
|
||||||
return self.add_unit_to_group(self.vg, unit_type, name,
|
unit_type: Type[VehicleType],
|
||||||
Point(pos_x, pos_y), heading)
|
name: str,
|
||||||
|
pos_x: float,
|
||||||
|
pos_y: float,
|
||||||
|
heading: int,
|
||||||
|
) -> Vehicle:
|
||||||
|
return self.add_unit_to_group(
|
||||||
|
self.vg, unit_type, name, Point(pos_x, pos_y), heading
|
||||||
|
)
|
||||||
|
|
||||||
def add_unit_to_group(self, group: unitgroup.VehicleGroup,
|
def add_unit_to_group(
|
||||||
unit_type: Type[VehicleType], name: str,
|
self,
|
||||||
position: Point, heading: int) -> Vehicle:
|
group: unitgroup.VehicleGroup,
|
||||||
unit = Vehicle(self.game.next_unit_id(),
|
unit_type: Type[VehicleType],
|
||||||
f"{group.name}|{name}", unit_type.id)
|
name: str,
|
||||||
|
position: Point,
|
||||||
|
heading: int,
|
||||||
|
) -> Vehicle:
|
||||||
|
unit = Vehicle(self.game.next_unit_id(), f"{group.name}|{name}", unit_type.id)
|
||||||
unit.position = position
|
unit.position = position
|
||||||
unit.heading = heading
|
unit.heading = heading
|
||||||
group.add_unit(unit)
|
group.add_unit(unit)
|
||||||
@@ -82,31 +91,36 @@ class GroupGenerator:
|
|||||||
current_offset = self.heading
|
current_offset = self.heading
|
||||||
current_offset -= outer_offset * (math.ceil(num_units / 2) - 1)
|
current_offset -= outer_offset * (math.ceil(num_units / 2) - 1)
|
||||||
for x in range(1, num_units + 1):
|
for x in range(1, num_units + 1):
|
||||||
positions.append((
|
positions.append(
|
||||||
self.position.x + launcher_distance * math.cos(math.radians(current_offset)),
|
(
|
||||||
self.position.y + launcher_distance * math.sin(math.radians(current_offset)),
|
self.position.x
|
||||||
current_offset,
|
+ launcher_distance * math.cos(math.radians(current_offset)),
|
||||||
))
|
self.position.y
|
||||||
|
+ launcher_distance * math.sin(math.radians(current_offset)),
|
||||||
|
current_offset,
|
||||||
|
)
|
||||||
|
)
|
||||||
current_offset += outer_offset
|
current_offset += outer_offset
|
||||||
return positions
|
return positions
|
||||||
|
|
||||||
|
|
||||||
class ShipGroupGenerator(GroupGenerator):
|
class ShipGroupGenerator(GroupGenerator):
|
||||||
"""Abstract class for other ship generator classes"""
|
"""Abstract class for other ship generator classes"""
|
||||||
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
|
|
||||||
|
def __init__(
|
||||||
|
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
||||||
|
):
|
||||||
self.game = game
|
self.game = game
|
||||||
self.go = ground_object
|
self.go = ground_object
|
||||||
self.position = ground_object.position
|
self.position = ground_object.position
|
||||||
self.heading = random.randint(0, 359)
|
self.heading = random.randint(0, 359)
|
||||||
self.faction = faction
|
self.faction = faction
|
||||||
self.vg = unitgroup.ShipGroup(self.game.next_group_id(),
|
self.vg = unitgroup.ShipGroup(self.game.next_group_id(), self.go.group_name)
|
||||||
self.go.group_name)
|
|
||||||
wp = self.vg.add_waypoint(self.position, 0)
|
wp = self.vg.add_waypoint(self.position, 0)
|
||||||
wp.ETA_locked = True
|
wp.ETA_locked = True
|
||||||
|
|
||||||
def add_unit(self, unit_type, name, pos_x, pos_y, heading) -> Ship:
|
def add_unit(self, unit_type, name, pos_x, pos_y, heading) -> Ship:
|
||||||
unit = Ship(self.game.next_unit_id(),
|
unit = Ship(self.game.next_unit_id(), f"{self.go.group_name}|{name}", unit_type)
|
||||||
f"{self.go.group_name}|{name}", unit_type)
|
|
||||||
unit.position.x = pos_x
|
unit.position.x = pos_x
|
||||||
unit.position.y = pos_y
|
unit.position.y = pos_y
|
||||||
unit.heading = heading
|
unit.heading = heading
|
||||||
|
|||||||
@@ -19,10 +19,24 @@ class AvengerGenerator(AirDefenseGroupGenerator):
|
|||||||
def generate(self):
|
def generate(self):
|
||||||
num_launchers = random.randint(2, 3)
|
num_launchers = random.randint(2, 3)
|
||||||
|
|
||||||
self.add_unit(Unarmed.Transport_M818, "TRUCK", self.position.x, self.position.y, self.heading)
|
self.add_unit(
|
||||||
positions = self.get_circular_position(num_launchers, launcher_distance=110, coverage=180)
|
Unarmed.Truck_M818_6x6,
|
||||||
|
"TRUCK",
|
||||||
|
self.position.x,
|
||||||
|
self.position.y,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
positions = self.get_circular_position(
|
||||||
|
num_launchers, launcher_distance=110, coverage=180
|
||||||
|
)
|
||||||
for i, position in enumerate(positions):
|
for i, position in enumerate(positions):
|
||||||
self.add_unit(AirDefence.SAM_Avenger_M1097, "SPAA#" + str(i), position[0], position[1], position[2])
|
self.add_unit(
|
||||||
|
AirDefence.SAM_Avenger__Stinger,
|
||||||
|
"SPAA#" + str(i),
|
||||||
|
position[0],
|
||||||
|
position[1],
|
||||||
|
position[2],
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def range(cls) -> AirDefenseRange:
|
def range(cls) -> AirDefenseRange:
|
||||||
|
|||||||
@@ -19,10 +19,24 @@ class ChaparralGenerator(AirDefenseGroupGenerator):
|
|||||||
def generate(self):
|
def generate(self):
|
||||||
num_launchers = random.randint(2, 4)
|
num_launchers = random.randint(2, 4)
|
||||||
|
|
||||||
self.add_unit(Unarmed.Transport_M818, "TRUCK", self.position.x, self.position.y, self.heading)
|
self.add_unit(
|
||||||
positions = self.get_circular_position(num_launchers, launcher_distance=110, coverage=180)
|
Unarmed.Truck_M818_6x6,
|
||||||
|
"TRUCK",
|
||||||
|
self.position.x,
|
||||||
|
self.position.y,
|
||||||
|
self.heading,
|
||||||
|
)
|
||||||
|
positions = self.get_circular_position(
|
||||||
|
num_launchers, launcher_distance=110, coverage=180
|
||||||
|
)
|
||||||
for i, position in enumerate(positions):
|
for i, position in enumerate(positions):
|
||||||
self.add_unit(AirDefence.SAM_Chaparral_M48, "SPAA#" + str(i), position[0], position[1], position[2])
|
self.add_unit(
|
||||||
|
AirDefence.SAM_Chaparral_M48,
|
||||||
|
"SPAA#" + str(i),
|
||||||
|
position[0],
|
||||||
|
position[1],
|
||||||
|
position[2],
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def range(cls) -> AirDefenseRange:
|
def range(cls) -> AirDefenseRange:
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user