mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Compare commits
187 Commits
plugin-sky
...
develop_2_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f65637f6b | ||
|
|
cceb3da693 | ||
|
|
a357bf3c08 | ||
|
|
3f251c38d8 | ||
|
|
01702f046e | ||
|
|
d394d01ea8 | ||
|
|
70d982b0ed | ||
|
|
c3b028ef4b | ||
|
|
206d09f7f8 | ||
|
|
87248fec53 | ||
|
|
b7634a8ac3 | ||
|
|
434755a620 | ||
|
|
3eb2529b0b | ||
|
|
e6e4cca076 | ||
|
|
8d57bbc777 | ||
|
|
483db564f9 | ||
|
|
63d5862319 | ||
|
|
365b379798 | ||
|
|
fd473f0a46 | ||
|
|
a816877d08 | ||
|
|
2d56ae1cb6 | ||
|
|
216adcc35a | ||
|
|
8d485d5fa2 | ||
|
|
fa549fcf94 | ||
|
|
b7d160631a | ||
|
|
d05897edcb | ||
|
|
1d98432c57 | ||
|
|
21cd764f66 | ||
|
|
dfc31dfd5c | ||
|
|
2e067aada6 | ||
|
|
78cd60f3df | ||
|
|
96c401e1b9 | ||
|
|
fad132dcca | ||
|
|
696710bf41 | ||
|
|
f8735927bf | ||
|
|
16cfc4e945 | ||
|
|
3987274764 | ||
|
|
73a97f9c2a | ||
|
|
169fba9ab8 | ||
|
|
0b902e19ee | ||
|
|
1f43fbe16e | ||
|
|
f29cb99530 | ||
|
|
9e32ea7413 | ||
|
|
95fd4cab05 | ||
|
|
c4d08fa7b7 | ||
|
|
cec28351e7 | ||
|
|
9620ac7e7e | ||
|
|
c0bfdbf4bb | ||
|
|
efb544a303 | ||
|
|
adfc4b7244 | ||
|
|
7a5ce98569 | ||
|
|
1fcceb0901 | ||
|
|
22c552053f | ||
|
|
818c679d4f | ||
|
|
3ff36c45aa | ||
|
|
623d461b06 | ||
|
|
7535013848 | ||
|
|
a63bac8826 | ||
|
|
d2c831c4ee | ||
|
|
840b5ce071 | ||
|
|
be6abc0025 | ||
|
|
ef585c59dd | ||
|
|
16d9c1ccad | ||
|
|
9a9ef78583 | ||
|
|
fe7ee5b610 | ||
|
|
680804040a | ||
|
|
0d4fe73daa | ||
|
|
407190c6c5 | ||
|
|
73998dbde0 | ||
|
|
8827f7df34 | ||
|
|
4c394a9e2d | ||
|
|
5946fc7404 | ||
|
|
5b8ecb2c14 | ||
|
|
61253e4d4d | ||
|
|
2c0ca5803f | ||
|
|
690f3d0f13 | ||
|
|
42c259bc58 | ||
|
|
11426a0713 | ||
|
|
e6af1b8645 | ||
|
|
103f18191d | ||
|
|
fb312236a2 | ||
|
|
c850c0095d | ||
|
|
58481268f7 | ||
|
|
11604671f8 | ||
|
|
e8feded4c3 | ||
|
|
18f9b38d25 | ||
|
|
676eea3ccc | ||
|
|
ee113d080e | ||
|
|
8bc69415a7 | ||
|
|
3979ee57ff | ||
|
|
c2ee169d16 | ||
|
|
56b51c85bb | ||
|
|
853ee5aac4 | ||
|
|
4cf406aefa | ||
|
|
1abb341cb6 | ||
|
|
44dce9598c | ||
|
|
aefc8685a1 | ||
|
|
26761342f5 | ||
|
|
ca777bcebb | ||
|
|
5742075ff2 | ||
|
|
c7a6ec9691 | ||
|
|
e0153cfa6a | ||
|
|
3fd5e1bae7 | ||
|
|
d9511a7edd | ||
|
|
040db055fd | ||
|
|
b9f8cfd10d | ||
|
|
ff46556927 | ||
|
|
d1815a3d6e | ||
|
|
fdfa4827ab | ||
|
|
5d579ccef9 | ||
|
|
4145d5578e | ||
|
|
43eb041bb8 | ||
|
|
0b8ac8fc47 | ||
|
|
de3ba5908f | ||
|
|
bbb6251aa9 | ||
|
|
6f71d92a7b | ||
|
|
a8b59cc567 | ||
|
|
f4d3660eac | ||
|
|
1f165835c6 | ||
|
|
9087f3487d | ||
|
|
4ca92ea22d | ||
|
|
e2682d633f | ||
|
|
e6cb1b5970 | ||
|
|
de2d548139 | ||
|
|
4d1a0b85e4 | ||
|
|
5cfbd8c3ad | ||
|
|
95f72be8eb | ||
|
|
95c4dfa52f | ||
|
|
b72a2f4a5f | ||
|
|
844f8595d1 | ||
|
|
1c9d9be667 | ||
|
|
2a02a743a4 | ||
|
|
968d9365d6 | ||
|
|
cdb16cc591 | ||
|
|
86bc41c15c | ||
|
|
ac05c7cfaa | ||
|
|
9c07fe5963 | ||
|
|
ed05f995b5 | ||
|
|
85491dca20 | ||
|
|
465399f803 | ||
|
|
3550c8a8f6 | ||
|
|
739c0f8f52 | ||
|
|
49aa79c612 | ||
|
|
cdde75b517 | ||
|
|
dde74af6b5 | ||
|
|
eff9c77c9a | ||
|
|
5ba633c8a1 | ||
|
|
ab67a38ca5 | ||
|
|
08f0c9d30a | ||
|
|
9d747a9f9b | ||
|
|
31ca121498 | ||
|
|
44b5f5acf1 | ||
|
|
eb4878dfc4 | ||
|
|
3dc7dc3d1a | ||
|
|
6a6133e5cd | ||
|
|
65c85d7f0b | ||
|
|
d519dfa5da | ||
|
|
73ea83bbdd | ||
|
|
235a5ec538 | ||
|
|
f81a3d03c0 | ||
|
|
6878b57fba | ||
|
|
0143e5641f | ||
|
|
5adc92c601 | ||
|
|
0b2fbddbc5 | ||
|
|
28035bf02b | ||
|
|
6c9a9de3f3 | ||
|
|
62139fc4eb | ||
|
|
88b9ed29ba | ||
|
|
d94c57afd6 | ||
|
|
b6421646ff | ||
|
|
611b6fc272 | ||
|
|
9cdbef9faf | ||
|
|
b34de70fc7 | ||
|
|
f5047fc0cc | ||
|
|
258c34e61d | ||
|
|
f365487fd6 | ||
|
|
8f65b7ee7c | ||
|
|
9397f1f39c | ||
|
|
f03121af5a | ||
|
|
41445c3092 | ||
|
|
2a3bf9821b | ||
|
|
819d775282 | ||
|
|
efbc6fe3ae | ||
|
|
262ba6c113 | ||
|
|
c41ecb6735 | ||
|
|
f5c32c6b98 | ||
|
|
a1886e37f8 |
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -41,6 +41,10 @@ jobs:
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
mypy theater
|
||||
|
||||
- name: update build number
|
||||
run: |
|
||||
[IO.File]::WriteAllLines($pwd.path + "\resources\buildnumber", $env:GITHUB_RUN_NUMBER)
|
||||
|
||||
- name: Build binaries
|
||||
run: |
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -29,6 +29,10 @@ jobs:
|
||||
# For some reason the shiboken2.abi3.dll is not found properly, so I copy it instead
|
||||
Copy-Item .\venv\Lib\site-packages\shiboken2\shiboken2.abi3.dll .\venv\Lib\site-packages\PySide2\ -Force
|
||||
|
||||
- name: Finalize version
|
||||
run: |
|
||||
New-Item -ItemType file resources\final
|
||||
|
||||
- name: mypy game
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"python.pythonPath": "g:\\python\\dcs_liberation\\venv\\Scripts\\python.exe",
|
||||
"vsintellicode.python.completionsEnabled": true
|
||||
}
|
||||
10
README.md
10
README.md
@@ -12,10 +12,10 @@
|
||||

|
||||
|
||||
## About DCS Liberation
|
||||
DCS Liberation is a [DCS World](https://www.digitalcombatsimulator.com/en/products/world/) turn based single-player semi dynamic campaign.
|
||||
DCS Liberation is a [DCS World](https://www.digitalcombatsimulator.com/en/products/world/) turn based single-player or co-op dynamic campaign.
|
||||
It is an external program that generates full and complex DCS missions and manage a persistent combat environment.
|
||||
|
||||

|
||||

|
||||
|
||||
## Downloads
|
||||
|
||||
@@ -31,6 +31,10 @@ First, a big thanks to shdwp, for starting the original DCS Liberation project.
|
||||
|
||||
Then, DCS Liberation uses [pydcs](http://github.com/pydcs/dcs) for mission generation, and nothing would be possible without this.
|
||||
It also uses the popular [Mist](https://github.com/mrSkortch/MissionScriptingTools) lua framework for mission scripting.
|
||||
And for the JTAC feature, DCS Liberation embed Ciribob's JTAC Autolase [script](https://github.com/ciribob/DCS-JTACAutoLaze).
|
||||
|
||||
Excellent lua scripts DCS Liberation uses as plugins:
|
||||
|
||||
* For the JTAC feature, DCS Liberation embeds Ciribob's JTAC Autolase [script](https://github.com/ciribob/DCS-JTACAutoLaze).
|
||||
* Walder's [Skynet-IADS](https://github.com/walder/Skynet-IADS) is used for Integrated Air Defense System.
|
||||
|
||||
Please also show some support to these projects !
|
||||
|
||||
68
changelog.md
68
changelog.md
@@ -1,16 +1,62 @@
|
||||
# 2.2.X
|
||||
# 2.2.1
|
||||
|
||||
## Features/Improvements :
|
||||
* **[Flight Planner]** Flight planner overhaul, with package and TOT system
|
||||
* **[Map]** Highlight the selected flight path on the map
|
||||
* **[Map]** Improved flight plan display settings
|
||||
* **[Map]** Improved SAM display settings
|
||||
* **[Map]** Added polygon debug mode display
|
||||
* **[New Game]** Starting budget can be freely selected
|
||||
* **[Moddability]** Custom campaigns can be designed through json files
|
||||
# Features/Improvements
|
||||
* **[Factions]** Added factions : Georgia 2008, USN 1985, France 2005 Frenchpack by HerrTom
|
||||
* **[Factions]** Added map Persian Gulf full by Plob
|
||||
* **[Flight Planner]** Player flights with start delays under ten minutes will spawn immediately.
|
||||
* **[UI]** Mission start screen now informs players about delayed flights.
|
||||
* **[Units]** Added support for F-14A-135-GR
|
||||
* **[Modding]** Possible to setup liveries overrides in factions definition files
|
||||
|
||||
## Fixes :
|
||||
* **[Campaign generator]** Ship group and offshore buildings should not be generated on land anymore
|
||||
* **[Flight Planner]** Hold, join, and split points are planned cautiously near enemy airfields. Ascend/descend points are no longer planned.
|
||||
* **[Flight Planner]** Custom waypoints are usable again. Not that in most cases custom flight plans will revert to the 2.1 flight planning behavior.
|
||||
* **[Flight Planner]** Fixed UI bug that made it possible to create empty flights which would throw an error.
|
||||
* **[Flight Planner]** Player flights from carriers will now be delayed correctly according to the player's settings.
|
||||
* **[Misc]** Spitfire variant with clipped wings was not seen as flyable by DCS Liberation (hence could not be setup as client/player slot)
|
||||
* **[Misc]** Updated Syria terrain parking slots database, the out-of-date database could end up generating aircraft in wrong slots (We are still experiencing issues with somes airbases, such as Khalkhalah though)
|
||||
|
||||
# 2.2.0
|
||||
|
||||
## Features/Improvements :
|
||||
* **[Campaign Generator]** Added early warning radar generation
|
||||
* **[Campaign Generator]** Added scud launcher sites
|
||||
* **[Cheat Menu]** Added ability to capture base from mission planner
|
||||
* **[Cheat Menu]** Added ability to show red ATO
|
||||
* **[Factions]** Added WW2 factions that do not depend on WW2 asset pack
|
||||
* **[Factions]** Cold War / Middle eastern factions will use Flak sites
|
||||
* **[Flight Planner]** Flight planner overhaul, with package and TOT system
|
||||
* **[Flight Planner]** Pick runways and ascent/descent based on headwind
|
||||
* **[Map]** Added polygon debug mode display
|
||||
* **[Map]** Highlight the selected flight path on the map
|
||||
* **[Map]** Improved SAM display settings
|
||||
* **[Map]** Improved flight plan display settings
|
||||
* **[Map]** Caucasus and The Channel map use a new system to generate SAM and strike target location to reduce probability of targets generated in the middle of a forests
|
||||
* **[Misc]** Flexible Dedicated Hosting Options for Mission Files via environment variables
|
||||
* **[Moddability]** Custom campaigns can be designed through json files
|
||||
* **[Moddability]** LUA plugins can now be injected into Liberation missions.
|
||||
* **[Moddability]** Optional Skynet IADS lua plugin now included
|
||||
* **[New Game]** Starting budget can be freely selected
|
||||
* **[New Game]** Exanded information for faction and campaign selection in the new game wizard
|
||||
* **[UI]** Add double and right click actions to many UI elements.
|
||||
* **[UI]** Add polygon drawing mode for map background
|
||||
* **[UI]** Added a warning if you press takeoff with no player enabled flights
|
||||
* **[UI]** Packages and flights now visible in the main window sidebar
|
||||
* **[Units/Factions]** Added bombers to some coalitions
|
||||
* **[Units/Factions]** Added support for SU-57 mod by Cubanace
|
||||
* **[Units]** Added Freya EWR sites to german WW2 factions
|
||||
* **[Units]** Added support for many bombers (B-52H, B-1B, Tu-22, Tu-142)
|
||||
* **[Units]** Added support for new P-47 variants
|
||||
|
||||
## Fixes :
|
||||
* **[Campaign Generator]** Big airbases could end up without any airbase defense.
|
||||
* **[Campaign generator]** Ship group and offshore buildings should not be generated on land anymore
|
||||
* **[Flight Planner]** Fix waypoint alitudes for helicopters
|
||||
* **[Flight Planner]** Fixed CAS aircraft wandering away from frontline
|
||||
* **[Maps]** Incirlik airbase was missing exclusions zones, so SAMS could end up being generated on the runway
|
||||
* **[Mission Generator]** Fixed player/client confusion when a flight had only one player slot.
|
||||
* **[Radios]** Fix A-10C radio
|
||||
* **[UI]** Many missing unit icons were added
|
||||
* **[UI]** Missing TER weapons in custom payload now selectable.
|
||||
|
||||
# 2.1.5
|
||||
@@ -289,4 +335,4 @@ Sorry :(
|
||||
* **[Mission Generator]** Planned flights will spawn even if their home base has been captured or is being contested by enemy ground units.
|
||||
* **[Campaign Generator]** Base defenses would not be generated on Normandy map and in some rare cases on others maps as well
|
||||
* **[Mission Planning]** CAS waypoints created from the "Predefined waypoint selector" would not be at the exact location of the frontline
|
||||
* **[Naming]** CAP mission flown from airbase are not named BARCAP anymore (CAP from carrier is still named BARCAP)
|
||||
* **[Naming]** CAP mission flown from airbase are not named BARCAP anymore (CAP from carrier is still named BARCAP)
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
from .game import Game
|
||||
from . import db
|
||||
from . import db
|
||||
from .version import VERSION
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import inspect
|
||||
import dcs
|
||||
|
||||
DEFAULT_AVAILABLE_BUILDINGS = ['fuel', 'ammo', 'comms', 'oil', 'ware', 'farp', 'fob', 'power', 'factory', 'derrick', 'aa']
|
||||
DEFAULT_AVAILABLE_BUILDINGS = ['fuel', 'ammo', 'comms', 'oil', 'ware', 'farp', 'fob', 'power', 'factory', 'derrick']
|
||||
|
||||
WW2_GERMANY_BUILDINGS = ['fuel', 'factory', 'ww2bunker', 'ww2bunker', 'ww2bunker', 'allycamp', 'allycamp', 'aa']
|
||||
WW2_ALLIES_BUILDINGS = ['fuel', 'factory', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'aa']
|
||||
WW2_FREE = ['fuel', 'factory', 'ware']
|
||||
WW2_GERMANY_BUILDINGS = ['fuel', 'factory', 'ww2bunker', 'ww2bunker', 'ww2bunker', 'allycamp', 'allycamp']
|
||||
WW2_ALLIES_BUILDINGS = ['fuel', 'factory', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'allycamp']
|
||||
|
||||
FORTIFICATION_BUILDINGS = ['Siegfried Line', 'Concertina wire', 'Concertina Wire', 'Czech hedgehogs 1', 'Czech hedgehogs 2',
|
||||
'Dragonteeth 1', 'Dragonteeth 2', 'Dragonteeth 3', 'Dragonteeth 4', 'Dragonteeth 5',
|
||||
|
||||
@@ -11,10 +11,12 @@ from dcs.planes import (
|
||||
MiG_19P,
|
||||
MiG_21Bis,
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
P_51D,
|
||||
P_51D_30_NA,
|
||||
SpitfireLFMkIX,
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIXCW
|
||||
)
|
||||
|
||||
from pydcs_extensions.a4ec.a4ec import A_4E_C
|
||||
@@ -41,6 +43,8 @@ GUNFIGHTERS = [
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
Bf_109K_4,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
|
||||
from game.utils import nm_to_meter, feet_to_meter
|
||||
|
||||
@@ -15,6 +16,8 @@ class Doctrine:
|
||||
sead_max_range: int
|
||||
|
||||
rendezvous_altitude: int
|
||||
hold_distance: int
|
||||
push_distance: int
|
||||
join_distance: int
|
||||
split_distance: int
|
||||
ingress_egress_distance: int
|
||||
@@ -25,11 +28,14 @@ class Doctrine:
|
||||
max_patrol_altitude: int
|
||||
pattern_altitude: int
|
||||
|
||||
cap_duration: timedelta
|
||||
cap_min_track_length: int
|
||||
cap_max_track_length: int
|
||||
cap_min_distance_from_cp: int
|
||||
cap_max_distance_from_cp: int
|
||||
|
||||
cas_duration: timedelta
|
||||
|
||||
|
||||
MODERN_DOCTRINE = Doctrine(
|
||||
cap=True,
|
||||
@@ -40,6 +46,8 @@ MODERN_DOCTRINE = Doctrine(
|
||||
strike_max_range=1500000,
|
||||
sead_max_range=1500000,
|
||||
rendezvous_altitude=feet_to_meter(25000),
|
||||
hold_distance=nm_to_meter(15),
|
||||
push_distance=nm_to_meter(20),
|
||||
join_distance=nm_to_meter(20),
|
||||
split_distance=nm_to_meter(20),
|
||||
ingress_egress_distance=nm_to_meter(45),
|
||||
@@ -48,10 +56,12 @@ MODERN_DOCTRINE = Doctrine(
|
||||
min_patrol_altitude=feet_to_meter(15000),
|
||||
max_patrol_altitude=feet_to_meter(33000),
|
||||
pattern_altitude=feet_to_meter(5000),
|
||||
cap_duration=timedelta(minutes=30),
|
||||
cap_min_track_length=nm_to_meter(15),
|
||||
cap_max_track_length=nm_to_meter(40),
|
||||
cap_min_distance_from_cp=nm_to_meter(10),
|
||||
cap_max_distance_from_cp=nm_to_meter(40),
|
||||
cas_duration=timedelta(minutes=30),
|
||||
)
|
||||
|
||||
COLDWAR_DOCTRINE = Doctrine(
|
||||
@@ -63,6 +73,8 @@ COLDWAR_DOCTRINE = Doctrine(
|
||||
strike_max_range=1500000,
|
||||
sead_max_range=1500000,
|
||||
rendezvous_altitude=feet_to_meter(22000),
|
||||
hold_distance=nm_to_meter(10),
|
||||
push_distance=nm_to_meter(10),
|
||||
join_distance=nm_to_meter(10),
|
||||
split_distance=nm_to_meter(10),
|
||||
ingress_egress_distance=nm_to_meter(30),
|
||||
@@ -71,10 +83,12 @@ COLDWAR_DOCTRINE = Doctrine(
|
||||
min_patrol_altitude=feet_to_meter(10000),
|
||||
max_patrol_altitude=feet_to_meter(24000),
|
||||
pattern_altitude=feet_to_meter(5000),
|
||||
cap_duration=timedelta(minutes=30),
|
||||
cap_min_track_length=nm_to_meter(12),
|
||||
cap_max_track_length=nm_to_meter(24),
|
||||
cap_min_distance_from_cp=nm_to_meter(8),
|
||||
cap_max_distance_from_cp=nm_to_meter(25),
|
||||
cas_duration=timedelta(minutes=30),
|
||||
)
|
||||
|
||||
WWII_DOCTRINE = Doctrine(
|
||||
@@ -85,6 +99,8 @@ WWII_DOCTRINE = Doctrine(
|
||||
antiship=True,
|
||||
strike_max_range=1500000,
|
||||
sead_max_range=1500000,
|
||||
hold_distance=nm_to_meter(5),
|
||||
push_distance=nm_to_meter(5),
|
||||
join_distance=nm_to_meter(5),
|
||||
split_distance=nm_to_meter(5),
|
||||
rendezvous_altitude=feet_to_meter(10000),
|
||||
@@ -94,8 +110,10 @@ WWII_DOCTRINE = Doctrine(
|
||||
min_patrol_altitude=feet_to_meter(4000),
|
||||
max_patrol_altitude=feet_to_meter(15000),
|
||||
pattern_altitude=feet_to_meter(5000),
|
||||
cap_duration=timedelta(minutes=30),
|
||||
cap_min_track_length=nm_to_meter(8),
|
||||
cap_max_track_length=nm_to_meter(18),
|
||||
cap_min_distance_from_cp=nm_to_meter(0),
|
||||
cap_max_distance_from_cp=nm_to_meter(5),
|
||||
cas_duration=timedelta(minutes=30),
|
||||
)
|
||||
|
||||
153
game/db.py
153
game/db.py
@@ -44,6 +44,7 @@ from dcs.planes import (
|
||||
FW_190A8,
|
||||
FW_190D9,
|
||||
F_117A,
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
F_15C,
|
||||
F_15E,
|
||||
@@ -103,7 +104,7 @@ from dcs.planes import (
|
||||
Tu_95MS,
|
||||
WingLoong_I,
|
||||
Yak_40,
|
||||
plane_map,
|
||||
plane_map
|
||||
)
|
||||
from dcs.ships import (
|
||||
Armed_speedboat,
|
||||
@@ -153,7 +154,6 @@ from dcs.vehicles import (
|
||||
)
|
||||
|
||||
import pydcs_extensions.frenchpack.frenchpack as frenchpack
|
||||
from game.factions.faction import Faction
|
||||
# PATCH pydcs data with MODS
|
||||
from game.factions.faction_loader import FactionLoader
|
||||
from pydcs_extensions.a4ec.a4ec import A_4E_C
|
||||
@@ -202,7 +202,6 @@ vehicle_map["Toyota_vert"] = frenchpack.DIM__TOYOTA_GREEN
|
||||
vehicle_map["Toyota_desert"] = frenchpack.DIM__TOYOTA_DESERT
|
||||
vehicle_map["Kamikaze"] = frenchpack.DIM__KAMIKAZE
|
||||
|
||||
|
||||
"""
|
||||
---------- BEGINNING OF CONFIGURATION SECTION
|
||||
"""
|
||||
@@ -271,6 +270,7 @@ PRICES = {
|
||||
F_15E: 24,
|
||||
F_16C_50: 20,
|
||||
F_16A: 14,
|
||||
F_14A_135_GR: 20,
|
||||
F_14B: 24,
|
||||
Tornado_IDS: 20,
|
||||
Tornado_GR4: 20,
|
||||
@@ -399,27 +399,30 @@ PRICES = {
|
||||
Unarmed.Transport_M818: 3,
|
||||
|
||||
# WW2
|
||||
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G:24,
|
||||
Armor.MT_Pz_Kpfw_IV_Ausf_H:16,
|
||||
Armor.HT_Pz_Kpfw_VI_Tiger_I:24,
|
||||
Armor.HT_Pz_Kpfw_VI_Ausf__B__Tiger_II:26,
|
||||
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G: 24,
|
||||
Armor.MT_Pz_Kpfw_IV_Ausf_H: 16,
|
||||
Armor.HT_Pz_Kpfw_VI_Tiger_I: 24,
|
||||
Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II: 26,
|
||||
Armor.TD_Jagdpanther_G1: 18,
|
||||
Armor.TD_Jagdpanzer_IV: 11,
|
||||
Armor.Sd_Kfz_184_Elefant: 18,
|
||||
Armor.APC_Sd_Kfz_251:4,
|
||||
Armor.IFV_Sd_Kfz_234_2_Puma:8,
|
||||
Armor.MT_M4_Sherman:12,
|
||||
Armor.MT_M4A4_Sherman_Firefly:16,
|
||||
Armor.CT_Cromwell_IV:12,
|
||||
Armor.M30_Cargo_Carrier:2,
|
||||
Armor.APC_M2A1:4,
|
||||
Armor.ST_Centaur_IV: 10,
|
||||
Armor.APC_Sd_Kfz_251: 4,
|
||||
Armor.AC_Sd_Kfz_234_2_Puma: 8,
|
||||
Armor.MT_M4_Sherman: 12,
|
||||
Armor.MT_M4A4_Sherman_Firefly: 16,
|
||||
Armor.CT_Cromwell_IV: 12,
|
||||
Armor.M30_Cargo_Carrier: 2,
|
||||
Armor.APC_M2A1: 4,
|
||||
Armor.CT_Centaur_IV: 10,
|
||||
Armor.HIT_Churchill_VII: 16,
|
||||
Armor.LAC_M8_Greyhound: 8,
|
||||
Armor.TD_M10_GMC: 14,
|
||||
Armor.StuG_III_Ausf__G: 12,
|
||||
Artillery.M12_GMC: 10,
|
||||
Artillery.Sturmpanzer_IV_Brummbär: 10,
|
||||
Armor.Daimler_Armoured_Car: 8,
|
||||
Armor.LT_Mk_VII_Tetrarch: 8,
|
||||
Armor.M4_Tractor: 2,
|
||||
|
||||
# ship
|
||||
CV_1143_5_Admiral_Kuznetsov: 100,
|
||||
@@ -498,12 +501,16 @@ PRICES = {
|
||||
AirDefence.AAA_Flak_38: 6,
|
||||
AirDefence.AAA_8_8cm_Flak_36: 8,
|
||||
AirDefence.AAA_8_8cm_Flak_37: 9,
|
||||
AirDefence.AAA_Flak_Vierling_38:6,
|
||||
AirDefence.AAA_Flak_Vierling_38: 5,
|
||||
AirDefence.AAA_Kdo_G_40: 8,
|
||||
AirDefence.Flak_Searchlight_37: 4,
|
||||
AirDefence.Maschinensatz_33: 10,
|
||||
AirDefence.AAA_8_8cm_Flak_41: 10,
|
||||
AirDefence.EWR_FuMG_401_Freya_LZ: 25,
|
||||
AirDefence.AAA_Bofors_40mm: 8,
|
||||
AirDefence.AAA_M1_37mm: 7,
|
||||
AirDefence.AAA_M45_Quadmount: 4,
|
||||
AirDefence.AA_gun_QF_3_7: 10,
|
||||
|
||||
# FRENCH PACK MOD
|
||||
frenchpack.AMX_10RCR: 10,
|
||||
@@ -568,6 +575,7 @@ UNIT_BY_TASK = {
|
||||
MiG_31,
|
||||
FA_18C_hornet,
|
||||
F_15C,
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
F_16A,
|
||||
F_16C_50,
|
||||
@@ -743,13 +751,13 @@ UNIT_BY_TASK = {
|
||||
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
|
||||
Armor.MT_Pz_Kpfw_IV_Ausf_H,
|
||||
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.APC_Sd_Kfz_251,
|
||||
Armor.APC_Sd_Kfz_251,
|
||||
Armor.APC_Sd_Kfz_251,
|
||||
Armor.APC_Sd_Kfz_251,
|
||||
Armor.IFV_Sd_Kfz_234_2_Puma,
|
||||
Armor.IFV_Sd_Kfz_234_2_Puma,
|
||||
Armor.AC_Sd_Kfz_234_2_Puma,
|
||||
Armor.AC_Sd_Kfz_234_2_Puma,
|
||||
Armor.MT_M4_Sherman,
|
||||
Armor.MT_M4A4_Sherman_Firefly,
|
||||
Armor.CT_Cromwell_IV,
|
||||
@@ -762,12 +770,12 @@ UNIT_BY_TASK = {
|
||||
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
|
||||
Armor.MT_Pz_Kpfw_IV_Ausf_H,
|
||||
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.TD_Jagdpanther_G1,
|
||||
Armor.TD_Jagdpanzer_IV,
|
||||
Armor.Sd_Kfz_184_Elefant,
|
||||
Armor.APC_Sd_Kfz_251,
|
||||
Armor.IFV_Sd_Kfz_234_2_Puma,
|
||||
Armor.AC_Sd_Kfz_234_2_Puma,
|
||||
Armor.MT_M4_Sherman,
|
||||
Armor.MT_M4A4_Sherman_Firefly,
|
||||
Armor.CT_Cromwell_IV,
|
||||
@@ -776,8 +784,8 @@ UNIT_BY_TASK = {
|
||||
Armor.M30_Cargo_Carrier,
|
||||
Armor.APC_M2A1,
|
||||
Armor.APC_M2A1,
|
||||
Armor.ST_Centaur_IV,
|
||||
Armor.ST_Centaur_IV,
|
||||
Armor.CT_Centaur_IV,
|
||||
Armor.CT_Centaur_IV,
|
||||
Armor.HIT_Churchill_VII,
|
||||
Armor.LAC_M8_Greyhound,
|
||||
Armor.LAC_M8_Greyhound,
|
||||
@@ -893,7 +901,6 @@ SAM_CONVERT = {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
"""
|
||||
Units that will always be spawned in the air
|
||||
"""
|
||||
@@ -904,17 +911,18 @@ TAKEOFF_BAN: List[Type[FlyingType]] = [
|
||||
Units that will be always spawned in the air if launched from the carrier
|
||||
"""
|
||||
CARRIER_TAKEOFF_BAN: List[Type[FlyingType]] = [
|
||||
Su_33, # Kuznecow is bugged in a way that only 2 aircraft could be spawned
|
||||
Su_33, # Kuznecow is bugged in a way that only 2 aircraft could be spawned
|
||||
]
|
||||
|
||||
"""
|
||||
Units separated by country.
|
||||
country : DCS Country name
|
||||
"""
|
||||
FACTIONS: Dict[str, Faction] = FactionLoader.load_factions()
|
||||
FACTIONS = FactionLoader()
|
||||
|
||||
CARRIER_TYPE_BY_PLANE = {
|
||||
FA_18C_hornet: CVN_74_John_C__Stennis,
|
||||
F_14A_135_GR: CVN_74_John_C__Stennis,
|
||||
F_14B: CVN_74_John_C__Stennis,
|
||||
Ka_50: LHA_1_Tarawa,
|
||||
SA342M: LHA_1_Tarawa,
|
||||
@@ -953,23 +961,10 @@ COMMON_OVERRIDE = {
|
||||
|
||||
PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
|
||||
|
||||
B_1B: {
|
||||
CAS: "GBU-38*16, CBU-97*20",
|
||||
PinpointStrike: "GBU-31*8, GBU-38*32",
|
||||
GroundAttack: "GBU-31*8, GBU-38*32",
|
||||
},
|
||||
B_52H: {
|
||||
PinpointStrike: "AGM-86C*20",
|
||||
GroundAttack: "Mk 82*51",
|
||||
},
|
||||
F_117A: {
|
||||
PinpointStrike: "GBU-10*2",
|
||||
},
|
||||
F_15E: {
|
||||
CAS: "AIM-120B*2,AIM-9M*2,FUEL,GBU-12*4,GBU-38*4,AGM-65D*2",
|
||||
GroundAttack: "AIM-120B*2,AIM-9M*2,FUEL*3,CBU-97*12",
|
||||
PinpointStrike: "AIM-120B*2,AIM-9M*2,FUEL,GBU-31*4,AGM-154C*2",
|
||||
},
|
||||
B_1B: COMMON_OVERRIDE,
|
||||
B_52H: COMMON_OVERRIDE,
|
||||
F_117A: COMMON_OVERRIDE,
|
||||
F_15E: COMMON_OVERRIDE,
|
||||
FA_18C_hornet: {
|
||||
CAP: "CAP HEAVY",
|
||||
Intercept: "CAP HEAVY",
|
||||
@@ -993,18 +988,15 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
|
||||
Tu_160: {
|
||||
PinpointStrike: "Kh-65*12",
|
||||
},
|
||||
Tu_22M3: {
|
||||
GroundAttack: "FAB-500*33, FAB-250*36",
|
||||
},
|
||||
Tu_95MS: {
|
||||
PinpointStrike: "Kh-65*6",
|
||||
},
|
||||
Tu_22M3: COMMON_OVERRIDE,
|
||||
Tu_95MS: COMMON_OVERRIDE,
|
||||
A_10A: COMMON_OVERRIDE,
|
||||
A_10C: COMMON_OVERRIDE,
|
||||
A_10C_2: COMMON_OVERRIDE,
|
||||
AV8BNA: COMMON_OVERRIDE,
|
||||
C_101CC: COMMON_OVERRIDE,
|
||||
F_5E_3: COMMON_OVERRIDE,
|
||||
F_14A_135_GR: COMMON_OVERRIDE,
|
||||
F_14B: COMMON_OVERRIDE,
|
||||
F_15C: COMMON_OVERRIDE,
|
||||
F_16C_50: COMMON_OVERRIDE,
|
||||
@@ -1014,14 +1006,14 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
|
||||
MiG_19P: COMMON_OVERRIDE,
|
||||
MiG_21Bis: COMMON_OVERRIDE,
|
||||
AJS37: COMMON_OVERRIDE,
|
||||
Su_25T:COMMON_OVERRIDE,
|
||||
Su_25:COMMON_OVERRIDE,
|
||||
Su_27:COMMON_OVERRIDE,
|
||||
Su_33:COMMON_OVERRIDE,
|
||||
MiG_29A:COMMON_OVERRIDE,
|
||||
MiG_29G:COMMON_OVERRIDE,
|
||||
MiG_29S:COMMON_OVERRIDE,
|
||||
Su_24M:COMMON_OVERRIDE,
|
||||
Su_25T: COMMON_OVERRIDE,
|
||||
Su_25: COMMON_OVERRIDE,
|
||||
Su_27: COMMON_OVERRIDE,
|
||||
Su_33: COMMON_OVERRIDE,
|
||||
MiG_29A: COMMON_OVERRIDE,
|
||||
MiG_29G: COMMON_OVERRIDE,
|
||||
MiG_29S: COMMON_OVERRIDE,
|
||||
Su_24M: COMMON_OVERRIDE,
|
||||
Su_30: COMMON_OVERRIDE,
|
||||
Su_34: COMMON_OVERRIDE,
|
||||
Su_57: COMMON_OVERRIDE,
|
||||
@@ -1030,21 +1022,21 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
|
||||
Tornado_GR4: COMMON_OVERRIDE,
|
||||
Tornado_IDS: COMMON_OVERRIDE,
|
||||
Mirage_2000_5: COMMON_OVERRIDE,
|
||||
MiG_31:COMMON_OVERRIDE,
|
||||
SA342M:COMMON_OVERRIDE,
|
||||
SA342L:COMMON_OVERRIDE,
|
||||
SA342Mistral:COMMON_OVERRIDE,
|
||||
Mi_8MT:COMMON_OVERRIDE,
|
||||
Mi_24V:COMMON_OVERRIDE,
|
||||
Mi_28N:COMMON_OVERRIDE,
|
||||
Ka_50:COMMON_OVERRIDE,
|
||||
L_39ZA:COMMON_OVERRIDE,
|
||||
L_39C:COMMON_OVERRIDE,
|
||||
MiG_31: COMMON_OVERRIDE,
|
||||
SA342M: COMMON_OVERRIDE,
|
||||
SA342L: COMMON_OVERRIDE,
|
||||
SA342Mistral: COMMON_OVERRIDE,
|
||||
Mi_8MT: COMMON_OVERRIDE,
|
||||
Mi_24V: COMMON_OVERRIDE,
|
||||
Mi_28N: COMMON_OVERRIDE,
|
||||
Ka_50: COMMON_OVERRIDE,
|
||||
L_39ZA: COMMON_OVERRIDE,
|
||||
L_39C: COMMON_OVERRIDE,
|
||||
Su_17M4: COMMON_OVERRIDE,
|
||||
F_4E: COMMON_OVERRIDE,
|
||||
P_47D_30:COMMON_OVERRIDE,
|
||||
P_47D_30bl1:COMMON_OVERRIDE,
|
||||
P_47D_40:COMMON_OVERRIDE,
|
||||
P_47D_30: COMMON_OVERRIDE,
|
||||
P_47D_30bl1: COMMON_OVERRIDE,
|
||||
P_47D_40: COMMON_OVERRIDE,
|
||||
B_17G: COMMON_OVERRIDE,
|
||||
P_51D: COMMON_OVERRIDE,
|
||||
P_51D_30_NA: COMMON_OVERRIDE,
|
||||
@@ -1137,6 +1129,7 @@ PLAYER_BUDGET_BASE = 20
|
||||
|
||||
CARRIER_CAPABLE = [
|
||||
FA_18C_hornet,
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
AV8BNA,
|
||||
Su_33,
|
||||
@@ -1172,7 +1165,6 @@ LHA_CAPABLE = [
|
||||
SA342Mistral
|
||||
]
|
||||
|
||||
|
||||
"""
|
||||
---------- END OF CONFIGURATION SECTION
|
||||
"""
|
||||
@@ -1224,16 +1216,20 @@ def find_unittype(for_task: Task, country_name: str) -> List[UnitType]:
|
||||
|
||||
def find_infantry(country_name: str) -> List[UnitType]:
|
||||
inf = [
|
||||
Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS,
|
||||
Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS,
|
||||
Infantry.Paratrooper_AKS,
|
||||
Infantry.Soldier_RPG,
|
||||
Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4,
|
||||
Infantry.Soldier_M249,
|
||||
Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK,
|
||||
Infantry.Paratrooper_RPG_16,
|
||||
Infantry.Georgian_soldier_with_M4, Infantry.Georgian_soldier_with_M4, Infantry.Georgian_soldier_with_M4, Infantry.Georgian_soldier_with_M4,
|
||||
Infantry.Infantry_Soldier_Rus, Infantry.Infantry_Soldier_Rus, Infantry.Infantry_Soldier_Rus, Infantry.Infantry_Soldier_Rus,
|
||||
Infantry.Georgian_soldier_with_M4, Infantry.Georgian_soldier_with_M4, Infantry.Georgian_soldier_with_M4,
|
||||
Infantry.Georgian_soldier_with_M4,
|
||||
Infantry.Infantry_Soldier_Rus, Infantry.Infantry_Soldier_Rus, Infantry.Infantry_Soldier_Rus,
|
||||
Infantry.Infantry_Soldier_Rus,
|
||||
Infantry.Infantry_SMLE_No_4_Mk_1, Infantry.Infantry_SMLE_No_4_Mk_1, Infantry.Infantry_SMLE_No_4_Mk_1,
|
||||
Infantry.Infantry_Mauser_98, Infantry.Infantry_Mauser_98, Infantry.Infantry_Mauser_98, Infantry.Infantry_Mauser_98,
|
||||
Infantry.Infantry_Mauser_98, Infantry.Infantry_Mauser_98, Infantry.Infantry_Mauser_98,
|
||||
Infantry.Infantry_Mauser_98,
|
||||
Infantry.Infantry_M1_Garand, Infantry.Infantry_M1_Garand, Infantry.Infantry_M1_Garand,
|
||||
Infantry.Infantry_Soldier_Insurgents, Infantry.Infantry_Soldier_Insurgents, Infantry.Infantry_Soldier_Insurgents
|
||||
]
|
||||
@@ -1364,7 +1360,7 @@ def unitdict_from(fd: AssignedUnitsDict) -> Dict:
|
||||
|
||||
|
||||
def country_id_from_name(name):
|
||||
for k,v in country_dict.items():
|
||||
for k, v in country_dict.items():
|
||||
if v.name == name:
|
||||
return k
|
||||
return -1
|
||||
@@ -1382,14 +1378,17 @@ def _validate_db():
|
||||
for unit_type in total_set:
|
||||
assert unit_type in PRICES, "{} not in prices".format(unit_type)
|
||||
|
||||
|
||||
_validate_db()
|
||||
|
||||
|
||||
class DefaultLiveries:
|
||||
class Default(Enum):
|
||||
af_standard = ""
|
||||
|
||||
|
||||
OH_58D.Liveries = DefaultLiveries
|
||||
F_16C_50.Liveries = DefaultLiveries
|
||||
P_51D_30_NA.Liveries = DefaultLiveries
|
||||
Ju_88A4.Liveries = DefaultLiveries
|
||||
B_17G.Liveries = DefaultLiveries
|
||||
B_17G.Liveries = DefaultLiveries
|
||||
|
||||
@@ -24,7 +24,7 @@ class DebriefingDeadUnitInfo:
|
||||
|
||||
class Debriefing:
|
||||
def __init__(self, state_data, game):
|
||||
self.base_capture_events = state_data["base_capture_events"]
|
||||
self.state_data = state_data
|
||||
self.killed_aircrafts = state_data["killed_aircrafts"]
|
||||
self.killed_ground_units = state_data["killed_ground_units"]
|
||||
self.weapons_fired = state_data["weapons_fired"]
|
||||
@@ -87,8 +87,8 @@ class Debriefing:
|
||||
|
||||
for i, ground_object in enumerate(cp.ground_objects):
|
||||
logging.info(unit)
|
||||
logging.info(ground_object.string_identifier)
|
||||
if ground_object.matches_string_identifier(unit):
|
||||
logging.info(ground_object.group_name)
|
||||
if ground_object.is_same_group(unit):
|
||||
unit = DebriefingDeadUnitInfo(country, player_unit, ground_object.dcs_identifier)
|
||||
self.dead_buildings.append(unit)
|
||||
elif ground_object.dcs_identifier in ["AA", "CARRIER", "LHA"]:
|
||||
@@ -162,6 +162,18 @@ class Debriefing:
|
||||
logging.info(self.player_dead_buildings_dict)
|
||||
logging.info(self.enemy_dead_buildings_dict)
|
||||
|
||||
@property
|
||||
def base_capture_events(self):
|
||||
"""Keeps only the last instance of a base capture event for each base ID"""
|
||||
reversed_captures = [i for i in self.state_data["base_capture_events"][::-1]]
|
||||
last_base_cap_indexes = []
|
||||
for idx, base in enumerate(i.split("||")[0] for i in reversed_captures):
|
||||
if base in [x[1] for x in last_base_cap_indexes]:
|
||||
continue
|
||||
else:
|
||||
last_base_cap_indexes.append((idx, base))
|
||||
return [reversed_captures[idx[0]] for idx in last_base_cap_indexes]
|
||||
|
||||
|
||||
class PollDebriefingFileThread(threading.Thread):
|
||||
"""Thread class with a stop() method. The thread itself has to check
|
||||
|
||||
@@ -14,7 +14,6 @@ from game.infos.information import Information
|
||||
from game.operation.operation import Operation
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
from theater import ControlPoint
|
||||
from theater.start_generator import generate_airbase_defense_group
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..game import Game
|
||||
@@ -145,9 +144,13 @@ class Event:
|
||||
for i, ground_object in enumerate(cp.ground_objects):
|
||||
if ground_object.is_dead:
|
||||
continue
|
||||
|
||||
if ground_object.matches_string_identifier(destroyed_ground_unit_name):
|
||||
logging.info("cp {} killing ground object {}".format(cp, ground_object.string_identifier))
|
||||
|
||||
if (
|
||||
(ground_object.group_name == destroyed_ground_unit_name)
|
||||
or
|
||||
(ground_object.is_same_group(destroyed_ground_unit_name))
|
||||
):
|
||||
logging.info("cp {} killing ground object {}".format(cp, ground_object.group_name))
|
||||
cp.ground_objects[i].is_dead = True
|
||||
|
||||
info = Information("Building destroyed",
|
||||
@@ -162,7 +165,7 @@ class Event:
|
||||
"",
|
||||
self.game.turn)
|
||||
for i, ground_object in enumerate(cp.ground_objects):
|
||||
if ground_object.dcs_identifier in ["AA", "CARRIER", "LHA"]:
|
||||
if ground_object.dcs_identifier in ["AA", "CARRIER", "LHA", "EWR"]:
|
||||
for g in ground_object.groups:
|
||||
if not hasattr(g, "units_losts"):
|
||||
g.units_losts = []
|
||||
|
||||
@@ -10,7 +10,7 @@ from dcs.planes import plane_map
|
||||
from dcs.unittype import FlyingType, ShipType, VehicleType, UnitType
|
||||
from dcs.vehicles import Armor, Unarmed, Infantry, Artillery, AirDefence
|
||||
|
||||
from game.data.building_data import WW2_ALLIES_BUILDINGS, DEFAULT_AVAILABLE_BUILDINGS, WW2_GERMANY_BUILDINGS
|
||||
from game.data.building_data import WW2_ALLIES_BUILDINGS, DEFAULT_AVAILABLE_BUILDINGS, WW2_GERMANY_BUILDINGS, WW2_FREE
|
||||
from game.data.doctrine import Doctrine, MODERN_DOCTRINE, COLDWAR_DOCTRINE, WWII_DOCTRINE
|
||||
from pydcs_extensions.mod_units import MODDED_VEHICLES, MODDED_AIRPLANES
|
||||
|
||||
@@ -57,6 +57,9 @@ class Faction:
|
||||
# Possible SAMS site generators for this faction
|
||||
sams: List[str] = field(default_factory=list)
|
||||
|
||||
# Possible EWR generators for this faction.
|
||||
ewrs: List[str] = field(default_factory=list)
|
||||
|
||||
# Possible Missile site generators for this faction
|
||||
missiles: List[str] = field(default_factory=list)
|
||||
|
||||
@@ -102,6 +105,9 @@ class Faction:
|
||||
# List of available buildings for this faction
|
||||
building_set: List[str] = field(default_factory=list)
|
||||
|
||||
# List of default livery overrides
|
||||
liveries_overrides: Dict[UnitType, List[str]] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
|
||||
|
||||
@@ -132,6 +138,7 @@ class Faction:
|
||||
json.get("logistics_units", []))
|
||||
|
||||
faction.sams = json.get("sams", [])
|
||||
faction.ewrs = json.get("ewrs", [])
|
||||
faction.shorads = json.get("shorads", [])
|
||||
faction.missiles = json.get("missiles", [])
|
||||
faction.requirements = json.get("requirements", {})
|
||||
@@ -170,6 +177,8 @@ class Faction:
|
||||
building_set = json.get("building_set", "default")
|
||||
if building_set == "default":
|
||||
faction.building_set = DEFAULT_AVAILABLE_BUILDINGS
|
||||
elif building_set == "ww2free":
|
||||
faction.building_set = WW2_FREE
|
||||
elif building_set == "ww2ally":
|
||||
faction.building_set = WW2_ALLIES_BUILDINGS
|
||||
elif building_set == "ww2germany":
|
||||
@@ -177,6 +186,14 @@ class Faction:
|
||||
else:
|
||||
faction.building_set = DEFAULT_AVAILABLE_BUILDINGS
|
||||
|
||||
# Load liveries override
|
||||
faction.liveries_overrides = {}
|
||||
liveries_overrides = json.get("liveries_overrides", {})
|
||||
for k, v in liveries_overrides.items():
|
||||
k = load_aircraft(k)
|
||||
if k is not None:
|
||||
faction.liveries_overrides[k] = [s.lower() for s in v]
|
||||
|
||||
return faction
|
||||
|
||||
@property
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Dict, Type
|
||||
from typing import Dict, Iterator, Optional, Type
|
||||
|
||||
from game.factions.faction import Faction
|
||||
|
||||
@@ -10,6 +10,18 @@ FACTION_DIRECTORY = Path("./resources/factions/")
|
||||
|
||||
|
||||
class FactionLoader:
|
||||
def __init__(self) -> None:
|
||||
self._factions: Optional[Dict[str, Faction]] = None
|
||||
|
||||
@property
|
||||
def factions(self) -> Dict[str, Faction]:
|
||||
self.initialize()
|
||||
assert self._factions is not None
|
||||
return self._factions
|
||||
|
||||
def initialize(self) -> None:
|
||||
if self._factions is None:
|
||||
self._factions = self.load_factions()
|
||||
|
||||
@classmethod
|
||||
def load_factions(cls: Type[FactionLoader]) -> Dict[str, Faction]:
|
||||
@@ -26,3 +38,9 @@ class FactionLoader:
|
||||
logging.exception(f"Unable to load faction : {f}")
|
||||
|
||||
return factions
|
||||
|
||||
def __getitem__(self, name: str) -> Faction:
|
||||
return self.factions[name]
|
||||
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
return iter(self.factions.keys())
|
||||
|
||||
37
game/game.py
37
game/game.py
@@ -3,7 +3,7 @@ import math
|
||||
import random
|
||||
import sys
|
||||
from datetime import date, datetime, timedelta
|
||||
from typing import Any, Dict, List
|
||||
from typing import Dict, List
|
||||
|
||||
from dcs.action import Coalition
|
||||
from dcs.mapping import Point
|
||||
@@ -15,6 +15,7 @@ from game import db
|
||||
from game.db import PLAYER_BUDGET_BASE, REWARDS
|
||||
from game.inventory import GlobalAircraftInventory
|
||||
from game.models.game_stats import GameStats
|
||||
from game.plugins import LuaPluginManager
|
||||
from gen.ato import AirTaskingOrder
|
||||
from gen.conflictgen import Conflict
|
||||
from gen.flights.ai_flight_planner import CoalitionMissionPlanner
|
||||
@@ -29,7 +30,6 @@ from .event.frontlineattack import FrontlineAttackEvent
|
||||
from .factions.faction import Faction
|
||||
from .infos.information import Information
|
||||
from .settings import Settings
|
||||
from plugin import LuaPluginManager
|
||||
from .weather import Conditions, TimeOfDay
|
||||
|
||||
COMMISION_UNIT_VARIETY = 4
|
||||
@@ -147,30 +147,6 @@ class Game:
|
||||
front_line.control_point_a,
|
||||
front_line.control_point_b)
|
||||
|
||||
def commision_unit_types(self, cp: ControlPoint, for_task: Task) -> List[UnitType]:
|
||||
importance_factor = (cp.importance - IMPORTANCE_LOW) / (IMPORTANCE_HIGH - IMPORTANCE_LOW)
|
||||
|
||||
if for_task == AirDefence and not self.settings.sams:
|
||||
return [x for x in db.find_unittype(AirDefence, self.enemy_name) if x not in db.SAM_BAN]
|
||||
else:
|
||||
return db.choose_units(for_task, importance_factor, COMMISION_UNIT_VARIETY, self.enemy_name)
|
||||
|
||||
def _commision_units(self, cp: ControlPoint):
|
||||
for for_task in [CAS, CAP, AirDefence]:
|
||||
limit = COMMISION_LIMITS_FACTORS[for_task] * math.pow(cp.importance,
|
||||
COMMISION_LIMITS_SCALE) * self.settings.multiplier
|
||||
missing_units = limit - cp.base.total_units(for_task)
|
||||
if missing_units > 0:
|
||||
awarded_points = COMMISION_AMOUNTS_FACTORS[for_task] * math.pow(cp.importance,
|
||||
COMMISION_AMOUNTS_SCALE) * self.settings.multiplier
|
||||
points_to_spend = cp.base.append_commision_points(for_task, awarded_points)
|
||||
if points_to_spend > 0:
|
||||
unittypes = self.commision_unit_types(cp, for_task)
|
||||
if len(unittypes) > 0:
|
||||
d = {random.choice(unittypes): points_to_spend}
|
||||
logging.info("Commision {}: {}".format(cp, d))
|
||||
cp.base.commision_units(d)
|
||||
|
||||
@property
|
||||
def budget_reward_amount(self):
|
||||
reward = 0
|
||||
@@ -178,7 +154,7 @@ class Game:
|
||||
reward = PLAYER_BUDGET_BASE * len(self.theater.player_points())
|
||||
for cp in self.theater.player_points():
|
||||
for g in cp.ground_objects:
|
||||
if g.category in REWARDS.keys():
|
||||
if g.category in REWARDS.keys() and not g.is_dead:
|
||||
reward = reward + REWARDS[g.category]
|
||||
return reward
|
||||
else:
|
||||
@@ -226,11 +202,8 @@ class Game:
|
||||
return event and event.name and event.name == self.player_name
|
||||
|
||||
def on_load(self) -> None:
|
||||
LuaPluginManager.load_settings(self.settings)
|
||||
ObjectiveDistanceCache.set_theater(self.theater)
|
||||
|
||||
# set the settings in all plugins
|
||||
for plugin in LuaPluginManager().getPlugins():
|
||||
plugin.setSettings(self.settings)
|
||||
|
||||
# Save game compatibility.
|
||||
|
||||
@@ -304,7 +277,7 @@ class Game:
|
||||
production = 0.0
|
||||
for enemy_point in self.theater.enemy_points():
|
||||
for g in enemy_point.ground_objects:
|
||||
if g.category in REWARDS.keys():
|
||||
if g.category in REWARDS.keys() and not g.is_dead:
|
||||
production = production + REWARDS[g.category]
|
||||
|
||||
production = production * 0.75
|
||||
|
||||
@@ -49,7 +49,10 @@ class ControlPointAircraftInventory:
|
||||
Args:
|
||||
aircraft: The type of aircraft to query.
|
||||
"""
|
||||
return self.inventory[aircraft]
|
||||
try:
|
||||
return self.inventory[aircraft]
|
||||
except KeyError:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def types_available(self) -> Iterator[UnitType]:
|
||||
|
||||
@@ -35,6 +35,4 @@ class FrontlineAttackOperation(Operation):
|
||||
conflict=conflict)
|
||||
|
||||
def generate(self):
|
||||
self.briefinggen.title = "Frontline CAS"
|
||||
self.briefinggen.description = "Provide CAS for the ground forces attacking enemy lines. Operation will be considered successful if total number of enemy units will be lower than your own by a factor of 1.5 (i.e. with 12 units from both sides, enemy forces need to be reduced to at least 8), meaning that you (and, probably, your wingmans) should concentrate on destroying the enemy units. Target base strength will be lowered as a result. Be advised that your flight will not attack anything until you explicitly tell them so by comms menu."
|
||||
super(FrontlineAttackOperation, self).generate()
|
||||
|
||||
@@ -14,13 +14,14 @@ from dcs.translation import String
|
||||
from dcs.triggers import TriggerStart
|
||||
from dcs.unittype import UnitType
|
||||
|
||||
from game.plugins import LuaPluginManager
|
||||
from gen import Conflict, FlightType, VisualGenerator
|
||||
from gen.aircraft import AIRCRAFT_DATA, AircraftConflictGenerator, FlightData
|
||||
from gen.airfields import AIRFIELD_DATA
|
||||
from gen.airsupportgen import AirSupport, AirSupportConflictGenerator
|
||||
from gen.armor import GroundConflictGenerator, JtacInfo
|
||||
from gen.beacons import load_beacons_for_terrain
|
||||
from gen.briefinggen import BriefingGenerator
|
||||
from gen.briefinggen import BriefingGenerator, MissionInfoGenerator
|
||||
from gen.environmentgen import EnvironmentGenerator
|
||||
from gen.forcedoptionsgen import ForcedOptionsGenerator
|
||||
from gen.groundobjectsgen import GroundObjectsGenerator
|
||||
@@ -28,7 +29,6 @@ from gen.kneeboard import KneeboardGenerator
|
||||
from gen.radios import RadioFrequency, RadioRegistry
|
||||
from gen.tacan import TacanRegistry
|
||||
from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator
|
||||
from plugin import LuaPluginManager
|
||||
from theater import ControlPoint
|
||||
from .. import db
|
||||
from ..debriefing import Debriefing
|
||||
@@ -90,8 +90,7 @@ class Operation:
|
||||
def initialize(self, mission: Mission, conflict: Conflict):
|
||||
self.current_mission = mission
|
||||
self.conflict = conflict
|
||||
self.briefinggen = BriefingGenerator(self.current_mission,
|
||||
self.conflict, self.game)
|
||||
# self.briefinggen = BriefingGenerator(self.current_mission, self.game) Is it safe to remove this, or does it also break save compat?
|
||||
|
||||
def prepare(self, terrain: Terrain, is_quick: bool):
|
||||
with open("resources/default_options.lua", "r") as f:
|
||||
@@ -166,6 +165,37 @@ class Operation:
|
||||
trigger.add_action(DoScriptFile(fileref))
|
||||
self.current_mission.triggerrules.triggers.append(trigger)
|
||||
|
||||
def notify_info_generators(
|
||||
self,
|
||||
groundobjectgen: GroundObjectsGenerator,
|
||||
airsupportgen: AirSupportConflictGenerator,
|
||||
jtacs: List[JtacInfo],
|
||||
airgen: AircraftConflictGenerator,
|
||||
):
|
||||
"""Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings)
|
||||
"""
|
||||
gens: List[MissionInfoGenerator] = [
|
||||
KneeboardGenerator(self.current_mission, self.game),
|
||||
BriefingGenerator(self.current_mission, self.game)
|
||||
]
|
||||
for gen in gens:
|
||||
for dynamic_runway in groundobjectgen.runways.values():
|
||||
gen.add_dynamic_runway(dynamic_runway)
|
||||
|
||||
for tanker in airsupportgen.air_support.tankers:
|
||||
gen.add_tanker(tanker)
|
||||
|
||||
if self.is_awacs_enabled:
|
||||
for awacs in airsupportgen.air_support.awacs:
|
||||
gen.add_awacs(awacs)
|
||||
|
||||
for jtac in jtacs:
|
||||
gen.add_jtac(jtac)
|
||||
|
||||
for flight in airgen.flights:
|
||||
gen.add_flight(flight)
|
||||
gen.generate()
|
||||
|
||||
def generate(self):
|
||||
radio_registry = RadioRegistry()
|
||||
tacan_registry = TacanRegistry()
|
||||
@@ -300,13 +330,7 @@ class Operation:
|
||||
self.assign_channels_to_flights(airgen.flights,
|
||||
airsupportgen.air_support)
|
||||
|
||||
kneeboard_generator = KneeboardGenerator(self.current_mission)
|
||||
for dynamic_runway in groundobjectgen.runways.values():
|
||||
self.briefinggen.add_dynamic_runway(dynamic_runway)
|
||||
|
||||
for tanker in airsupportgen.air_support.tankers:
|
||||
self.briefinggen.add_tanker(tanker)
|
||||
kneeboard_generator.add_tanker(tanker)
|
||||
luaData["Tankers"][tanker.callsign] = {
|
||||
"dcsGroupName": tanker.dcsGroupName,
|
||||
"callsign": tanker.callsign,
|
||||
@@ -317,8 +341,6 @@ class Operation:
|
||||
|
||||
if self.is_awacs_enabled:
|
||||
for awacs in airsupportgen.air_support.awacs:
|
||||
self.briefinggen.add_awacs(awacs)
|
||||
kneeboard_generator.add_awacs(awacs)
|
||||
luaData["AWACs"][awacs.callsign] = {
|
||||
"dcsGroupName": awacs.dcsGroupName,
|
||||
"callsign": awacs.callsign,
|
||||
@@ -326,8 +348,6 @@ class Operation:
|
||||
}
|
||||
|
||||
for jtac in jtacs:
|
||||
self.briefinggen.add_jtac(jtac)
|
||||
kneeboard_generator.add_jtac(jtac)
|
||||
luaData["JTACs"][jtac.callsign] = {
|
||||
"dcsGroupName": jtac.dcsGroupName,
|
||||
"callsign": jtac.callsign,
|
||||
@@ -337,8 +357,6 @@ class Operation:
|
||||
}
|
||||
|
||||
for flight in airgen.flights:
|
||||
self.briefinggen.add_flight(flight)
|
||||
kneeboard_generator.add_flight(flight)
|
||||
if flight.friendly and flight.flight_type in [FlightType.ANTISHIP, FlightType.DEAD, FlightType.SEAD, FlightType.STRIKE]:
|
||||
flightType = flight.flight_type.name
|
||||
flightTarget = flight.package.target
|
||||
@@ -356,11 +374,6 @@ class Operation:
|
||||
"type": flightTargetType,
|
||||
"position": { "x": flightTarget.position.x, "y": flightTarget.position.y}
|
||||
}
|
||||
|
||||
|
||||
self.briefinggen.generate()
|
||||
kneeboard_generator.generate()
|
||||
|
||||
|
||||
# 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
|
||||
@@ -460,37 +473,14 @@ dcsLiberation.TargetPoints = {
|
||||
self.current_mission.triggerrules.triggers.append(trigger)
|
||||
|
||||
# Inject Plugins Lua Scripts and data
|
||||
for plugin in LuaPluginManager().getPlugins():
|
||||
plugin.injectScripts(self)
|
||||
plugin.injectConfiguration(self)
|
||||
for plugin in LuaPluginManager.plugins():
|
||||
if plugin.enabled:
|
||||
plugin.inject_scripts(self)
|
||||
plugin.inject_configuration(self)
|
||||
|
||||
self.assign_channels_to_flights(airgen.flights,
|
||||
airsupportgen.air_support)
|
||||
|
||||
kneeboard_generator = KneeboardGenerator(self.current_mission)
|
||||
|
||||
for dynamic_runway in groundobjectgen.runways.values():
|
||||
self.briefinggen.add_dynamic_runway(dynamic_runway)
|
||||
|
||||
for tanker in airsupportgen.air_support.tankers:
|
||||
self.briefinggen.add_tanker(tanker)
|
||||
kneeboard_generator.add_tanker(tanker)
|
||||
|
||||
if self.is_awacs_enabled:
|
||||
for awacs in airsupportgen.air_support.awacs:
|
||||
self.briefinggen.add_awacs(awacs)
|
||||
kneeboard_generator.add_awacs(awacs)
|
||||
|
||||
for jtac in jtacs:
|
||||
self.briefinggen.add_jtac(jtac)
|
||||
kneeboard_generator.add_jtac(jtac)
|
||||
|
||||
for flight in airgen.flights:
|
||||
self.briefinggen.add_flight(flight)
|
||||
kneeboard_generator.add_flight(flight)
|
||||
|
||||
self.briefinggen.generate()
|
||||
kneeboard_generator.generate()
|
||||
self.notify_info_generators(groundobjectgen, airsupportgen, jtacs, airgen)
|
||||
|
||||
def assign_channels_to_flights(self, flights: List[FlightData],
|
||||
air_support: AirSupport) -> None:
|
||||
|
||||
2
game/plugins/__init__.py
Normal file
2
game/plugins/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .luaplugin import LuaPlugin
|
||||
from .manager import LuaPluginManager
|
||||
180
game/plugins/luaplugin.py
Normal file
180
game/plugins/luaplugin.py
Normal file
@@ -0,0 +1,180 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import textwrap
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, TYPE_CHECKING
|
||||
|
||||
from game.settings import Settings
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.operation.operation import Operation
|
||||
|
||||
|
||||
class LuaPluginWorkOrder:
|
||||
|
||||
def __init__(self, parent_mnemonic: str, filename: str, mnemonic: str,
|
||||
disable: bool) -> None:
|
||||
self.parent_mnemonic = parent_mnemonic
|
||||
self.filename = filename
|
||||
self.mnemonic = mnemonic
|
||||
self.disable = disable
|
||||
|
||||
def work(self, operation: Operation) -> None:
|
||||
if self.disable:
|
||||
operation.bypass_plugin_script(self.mnemonic)
|
||||
else:
|
||||
operation.inject_plugin_script(self.parent_mnemonic, self.filename,
|
||||
self.mnemonic)
|
||||
|
||||
|
||||
class PluginSettings:
|
||||
def __init__(self, identifier: str, enabled_by_default: bool) -> None:
|
||||
self.identifier = identifier
|
||||
self.enabled_by_default = enabled_by_default
|
||||
self.settings = Settings()
|
||||
self.initialize_settings()
|
||||
|
||||
def set_settings(self, settings: Settings):
|
||||
self.settings = settings
|
||||
self.initialize_settings()
|
||||
|
||||
def initialize_settings(self) -> None:
|
||||
# Plugin options are saved in the game's Settings, but it's possible for
|
||||
# plugins to change across loads. If new plugins are added or new
|
||||
# options added to those plugins, initialize the new settings.
|
||||
self.settings.initialize_plugin_option(self.identifier,
|
||||
self.enabled_by_default)
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
return self.settings.plugin_option(self.identifier)
|
||||
|
||||
def set_enabled(self, enabled: bool) -> None:
|
||||
self.settings.set_plugin_option(self.identifier, enabled)
|
||||
|
||||
|
||||
class LuaPluginOption(PluginSettings):
|
||||
def __init__(self, identifier: str, name: str,
|
||||
enabled_by_default: bool) -> None:
|
||||
super().__init__(identifier, enabled_by_default)
|
||||
self.name = name
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LuaPluginDefinition:
|
||||
identifier: str
|
||||
name: str
|
||||
present_in_ui: bool
|
||||
enabled_by_default: bool
|
||||
options: List[LuaPluginOption]
|
||||
work_orders: List[LuaPluginWorkOrder]
|
||||
config_work_orders: List[LuaPluginWorkOrder]
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, name: str, path: Path) -> LuaPluginDefinition:
|
||||
data = json.loads(path.read_text())
|
||||
|
||||
options = []
|
||||
for option in data.get("specificOptions"):
|
||||
option_id = option["mnemonic"]
|
||||
options.append(LuaPluginOption(
|
||||
identifier=f"{name}.{option_id}",
|
||||
name=option.get("nameInUI", name),
|
||||
enabled_by_default=option.get("defaultValue")
|
||||
))
|
||||
|
||||
work_orders = []
|
||||
for work_order in data.get("scriptsWorkOrders"):
|
||||
work_orders.append(LuaPluginWorkOrder(
|
||||
name, work_order.get("file"), work_order["mnemonic"],
|
||||
work_order.get("disable", False)
|
||||
))
|
||||
config_work_orders = []
|
||||
for work_order in data.get("configurationWorkOrders"):
|
||||
config_work_orders.append(LuaPluginWorkOrder(
|
||||
name, work_order.get("file"), work_order["mnemonic"],
|
||||
work_order.get("disable", False)
|
||||
))
|
||||
|
||||
return cls(
|
||||
identifier=name,
|
||||
name=data["nameInUI"],
|
||||
present_in_ui=not data.get("skipUI", False),
|
||||
enabled_by_default=data.get("defaultValue", False),
|
||||
options=options,
|
||||
work_orders=work_orders,
|
||||
config_work_orders=config_work_orders
|
||||
)
|
||||
|
||||
|
||||
class LuaPlugin(PluginSettings):
|
||||
|
||||
def __init__(self, definition: LuaPluginDefinition) -> None:
|
||||
self.definition = definition
|
||||
super().__init__(self.definition.identifier,
|
||||
self.definition.enabled_by_default)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.definition.name
|
||||
|
||||
@property
|
||||
def show_in_ui(self) -> bool:
|
||||
return self.definition.present_in_ui
|
||||
|
||||
@property
|
||||
def options(self) -> List[LuaPluginOption]:
|
||||
return self.definition.options
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, name: str, path: Path) -> Optional[LuaPlugin]:
|
||||
try:
|
||||
definition = LuaPluginDefinition.from_json(name, path)
|
||||
except KeyError:
|
||||
logging.exception("Required plugin configuration value missing")
|
||||
return None
|
||||
|
||||
return cls(definition)
|
||||
|
||||
def set_settings(self, settings: Settings):
|
||||
super().set_settings(settings)
|
||||
for option in self.definition.options:
|
||||
option.set_settings(self.settings)
|
||||
|
||||
def inject_scripts(self, operation: Operation) -> None:
|
||||
for work_order in self.definition.work_orders:
|
||||
work_order.work(operation)
|
||||
|
||||
def inject_configuration(self, operation: Operation) -> None:
|
||||
# inject the plugin options
|
||||
if self.options:
|
||||
option_decls = []
|
||||
for option in self.options:
|
||||
enabled = str(option.enabled).lower()
|
||||
name = option.identifier
|
||||
option_decls.append(
|
||||
f" dcsLiberation.plugins.{name} = {enabled}")
|
||||
|
||||
joined_options = "\n".join(option_decls)
|
||||
|
||||
lua = textwrap.dedent(f"""\
|
||||
-- {self.identifier} plugin configuration.
|
||||
|
||||
if dcsLiberation then
|
||||
if not dcsLiberation.plugins then
|
||||
dcsLiberation.plugins = {{}}
|
||||
end
|
||||
dcsLiberation.plugins.{self.identifier} = {{}}
|
||||
{joined_options}
|
||||
end
|
||||
|
||||
""")
|
||||
|
||||
operation.inject_lua_trigger(
|
||||
lua, f"{self.identifier} plugin configuration")
|
||||
|
||||
for work_order in self.definition.config_work_orders:
|
||||
work_order.work(operation)
|
||||
50
game/plugins/manager.py
Normal file
50
game/plugins/manager.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from game.settings import Settings
|
||||
from game.plugins.luaplugin import LuaPlugin
|
||||
|
||||
|
||||
class LuaPluginManager:
|
||||
_plugins_loaded = False
|
||||
_plugins: Dict[str, LuaPlugin] = {}
|
||||
|
||||
@classmethod
|
||||
def _load_plugins(cls) -> None:
|
||||
plugins_path = Path("resources/plugins")
|
||||
|
||||
path = plugins_path / "plugins.json"
|
||||
if not path.exists():
|
||||
raise RuntimeError(f"{path} does not exist. Cannot continue.")
|
||||
|
||||
logging.info(f"Reading plugins list from {path}")
|
||||
|
||||
data = json.loads(path.read_text())
|
||||
for name in data:
|
||||
plugin_path = plugins_path / name / "plugin.json"
|
||||
if not plugin_path.exists():
|
||||
raise RuntimeError(
|
||||
f"Invalid plugin configuration: required plugin {name} "
|
||||
f"does not exist at {plugin_path}")
|
||||
logging.info(f"Loading plugin {name} from {plugin_path}")
|
||||
plugin = LuaPlugin.from_json(name, plugin_path)
|
||||
if plugin is not None:
|
||||
cls._plugins[name] = plugin
|
||||
cls._plugins_loaded = True
|
||||
|
||||
@classmethod
|
||||
def _get_plugins(cls) -> Dict[str, LuaPlugin]:
|
||||
if not cls._plugins_loaded:
|
||||
cls._load_plugins()
|
||||
return cls._plugins
|
||||
|
||||
@classmethod
|
||||
def plugins(cls) -> List[LuaPlugin]:
|
||||
return list(cls._get_plugins().values())
|
||||
|
||||
@classmethod
|
||||
def load_settings(cls, settings: Settings) -> None:
|
||||
for plugin in cls.plugins():
|
||||
plugin.set_settings(settings)
|
||||
@@ -1,4 +1,5 @@
|
||||
from plugin import LuaPluginManager
|
||||
from typing import Dict
|
||||
|
||||
|
||||
class Settings:
|
||||
|
||||
@@ -20,7 +21,7 @@ class Settings:
|
||||
self.night_disabled = False
|
||||
self.external_views_allowed = True
|
||||
self.supercarrier = False
|
||||
self.multiplier = 1
|
||||
self.multiplier = 1.0
|
||||
self.generate_marks = True
|
||||
self.sams = True # Legacy parameter do not use
|
||||
self.cold_start = False # Legacy parameter do not use
|
||||
@@ -40,14 +41,30 @@ class Settings:
|
||||
self.perf_culling_distance = 100
|
||||
|
||||
# LUA Plugins system
|
||||
self.plugins = {}
|
||||
for plugin in LuaPluginManager().getPlugins():
|
||||
plugin.setSettings(self)
|
||||
|
||||
self.plugins: Dict[str, bool] = {}
|
||||
|
||||
# Cheating
|
||||
self.show_red_ato = False
|
||||
|
||||
self.never_delay_player_flights = False
|
||||
|
||||
@staticmethod
|
||||
def plugin_settings_key(identifier: str) -> str:
|
||||
return f"plugins.{identifier}"
|
||||
|
||||
def initialize_plugin_option(self, identifier: str,
|
||||
default_value: bool) -> None:
|
||||
try:
|
||||
self.plugin_option(identifier)
|
||||
except KeyError:
|
||||
self.set_plugin_option(identifier, default_value)
|
||||
|
||||
def plugin_option(self, identifier: str) -> bool:
|
||||
return self.plugins[self.plugin_settings_key(identifier)]
|
||||
|
||||
def set_plugin_option(self, identifier: str, enabled: bool) -> None:
|
||||
self.plugins[self.plugin_settings_key(identifier)] = enabled
|
||||
|
||||
def __setstate__(self, state) -> None:
|
||||
# __setstate__ is called with the dict of the object being unpickled. We
|
||||
# can provide save compatibility for new settings options (which
|
||||
|
||||
18
game/version.py
Normal file
18
game/version.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _build_version_string() -> str:
|
||||
components = ["2.2.0"]
|
||||
build_number_path = Path("resources/buildnumber")
|
||||
if build_number_path.exists():
|
||||
with build_number_path.open("r") as build_number_file:
|
||||
components.append(build_number_file.readline())
|
||||
|
||||
if not Path("resources/final").exists():
|
||||
components.append("preview")
|
||||
|
||||
return "-".join(components)
|
||||
|
||||
|
||||
#: Current version of Liberation.
|
||||
VERSION = _build_version_string()
|
||||
465
gen/aircraft.py
465
gen/aircraft.py
@@ -3,7 +3,9 @@ from __future__ import annotations
|
||||
import logging
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Optional, Type, Union
|
||||
from datetime import timedelta
|
||||
from functools import cached_property
|
||||
from typing import Dict, List, Optional, Type, Union, TYPE_CHECKING
|
||||
|
||||
from dcs import helicopters
|
||||
from dcs.action import AITaskPush, ActivateGroup
|
||||
@@ -11,7 +13,6 @@ from dcs.condition import CoalitionHasAirdrome, TimeAfter
|
||||
from dcs.country import Country
|
||||
from dcs.flyingunit import FlyingUnit
|
||||
from dcs.helicopters import UH_1H, helicopter_map
|
||||
from dcs.mapping import Point
|
||||
from dcs.mission import Mission, StartType
|
||||
from dcs.planes import (
|
||||
AJS37,
|
||||
@@ -24,11 +25,13 @@ from dcs.planes import (
|
||||
JF_17,
|
||||
Ju_88A4,
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
P_51D,
|
||||
P_51D_30_NA,
|
||||
SpitfireLFMkIX,
|
||||
SpitfireLFMkIXCW,
|
||||
Su_33,
|
||||
Su_33, A_20G, Tu_22M3, B_52H,
|
||||
)
|
||||
from dcs.point import MovingPoint, PointAction
|
||||
from dcs.task import (
|
||||
@@ -53,7 +56,7 @@ from dcs.task import (
|
||||
SEAD,
|
||||
StartCommand,
|
||||
Targets,
|
||||
Task,
|
||||
Task, WeaponType,
|
||||
)
|
||||
from dcs.terrain.terrain import Airport
|
||||
from dcs.translation import String
|
||||
@@ -81,10 +84,18 @@ from dcs.mapping import Point
|
||||
from theater import TheaterGroundObject
|
||||
from theater.controlpoint import ControlPoint, ControlPointType
|
||||
from .conflictgen import Conflict
|
||||
from .flights.traveltime import PackageWaypointTiming, TotEstimator
|
||||
from .flights.flightplan import (
|
||||
CasFlightPlan,
|
||||
FormationFlightPlan,
|
||||
PatrollingFlightPlan,
|
||||
)
|
||||
from .flights.traveltime import TotEstimator
|
||||
from .naming import namegen
|
||||
from .runways import RunwayAssigner
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
WARM_START_HELI_AIRSPEED = 120
|
||||
WARM_START_HELI_ALT = 500
|
||||
WARM_START_ALTITUDE = 3000
|
||||
@@ -104,6 +115,11 @@ GERMAN_WW2_CHANNEL = MHz(40)
|
||||
HELICOPTER_CHANNEL = MHz(127)
|
||||
UHF_FALLBACK_CHANNEL = MHz(251)
|
||||
|
||||
TARGET_WAYPOINTS = (
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
FlightWaypointType.TARGET_POINT,
|
||||
FlightWaypointType.TARGET_SHIP,
|
||||
)
|
||||
|
||||
# TODO: Get radio information for all the special cases.
|
||||
def get_fallback_channel(unit_type: UnitType) -> RadioFrequency:
|
||||
@@ -123,6 +139,8 @@ def get_fallback_channel(unit_type: UnitType) -> RadioFrequency:
|
||||
allied_ww2_aircraft = [
|
||||
I_16,
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
P_51D,
|
||||
P_51D_30_NA,
|
||||
SpitfireLFMkIX,
|
||||
@@ -148,6 +166,26 @@ class ChannelNamer:
|
||||
return f"COMM{radio_id} Ch {channel_id}"
|
||||
|
||||
|
||||
class SingleRadioChannelNamer(ChannelNamer):
|
||||
"""Channel namer for the aircraft with only a single radio.
|
||||
|
||||
Aircraft like the MiG-19P and the MiG-21bis only have a single radio, so
|
||||
it's not necessary for us to name the radio when naming the channel.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def channel_name(radio_id: int, channel_id: int) -> str:
|
||||
return f"Ch {channel_id}"
|
||||
|
||||
|
||||
class HueyChannelNamer(ChannelNamer):
|
||||
"""Channel namer for the UH-1H."""
|
||||
|
||||
@staticmethod
|
||||
def channel_name(radio_id: int, channel_id: int) -> str:
|
||||
return f"COM3 Ch {channel_id}"
|
||||
|
||||
|
||||
class MirageChannelNamer(ChannelNamer):
|
||||
"""Channel namer for the M-2000."""
|
||||
|
||||
@@ -223,7 +261,7 @@ class FlightData:
|
||||
friendly: bool
|
||||
|
||||
#: Number of seconds after mission start the flight is set to depart.
|
||||
departure_delay: int
|
||||
departure_delay: timedelta
|
||||
|
||||
#: Arrival airport.
|
||||
arrival: RunwayData
|
||||
@@ -245,7 +283,7 @@ class FlightData:
|
||||
|
||||
def __init__(self, package: Package, flight_type: FlightType,
|
||||
units: List[FlyingUnit], size: int, friendly: bool,
|
||||
departure_delay: int, departure: RunwayData,
|
||||
departure_delay: timedelta, departure: RunwayData,
|
||||
arrival: RunwayData, divert: Optional[RunwayData],
|
||||
waypoints: List[FlightWaypoint],
|
||||
intra_flight_channel: RadioFrequency) -> None:
|
||||
@@ -373,16 +411,28 @@ class CommonRadioChannelAllocator(RadioChannelAllocator):
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WarthogRadioChannelAllocator(RadioChannelAllocator):
|
||||
"""Preset channel allocator for the A-10C."""
|
||||
class NoOpChannelAllocator(RadioChannelAllocator):
|
||||
"""Channel allocator for aircraft that don't support preset channels."""
|
||||
|
||||
def assign_channels_for_flight(self, flight: FlightData,
|
||||
air_support: AirSupport) -> None:
|
||||
# The A-10's radio works differently than most aircraft. Doesn't seem to
|
||||
# be a way to set these from the mission editor, let alone pydcs.
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FarmerRadioChannelAllocator(RadioChannelAllocator):
|
||||
"""Preset channel allocator for the MiG-19P."""
|
||||
|
||||
def assign_channels_for_flight(self, flight: FlightData,
|
||||
air_support: AirSupport) -> None:
|
||||
# The Farmer only has 6 preset channels. It also only has a VHF radio,
|
||||
# and currently our ATC data and AWACS are only in the UHF band.
|
||||
radio_id = 1
|
||||
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
|
||||
# TODO: Assign 4-6 to VHF frequencies of departure, arrival, and divert.
|
||||
# TODO: Assign 2 and 3 to AWACS if it is VHF.
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ViggenRadioChannelAllocator(RadioChannelAllocator):
|
||||
"""Preset channel allocator for the AJS37."""
|
||||
@@ -450,7 +500,7 @@ AIRCRAFT_DATA: Dict[str, AircraftData] = {
|
||||
# VHF for intraflight is not accepted anymore by DCS
|
||||
# (see https://forums.eagle.ru/showthread.php?p=4499738).
|
||||
intra_flight_radio=get_radio("AN/ARC-164"),
|
||||
channel_allocator=WarthogRadioChannelAllocator()
|
||||
channel_allocator=NoOpChannelAllocator()
|
||||
),
|
||||
|
||||
"AJS37": AircraftData(
|
||||
@@ -519,6 +569,15 @@ AIRCRAFT_DATA: Dict[str, AircraftData] = {
|
||||
channel_namer=ViperChannelNamer
|
||||
),
|
||||
|
||||
"Ka-50": AircraftData(
|
||||
inter_flight_radio=get_radio("R-800L1"),
|
||||
intra_flight_radio=get_radio("R-800L1"),
|
||||
# The R-800L1 doesn't have preset channels, and the other radio is for
|
||||
# communications with FAC and ground units, which don't currently have
|
||||
# radios assigned, so no channels to configure.
|
||||
channel_allocator=NoOpChannelAllocator(),
|
||||
),
|
||||
|
||||
"M-2000C": AircraftData(
|
||||
inter_flight_radio=get_radio("TRT ERA 7000 V/UHF"),
|
||||
intra_flight_radio=get_radio("TRT ERA 7200 UHF"),
|
||||
@@ -529,6 +588,29 @@ AIRCRAFT_DATA: Dict[str, AircraftData] = {
|
||||
channel_namer=MirageChannelNamer
|
||||
),
|
||||
|
||||
"MiG-15bis": AircraftData(
|
||||
inter_flight_radio=get_radio("RSI-6K HF"),
|
||||
intra_flight_radio=get_radio("RSI-6K HF"),
|
||||
channel_allocator=NoOpChannelAllocator(),
|
||||
),
|
||||
|
||||
"MiG-19P": AircraftData(
|
||||
inter_flight_radio=get_radio("RSIU-4V"),
|
||||
intra_flight_radio=get_radio("RSIU-4V"),
|
||||
channel_allocator=FarmerRadioChannelAllocator(),
|
||||
channel_namer=SingleRadioChannelNamer
|
||||
),
|
||||
|
||||
"MiG-21Bis": AircraftData(
|
||||
inter_flight_radio=get_radio("RSIU-5V"),
|
||||
intra_flight_radio=get_radio("RSIU-5V"),
|
||||
channel_allocator=CommonRadioChannelAllocator(
|
||||
inter_flight_radio_index=1,
|
||||
intra_flight_radio_index=1
|
||||
),
|
||||
channel_namer=SingleRadioChannelNamer,
|
||||
),
|
||||
|
||||
"P-51D": AircraftData(
|
||||
inter_flight_radio=get_radio("SCR522"),
|
||||
intra_flight_radio=get_radio("SCR522"),
|
||||
@@ -538,6 +620,19 @@ AIRCRAFT_DATA: Dict[str, AircraftData] = {
|
||||
),
|
||||
channel_namer=SCR522ChannelNamer
|
||||
),
|
||||
|
||||
"UH-1H": AircraftData(
|
||||
inter_flight_radio=get_radio("AN/ARC-51BX"),
|
||||
# Ideally this would use the AN/ARC-131 because that radio is supposed
|
||||
# to be used for flight comms, but DCS won't allow it as the flight's
|
||||
# frequency, nor will it allow the AN/ARC-134.
|
||||
intra_flight_radio=get_radio("AN/ARC-51BX"),
|
||||
channel_allocator=CommonRadioChannelAllocator(
|
||||
inter_flight_radio_index=1,
|
||||
intra_flight_radio_index=1
|
||||
),
|
||||
channel_namer=HueyChannelNamer
|
||||
)
|
||||
}
|
||||
AIRCRAFT_DATA["A-10C_2"] = AIRCRAFT_DATA["A-10C"]
|
||||
AIRCRAFT_DATA["P-51D-30-NA"] = AIRCRAFT_DATA["P-51D"]
|
||||
@@ -546,7 +641,7 @@ AIRCRAFT_DATA["P-47D-30"] = AIRCRAFT_DATA["P-51D"]
|
||||
|
||||
class AircraftConflictGenerator:
|
||||
def __init__(self, mission: Mission, conflict: Conflict, settings: Settings,
|
||||
game, radio_registry: RadioRegistry):
|
||||
game: Game, radio_registry: RadioRegistry):
|
||||
self.m = mission
|
||||
self.game = game
|
||||
self.settings = settings
|
||||
@@ -554,6 +649,21 @@ class AircraftConflictGenerator:
|
||||
self.radio_registry = radio_registry
|
||||
self.flights: List[FlightData] = []
|
||||
|
||||
@cached_property
|
||||
def use_client(self) -> bool:
|
||||
"""True if Client should be used instead of Player."""
|
||||
blue_clients = self.client_slots_in_ato(self.game.blue_ato)
|
||||
red_clients = self.client_slots_in_ato(self.game.red_ato)
|
||||
return blue_clients + red_clients > 1
|
||||
|
||||
@staticmethod
|
||||
def client_slots_in_ato(ato: AirTaskingOrder) -> int:
|
||||
total = 0
|
||||
for package in ato.packages:
|
||||
for flight in package.flights:
|
||||
total += flight.client_count
|
||||
return total
|
||||
|
||||
def get_intra_flight_channel(self, airframe: UnitType) -> RadioFrequency:
|
||||
"""Allocates an intra-flight channel to a group.
|
||||
|
||||
@@ -603,13 +713,23 @@ class AircraftConflictGenerator:
|
||||
for unit_instance in group.units:
|
||||
unit_instance.livery_id = db.PLANE_LIVERY_OVERRIDES[unit_type]
|
||||
|
||||
single_client = flight.client_count == 1
|
||||
# Override livery by faction file data
|
||||
if flight.from_cp.captured:
|
||||
faction = self.game.player_faction
|
||||
else:
|
||||
faction = self.game.enemy_faction
|
||||
|
||||
if unit_type in faction.liveries_overrides:
|
||||
livery = random.choice(faction.liveries_overrides[unit_type])
|
||||
for unit_instance in group.units:
|
||||
unit_instance.livery_id = livery
|
||||
|
||||
for idx in range(0, min(len(group.units), flight.client_count)):
|
||||
unit = group.units[idx]
|
||||
if single_client:
|
||||
unit.set_player()
|
||||
else:
|
||||
if self.use_client:
|
||||
unit.set_client()
|
||||
else:
|
||||
unit.set_player()
|
||||
|
||||
# Do not generate player group with late activation.
|
||||
if group.late_activation:
|
||||
@@ -645,7 +765,8 @@ class AircraftConflictGenerator:
|
||||
units=group.units,
|
||||
size=len(group.units),
|
||||
friendly=flight.from_cp.captured,
|
||||
departure_delay=flight.scheduled_in,
|
||||
# Set later.
|
||||
departure_delay=timedelta(),
|
||||
departure=departure_runway,
|
||||
arrival=departure_runway,
|
||||
# TODO: Support for divert airfields.
|
||||
@@ -785,7 +906,6 @@ class AircraftConflictGenerator:
|
||||
for package in ato.packages:
|
||||
if not package.flights:
|
||||
continue
|
||||
timing = PackageWaypointTiming.for_package(package)
|
||||
for flight in package.flights:
|
||||
culled = self.game.position_culled(flight.from_cp.position)
|
||||
if flight.client_count == 0 and culled:
|
||||
@@ -795,10 +915,10 @@ class AircraftConflictGenerator:
|
||||
group = self.generate_planned_flight(flight.from_cp, country,
|
||||
flight)
|
||||
self.setup_flight_group(group, package, flight, dynamic_runways)
|
||||
self.create_waypoints(group, package, flight, timing)
|
||||
self.create_waypoints(group, package, flight)
|
||||
|
||||
def set_activation_time(self, flight: Flight, group: FlyingGroup,
|
||||
delay: int) -> None:
|
||||
delay: timedelta) -> None:
|
||||
# Note: Late activation causes the waypoint TOTs to look *weird* in the
|
||||
# mission editor. Waypoint times will be relative to the group
|
||||
# activation time rather than in absolute local time. A flight delayed
|
||||
@@ -808,20 +928,22 @@ class AircraftConflictGenerator:
|
||||
|
||||
activation_trigger = TriggerOnce(
|
||||
Event.NoEvent, f"FlightLateActivationTrigger{group.id}")
|
||||
activation_trigger.add_condition(TimeAfter(seconds=delay))
|
||||
activation_trigger.add_condition(
|
||||
TimeAfter(seconds=int(delay.total_seconds())))
|
||||
|
||||
self.prevent_spawn_at_hostile_airbase(flight, activation_trigger)
|
||||
activation_trigger.add_action(ActivateGroup(group.id))
|
||||
self.m.triggerrules.triggers.append(activation_trigger)
|
||||
|
||||
def set_startup_time(self, flight: Flight, group: FlyingGroup,
|
||||
delay: int) -> None:
|
||||
delay: timedelta) -> None:
|
||||
# Uncontrolled causes the AI unit to spawn, but not begin startup.
|
||||
group.uncontrolled = True
|
||||
|
||||
activation_trigger = TriggerOnce(Event.NoEvent,
|
||||
f"FlightStartTrigger{group.id}")
|
||||
activation_trigger.add_condition(TimeAfter(seconds=delay))
|
||||
activation_trigger.add_condition(
|
||||
TimeAfter(seconds=int(delay.total_seconds())))
|
||||
|
||||
self.prevent_spawn_at_hostile_airbase(flight, activation_trigger)
|
||||
group.add_trigger_action(StartCommand())
|
||||
@@ -882,7 +1004,6 @@ class AircraftConflictGenerator:
|
||||
at=cp.position)
|
||||
group.points[0].alt = 1500
|
||||
|
||||
flight.group = group
|
||||
return group
|
||||
|
||||
@staticmethod
|
||||
@@ -903,7 +1024,8 @@ class AircraftConflictGenerator:
|
||||
group.points[0].tasks.append(OptRTBOnOutOfAmmo(rtb_winchester))
|
||||
|
||||
group.points[0].tasks.append(OptRTBOnBingoFuel(True))
|
||||
group.points[0].tasks.append(OptRestrictAfterburner(True))
|
||||
# Do not restrict afterburner.
|
||||
# https://forums.eagle.ru/forum/english/digital-combat-simulator/dcs-world-2-5/bugs-and-problems-ai/ai-ad/7121294-ai-stuck-at-high-aoa-after-making-sharp-turn-if-afterburner-is-restricted
|
||||
|
||||
@staticmethod
|
||||
def configure_eplrs(group: FlyingGroup, flight: Flight) -> None:
|
||||
@@ -935,13 +1057,25 @@ class AircraftConflictGenerator:
|
||||
self.configure_behavior(
|
||||
group,
|
||||
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||
roe=OptROE.Values.WeaponHold,
|
||||
roe=OptROE.Values.OpenFire,
|
||||
rtb_winchester=OptRTBOnOutOfAmmo.Values.Unguided,
|
||||
restrict_jettison=True)
|
||||
|
||||
def configure_dead(self, group: FlyingGroup, package: Package,
|
||||
flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> None:
|
||||
group.task = SEAD.name
|
||||
self._setup_group(group, SEAD, package, flight, dynamic_runways)
|
||||
self.configure_behavior(
|
||||
group,
|
||||
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||
roe=OptROE.Values.OpenFire,
|
||||
rtb_winchester=OptRTBOnOutOfAmmo.Values.ASM,
|
||||
restrict_jettison=True)
|
||||
|
||||
def configure_sead(self, group: FlyingGroup, package: Package,
|
||||
flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> None:
|
||||
flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> None:
|
||||
group.task = SEAD.name
|
||||
self._setup_group(group, SEAD, package, flight, dynamic_runways)
|
||||
self.configure_behavior(
|
||||
@@ -954,7 +1088,7 @@ class AircraftConflictGenerator:
|
||||
def configure_strike(self, group: FlyingGroup, package: Package,
|
||||
flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> None:
|
||||
group.task = PinpointStrike.name
|
||||
group.task = GroundAttack.name
|
||||
self._setup_group(group, GroundAttack, package, flight, dynamic_runways)
|
||||
self.configure_behavior(
|
||||
group,
|
||||
@@ -999,7 +1133,9 @@ class AircraftConflictGenerator:
|
||||
self.configure_cap(group, package, flight, dynamic_runways)
|
||||
elif flight_type in [FlightType.CAS, FlightType.BAI]:
|
||||
self.configure_cas(group, package, flight, dynamic_runways)
|
||||
elif flight_type in [FlightType.SEAD, FlightType.DEAD]:
|
||||
elif flight_type in [FlightType.DEAD, ]:
|
||||
self.configure_dead(group, package, flight, dynamic_runways)
|
||||
elif flight_type in [FlightType.SEAD, ]:
|
||||
self.configure_sead(group, package, flight, dynamic_runways)
|
||||
elif flight_type in [FlightType.STRIKE]:
|
||||
self.configure_strike(group, package, flight, dynamic_runways)
|
||||
@@ -1012,8 +1148,8 @@ class AircraftConflictGenerator:
|
||||
|
||||
self.configure_eplrs(group, flight)
|
||||
|
||||
def create_waypoints(self, group: FlyingGroup, package: Package,
|
||||
flight: Flight, timing: PackageWaypointTiming) -> None:
|
||||
def create_waypoints(
|
||||
self, group: FlyingGroup, package: Package, flight: Flight) -> None:
|
||||
|
||||
for waypoint in flight.points:
|
||||
waypoint.tot = None
|
||||
@@ -1022,15 +1158,32 @@ class AircraftConflictGenerator:
|
||||
flight.from_cp)
|
||||
self.set_takeoff_time(takeoff_point, package, flight, group)
|
||||
|
||||
filtered_points = []
|
||||
filtered_points = [] # type: List[FlightWaypoint]
|
||||
|
||||
for point in flight.points:
|
||||
if point.only_for_player and not flight.client_count:
|
||||
continue
|
||||
filtered_points.append(point)
|
||||
|
||||
# Only add 1 target waypoint for Viggens. This only affects player flights,
|
||||
# the Viggen can't have more than 9 waypoints which leaves us with two target point
|
||||
# under the current flight plans.
|
||||
# TODO: Make this smarter, it currently selects a random unit in the group for target,
|
||||
# this could be updated to make it pick the "best" two targets in the group.
|
||||
if flight.unit_type is AJS37 and flight.client_count:
|
||||
viggen_target_points = [
|
||||
(idx, point) for idx, point in enumerate(filtered_points) if point.waypoint_type in TARGET_WAYPOINTS
|
||||
]
|
||||
if viggen_target_points:
|
||||
keep_target = viggen_target_points[random.randint(0, len(viggen_target_points) - 1)]
|
||||
filtered_points = [
|
||||
point for idx, point in enumerate(filtered_points) if (
|
||||
point.waypoint_type not in TARGET_WAYPOINTS or idx == keep_target[0]
|
||||
)
|
||||
]
|
||||
|
||||
for idx, point in enumerate(filtered_points):
|
||||
PydcsWaypointBuilder.for_waypoint(
|
||||
point, group, flight, timing, self.m
|
||||
point, group, package, flight, self.m
|
||||
).build()
|
||||
|
||||
# Set here rather than when the FlightData is created so they waypoints
|
||||
@@ -1038,15 +1191,31 @@ class AircraftConflictGenerator:
|
||||
self.flights[-1].waypoints = [takeoff_point] + flight.points
|
||||
self._setup_custom_payload(flight, group)
|
||||
|
||||
def should_delay_flight(self, flight: Flight,
|
||||
start_time: timedelta) -> bool:
|
||||
if start_time.total_seconds() <= 0:
|
||||
return False
|
||||
|
||||
if not flight.client_count:
|
||||
return True
|
||||
|
||||
if start_time < timedelta(minutes=10):
|
||||
# Don't bother delaying client flights with short start delays. Much
|
||||
# more than ten minutes starts to eat into fuel a bit more
|
||||
# (espeicially for something fuel limited like a Harrier).
|
||||
return False
|
||||
|
||||
return not self.settings.never_delay_player_flights
|
||||
|
||||
def set_takeoff_time(self, waypoint: FlightWaypoint, package: Package,
|
||||
flight: Flight, group: FlyingGroup) -> None:
|
||||
estimator = TotEstimator(package)
|
||||
start_time = estimator.mission_start_time(flight)
|
||||
|
||||
if start_time > 0:
|
||||
if self.should_delay_flight(flight, start_time):
|
||||
if self.should_activate_late(flight):
|
||||
# Late activation causes the aircraft to not be spawned until
|
||||
# triggered.
|
||||
# Late activation causes the aircraft to not be spawned
|
||||
# until triggered.
|
||||
self.set_activation_time(flight, group, start_time)
|
||||
elif flight.start_type == "Cold":
|
||||
# Setting the start time causes the AI to wait until the
|
||||
@@ -1056,18 +1225,12 @@ class AircraftConflictGenerator:
|
||||
# And setting *our* waypoint TOT causes the takeoff time to show up in
|
||||
# the player's kneeboard.
|
||||
waypoint.tot = estimator.takeoff_time_for_flight(flight)
|
||||
# And finally assign it to the FlightData info so it shows correctly in
|
||||
# the briefing.
|
||||
self.flights[-1].departure_delay = start_time
|
||||
|
||||
@staticmethod
|
||||
def should_activate_late(flight: Flight) -> bool:
|
||||
if flight.client_count:
|
||||
# Never delay players. Note that cold start player flights with
|
||||
# AI members will still be marked as uncontrolled until the start
|
||||
# trigger fires to postpone engine start.
|
||||
#
|
||||
# Player flights that start on the runway or in the air will start
|
||||
# immediately, and AI flight members will not be delayed.
|
||||
return False
|
||||
|
||||
if flight.start_type != "Cold":
|
||||
# Avoid spawning aircraft in the air or on the runway until it's
|
||||
# time for their mission. Also avoid burning through gas spawning
|
||||
@@ -1085,12 +1248,12 @@ class AircraftConflictGenerator:
|
||||
|
||||
class PydcsWaypointBuilder:
|
||||
def __init__(self, waypoint: FlightWaypoint, group: FlyingGroup,
|
||||
flight: Flight, timing: PackageWaypointTiming,
|
||||
package: Package, flight: Flight,
|
||||
mission: Mission) -> None:
|
||||
self.waypoint = waypoint
|
||||
self.group = group
|
||||
self.package = package
|
||||
self.flight = flight
|
||||
self.timing = timing
|
||||
self.mission = mission
|
||||
|
||||
def build(self) -> MovingPoint:
|
||||
@@ -1099,35 +1262,54 @@ class PydcsWaypointBuilder:
|
||||
|
||||
waypoint.alt_type = self.waypoint.alt_type
|
||||
waypoint.name = String(self.waypoint.name)
|
||||
tot = self.flight.flight_plan.tot_for_waypoint(self.waypoint)
|
||||
if tot is not None:
|
||||
self.set_waypoint_tot(waypoint, tot)
|
||||
return waypoint
|
||||
|
||||
def set_waypoint_tot(self, waypoint: MovingPoint, tot: int) -> None:
|
||||
def set_waypoint_tot(self, waypoint: MovingPoint, tot: timedelta) -> None:
|
||||
self.waypoint.tot = tot
|
||||
waypoint.ETA = tot
|
||||
waypoint.ETA_locked = True
|
||||
waypoint.speed_locked = False
|
||||
if not self._viggen_client_tot():
|
||||
waypoint.ETA = int(tot.total_seconds())
|
||||
waypoint.ETA_locked = True
|
||||
waypoint.speed_locked = False
|
||||
|
||||
@classmethod
|
||||
def for_waypoint(cls, waypoint: FlightWaypoint, group: FlyingGroup,
|
||||
flight: Flight, timing: PackageWaypointTiming,
|
||||
package: Package, flight: Flight,
|
||||
mission: Mission) -> PydcsWaypointBuilder:
|
||||
builders = {
|
||||
FlightWaypointType.EGRESS: EgressPointBuilder,
|
||||
FlightWaypointType.INGRESS_CAS: CasIngressBuilder,
|
||||
FlightWaypointType.INGRESS_ESCORT: IngressBuilder,
|
||||
FlightWaypointType.INGRESS_DEAD: DeadIngressBuilder,
|
||||
FlightWaypointType.INGRESS_SEAD: SeadIngressBuilder,
|
||||
FlightWaypointType.INGRESS_STRIKE: StrikeIngressBuilder,
|
||||
FlightWaypointType.JOIN: JoinPointBuilder,
|
||||
FlightWaypointType.LANDING_POINT: LandingPointBuilder,
|
||||
FlightWaypointType.LOITER: HoldPointBuilder,
|
||||
FlightWaypointType.PATROL_TRACK: RaceTrackBuilder,
|
||||
FlightWaypointType.SPLIT: SplitPointBuilder,
|
||||
FlightWaypointType.TARGET_GROUP_LOC: TargetPointBuilder,
|
||||
FlightWaypointType.TARGET_POINT: TargetPointBuilder,
|
||||
FlightWaypointType.TARGET_SHIP: TargetPointBuilder,
|
||||
}
|
||||
builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder)
|
||||
return builder(waypoint, group, flight, timing, mission)
|
||||
return builder(waypoint, group, package, flight, mission)
|
||||
|
||||
def _viggen_client_tot(self) -> bool:
|
||||
"""Viggen player aircraft consider any waypoint with a TOT set to be a target ("M") waypoint.
|
||||
If the flight is a player controlled Viggen flight, no TOT should be set on any waypoint except actual target waypoints.
|
||||
"""
|
||||
if (
|
||||
(self.flight.client_count > 0 and self.flight.unit_type == AJS37) and
|
||||
(self.waypoint.waypoint_type not in TARGET_WAYPOINTS)
|
||||
):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def register_special_waypoints(self, targets) -> None:
|
||||
"""Create special target waypoints for various aircraft"""
|
||||
for i, t in enumerate(targets):
|
||||
if self.group.units[0].unit_type == JF_17 and i < 4:
|
||||
self.group.add_nav_target_point(t.position, "PP" + str(i + 1))
|
||||
if self.group.units[0].unit_type == F_14B and i == 0:
|
||||
self.group.add_nav_target_point(t.position, "ST")
|
||||
|
||||
|
||||
class DefaultWaypointBuilder(PydcsWaypointBuilder):
|
||||
@@ -1141,32 +1323,35 @@ class HoldPointBuilder(PydcsWaypointBuilder):
|
||||
altitude=waypoint.alt,
|
||||
pattern=OrbitAction.OrbitPattern.Circle
|
||||
))
|
||||
push_time = self.timing.push_time(self.flight, self.waypoint)
|
||||
if not isinstance(self.flight.flight_plan, FormationFlightPlan):
|
||||
flight_plan_type = self.flight.flight_plan.__class__.__name__
|
||||
logging.error(
|
||||
f"Cannot configure hold for for {self.flight} because "
|
||||
f"{flight_plan_type} does not define a push time. AI will push "
|
||||
"immediately and may flight unsuitable speeds."
|
||||
)
|
||||
return waypoint
|
||||
push_time = self.flight.flight_plan.push_time
|
||||
self.waypoint.departure_time = push_time
|
||||
loiter.stop_after_time(push_time)
|
||||
loiter.stop_after_time(int(push_time.total_seconds()))
|
||||
waypoint.add_task(loiter)
|
||||
return waypoint
|
||||
|
||||
|
||||
class EgressPointBuilder(PydcsWaypointBuilder):
|
||||
class CasIngressBuilder(PydcsWaypointBuilder):
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
self.set_waypoint_tot(waypoint, self.timing.egress)
|
||||
return waypoint
|
||||
|
||||
|
||||
class IngressBuilder(PydcsWaypointBuilder):
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
self.set_waypoint_tot(waypoint, self.timing.ingress)
|
||||
return waypoint
|
||||
|
||||
|
||||
class CasIngressBuilder(IngressBuilder):
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
cas_waypoint = self.flight.waypoint_with_type((FlightWaypointType.CAS,))
|
||||
if cas_waypoint is None:
|
||||
if isinstance(self.flight.flight_plan, CasFlightPlan):
|
||||
waypoint.add_task(EngageTargetsInZone(
|
||||
position=self.flight.flight_plan.target,
|
||||
radius=FRONTLINE_LENGTH / 2,
|
||||
targets=[
|
||||
Targets.All.GroundUnits.GroundVehicles,
|
||||
Targets.All.GroundUnits.AirDefence.AAA,
|
||||
Targets.All.GroundUnits.Infantry,
|
||||
])
|
||||
)
|
||||
else:
|
||||
logging.error(
|
||||
"No CAS waypoint found. Falling back to search and engage")
|
||||
waypoint.add_task(EngageTargets(
|
||||
@@ -1177,28 +1362,17 @@ class CasIngressBuilder(IngressBuilder):
|
||||
Targets.All.GroundUnits.Infantry,
|
||||
])
|
||||
)
|
||||
else:
|
||||
waypoint.add_task(EngageTargetsInZone(
|
||||
position=cas_waypoint.position,
|
||||
radius=FRONTLINE_LENGTH / 2,
|
||||
targets=[
|
||||
Targets.All.GroundUnits.GroundVehicles,
|
||||
Targets.All.GroundUnits.AirDefence.AAA,
|
||||
Targets.All.GroundUnits.Infantry,
|
||||
])
|
||||
)
|
||||
waypoint.add_task(OptROE(OptROE.Values.OpenFireWeaponFree))
|
||||
return waypoint
|
||||
|
||||
|
||||
class SeadIngressBuilder(IngressBuilder):
|
||||
class DeadIngressBuilder(PydcsWaypointBuilder):
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
|
||||
target_group = self.waypoint.targetGroup
|
||||
target_group = self.package.target
|
||||
if isinstance(target_group, TheaterGroundObject):
|
||||
tgroup = self.mission.find_group(target_group.group_identifier)
|
||||
if tgroup is not None:
|
||||
tgroup = self.mission.find_group(target_group.group_name, search="match") # Match search is used due to TheaterGroundObject.name not matching
|
||||
if tgroup is not None: # the Mission group name because of SkyNet prefixes.
|
||||
task = AttackGroup(tgroup.id)
|
||||
task.params["expend"] = "All"
|
||||
task.params["attackQtyLimit"] = False
|
||||
@@ -1207,20 +1381,36 @@ class SeadIngressBuilder(IngressBuilder):
|
||||
task.params["weaponType"] = 268402702 # Guided Weapons
|
||||
task.params["groupAttack"] = True
|
||||
waypoint.tasks.append(task)
|
||||
|
||||
for i, t in enumerate(self.waypoint.targets):
|
||||
if self.group.units[0].unit_type == JF_17 and i < 4:
|
||||
self.group.add_nav_target_point(t.position, "PP" + str(i + 1))
|
||||
if self.group.units[0].unit_type == F_14B and i == 0:
|
||||
self.group.add_nav_target_point(t.position, "ST")
|
||||
if self.group.units[0].unit_type == AJS37 and i < 9:
|
||||
self.group.add_nav_target_point(t.position, "M" + str(i + 1))
|
||||
else:
|
||||
logging.error(f"Could not find group for DEAD mission {target_group.group_name}")
|
||||
self.register_special_waypoints(self.waypoint.targets)
|
||||
return waypoint
|
||||
|
||||
|
||||
class StrikeIngressBuilder(IngressBuilder):
|
||||
class SeadIngressBuilder(PydcsWaypointBuilder):
|
||||
def build(self) -> MovingPoint:
|
||||
if self.group.units[0].unit_type == B_17G:
|
||||
waypoint = super().build()
|
||||
|
||||
target_group = self.package.target
|
||||
if isinstance(target_group, TheaterGroundObject):
|
||||
tgroup = self.mission.find_group(target_group.group_name, search="match") # Match search is used due to TheaterGroundObject.name not matching
|
||||
if tgroup is not None: # the Mission group name because of SkyNet prefixes.
|
||||
waypoint.add_task(EngageTargetsInZone(
|
||||
position=tgroup.position,
|
||||
radius=nm_to_meter(30),
|
||||
targets=[
|
||||
Targets.All.GroundUnits.AirDefence,
|
||||
])
|
||||
)
|
||||
else:
|
||||
logging.error(f"Could not find group for DEAD mission {target_group.group_name}")
|
||||
self.register_special_waypoints(self.waypoint.targets)
|
||||
return waypoint
|
||||
|
||||
|
||||
class StrikeIngressBuilder(PydcsWaypointBuilder):
|
||||
def build(self) -> MovingPoint:
|
||||
if self.group.units[0].unit_type in [B_17G, B_52H, Tu_22M3]:
|
||||
return self.build_bombing()
|
||||
else:
|
||||
return self.build_strike()
|
||||
@@ -1243,29 +1433,43 @@ class StrikeIngressBuilder(IngressBuilder):
|
||||
bombing.params["attackQtyLimit"] = False
|
||||
bombing.params["directionEnabled"] = False
|
||||
bombing.params["altitudeEnabled"] = False
|
||||
bombing.params["weaponType"] = 2032
|
||||
bombing.params["weaponType"] = WeaponType.Bombs.value
|
||||
bombing.params["groupAttack"] = True
|
||||
waypoint.tasks.append(bombing)
|
||||
return waypoint
|
||||
|
||||
def build_strike(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
for target in self.waypoint.targets:
|
||||
|
||||
for i, t in enumerate(self.waypoint.targets):
|
||||
waypoint.tasks.append(Bombing(t.position))
|
||||
if self.group.units[0].unit_type == JF_17 and i < 4:
|
||||
self.group.add_nav_target_point(t.position, "PP" + str(i + 1))
|
||||
if self.group.units[0].unit_type == F_14B and i == 0:
|
||||
self.group.add_nav_target_point(t.position, "ST")
|
||||
if self.group.units[0].unit_type == AJS37 and i < 9:
|
||||
self.group.add_nav_target_point(t.position, "M" + str(i + 1))
|
||||
targets = [target]
|
||||
# If the target type is a group of units,
|
||||
# then target each unit in the group with a Bombing task on their position
|
||||
# (It is not perfect, we should have an engage Group task instead,
|
||||
# but we don't have the group ref in the model there)
|
||||
# TODO : for building group, engage all the buildings as well
|
||||
if isinstance(target, TheaterGroundObject):
|
||||
if len(target.units) > 0:
|
||||
targets = target.units
|
||||
|
||||
for t in targets:
|
||||
bombing = Bombing(t.position)
|
||||
# If there is only one target, drop all ordnance in one pass
|
||||
if len(self.waypoint.targets) == 1 and len(targets) == 1:
|
||||
bombing.params["expend"] = "All"
|
||||
bombing.params["weaponType"] = WeaponType.Auto.value
|
||||
bombing.params["groupAttack"] = True
|
||||
waypoint.tasks.append(bombing)
|
||||
print(bombing)
|
||||
|
||||
# Register special waypoints
|
||||
self.register_special_waypoints(targets)
|
||||
return waypoint
|
||||
|
||||
|
||||
class JoinPointBuilder(PydcsWaypointBuilder):
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
self.set_waypoint_tot(waypoint, self.timing.join)
|
||||
if self.flight.flight_type == FlightType.ESCORT:
|
||||
self.configure_escort_tasks(waypoint)
|
||||
return waypoint
|
||||
@@ -1321,27 +1525,20 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
|
||||
if not isinstance(self.flight.flight_plan, PatrollingFlightPlan):
|
||||
flight_plan_type = self.flight.flight_plan.__class__.__name__
|
||||
logging.error(
|
||||
f"Cannot create race track for {self.flight} because "
|
||||
f"{flight_plan_type} does not define a patrol.")
|
||||
return waypoint
|
||||
|
||||
racetrack = ControlledTask(OrbitAction(
|
||||
altitude=waypoint.alt,
|
||||
pattern=OrbitAction.OrbitPattern.RaceTrack
|
||||
))
|
||||
|
||||
self.set_waypoint_tot(waypoint,
|
||||
self.timing.race_track_start(self.flight))
|
||||
racetrack.stop_after_time(self.timing.race_track_end(self.flight))
|
||||
self.set_waypoint_tot(
|
||||
waypoint, self.flight.flight_plan.patrol_start_time)
|
||||
racetrack.stop_after_time(
|
||||
int(self.flight.flight_plan.patrol_end_time.total_seconds()))
|
||||
waypoint.add_task(racetrack)
|
||||
return waypoint
|
||||
|
||||
|
||||
class SplitPointBuilder(PydcsWaypointBuilder):
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
self.set_waypoint_tot(waypoint, self.timing.split)
|
||||
return waypoint
|
||||
|
||||
|
||||
class TargetPointBuilder(PydcsWaypointBuilder):
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
self.set_waypoint_tot(waypoint, self.timing.target)
|
||||
return waypoint
|
||||
|
||||
@@ -34,7 +34,7 @@ from gen.ground_forces.ai_ground_planner import (
|
||||
from .callsigns import callsign_for_support_unit
|
||||
from .conflictgen import Conflict
|
||||
from .ground_forces.combat_stance import CombatStance
|
||||
from plugin import LuaPluginManager
|
||||
from game.plugins import LuaPluginManager
|
||||
|
||||
SPREAD_DISTANCE_FACTOR = 0.1, 0.3
|
||||
SPREAD_DISTANCE_SIZE_FACTOR = 0.1
|
||||
@@ -140,9 +140,7 @@ class GroundConflictGenerator:
|
||||
self.plan_action_for_groups(self.enemy_stance, enemy_groups, player_groups, self.conflict.heading - 90, self.conflict.to_cp, self.conflict.from_cp)
|
||||
|
||||
# Add JTAC
|
||||
jtacPlugin = LuaPluginManager().getPlugin("jtacautolase")
|
||||
useJTAC = jtacPlugin and jtacPlugin.isEnabled()
|
||||
if self.game.player_faction.has_jtac and useJTAC:
|
||||
if self.game.player_faction.has_jtac:
|
||||
n = "JTAC" + str(self.conflict.from_cp.id) + str(self.conflict.to_cp.id)
|
||||
code = 1688 - len(self.jtacs)
|
||||
|
||||
|
||||
65
gen/ato.py
65
gen/ato.py
@@ -11,12 +11,14 @@ the single CAP flight.
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import timedelta
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from dcs.mapping import Point
|
||||
|
||||
from theater.missiontarget import MissionTarget
|
||||
from .flights.flight import Flight, FlightType
|
||||
from .flights.flightplan import FormationFlightPlan
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -51,11 +53,70 @@ class Package:
|
||||
|
||||
delay: int = field(default=0)
|
||||
|
||||
#: Desired TOT measured in seconds from mission start.
|
||||
time_over_target: int = field(default=0)
|
||||
#: Desired TOT as an offset from mission start.
|
||||
time_over_target: timedelta = field(default=timedelta())
|
||||
|
||||
waypoints: Optional[PackageWaypoints] = field(default=None)
|
||||
|
||||
@property
|
||||
def formation_speed(self) -> Optional[int]:
|
||||
"""The speed of the package when in formation.
|
||||
|
||||
If none of the flights in the package will join a formation, this
|
||||
returns None. This is nto uncommon, since only strike-like (strike,
|
||||
DEAD, anti-ship, BAI, etc.) flights and their escorts fly in formation.
|
||||
Others (CAP and CAS, currently) will coordinate in target timing but
|
||||
fly their own path to the target.
|
||||
"""
|
||||
speeds = []
|
||||
for flight in self.flights:
|
||||
if isinstance(flight.flight_plan, FormationFlightPlan):
|
||||
speeds.append(flight.flight_plan.best_flight_formation_speed)
|
||||
if not speeds:
|
||||
return None
|
||||
return min(speeds)
|
||||
|
||||
# TODO: Should depend on the type of escort.
|
||||
# SEAD might be able to leave before CAP.
|
||||
@property
|
||||
def escort_start_time(self) -> Optional[timedelta]:
|
||||
times = []
|
||||
for flight in self.flights:
|
||||
waypoint = flight.flight_plan.request_escort_at()
|
||||
if waypoint is None:
|
||||
continue
|
||||
tot = flight.flight_plan.tot_for_waypoint(waypoint)
|
||||
if tot is None:
|
||||
logging.error(
|
||||
f"{flight} requested escort at {waypoint} but that "
|
||||
"waypoint has no TOT. It may not be escorted.")
|
||||
continue
|
||||
times.append(tot)
|
||||
if times:
|
||||
return min(times)
|
||||
return None
|
||||
|
||||
@property
|
||||
def escort_end_time(self) -> Optional[timedelta]:
|
||||
times = []
|
||||
for flight in self.flights:
|
||||
waypoint = flight.flight_plan.dismiss_escort_at()
|
||||
if waypoint is None:
|
||||
continue
|
||||
tot = flight.flight_plan.tot_for_waypoint(waypoint)
|
||||
if tot is None:
|
||||
tot = flight.flight_plan.depart_time_for_waypoint(waypoint)
|
||||
if tot is None:
|
||||
logging.error(
|
||||
f"{flight} dismissed escort at {waypoint} but that "
|
||||
"waypoint has no TOT or departure time. It may not be "
|
||||
"escorted.")
|
||||
continue
|
||||
times.append(tot)
|
||||
if times:
|
||||
return max(times)
|
||||
return None
|
||||
|
||||
def add_flight(self, flight: Flight) -> None:
|
||||
"""Adds a flight to the package."""
|
||||
self.flights.append(flight)
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
import datetime
|
||||
"""
|
||||
Briefing generation logic
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import random
|
||||
from collections import defaultdict
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
from theater.frontline import FrontLine
|
||||
from typing import List, Dict, TYPE_CHECKING
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
|
||||
from dcs.mission import Mission
|
||||
|
||||
from game import db
|
||||
from .aircraft import FlightData
|
||||
from .airsupportgen import AwacsInfo, TankerInfo
|
||||
from .armor import JtacInfo
|
||||
from .conflictgen import Conflict
|
||||
from theater import ControlPoint
|
||||
from .ground_forces.combat_stance import CombatStance
|
||||
from .radios import RadioFrequency
|
||||
from .runways import RunwayData
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
@dataclass
|
||||
class CommInfo:
|
||||
@@ -24,19 +29,33 @@ class CommInfo:
|
||||
freq: RadioFrequency
|
||||
|
||||
|
||||
class FrontLineInfo:
|
||||
def __init__(self, front_line: FrontLine):
|
||||
self.front_line: FrontLine = front_line
|
||||
self.player_base: ControlPoint = front_line.control_point_a
|
||||
self.enemy_base: ControlPoint = front_line.control_point_b
|
||||
self.player_zero: bool = self.player_base.base.total_armor == 0
|
||||
self.enemy_zero: bool = self.enemy_base.base.total_armor == 0
|
||||
self.advantage: bool = self.player_base.base.total_armor > self.enemy_base.base.total_armor
|
||||
self.stance: CombatStance = self.player_base.stances[self.enemy_base.id]
|
||||
self.combat_stances = CombatStance
|
||||
|
||||
class MissionInfoGenerator:
|
||||
"""Base type for generators of mission information for the player.
|
||||
|
||||
Examples of subtypes include briefing generators, kneeboard generators, etc.
|
||||
"""
|
||||
|
||||
def __init__(self, mission: Mission) -> None:
|
||||
def __init__(self, mission: Mission, game: Game) -> None:
|
||||
self.mission = mission
|
||||
self.game = game
|
||||
self.awacs: List[AwacsInfo] = []
|
||||
self.comms: List[CommInfo] = []
|
||||
self.flights: List[FlightData] = []
|
||||
self.jtacs: List[JtacInfo] = []
|
||||
self.tankers: List[TankerInfo] = []
|
||||
self.frontlines: List[FrontLineInfo] = []
|
||||
self.dynamic_runways: List[RunwayData] = []
|
||||
|
||||
def add_awacs(self, awacs: AwacsInfo) -> None:
|
||||
"""Adds an AWACS/GCI to the mission.
|
||||
@@ -79,20 +98,13 @@ class MissionInfoGenerator:
|
||||
"""
|
||||
self.tankers.append(tanker)
|
||||
|
||||
def generate(self) -> None:
|
||||
"""Generates the mission information."""
|
||||
raise NotImplementedError
|
||||
def add_frontline(self, frontline: FrontLineInfo) -> None:
|
||||
"""Adds a frontline to the briefing
|
||||
|
||||
|
||||
class BriefingGenerator(MissionInfoGenerator):
|
||||
|
||||
def __init__(self, mission: Mission, conflict: Conflict, game):
|
||||
super().__init__(mission)
|
||||
self.conflict = conflict
|
||||
self.game = game
|
||||
self.title = ""
|
||||
self.description = ""
|
||||
self.dynamic_runways: List[RunwayData] = []
|
||||
Arguments:
|
||||
frontline: Frontline conflict information
|
||||
"""
|
||||
self.frontlines.append(frontline)
|
||||
|
||||
def add_dynamic_runway(self, runway: RunwayData) -> None:
|
||||
"""Adds a dynamically generated runway to the briefing.
|
||||
@@ -102,150 +114,51 @@ class BriefingGenerator(MissionInfoGenerator):
|
||||
"""
|
||||
self.dynamic_runways.append(runway)
|
||||
|
||||
def add_flight_description(self, flight: FlightData):
|
||||
assert flight.client_units
|
||||
|
||||
aircraft = flight.aircraft_type
|
||||
flight_unit_name = db.unit_type_name(aircraft)
|
||||
self.description += "-" * 50 + "\n"
|
||||
self.description += f"{flight_unit_name} x {flight.size}\n\n"
|
||||
|
||||
for i, wpt in enumerate(flight.waypoints):
|
||||
self.description += f"#{i + 1} -- {wpt.name} : {wpt.description}\n"
|
||||
self.description += f"#{len(flight.waypoints) + 1} -- RTB\n\n"
|
||||
|
||||
def add_ally_flight_description(self, flight: FlightData):
|
||||
assert not flight.client_units
|
||||
aircraft = flight.aircraft_type
|
||||
flight_unit_name = db.unit_type_name(aircraft)
|
||||
delay = datetime.timedelta(seconds=flight.departure_delay)
|
||||
self.description += (
|
||||
f"{flight.flight_type.name} {flight_unit_name} x {flight.size}, "
|
||||
f"departing in {delay}\n"
|
||||
)
|
||||
|
||||
def generate(self):
|
||||
self.description = ""
|
||||
|
||||
self.description += "DCS Liberation turn #" + str(self.game.turn) + "\n"
|
||||
self.description += "=" * 15 + "\n\n"
|
||||
|
||||
self.description += (
|
||||
"Most briefing information, including communications and flight "
|
||||
"plan information, can be found on your kneeboard.\n\n"
|
||||
)
|
||||
|
||||
self.generate_ongoing_war_text()
|
||||
|
||||
self.description += "\n"*2
|
||||
self.description += "Your flights:" + "\n"
|
||||
self.description += "=" * 15 + "\n\n"
|
||||
|
||||
for flight in self.flights:
|
||||
if flight.client_units:
|
||||
self.add_flight_description(flight)
|
||||
|
||||
self.description += "\n"*2
|
||||
self.description += "Planned ally flights:" + "\n"
|
||||
self.description += "=" * 15 + "\n"
|
||||
allied_flights_by_departure = defaultdict(list)
|
||||
for flight in self.flights:
|
||||
if not flight.client_units and flight.friendly:
|
||||
name = flight.departure.airfield_name
|
||||
allied_flights_by_departure[name].append(flight)
|
||||
for departure, flights in allied_flights_by_departure.items():
|
||||
self.description += f"\nFrom {departure}\n"
|
||||
self.description += "-" * 50 + "\n\n"
|
||||
for flight in flights:
|
||||
self.add_ally_flight_description(flight)
|
||||
|
||||
if self.comms:
|
||||
self.description += "\n\nComms Frequencies:\n"
|
||||
self.description += "=" * 15 + "\n"
|
||||
for comm_info in self.comms:
|
||||
self.description += f"{comm_info.name}: {comm_info.freq}\n"
|
||||
self.description += ("-" * 50) + "\n"
|
||||
|
||||
for runway in self.dynamic_runways:
|
||||
self.description += f"{runway.airfield_name}\n"
|
||||
self.description += f"RADIO : {runway.atc}\n"
|
||||
if runway.tacan is not None:
|
||||
self.description += f"TACAN : {runway.tacan} {runway.tacan_callsign}\n"
|
||||
if runway.icls is not None:
|
||||
self.description += f"ICLS Channel : {runway.icls}\n"
|
||||
self.description += "-" * 50 + "\n"
|
||||
def generate(self) -> None:
|
||||
"""Generates the mission information."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
self.description += "JTACS [F-10 Menu] : \n"
|
||||
self.description += "===================\n\n"
|
||||
for jtac in self.jtacs:
|
||||
self.description += f"{jtac.region} -- Code : {jtac.code}\n"
|
||||
class BriefingGenerator(MissionInfoGenerator):
|
||||
|
||||
self.mission.set_description_text(self.description)
|
||||
def __init__(self, mission: Mission, game: Game):
|
||||
super().__init__(mission, game)
|
||||
self.allied_flights_by_departure: Dict[str, List[FlightData]] = {}
|
||||
env = Environment(
|
||||
loader=FileSystemLoader("resources/briefing/templates"),
|
||||
autoescape=select_autoescape(
|
||||
disabled_extensions=("",),
|
||||
default_for_string=True,
|
||||
default=True,
|
||||
),
|
||||
trim_blocks=True,
|
||||
lstrip_blocks=True,
|
||||
)
|
||||
self.template = env.get_template("briefingtemplate_EN.j2")
|
||||
|
||||
def generate(self) -> None:
|
||||
"""Generate the mission briefing
|
||||
"""
|
||||
self._generate_frontline_info()
|
||||
self.generate_allied_flights_by_departure()
|
||||
self.mission.set_description_text(self.template.render(vars(self)))
|
||||
self.mission.add_picture_blue(os.path.abspath(
|
||||
"./resources/ui/splash_screen.png"))
|
||||
|
||||
|
||||
def generate_ongoing_war_text(self):
|
||||
|
||||
self.description += "Current situation:\n"
|
||||
self.description += "=" * 15 + "\n\n"
|
||||
|
||||
conflict_number = 0
|
||||
|
||||
def _generate_frontline_info(self) -> None:
|
||||
"""Build FrontLineInfo objects from FrontLine type and append to briefing.
|
||||
"""
|
||||
for front_line in self.game.theater.conflicts(from_player=True):
|
||||
conflict_number = conflict_number + 1
|
||||
player_base = front_line.control_point_a
|
||||
enemy_base = front_line.control_point_b
|
||||
|
||||
has_numerical_superiority = player_base.base.total_armor > enemy_base.base.total_armor
|
||||
self.description += self.__random_frontline_sentence(player_base.name, enemy_base.name)
|
||||
|
||||
if enemy_base.id in player_base.stances.keys():
|
||||
stance = player_base.stances[enemy_base.id]
|
||||
|
||||
if player_base.base.total_armor == 0:
|
||||
self.description += "We do not have a single vehicle available to hold our position, the situation is critical, and we will lose ground inevitably.\n"
|
||||
elif enemy_base.base.total_armor == 0:
|
||||
self.description += "The enemy forces have been crushed, we will be able to make significant progress toward " + enemy_base.name + ". \n"
|
||||
if stance == CombatStance.AGGRESSIVE:
|
||||
if has_numerical_superiority:
|
||||
self.description += "On this location, our ground forces will try to make progress against the enemy"
|
||||
self.description += ". As the enemy is outnumbered, our forces should have no issue making progress.\n"
|
||||
elif has_numerical_superiority:
|
||||
self.description += "On this location, our ground forces will try an audacious assault against enemies in superior numbers. The operation is risky, and the enemy might counter attack.\n"
|
||||
elif stance == CombatStance.ELIMINATION:
|
||||
if has_numerical_superiority:
|
||||
self.description += "On this location, our ground forces will focus on the destruction of enemy assets, before attempting to make progress toward " + enemy_base.name + ". "
|
||||
self.description += "The enemy is already outnumbered, and this maneuver might draw a final blow to their forces.\n"
|
||||
elif has_numerical_superiority:
|
||||
self.description += "On this location, our ground forces will try an audacious assault against enemies in superior numbers. The operation is risky, and the enemy might counter attack.\n"
|
||||
elif stance == CombatStance.BREAKTHROUGH:
|
||||
if has_numerical_superiority:
|
||||
self.description += "On this location, our ground forces will focus on progression toward " + enemy_base.name + ".\n"
|
||||
elif has_numerical_superiority:
|
||||
self.description += "On this location, our ground forces have been ordered to rush toward " + enemy_base.name + ". Wish them luck... We are also expecting a counter attack.\n"
|
||||
elif stance in [CombatStance.DEFENSIVE, CombatStance.AMBUSH]:
|
||||
if has_numerical_superiority:
|
||||
self.description += "On this location, our ground forces will hold position. We are not expecting an enemy assault.\n"
|
||||
elif has_numerical_superiority:
|
||||
self.description += "On this location, our ground forces have been ordered to hold still, and defend against enemy attacks. An enemy assault might be iminent.\n"
|
||||
|
||||
if conflict_number == 0:
|
||||
self.description += "There are currently no fights on the ground.\n"
|
||||
|
||||
self.description += "\n\n"
|
||||
|
||||
|
||||
def __random_frontline_sentence(self, player_base_name, enemy_base_name):
|
||||
templates = [
|
||||
"There are combats between {} and {}. ",
|
||||
"The war on the ground is still going on between {} and {}. ",
|
||||
"Our ground forces in {} are opposed to enemy forces based in {}. ",
|
||||
"Our forces from {} are fighting enemies based in {}. ",
|
||||
"There is an active frontline between {} and {}. ",
|
||||
]
|
||||
return random.choice(templates).format(player_base_name, enemy_base_name)
|
||||
|
||||
self.add_frontline(FrontLineInfo(front_line))
|
||||
|
||||
# TODO: This should determine if runway is friendly through a method more robust than the existing string match
|
||||
def generate_allied_flights_by_departure(self) -> None:
|
||||
"""Create iterable to display allied flights grouped by departure airfield.
|
||||
"""
|
||||
for flight in self.flights:
|
||||
if not flight.client_units and flight.friendly:
|
||||
name = flight.departure.airfield_name
|
||||
if name in self.allied_flights_by_departure: # where else can we get this?
|
||||
self.allied_flights_by_departure[name].append(flight)
|
||||
else:
|
||||
self.allied_flights_by_departure[name] = [flight]
|
||||
|
||||
@@ -144,6 +144,16 @@ class Conflict:
|
||||
position = middle_point.point_from_heading(attack_heading, strength_delta * attack_distance / 2 - FRONTLINE_MIN_CP_DISTANCE)
|
||||
return position, _opposite_heading(attack_heading)
|
||||
|
||||
@classmethod
|
||||
def flight_frontline_vector(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int, int]:
|
||||
"""Returns the frontline vector without regard for exclusion zones, used in CAS flight plan"""
|
||||
frontline = cls.frontline_position(theater, from_cp, to_cp)
|
||||
center_position, heading = frontline
|
||||
left_position = center_position.point_from_heading(_heading_sum(heading, -90), int(FRONTLINE_LENGTH/2))
|
||||
right_position = center_position.point_from_heading(_heading_sum(heading, 90), int(FRONTLINE_LENGTH/2))
|
||||
|
||||
return left_position, _heading_sum(heading, 90), int(right_position.distance_to_point(left_position))
|
||||
|
||||
|
||||
@classmethod
|
||||
def frontline_vector(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int, int]:
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import random
|
||||
|
||||
from gen.sam.group_generator import GroupGenerator
|
||||
from gen.sam.group_generator import ShipGroupGenerator
|
||||
|
||||
|
||||
class CarrierGroupGenerator(GroupGenerator):
|
||||
|
||||
def __init__(self, game, ground_object, faction):
|
||||
super(CarrierGroupGenerator, self).__init__(game, ground_object)
|
||||
self.faction = faction
|
||||
class CarrierGroupGenerator(ShipGroupGenerator):
|
||||
|
||||
def generate(self):
|
||||
|
||||
@@ -27,4 +23,4 @@ class CarrierGroupGenerator(GroupGenerator):
|
||||
self.add_unit(dd_type, "DD3", self.position.x + 4500, self.position.y + 8500, self.heading)
|
||||
self.add_unit(dd_type, "DD4", self.position.x + 4500, self.position.y - 8500, self.heading)
|
||||
|
||||
self.get_generated_group().points[0].speed = 20
|
||||
self.get_generated_group().points[0].speed = 20
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
from dcs.ships import (
|
||||
Type_052C_Destroyer,
|
||||
Type_052B_Destroyer,
|
||||
Type_054A_Frigate,
|
||||
CGN_1144_2_Pyotr_Velikiy,
|
||||
)
|
||||
|
||||
from game.factions.faction import Faction
|
||||
from gen.fleet.dd_group import DDGroupGenerator
|
||||
from gen.sam.group_generator import GroupGenerator
|
||||
from dcs.ships import *
|
||||
from gen.sam.group_generator import ShipGroupGenerator
|
||||
from theater.theatergroundobject import TheaterGroundObject
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.game import Game
|
||||
|
||||
|
||||
class ChineseNavyGroupGenerator(GroupGenerator):
|
||||
|
||||
def __init__(self, game, ground_object, faction):
|
||||
super(ChineseNavyGroupGenerator, self).__init__(game, ground_object)
|
||||
self.faction = faction
|
||||
class ChineseNavyGroupGenerator(ShipGroupGenerator):
|
||||
|
||||
def generate(self):
|
||||
|
||||
@@ -38,5 +49,5 @@ class ChineseNavyGroupGenerator(GroupGenerator):
|
||||
|
||||
|
||||
class Type54GroupGenerator(DDGroupGenerator):
|
||||
def __init__(self, game, ground_object, faction):
|
||||
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
|
||||
super(Type54GroupGenerator, self).__init__(game, ground_object, faction, Type_054A_Frigate)
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import random
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from gen.sam.group_generator import GroupGenerator
|
||||
from dcs.ships import *
|
||||
from game.factions.faction import Faction
|
||||
from theater.theatergroundobject import TheaterGroundObject
|
||||
|
||||
from gen.sam.group_generator import ShipGroupGenerator
|
||||
from dcs.unittype import ShipType
|
||||
from dcs.ships import Oliver_Hazzard_Perry_class, USS_Arleigh_Burke_IIa
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.game import Game
|
||||
|
||||
|
||||
class DDGroupGenerator(GroupGenerator):
|
||||
class DDGroupGenerator(ShipGroupGenerator):
|
||||
|
||||
def __init__(self, game, ground_object, faction, ddtype):
|
||||
super(DDGroupGenerator, self).__init__(game, ground_object)
|
||||
self.faction = faction
|
||||
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction, ddtype: ShipType):
|
||||
super(DDGroupGenerator, self).__init__(game, ground_object, faction)
|
||||
self.ddtype = ddtype
|
||||
|
||||
def generate(self):
|
||||
@@ -18,10 +25,10 @@ class DDGroupGenerator(GroupGenerator):
|
||||
|
||||
|
||||
class OliverHazardPerryGroupGenerator(DDGroupGenerator):
|
||||
def __init__(self, game, ground_object, faction):
|
||||
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
|
||||
super(OliverHazardPerryGroupGenerator, self).__init__(game, ground_object, faction, Oliver_Hazzard_Perry_class)
|
||||
|
||||
|
||||
class ArleighBurkeGroupGenerator(DDGroupGenerator):
|
||||
def __init__(self, game, ground_object, faction):
|
||||
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
|
||||
super(ArleighBurkeGroupGenerator, self).__init__(game, ground_object, faction, USS_Arleigh_Burke_IIa)
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import random
|
||||
|
||||
from gen.sam.group_generator import GroupGenerator
|
||||
from gen.sam.group_generator import ShipGroupGenerator
|
||||
|
||||
|
||||
class LHAGroupGenerator(GroupGenerator):
|
||||
|
||||
def __init__(self, game, ground_object, faction):
|
||||
super(LHAGroupGenerator, self).__init__(game, ground_object)
|
||||
self.faction = faction
|
||||
class LHAGroupGenerator(ShipGroupGenerator):
|
||||
|
||||
def generate(self):
|
||||
|
||||
@@ -22,4 +18,4 @@ class LHAGroupGenerator(GroupGenerator):
|
||||
self.add_unit(dd_type, "DD1", self.position.x + 1250, self.position.y + 1450, self.heading)
|
||||
self.add_unit(dd_type, "DD2", self.position.x + 1250, self.position.y - 1450, self.heading)
|
||||
|
||||
self.get_generated_group().points[0].speed = 20
|
||||
self.get_generated_group().points[0].speed = 20
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
from __future__ import annotations
|
||||
import random
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dcs.ships import (
|
||||
FFL_1124_4_Grisha,
|
||||
FSG_1241_1MP_Molniya,
|
||||
FFG_11540_Neustrashimy,
|
||||
FF_1135M_Rezky,
|
||||
CG_1164_Moskva,
|
||||
CGN_1144_2_Pyotr_Velikiy,
|
||||
SSK_877,
|
||||
SSK_641B
|
||||
)
|
||||
|
||||
from gen.fleet.dd_group import DDGroupGenerator
|
||||
from gen.sam.group_generator import GroupGenerator
|
||||
from dcs.ships import *
|
||||
from gen.sam.group_generator import ShipGroupGenerator
|
||||
from game.factions.faction import Faction
|
||||
from theater.theatergroundobject import TheaterGroundObject
|
||||
|
||||
|
||||
class RussianNavyGroupGenerator(GroupGenerator):
|
||||
if TYPE_CHECKING:
|
||||
from game.game import Game
|
||||
|
||||
def __init__(self, game, ground_object, faction):
|
||||
super(RussianNavyGroupGenerator, self).__init__(game, ground_object)
|
||||
self.faction = faction
|
||||
|
||||
class RussianNavyGroupGenerator(ShipGroupGenerator):
|
||||
|
||||
def generate(self):
|
||||
|
||||
@@ -39,21 +53,20 @@ class RussianNavyGroupGenerator(GroupGenerator):
|
||||
|
||||
|
||||
class GrishaGroupGenerator(DDGroupGenerator):
|
||||
def __init__(self, game, ground_object, faction):
|
||||
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
|
||||
super(GrishaGroupGenerator, self).__init__(game, ground_object, faction, FFL_1124_4_Grisha)
|
||||
|
||||
|
||||
class MolniyaGroupGenerator(DDGroupGenerator):
|
||||
def __init__(self, game, ground_object, faction):
|
||||
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
|
||||
super(MolniyaGroupGenerator, self).__init__(game, ground_object, faction, FSG_1241_1MP_Molniya)
|
||||
|
||||
|
||||
class KiloSubGroupGenerator(DDGroupGenerator):
|
||||
def __init__(self, game, ground_object, faction):
|
||||
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
|
||||
super(KiloSubGroupGenerator, self).__init__(game, ground_object, faction, SSK_877)
|
||||
|
||||
|
||||
class TangoSubGroupGenerator(DDGroupGenerator):
|
||||
def __init__(self, game, ground_object, faction):
|
||||
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
|
||||
super(TangoSubGroupGenerator, self).__init__(game, ground_object, faction, SSK_641B)
|
||||
|
||||
|
||||
@@ -2,18 +2,14 @@ import random
|
||||
|
||||
from dcs.ships import Schnellboot_type_S130
|
||||
|
||||
from gen.sam.group_generator import GroupGenerator
|
||||
from gen.sam.group_generator import ShipGroupGenerator
|
||||
|
||||
|
||||
class SchnellbootGroupGenerator(GroupGenerator):
|
||||
|
||||
def __init__(self, game, ground_object, faction):
|
||||
super(SchnellbootGroupGenerator, self).__init__(game, ground_object)
|
||||
self.faction = faction
|
||||
class SchnellbootGroupGenerator(ShipGroupGenerator):
|
||||
|
||||
def generate(self):
|
||||
|
||||
for i in range(random.randint(2, 4)):
|
||||
self.add_unit(Schnellboot_type_S130, "Schnellboot" + str(i), self.position.x + i * random.randint(100, 250), self.position.y + (random.randint(100, 200)-100), self.heading)
|
||||
|
||||
self.get_generated_group().points[0].speed = 20
|
||||
self.get_generated_group().points[0].speed = 20
|
||||
|
||||
@@ -12,6 +12,7 @@ from gen.fleet.schnellboot import SchnellbootGroupGenerator
|
||||
from gen.fleet.uboat import UBoatGroupGenerator
|
||||
from gen.fleet.ww2lst import WW2LSTGroupGenerator
|
||||
|
||||
|
||||
SHIP_MAP = {
|
||||
"SchnellbootGroupGenerator": SchnellbootGroupGenerator,
|
||||
"WW2LSTGroupGenerator": WW2LSTGroupGenerator,
|
||||
@@ -45,7 +46,7 @@ def generate_ship_group(game, ground_object, faction_name: str):
|
||||
return None
|
||||
|
||||
|
||||
def generate_carrier_group(faction:str, game, ground_object):
|
||||
def generate_carrier_group(faction: str, game, ground_object):
|
||||
"""
|
||||
This generate a carrier group
|
||||
:param parentCp: The parent control point
|
||||
@@ -58,7 +59,7 @@ def generate_carrier_group(faction:str, game, ground_object):
|
||||
return generator.get_generated_group()
|
||||
|
||||
|
||||
def generate_lha_group(faction:str, game, ground_object):
|
||||
def generate_lha_group(faction: str, game, ground_object):
|
||||
"""
|
||||
This generate a lha carrier group
|
||||
:param parentCp: The parent control point
|
||||
@@ -68,4 +69,4 @@ def generate_lha_group(faction:str, game, ground_object):
|
||||
"""
|
||||
generator = LHAGroupGenerator(game, ground_object, db.FACTIONS[faction])
|
||||
generator.generate()
|
||||
return generator.get_generated_group()
|
||||
return generator.get_generated_group()
|
||||
|
||||
@@ -2,14 +2,10 @@ import random
|
||||
|
||||
from dcs.ships import Uboat_VIIC_U_flak
|
||||
|
||||
from gen.sam.group_generator import GroupGenerator
|
||||
from gen.sam.group_generator import ShipGroupGenerator
|
||||
|
||||
|
||||
class UBoatGroupGenerator(GroupGenerator):
|
||||
|
||||
def __init__(self, game, ground_object, faction):
|
||||
super(UBoatGroupGenerator, self).__init__(game, ground_object)
|
||||
self.faction = faction
|
||||
class UBoatGroupGenerator(ShipGroupGenerator):
|
||||
|
||||
def generate(self):
|
||||
|
||||
|
||||
@@ -2,14 +2,10 @@ import random
|
||||
|
||||
from dcs.ships import LS_Samuel_Chase, LST_Mk_II
|
||||
|
||||
from gen.sam.group_generator import GroupGenerator
|
||||
from gen.sam.group_generator import ShipGroupGenerator
|
||||
|
||||
|
||||
class WW2LSTGroupGenerator(GroupGenerator):
|
||||
|
||||
def __init__(self, game, ground_object, faction):
|
||||
super(WW2LSTGroupGenerator, self).__init__(game, ground_object)
|
||||
self.faction = faction
|
||||
class WW2LSTGroupGenerator(ShipGroupGenerator):
|
||||
|
||||
def generate(self):
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import random
|
||||
import operator
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple, Type
|
||||
|
||||
from dcs.unittype import FlyingType, UnitType
|
||||
@@ -12,7 +13,7 @@ from game import db
|
||||
from game.data.radar_db import UNITS_WITH_RADAR
|
||||
from game.infos.information import Information
|
||||
from game.utils import nm_to_meter
|
||||
from gen import Conflict, PackageWaypointTiming
|
||||
from gen import Conflict
|
||||
from gen.ato import Package
|
||||
from gen.flights.ai_flight_planner_db import (
|
||||
CAP_CAPABLE,
|
||||
@@ -39,6 +40,7 @@ from theater import (
|
||||
FrontLine,
|
||||
MissionTarget,
|
||||
TheaterGroundObject,
|
||||
SamGroundObject,
|
||||
)
|
||||
|
||||
# Avoid importing some types that cause circular imports unless type checking.
|
||||
@@ -241,10 +243,13 @@ class ObjectiveFinder:
|
||||
found_targets: Set[str] = set()
|
||||
for cp in self.enemy_control_points():
|
||||
for ground_object in cp.ground_objects:
|
||||
if ground_object.name in found_targets:
|
||||
if not isinstance(ground_object, SamGroundObject):
|
||||
continue
|
||||
|
||||
if ground_object.dcs_identifier != "AA":
|
||||
if ground_object.is_dead:
|
||||
continue
|
||||
|
||||
if ground_object.name in found_targets:
|
||||
continue
|
||||
|
||||
if not self.object_has_radar(ground_object):
|
||||
@@ -286,6 +291,8 @@ class ObjectiveFinder:
|
||||
found_targets: Set[str] = set()
|
||||
for enemy_cp in self.enemy_control_points():
|
||||
for ground_object in enemy_cp.ground_objects:
|
||||
if ground_object.is_dead:
|
||||
continue
|
||||
if ground_object.name in found_targets:
|
||||
continue
|
||||
ranges: List[int] = []
|
||||
@@ -483,11 +490,11 @@ class CoalitionMissionPlanner:
|
||||
|
||||
def stagger_missions(self) -> None:
|
||||
def start_time_generator(count: int, earliest: int, latest: int,
|
||||
margin: int) -> Iterator[int]:
|
||||
interval = latest // count
|
||||
margin: int) -> Iterator[timedelta]:
|
||||
interval = (latest - earliest) // count
|
||||
for time in range(earliest, latest, interval):
|
||||
error = random.randint(-margin, margin)
|
||||
yield max(0, time + error)
|
||||
yield timedelta(minutes=max(0, time + error))
|
||||
|
||||
dca_types = (FlightType.BARCAP, FlightType.INTERCEPTION)
|
||||
|
||||
@@ -512,7 +519,7 @@ class CoalitionMissionPlanner:
|
||||
# airfields to hit grounded aircraft, since they're more likely
|
||||
# to be present. Runway and air started aircraft will be
|
||||
# delayed until their takeoff time by AirConflictGenerator.
|
||||
package.time_over_target = next(start_time) * 60 + tot
|
||||
package.time_over_target = next(start_time) + tot
|
||||
|
||||
def message(self, title, text) -> None:
|
||||
"""Emits a planning message to the player.
|
||||
|
||||
@@ -27,6 +27,7 @@ from dcs.planes import (
|
||||
FW_190A8,
|
||||
FW_190D9,
|
||||
F_117A,
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
F_15C,
|
||||
F_15E,
|
||||
@@ -104,6 +105,7 @@ INTERCEPT_CAPABLE = [
|
||||
Mirage_2000_5,
|
||||
Rafale_M,
|
||||
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
F_15C,
|
||||
|
||||
@@ -135,6 +137,7 @@ CAP_CAPABLE = [
|
||||
F_86F_Sabre,
|
||||
F_4E,
|
||||
F_5E_3,
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
F_15C,
|
||||
F_15E,
|
||||
@@ -183,6 +186,7 @@ CAP_PREFERRED = [
|
||||
Mirage_2000_5,
|
||||
|
||||
F_86F_Sabre,
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
F_15C,
|
||||
|
||||
@@ -226,6 +230,7 @@ CAS_CAPABLE = [
|
||||
|
||||
F_86F_Sabre,
|
||||
F_5E_3,
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
F_15E,
|
||||
F_16A,
|
||||
@@ -390,6 +395,7 @@ STRIKE_CAPABLE = [
|
||||
|
||||
F_86F_Sabre,
|
||||
F_5E_3,
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
F_15E,
|
||||
F_16A,
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from enum import Enum
|
||||
from typing import Dict, Iterable, List, Optional, TYPE_CHECKING
|
||||
from typing import Dict, List, Optional, TYPE_CHECKING
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.point import MovingPoint, PointAction
|
||||
from dcs.unittype import UnitType
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game import db
|
||||
from theater.controlpoint import ControlPoint, MissionTarget
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gen.ato import Package
|
||||
from gen.flights.flightplan import FlightPlan
|
||||
|
||||
|
||||
class FlightType(Enum):
|
||||
@@ -58,17 +60,7 @@ class FlightWaypointType(Enum):
|
||||
SPLIT = 17
|
||||
LOITER = 18
|
||||
INGRESS_ESCORT = 19
|
||||
|
||||
|
||||
class PredefinedWaypointCategory(Enum):
|
||||
NOT_PREDEFINED = 0
|
||||
ALLY_CP = 1
|
||||
ENEMY_CP = 2
|
||||
FRONTLINE = 3
|
||||
ENEMY_BUILDING = 4
|
||||
ENEMY_UNIT = 5
|
||||
ALLY_BUILDING = 6
|
||||
ALLY_UNIT = 7
|
||||
INGRESS_DEAD = 20
|
||||
|
||||
|
||||
class FlightWaypoint:
|
||||
@@ -92,19 +84,16 @@ class FlightWaypoint:
|
||||
self.name = ""
|
||||
self.description = ""
|
||||
self.targets: List[MissionTarget] = []
|
||||
self.targetGroup: Optional[MissionTarget] = None
|
||||
self.obj_name = ""
|
||||
self.pretty_name = ""
|
||||
self.category: PredefinedWaypointCategory = PredefinedWaypointCategory.NOT_PREDEFINED
|
||||
self.only_for_player = False
|
||||
self.data = None
|
||||
|
||||
# These are set very late by the air conflict generator (part of mission
|
||||
# generation). We do it late so that we don't need to propagate changes
|
||||
# to waypoint times whenever the player alters the package TOT or the
|
||||
# flight's offset in the UI.
|
||||
self.tot: Optional[int] = None
|
||||
self.departure_time: Optional[int] = None
|
||||
self.tot: Optional[timedelta] = None
|
||||
self.departure_time: Optional[timedelta] = None
|
||||
|
||||
@property
|
||||
def position(self) -> Point:
|
||||
@@ -138,13 +127,8 @@ class FlightWaypoint:
|
||||
|
||||
|
||||
class Flight:
|
||||
count: int = 0
|
||||
client_count: int = 0
|
||||
use_custom_loadout = False
|
||||
preset_loadout_name = ""
|
||||
group = False # Contains DCS Mission group data after mission has been generated
|
||||
|
||||
def __init__(self, package: Package, unit_type: UnitType, count: int,
|
||||
def __init__(self, package: Package, unit_type: FlyingType, count: int,
|
||||
from_cp: ControlPoint, flight_type: FlightType,
|
||||
start_type: str) -> None:
|
||||
self.package = package
|
||||
@@ -152,24 +136,27 @@ class Flight:
|
||||
self.count = count
|
||||
self.from_cp = from_cp
|
||||
self.flight_type = flight_type
|
||||
self.points: List[FlightWaypoint] = []
|
||||
# TODO: Replace with FlightPlan.
|
||||
self.targets: List[MissionTarget] = []
|
||||
self.loadout: Dict[str, str] = {}
|
||||
self.start_type = start_type
|
||||
# Late activation delay in seconds from mission start. This is not
|
||||
# the same as the flight's takeoff time. Takeoff time depends on the
|
||||
# mission's TOT and the other flights in the package. Takeoff time is
|
||||
# determined by AirConflictGenerator.
|
||||
self.scheduled_in = 0
|
||||
self.use_custom_loadout = False
|
||||
self.client_count = 0
|
||||
|
||||
# Will be replaced with a more appropriate FlightPlan by
|
||||
# FlightPlanBuilder, but an empty flight plan the flight begins with an
|
||||
# empty flight plan.
|
||||
from gen.flights.flightplan import CustomFlightPlan
|
||||
self.flight_plan: FlightPlan = CustomFlightPlan(
|
||||
package=package,
|
||||
flight=self,
|
||||
custom_waypoints=[]
|
||||
)
|
||||
|
||||
@property
|
||||
def points(self) -> List[FlightWaypoint]:
|
||||
return self.flight_plan.waypoints[1:]
|
||||
|
||||
def __repr__(self):
|
||||
return self.flight_type.name + " | " + str(self.count) + "x" + db.unit_type_name(self.unit_type) \
|
||||
+ " (" + str(len(self.points)) + " wpt)"
|
||||
|
||||
def waypoint_with_type(
|
||||
self,
|
||||
types: Iterable[FlightWaypointType]) -> Optional[FlightWaypoint]:
|
||||
for waypoint in self.points:
|
||||
if waypoint.waypoint_type in types:
|
||||
return waypoint
|
||||
return None
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,61 +2,20 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterable, Optional
|
||||
from datetime import timedelta
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game.utils import meter_to_nm
|
||||
from gen.ato import Package
|
||||
from gen.flights.flight import (
|
||||
Flight,
|
||||
FlightType,
|
||||
FlightWaypoint,
|
||||
FlightWaypointType,
|
||||
)
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
|
||||
CAP_DURATION = 30 # Minutes
|
||||
|
||||
INGRESS_TYPES = {
|
||||
FlightWaypointType.INGRESS_CAS,
|
||||
FlightWaypointType.INGRESS_ESCORT,
|
||||
FlightWaypointType.INGRESS_SEAD,
|
||||
FlightWaypointType.INGRESS_STRIKE,
|
||||
}
|
||||
if TYPE_CHECKING:
|
||||
from gen.ato import Package
|
||||
|
||||
|
||||
class GroundSpeed:
|
||||
@staticmethod
|
||||
def mission_speed(package: Package) -> int:
|
||||
speeds = set()
|
||||
for flight in package.flights:
|
||||
# Find a waypoint that matches the mission start waypoint and use
|
||||
# that for the altitude of the mission. That may not be true for the
|
||||
# whole mission, but it's probably good enough for now.
|
||||
waypoint = flight.waypoint_with_type({
|
||||
FlightWaypointType.INGRESS_CAS,
|
||||
FlightWaypointType.INGRESS_ESCORT,
|
||||
FlightWaypointType.INGRESS_SEAD,
|
||||
FlightWaypointType.INGRESS_STRIKE,
|
||||
FlightWaypointType.PATROL_TRACK,
|
||||
})
|
||||
if waypoint is None:
|
||||
logging.error(f"Could not find ingress point for {flight}.")
|
||||
if flight.points:
|
||||
logging.warning(
|
||||
"Using first waypoint for mission altitude.")
|
||||
waypoint = flight.points[0]
|
||||
else:
|
||||
logging.warning(
|
||||
"Flight has no waypoints. Assuming mission altitude "
|
||||
"of 25000 feet.")
|
||||
waypoint = FlightWaypoint(FlightWaypointType.NAV, 0, 0,
|
||||
25000)
|
||||
speeds.add(GroundSpeed.for_flight(flight, waypoint.alt))
|
||||
return min(speeds)
|
||||
|
||||
@classmethod
|
||||
def for_flight(cls, flight: Flight, altitude: int) -> int:
|
||||
@@ -121,52 +80,77 @@ class GroundSpeed:
|
||||
|
||||
class TravelTime:
|
||||
@staticmethod
|
||||
def between_points(a: Point, b: Point, speed: float) -> int:
|
||||
def between_points(a: Point, b: Point, speed: float) -> timedelta:
|
||||
error_factor = 1.1
|
||||
distance = meter_to_nm(a.distance_to_point(b))
|
||||
hours = distance / speed
|
||||
seconds = hours * 3600
|
||||
return int(seconds * error_factor)
|
||||
return timedelta(hours=distance / speed * error_factor)
|
||||
|
||||
|
||||
class TotEstimator:
|
||||
# An extra five minutes given as wiggle room. Expected to be spent at the
|
||||
# hold point performing any last minute configuration.
|
||||
HOLD_TIME = 5 * 60
|
||||
HOLD_TIME = timedelta(minutes=5)
|
||||
|
||||
def __init__(self, package: Package) -> None:
|
||||
self.package = package
|
||||
self.timing = PackageWaypointTiming.for_package(package)
|
||||
|
||||
def mission_start_time(self, flight: Flight) -> int:
|
||||
def mission_start_time(self, flight: Flight) -> timedelta:
|
||||
takeoff_time = self.takeoff_time_for_flight(flight)
|
||||
if takeoff_time is None:
|
||||
# Could not determine takeoff time, probably due to a custom flight
|
||||
# plan. Start immediately.
|
||||
return timedelta()
|
||||
|
||||
startup_time = self.estimate_startup(flight)
|
||||
ground_ops_time = self.estimate_ground_ops(flight)
|
||||
return takeoff_time - startup_time - ground_ops_time
|
||||
start_time = takeoff_time - startup_time - ground_ops_time
|
||||
# In case FP math has given us some barely below zero time, round to
|
||||
# zero.
|
||||
if math.isclose(start_time.total_seconds(), 0):
|
||||
return timedelta()
|
||||
# Trim microseconds. DCS doesn't handle sub-second resolution for tasks,
|
||||
# and they're not interesting from a mission planning perspective so we
|
||||
# don't want them in the UI.
|
||||
#
|
||||
# Round down so *barely* above zero start times are just zero.
|
||||
return timedelta(seconds=math.floor(start_time.total_seconds()))
|
||||
|
||||
def takeoff_time_for_flight(self, flight: Flight) -> int:
|
||||
stop_types = {FlightWaypointType.JOIN, FlightWaypointType.PATROL_TRACK}
|
||||
travel_time = self.estimate_waypoints_to_target(flight, stop_types)
|
||||
def takeoff_time_for_flight(self, flight: Flight) -> Optional[timedelta]:
|
||||
travel_time = self.travel_time_to_rendezvous_or_target(flight)
|
||||
if travel_time is None:
|
||||
logging.warning("Found no join point or patrol point. Cannot "
|
||||
f"estimate takeoff time takeoff time for {flight}")
|
||||
# Takeoff immediately.
|
||||
return 0
|
||||
from gen.flights.flightplan import CustomFlightPlan
|
||||
if not isinstance(flight.flight_plan, CustomFlightPlan):
|
||||
logging.warning(
|
||||
"Found no rendezvous or target point. Cannot estimate "
|
||||
f"takeoff time takeoff time for {flight}.")
|
||||
return None
|
||||
|
||||
# BARCAP flights do not coordinate with the rest of the package on join
|
||||
# or ingress points.
|
||||
if flight.flight_type == FlightType.BARCAP:
|
||||
start_time = self.timing.race_track_start(flight)
|
||||
from gen.flights.flightplan import FormationFlightPlan
|
||||
if isinstance(flight.flight_plan, FormationFlightPlan):
|
||||
tot = flight.flight_plan.tot_for_waypoint(
|
||||
flight.flight_plan.join)
|
||||
if tot is None:
|
||||
logging.warning(
|
||||
"Could not determine the TOT of the join point. Takeoff "
|
||||
f"time for {flight} will be immediate.")
|
||||
return None
|
||||
else:
|
||||
start_time = self.timing.join
|
||||
return start_time - travel_time - self.HOLD_TIME
|
||||
tot = self.package.time_over_target
|
||||
return tot - travel_time - self.HOLD_TIME
|
||||
|
||||
def earliest_tot(self) -> int:
|
||||
return max((
|
||||
def earliest_tot(self) -> timedelta:
|
||||
earliest_tot = max((
|
||||
self.earliest_tot_for_flight(f) for f in self.package.flights
|
||||
)) + self.HOLD_TIME
|
||||
|
||||
def earliest_tot_for_flight(self, flight: Flight) -> int:
|
||||
# Trim microseconds. DCS doesn't handle sub-second resolution for tasks,
|
||||
# and they're not interesting from a mission planning perspective so we
|
||||
# don't want them in the UI.
|
||||
#
|
||||
# Round up so we don't get negative start times.
|
||||
return timedelta(seconds=math.ceil(earliest_tot.total_seconds()))
|
||||
|
||||
def earliest_tot_for_flight(self, flight: Flight) -> timedelta:
|
||||
"""Estimate fastest time from mission start to the target position.
|
||||
|
||||
For BARCAP flights, this is time to race track start. This ensures that
|
||||
@@ -182,211 +166,47 @@ class TotEstimator:
|
||||
The earliest possible TOT for the given flight in seconds. Returns 0
|
||||
if an ingress point cannot be found.
|
||||
"""
|
||||
if flight.flight_type == FlightType.BARCAP:
|
||||
time_to_target = self.estimate_waypoints_to_target(flight, {
|
||||
FlightWaypointType.PATROL_TRACK
|
||||
})
|
||||
if time_to_target is None:
|
||||
logging.warning(
|
||||
f"Found no race track. Cannot estimate TOT for {flight}")
|
||||
# Return 0 so this flight's travel time does not affect the rest
|
||||
# of the package.
|
||||
return 0
|
||||
else:
|
||||
time_to_ingress = self.estimate_waypoints_to_target(
|
||||
flight, INGRESS_TYPES
|
||||
)
|
||||
if time_to_ingress is None:
|
||||
logging.warning(
|
||||
f"Found no ingress types. Cannot estimate TOT for {flight}")
|
||||
# Return 0 so this flight's travel time does not affect the rest
|
||||
# of the package.
|
||||
return 0
|
||||
|
||||
assert self.package.waypoints is not None
|
||||
time_to_target = time_to_ingress + TravelTime.between_points(
|
||||
self.package.waypoints.ingress, self.package.target.position,
|
||||
GroundSpeed.mission_speed(self.package))
|
||||
return sum([
|
||||
self.estimate_startup(flight),
|
||||
self.estimate_ground_ops(flight),
|
||||
time_to_target,
|
||||
])
|
||||
time_to_target = self.travel_time_to_target(flight)
|
||||
if time_to_target is None:
|
||||
logging.warning(f"Cannot estimate TOT for {flight}")
|
||||
# Return 0 so this flight's travel time does not affect the rest
|
||||
# of the package.
|
||||
return timedelta()
|
||||
startup = self.estimate_startup(flight)
|
||||
ground_ops = self.estimate_ground_ops(flight)
|
||||
return startup + ground_ops + time_to_target
|
||||
|
||||
@staticmethod
|
||||
def estimate_startup(flight: Flight) -> int:
|
||||
def estimate_startup(flight: Flight) -> timedelta:
|
||||
if flight.start_type == "Cold":
|
||||
if flight.client_count:
|
||||
return 10 * 60
|
||||
return timedelta(minutes=10)
|
||||
else:
|
||||
# The AI doesn't seem to have a real startup procedure.
|
||||
return 2 * 60
|
||||
return 0
|
||||
return timedelta(minutes=2)
|
||||
return timedelta()
|
||||
|
||||
@staticmethod
|
||||
def estimate_ground_ops(flight: Flight) -> int:
|
||||
def estimate_ground_ops(flight: Flight) -> timedelta:
|
||||
if flight.start_type in ("Runway", "In Flight"):
|
||||
return 0
|
||||
return timedelta()
|
||||
if flight.from_cp.is_fleet:
|
||||
return 2 * 60
|
||||
return timedelta(minutes=2)
|
||||
else:
|
||||
return 5 * 60
|
||||
return timedelta(minutes=5)
|
||||
|
||||
def estimate_waypoints_to_target(
|
||||
self, flight: Flight,
|
||||
stop_types: Iterable[FlightWaypointType]) -> Optional[int]:
|
||||
total = 0
|
||||
# TODO: This is AGL. We want MSL.
|
||||
previous_altitude = 0
|
||||
previous_position = flight.from_cp.position
|
||||
for waypoint in flight.points:
|
||||
position = Point(waypoint.x, waypoint.y)
|
||||
total += TravelTime.between_points(
|
||||
previous_position, position,
|
||||
self.speed_to_waypoint(flight, waypoint, previous_altitude)
|
||||
)
|
||||
previous_position = position
|
||||
previous_altitude = waypoint.alt
|
||||
if waypoint.waypoint_type in stop_types:
|
||||
return total
|
||||
@staticmethod
|
||||
def travel_time_to_target(flight: Flight) -> Optional[timedelta]:
|
||||
if flight.flight_plan is None:
|
||||
return None
|
||||
return flight.flight_plan.travel_time_to_target
|
||||
|
||||
return None
|
||||
|
||||
def speed_to_waypoint(self, flight: Flight, waypoint: FlightWaypoint,
|
||||
from_altitude: int) -> int:
|
||||
# TODO: Adjust if AGL.
|
||||
# We don't have an exact heightmap, but we should probably be performing
|
||||
# *some* adjustment for NTTR since the minimum altitude of the map is
|
||||
# near 2000 ft MSL.
|
||||
alt_for_speed = min(from_altitude, waypoint.alt)
|
||||
pre_join = (FlightWaypointType.LOITER, FlightWaypointType.JOIN)
|
||||
if waypoint.waypoint_type == FlightWaypointType.ASCEND_POINT:
|
||||
# Flights that start airborne already have some altitude and a good
|
||||
# amount of speed.
|
||||
factor = 1.0 if flight.start_type == "In Flight" else 0.5
|
||||
return int(GroundSpeed.for_flight(flight, alt_for_speed) * factor)
|
||||
elif waypoint.waypoint_type in pre_join:
|
||||
return GroundSpeed.for_flight(flight, alt_for_speed)
|
||||
return GroundSpeed.mission_speed(self.package)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PackageWaypointTiming:
|
||||
#: The package being scheduled.
|
||||
package: Package
|
||||
|
||||
#: The package join time.
|
||||
join: int
|
||||
|
||||
#: The ingress waypoint TOT.
|
||||
ingress: int
|
||||
|
||||
#: The egress waypoint TOT.
|
||||
egress: int
|
||||
|
||||
#: The package split time.
|
||||
split: int
|
||||
|
||||
@property
|
||||
def target(self) -> int:
|
||||
"""The package time over target."""
|
||||
assert self.package.time_over_target is not None
|
||||
return self.package.time_over_target
|
||||
|
||||
def race_track_start(self, flight: Flight) -> int:
|
||||
if flight.flight_type == FlightType.BARCAP:
|
||||
return self.target
|
||||
else:
|
||||
# The only other type that (currently) uses race tracks is TARCAP,
|
||||
# which is sort of in need of cleanup. TARCAP is only valid on front
|
||||
# lines and they participate in join points and patrol between the
|
||||
# ingress and egress points rather than on a race track actually
|
||||
# pointed at the enemy.
|
||||
return self.ingress
|
||||
|
||||
def race_track_end(self, flight: Flight) -> int:
|
||||
if flight.flight_type == FlightType.BARCAP:
|
||||
return self.target + CAP_DURATION * 60
|
||||
else:
|
||||
# For TARCAP. See the explanation in race_track_start.
|
||||
return self.egress
|
||||
|
||||
def push_time(self, flight: Flight, hold_point: FlightWaypoint) -> int:
|
||||
assert self.package.waypoints is not None
|
||||
return self.join - TravelTime.between_points(
|
||||
Point(hold_point.x, hold_point.y),
|
||||
self.package.waypoints.join,
|
||||
GroundSpeed.for_flight(flight, hold_point.alt)
|
||||
)
|
||||
|
||||
def tot_for_waypoint(self, flight: Flight,
|
||||
waypoint: FlightWaypoint) -> Optional[int]:
|
||||
target_types = (
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
FlightWaypointType.TARGET_POINT,
|
||||
FlightWaypointType.TARGET_SHIP,
|
||||
)
|
||||
|
||||
if waypoint.waypoint_type == FlightWaypointType.JOIN:
|
||||
return self.join
|
||||
elif waypoint.waypoint_type in INGRESS_TYPES:
|
||||
return self.ingress
|
||||
elif waypoint.waypoint_type in target_types:
|
||||
return self.target
|
||||
elif waypoint.waypoint_type == FlightWaypointType.EGRESS:
|
||||
return self.egress
|
||||
elif waypoint.waypoint_type == FlightWaypointType.SPLIT:
|
||||
return self.split
|
||||
elif waypoint.waypoint_type == FlightWaypointType.PATROL_TRACK:
|
||||
return self.race_track_start(flight)
|
||||
return None
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint,
|
||||
flight: Flight) -> Optional[int]:
|
||||
if waypoint.waypoint_type == FlightWaypointType.LOITER:
|
||||
return self.push_time(flight, waypoint)
|
||||
elif waypoint.waypoint_type == FlightWaypointType.PATROL:
|
||||
return self.race_track_end(flight)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def for_package(cls, package: Package) -> PackageWaypointTiming:
|
||||
assert package.waypoints is not None
|
||||
|
||||
# TODO: Plan similar altitudes for the in-country leg of the mission.
|
||||
# Waypoint altitudes for a given flight *shouldn't* differ too much
|
||||
# between the join and split points, so we don't need speeds for each
|
||||
# leg individually since they should all be fairly similar. This doesn't
|
||||
# hold too well right now since nothing is stopping each waypoint from
|
||||
# jumping 20k feet each time, but that's a huge waste of energy we
|
||||
# should be avoiding anyway.
|
||||
if not package.flights:
|
||||
raise ValueError("Cannot plan TOT for package with no flights")
|
||||
|
||||
group_ground_speed = GroundSpeed.mission_speed(package)
|
||||
|
||||
ingress = package.time_over_target - TravelTime.between_points(
|
||||
package.waypoints.ingress,
|
||||
package.target.position,
|
||||
group_ground_speed
|
||||
)
|
||||
|
||||
join = ingress - TravelTime.between_points(
|
||||
package.waypoints.join,
|
||||
package.waypoints.ingress,
|
||||
group_ground_speed
|
||||
)
|
||||
|
||||
egress = package.time_over_target + TravelTime.between_points(
|
||||
package.target.position,
|
||||
package.waypoints.egress,
|
||||
group_ground_speed
|
||||
)
|
||||
|
||||
split = egress + TravelTime.between_points(
|
||||
package.waypoints.egress,
|
||||
package.waypoints.split,
|
||||
group_ground_speed
|
||||
)
|
||||
|
||||
return cls(package, join, ingress, egress, split)
|
||||
@staticmethod
|
||||
def travel_time_to_rendezvous_or_target(
|
||||
flight: Flight) -> Optional[timedelta]:
|
||||
if flight.flight_plan is None:
|
||||
return None
|
||||
from gen.flights.flightplan import FormationFlightPlan
|
||||
if isinstance(flight.flight_plan, FormationFlightPlan):
|
||||
return flight.flight_plan.travel_time_to_rendezvous
|
||||
return flight.flight_plan.travel_time_to_target
|
||||
|
||||
@@ -1,81 +1,62 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional, Union
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional, Tuple, Union
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.unit import Unit
|
||||
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.utils import nm_to_meter
|
||||
from game.weather import Conditions
|
||||
from theater import ControlPoint, MissionTarget, TheaterGroundObject
|
||||
from .flight import Flight, FlightWaypoint, FlightWaypointType
|
||||
from ..runways import RunwayAssigner
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StrikeTarget:
|
||||
name: str
|
||||
target: Union[TheaterGroundObject, Unit]
|
||||
|
||||
|
||||
class WaypointBuilder:
|
||||
def __init__(self, conditions: Conditions, flight: Flight,
|
||||
doctrine: Doctrine) -> None:
|
||||
doctrine: Doctrine,
|
||||
targets: Optional[List[StrikeTarget]] = None) -> None:
|
||||
self.conditions = conditions
|
||||
self.flight = flight
|
||||
self.doctrine = doctrine
|
||||
self.waypoints: List[FlightWaypoint] = []
|
||||
self.ingress_point: Optional[FlightWaypoint] = None
|
||||
self.targets = targets
|
||||
|
||||
@property
|
||||
def is_helo(self) -> bool:
|
||||
return getattr(self.flight.unit_type, "helicopter", False)
|
||||
|
||||
def build(self) -> List[FlightWaypoint]:
|
||||
return self.waypoints
|
||||
@staticmethod
|
||||
def takeoff(departure: ControlPoint) -> FlightWaypoint:
|
||||
"""Create takeoff waypoint for the given arrival airfield or carrier.
|
||||
|
||||
def ascent(self, departure: ControlPoint) -> None:
|
||||
"""Create ascent waypoint for the given departure airfield or carrier.
|
||||
Note that the takeoff waypoint will automatically be created by pydcs
|
||||
when we create the group, but creating our own before generation makes
|
||||
the planning code simpler.
|
||||
|
||||
Args:
|
||||
departure: Departure airfield or carrier.
|
||||
"""
|
||||
heading = RunwayAssigner(self.conditions).takeoff_heading(departure)
|
||||
position = departure.position.point_from_heading(
|
||||
heading, nm_to_meter(5)
|
||||
)
|
||||
position = departure.position
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.ASCEND_POINT,
|
||||
FlightWaypointType.TAKEOFF,
|
||||
position.x,
|
||||
position.y,
|
||||
500 if self.is_helo else self.doctrine.pattern_altitude
|
||||
0
|
||||
)
|
||||
waypoint.name = "ASCEND"
|
||||
waypoint.name = "TAKEOFF"
|
||||
waypoint.alt_type = "RADIO"
|
||||
waypoint.description = "Ascend"
|
||||
waypoint.pretty_name = "Ascend"
|
||||
self.waypoints.append(waypoint)
|
||||
waypoint.description = "Takeoff"
|
||||
waypoint.pretty_name = "Takeoff"
|
||||
return waypoint
|
||||
|
||||
def descent(self, arrival: ControlPoint) -> None:
|
||||
"""Create descent waypoint for the given arrival airfield or carrier.
|
||||
|
||||
Args:
|
||||
arrival: Arrival airfield or carrier.
|
||||
"""
|
||||
landing_heading = RunwayAssigner(self.conditions).landing_heading(
|
||||
arrival)
|
||||
heading = (landing_heading + 180) % 360
|
||||
position = arrival.position.point_from_heading(
|
||||
heading, nm_to_meter(5)
|
||||
)
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.DESCENT_POINT,
|
||||
position.x,
|
||||
position.y,
|
||||
300 if self.is_helo else self.doctrine.pattern_altitude
|
||||
)
|
||||
waypoint.name = "DESCEND"
|
||||
waypoint.alt_type = "RADIO"
|
||||
waypoint.description = "Descend to pattern altitude"
|
||||
waypoint.pretty_name = "Descend"
|
||||
self.waypoints.append(waypoint)
|
||||
|
||||
def land(self, arrival: ControlPoint) -> None:
|
||||
@staticmethod
|
||||
def land(arrival: ControlPoint) -> FlightWaypoint:
|
||||
"""Create descent waypoint for the given arrival airfield or carrier.
|
||||
|
||||
Args:
|
||||
@@ -92,9 +73,9 @@ class WaypointBuilder:
|
||||
waypoint.alt_type = "RADIO"
|
||||
waypoint.description = "Land"
|
||||
waypoint.pretty_name = "Land"
|
||||
self.waypoints.append(waypoint)
|
||||
return waypoint
|
||||
|
||||
def hold(self, position: Point) -> None:
|
||||
def hold(self, position: Point) -> FlightWaypoint:
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.LOITER,
|
||||
position.x,
|
||||
@@ -104,9 +85,9 @@ class WaypointBuilder:
|
||||
waypoint.pretty_name = "Hold"
|
||||
waypoint.description = "Wait until push time"
|
||||
waypoint.name = "HOLD"
|
||||
self.waypoints.append(waypoint)
|
||||
return waypoint
|
||||
|
||||
def join(self, position: Point) -> None:
|
||||
def join(self, position: Point) -> FlightWaypoint:
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.JOIN,
|
||||
position.x,
|
||||
@@ -116,9 +97,9 @@ class WaypointBuilder:
|
||||
waypoint.pretty_name = "Join"
|
||||
waypoint.description = "Rendezvous with package"
|
||||
waypoint.name = "JOIN"
|
||||
self.waypoints.append(waypoint)
|
||||
return waypoint
|
||||
|
||||
def split(self, position: Point) -> None:
|
||||
def split(self, position: Point) -> FlightWaypoint:
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.SPLIT,
|
||||
position.x,
|
||||
@@ -128,25 +109,35 @@ class WaypointBuilder:
|
||||
waypoint.pretty_name = "Split"
|
||||
waypoint.description = "Depart from package"
|
||||
waypoint.name = "SPLIT"
|
||||
self.waypoints.append(waypoint)
|
||||
return waypoint
|
||||
|
||||
def ingress_cas(self, position: Point, objective: MissionTarget) -> None:
|
||||
self._ingress(FlightWaypointType.INGRESS_CAS, position, objective)
|
||||
def ingress_cas(self, position: Point,
|
||||
objective: MissionTarget) -> FlightWaypoint:
|
||||
return self._ingress(FlightWaypointType.INGRESS_CAS, position,
|
||||
objective)
|
||||
|
||||
def ingress_escort(self, position: Point, objective: MissionTarget) -> None:
|
||||
self._ingress(FlightWaypointType.INGRESS_ESCORT, position, objective)
|
||||
def ingress_escort(self, position: Point,
|
||||
objective: MissionTarget) -> FlightWaypoint:
|
||||
return self._ingress(FlightWaypointType.INGRESS_ESCORT, position,
|
||||
objective)
|
||||
|
||||
def ingress_dead(self, position:Point,
|
||||
objective: MissionTarget) -> FlightWaypoint:
|
||||
return self._ingress(FlightWaypointType.INGRESS_DEAD, position,
|
||||
objective)
|
||||
|
||||
def ingress_sead(self, position: Point, objective: MissionTarget) -> None:
|
||||
self._ingress(FlightWaypointType.INGRESS_SEAD, position, objective)
|
||||
def ingress_sead(self, position: Point,
|
||||
objective: MissionTarget) -> FlightWaypoint:
|
||||
return self._ingress(FlightWaypointType.INGRESS_SEAD, position,
|
||||
objective)
|
||||
|
||||
def ingress_strike(self, position: Point, objective: MissionTarget) -> None:
|
||||
self._ingress(FlightWaypointType.INGRESS_STRIKE, position, objective)
|
||||
def ingress_strike(self, position: Point,
|
||||
objective: MissionTarget) -> FlightWaypoint:
|
||||
return self._ingress(FlightWaypointType.INGRESS_STRIKE, position,
|
||||
objective)
|
||||
|
||||
def _ingress(self, ingress_type: FlightWaypointType, position: Point,
|
||||
objective: MissionTarget) -> None:
|
||||
if self.ingress_point is not None:
|
||||
raise RuntimeError("A flight plan can have only one ingress point.")
|
||||
|
||||
objective: MissionTarget) -> FlightWaypoint:
|
||||
waypoint = FlightWaypoint(
|
||||
ingress_type,
|
||||
position.x,
|
||||
@@ -156,10 +147,11 @@ class WaypointBuilder:
|
||||
waypoint.pretty_name = "INGRESS on " + objective.name
|
||||
waypoint.description = "INGRESS on " + objective.name
|
||||
waypoint.name = "INGRESS"
|
||||
self.waypoints.append(waypoint)
|
||||
self.ingress_point = waypoint
|
||||
# TODO: This seems wrong, but it's what was there before.
|
||||
waypoint.targets.append(objective)
|
||||
return waypoint
|
||||
|
||||
def egress(self, position: Point, target: MissionTarget) -> None:
|
||||
def egress(self, position: Point, target: MissionTarget) -> FlightWaypoint:
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.EGRESS,
|
||||
position.x,
|
||||
@@ -169,68 +161,46 @@ class WaypointBuilder:
|
||||
waypoint.pretty_name = "EGRESS from " + target.name
|
||||
waypoint.description = "EGRESS from " + target.name
|
||||
waypoint.name = "EGRESS"
|
||||
self.waypoints.append(waypoint)
|
||||
return waypoint
|
||||
|
||||
def dead_point(self, target: Union[TheaterGroundObject, Unit], name: str,
|
||||
location: MissionTarget) -> None:
|
||||
self._target_point(target, name, f"STRIKE {name}", location)
|
||||
# TODO: Seems fishy.
|
||||
if self.ingress_point is not None:
|
||||
self.ingress_point.targetGroup = location
|
||||
def dead_point(self, target: StrikeTarget) -> FlightWaypoint:
|
||||
return self._target_point(target, f"STRIKE {target.name}")
|
||||
|
||||
def sead_point(self, target: Union[TheaterGroundObject, Unit], name: str,
|
||||
location: MissionTarget) -> None:
|
||||
self._target_point(target, name, f"STRIKE {name}", location)
|
||||
# TODO: Seems fishy.
|
||||
if self.ingress_point is not None:
|
||||
self.ingress_point.targetGroup = location
|
||||
def sead_point(self, target: StrikeTarget) -> FlightWaypoint:
|
||||
return self._target_point(target, f"STRIKE {target.name}")
|
||||
|
||||
def strike_point(self, target: Union[TheaterGroundObject, Unit], name: str,
|
||||
location: MissionTarget) -> None:
|
||||
self._target_point(target, name, f"STRIKE {name}", location)
|
||||
|
||||
def _target_point(self, target: Union[TheaterGroundObject, Unit], name: str,
|
||||
description: str, location: MissionTarget) -> None:
|
||||
if self.ingress_point is None:
|
||||
raise RuntimeError(
|
||||
"An ingress point must be added before target points."
|
||||
)
|
||||
def strike_point(self, target: StrikeTarget) -> FlightWaypoint:
|
||||
return self._target_point(target, f"STRIKE {target.name}")
|
||||
|
||||
@staticmethod
|
||||
def _target_point(target: StrikeTarget, description: str) -> FlightWaypoint:
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.TARGET_POINT,
|
||||
target.position.x,
|
||||
target.position.y,
|
||||
target.target.position.x,
|
||||
target.target.position.y,
|
||||
0
|
||||
)
|
||||
waypoint.description = description
|
||||
waypoint.pretty_name = description
|
||||
waypoint.name = name
|
||||
waypoint.name = target.name
|
||||
waypoint.alt_type = "RADIO"
|
||||
# The target waypoints are only for the player's benefit. AI tasks for
|
||||
# the target are set on the ingress point so they begin their attack
|
||||
# *before* reaching the target.
|
||||
waypoint.only_for_player = True
|
||||
self.waypoints.append(waypoint)
|
||||
# TODO: This seems wrong, but it's what was there before.
|
||||
self.ingress_point.targets.append(location)
|
||||
return waypoint
|
||||
|
||||
def sead_area(self, target: MissionTarget) -> None:
|
||||
self._target_area(f"SEAD on {target.name}", target)
|
||||
# TODO: Seems fishy.
|
||||
if self.ingress_point is not None:
|
||||
self.ingress_point.targetGroup = target
|
||||
def strike_area(self, target: MissionTarget) -> FlightWaypoint:
|
||||
return self._target_area(f"STRIKE {target.name}", target)
|
||||
|
||||
def dead_area(self, target: MissionTarget) -> None:
|
||||
self._target_area(f"DEAD on {target.name}", target)
|
||||
# TODO: Seems fishy.
|
||||
if self.ingress_point is not None:
|
||||
self.ingress_point.targetGroup = target
|
||||
def sead_area(self, target: MissionTarget) -> FlightWaypoint:
|
||||
return self._target_area(f"SEAD on {target.name}", target)
|
||||
|
||||
def _target_area(self, name: str, location: MissionTarget) -> None:
|
||||
if self.ingress_point is None:
|
||||
raise RuntimeError(
|
||||
"An ingress point must be added before target points."
|
||||
)
|
||||
def dead_area(self, target: MissionTarget) -> FlightWaypoint:
|
||||
return self._target_area(f"DEAD on {target.name}", target)
|
||||
|
||||
@staticmethod
|
||||
def _target_area(name: str, location: MissionTarget) -> FlightWaypoint:
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
location.position.x,
|
||||
@@ -240,15 +210,14 @@ class WaypointBuilder:
|
||||
waypoint.description = name
|
||||
waypoint.pretty_name = name
|
||||
waypoint.name = name
|
||||
waypoint.alt_type = "RADIO"
|
||||
# The target waypoints are only for the player's benefit. AI tasks for
|
||||
# the target are set on the ingress point so they begin their attack
|
||||
# *before* reaching the target.
|
||||
waypoint.only_for_player = True
|
||||
self.waypoints.append(waypoint)
|
||||
# TODO: This seems wrong, but it's what was there before.
|
||||
self.ingress_point.targets.append(location)
|
||||
return waypoint
|
||||
|
||||
def cas(self, position: Point) -> None:
|
||||
def cas(self, position: Point) -> FlightWaypoint:
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.CAS,
|
||||
position.x,
|
||||
@@ -259,9 +228,10 @@ class WaypointBuilder:
|
||||
waypoint.description = "Provide CAS"
|
||||
waypoint.name = "CAS"
|
||||
waypoint.pretty_name = "CAS"
|
||||
self.waypoints.append(waypoint)
|
||||
return waypoint
|
||||
|
||||
def race_track_start(self, position: Point, altitude: int) -> None:
|
||||
@staticmethod
|
||||
def race_track_start(position: Point, altitude: int) -> FlightWaypoint:
|
||||
"""Creates a racetrack start waypoint.
|
||||
|
||||
Args:
|
||||
@@ -277,9 +247,10 @@ class WaypointBuilder:
|
||||
waypoint.name = "RACETRACK START"
|
||||
waypoint.description = "Orbit between this point and the next point"
|
||||
waypoint.pretty_name = "Race-track start"
|
||||
self.waypoints.append(waypoint)
|
||||
return waypoint
|
||||
|
||||
def race_track_end(self, position: Point, altitude: int) -> None:
|
||||
@staticmethod
|
||||
def race_track_end(position: Point, altitude: int) -> FlightWaypoint:
|
||||
"""Creates a racetrack end waypoint.
|
||||
|
||||
Args:
|
||||
@@ -295,9 +266,10 @@ class WaypointBuilder:
|
||||
waypoint.name = "RACETRACK END"
|
||||
waypoint.description = "Orbit between this point and the previous point"
|
||||
waypoint.pretty_name = "Race-track end"
|
||||
self.waypoints.append(waypoint)
|
||||
return waypoint
|
||||
|
||||
def race_track(self, start: Point, end: Point, altitude: int) -> None:
|
||||
def race_track(self, start: Point, end: Point,
|
||||
altitude: int) -> Tuple[FlightWaypoint, FlightWaypoint]:
|
||||
"""Creates two waypoint for a racetrack orbit.
|
||||
|
||||
Args:
|
||||
@@ -305,20 +277,11 @@ class WaypointBuilder:
|
||||
end: The ending racetrack waypoint.
|
||||
altitude: The racetrack altitude.
|
||||
"""
|
||||
self.race_track_start(start, altitude)
|
||||
self.race_track_end(end, altitude)
|
||||
return (self.race_track_start(start, altitude),
|
||||
self.race_track_end(end, altitude))
|
||||
|
||||
def rtb(self, arrival: ControlPoint) -> None:
|
||||
"""Creates descent ant landing waypoints for the given control point.
|
||||
|
||||
Args:
|
||||
arrival: Arrival airfield or carrier.
|
||||
"""
|
||||
self.descent(arrival)
|
||||
self.land(arrival)
|
||||
|
||||
def escort(self, ingress: Point, target: MissionTarget,
|
||||
egress: Point) -> None:
|
||||
def escort(self, ingress: Point, target: MissionTarget, egress: Point) -> \
|
||||
Tuple[FlightWaypoint, FlightWaypoint, FlightWaypoint]:
|
||||
"""Creates the waypoints needed to escort the package.
|
||||
|
||||
Args:
|
||||
@@ -332,7 +295,8 @@ class WaypointBuilder:
|
||||
# description in gen.aircraft.JoinPointBuilder), so instead we give
|
||||
# the escort flights a flight plan including the ingress point, target
|
||||
# area, and egress point.
|
||||
self._ingress(FlightWaypointType.INGRESS_ESCORT, ingress, target)
|
||||
ingress = self._ingress(FlightWaypointType.INGRESS_ESCORT, ingress,
|
||||
target)
|
||||
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
@@ -343,6 +307,6 @@ class WaypointBuilder:
|
||||
waypoint.name = "TARGET"
|
||||
waypoint.description = "Escort the package"
|
||||
waypoint.pretty_name = "Target area"
|
||||
self.waypoints.append(waypoint)
|
||||
|
||||
self.egress(egress, target)
|
||||
egress = self.egress(egress, target)
|
||||
return ingress, waypoint, egress
|
||||
|
||||
@@ -27,13 +27,14 @@ TYPE_TANKS = [
|
||||
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
|
||||
Armor.MT_Pz_Kpfw_IV_Ausf_H,
|
||||
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_M4A4_Sherman_Firefly,
|
||||
Armor.StuG_IV,
|
||||
Armor.ST_Centaur_IV,
|
||||
Armor.CT_Centaur_IV,
|
||||
Armor.CT_Cromwell_IV,
|
||||
Armor.HIT_Churchill_VII,
|
||||
Armor.LT_Mk_VII_Tetrarch,
|
||||
|
||||
# Mods
|
||||
frenchpack.DIM__TOYOTA_BLUE,
|
||||
@@ -73,14 +74,15 @@ TYPE_IFV = [
|
||||
Armor.IFV_Marder,
|
||||
Armor.IFV_MCV_80,
|
||||
Armor.IFV_LAV_25,
|
||||
Armor.IFV_Sd_Kfz_234_2_Puma,
|
||||
Armor.AC_Sd_Kfz_234_2_Puma,
|
||||
Armor.IFV_M2A2_Bradley,
|
||||
Armor.IFV_BMD_1,
|
||||
Armor.ZBD_04A,
|
||||
|
||||
# WW2
|
||||
Armor.IFV_Sd_Kfz_234_2_Puma,
|
||||
Armor.AC_Sd_Kfz_234_2_Puma,
|
||||
Armor.LAC_M8_Greyhound,
|
||||
Armor.Daimler_Armoured_Car,
|
||||
|
||||
# Mods
|
||||
frenchpack.ERC_90,
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
"""Generators for creating the groups for ground objectives.
|
||||
|
||||
The classes in this file are responsible for creating the vehicle groups, ship
|
||||
groups, statics, missile sites, and AA sites for the mission. Each of these
|
||||
objectives is defined in the Theater by a TheaterGroundObject. These classes
|
||||
create the pydcs groups and statics for those areas and add them to the mission.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import random
|
||||
from typing import Dict, Iterator
|
||||
from typing import Dict, Iterator, Optional, TYPE_CHECKING
|
||||
|
||||
from dcs import Mission
|
||||
from dcs.country import Country
|
||||
from dcs.statics import fortification_map, warehouse_map
|
||||
from dcs.task import (
|
||||
ActivateBeaconCommand,
|
||||
@@ -10,22 +20,339 @@ from dcs.task import (
|
||||
EPLRS,
|
||||
OptAlarmState,
|
||||
)
|
||||
from dcs.unit import Ship, Vehicle
|
||||
from dcs.unitgroup import StaticGroup
|
||||
from dcs.unit import Ship, Vehicle, Unit
|
||||
from dcs.unitgroup import Group, ShipGroup, StaticGroup
|
||||
from dcs.unittype import StaticType, UnitType
|
||||
|
||||
from game import db
|
||||
from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID
|
||||
from game.db import unit_type_from_name
|
||||
from theater import ControlPoint, TheaterGroundObject
|
||||
from theater.theatergroundobject import (
|
||||
BuildingGroundObject, CarrierGroundObject,
|
||||
GenericCarrierGroundObject,
|
||||
LhaGroundObject, ShipGroundObject,
|
||||
)
|
||||
from .conflictgen import Conflict
|
||||
from .radios import RadioRegistry
|
||||
from .radios import RadioFrequency, RadioRegistry
|
||||
from .runways import RunwayData
|
||||
from .tacan import TacanBand, TacanRegistry
|
||||
from .tacan import TacanBand, TacanChannel, TacanRegistry
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
|
||||
FARP_FRONTLINE_DISTANCE = 10000
|
||||
AA_CP_MIN_DISTANCE = 40000
|
||||
|
||||
|
||||
class GenericGroundObjectGenerator:
|
||||
"""An unspecialized ground object generator.
|
||||
|
||||
Currently used only for SAM and missile (V1/V2) sites.
|
||||
"""
|
||||
def __init__(self, ground_object: TheaterGroundObject, country: Country,
|
||||
game: Game, mission: Mission) -> None:
|
||||
self.ground_object = ground_object
|
||||
self.country = country
|
||||
self.game = game
|
||||
self.m = mission
|
||||
|
||||
def generate(self) -> None:
|
||||
if self.game.position_culled(self.ground_object.position):
|
||||
return
|
||||
|
||||
for group in self.ground_object.groups:
|
||||
if not group.units:
|
||||
logging.warning(f"Found empty group in {self.ground_object}")
|
||||
continue
|
||||
|
||||
unit_type = unit_type_from_name(group.units[0].type)
|
||||
if unit_type is None:
|
||||
raise RuntimeError(
|
||||
f"Unrecognized unit type: {group.units[0].type}")
|
||||
|
||||
vg = self.m.vehicle_group(self.country, group.name, unit_type,
|
||||
position=group.position,
|
||||
heading=group.units[0].heading)
|
||||
vg.units[0].name = self.m.string(group.units[0].name)
|
||||
vg.units[0].player_can_drive = True
|
||||
for i, u in enumerate(group.units):
|
||||
if i > 0:
|
||||
vehicle = Vehicle(self.m.next_unit_id(),
|
||||
self.m.string(u.name), u.type)
|
||||
vehicle.position.x = u.position.x
|
||||
vehicle.position.y = u.position.y
|
||||
vehicle.heading = u.heading
|
||||
vehicle.player_can_drive = True
|
||||
vg.add_unit(vehicle)
|
||||
|
||||
self.enable_eplrs(vg, unit_type)
|
||||
self.set_alarm_state(vg)
|
||||
|
||||
@staticmethod
|
||||
def enable_eplrs(group: Group, unit_type: UnitType) -> None:
|
||||
if hasattr(unit_type, 'eplrs'):
|
||||
if unit_type.eplrs:
|
||||
group.points[0].tasks.append(EPLRS(group.id))
|
||||
|
||||
def set_alarm_state(self, group: Group) -> None:
|
||||
if self.game.settings.perf_red_alert_state:
|
||||
group.points[0].tasks.append(OptAlarmState(2))
|
||||
else:
|
||||
group.points[0].tasks.append(OptAlarmState(1))
|
||||
|
||||
|
||||
class BuildingSiteGenerator(GenericGroundObjectGenerator):
|
||||
"""Generator for building sites.
|
||||
|
||||
Building sites are the primary type of non-airbase objective locations that
|
||||
appear on the map. They come in a handful of variants each with different
|
||||
types of buildings and ground units.
|
||||
"""
|
||||
|
||||
def generate(self) -> None:
|
||||
if self.game.position_culled(self.ground_object.position):
|
||||
return
|
||||
|
||||
if self.ground_object.dcs_identifier in warehouse_map:
|
||||
static_type = warehouse_map[self.ground_object.dcs_identifier]
|
||||
self.generate_static(static_type)
|
||||
elif self.ground_object.dcs_identifier in fortification_map:
|
||||
static_type = fortification_map[self.ground_object.dcs_identifier]
|
||||
self.generate_static(static_type)
|
||||
elif self.ground_object.dcs_identifier in FORTIFICATION_UNITS_ID:
|
||||
for f in FORTIFICATION_UNITS:
|
||||
if f.id == self.ground_object.dcs_identifier:
|
||||
unit_type = f
|
||||
self.generate_vehicle_group(unit_type)
|
||||
break
|
||||
else:
|
||||
logging.error(
|
||||
f"{self.ground_object.dcs_identifier} not found in static maps")
|
||||
|
||||
def generate_vehicle_group(self, unit_type: UnitType) -> None:
|
||||
if not self.ground_object.is_dead:
|
||||
self.m.vehicle_group(
|
||||
country=self.country,
|
||||
name=self.ground_object.group_name,
|
||||
_type=unit_type,
|
||||
position=self.ground_object.position,
|
||||
heading=self.ground_object.heading,
|
||||
)
|
||||
|
||||
def generate_static(self, static_type: StaticType) -> None:
|
||||
self.m.static_group(
|
||||
country=self.country,
|
||||
name=self.ground_object.group_name,
|
||||
_type=static_type,
|
||||
position=self.ground_object.position,
|
||||
heading=self.ground_object.heading,
|
||||
dead=self.ground_object.is_dead,
|
||||
)
|
||||
|
||||
|
||||
class GenericCarrierGenerator(GenericGroundObjectGenerator):
|
||||
"""Base type for carrier group generation.
|
||||
|
||||
Used by both CV(N) groups and LHA groups.
|
||||
"""
|
||||
def __init__(self, ground_object: GenericCarrierGroundObject,
|
||||
control_point: ControlPoint, country: Country, game: Game,
|
||||
mission: Mission, radio_registry: RadioRegistry,
|
||||
tacan_registry: TacanRegistry, icls_alloc: Iterator[int],
|
||||
runways: Dict[str, RunwayData]) -> None:
|
||||
super().__init__(ground_object, country, game, mission)
|
||||
self.ground_object = ground_object
|
||||
self.control_point = control_point
|
||||
self.radio_registry = radio_registry
|
||||
self.tacan_registry = tacan_registry
|
||||
self.icls_alloc = icls_alloc
|
||||
self.runways = runways
|
||||
|
||||
def generate(self) -> None:
|
||||
# TODO: Require single group?
|
||||
for group in self.ground_object.groups:
|
||||
if not group.units:
|
||||
logging.warning(
|
||||
f"Found empty carrier group in {self.control_point}")
|
||||
continue
|
||||
|
||||
atc = self.radio_registry.alloc_uhf()
|
||||
ship_group = self.configure_carrier(group, atc)
|
||||
for unit in group.units[1:]:
|
||||
ship_group.add_unit(self.create_ship(unit, atc))
|
||||
|
||||
tacan = self.tacan_registry.alloc_for_band(TacanBand.X)
|
||||
tacan_callsign = self.tacan_callsign()
|
||||
icls = next(self.icls_alloc)
|
||||
|
||||
brc = self.steam_into_wind(ship_group)
|
||||
self.activate_beacons(ship_group, tacan, tacan_callsign, icls)
|
||||
self.add_runway_data(brc or 0, atc, tacan, tacan_callsign, icls)
|
||||
|
||||
def get_carrier_type(self, group: Group) -> UnitType:
|
||||
unit_type = unit_type_from_name(group.units[0].type)
|
||||
if unit_type is None:
|
||||
raise RuntimeError(
|
||||
f"Unrecognized carrier name: {group.units[0].type}")
|
||||
return unit_type
|
||||
|
||||
def configure_carrier(self, group: Group,
|
||||
atc_channel: RadioFrequency) -> ShipGroup:
|
||||
unit_type = self.get_carrier_type(group)
|
||||
|
||||
ship_group = self.m.ship_group(self.country, group.name, unit_type,
|
||||
position=group.position,
|
||||
heading=group.units[0].heading)
|
||||
ship_group.set_frequency(atc_channel.hertz)
|
||||
ship_group.units[0].name = self.m.string(group.units[0].name)
|
||||
return ship_group
|
||||
|
||||
def create_ship(self, unit: Unit, atc_channel: RadioFrequency) -> Ship:
|
||||
ship = Ship(self.m.next_unit_id(),
|
||||
self.m.string(unit.name),
|
||||
unit_type_from_name(unit.type))
|
||||
ship.position.x = unit.position.x
|
||||
ship.position.y = unit.position.y
|
||||
ship.heading = unit.heading
|
||||
# TODO: Verify.
|
||||
ship.set_frequency(atc_channel.hertz)
|
||||
return ship
|
||||
|
||||
def steam_into_wind(self, group: ShipGroup) -> Optional[int]:
|
||||
brc = self.m.weather.wind_at_ground.direction + 180
|
||||
for attempt in range(5):
|
||||
point = group.points[0].position.point_from_heading(
|
||||
brc, 100000 - attempt * 20000)
|
||||
if self.game.theater.is_in_sea(point):
|
||||
group.add_waypoint(point)
|
||||
return brc
|
||||
return None
|
||||
|
||||
def tacan_callsign(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def activate_beacons(group: ShipGroup, tacan: TacanChannel,
|
||||
callsign: str, icls: int) -> None:
|
||||
group.points[0].tasks.append(ActivateBeaconCommand(
|
||||
channel=tacan.number,
|
||||
modechannel=tacan.band.value,
|
||||
callsign=callsign,
|
||||
unit_id=group.units[0].id,
|
||||
aa=False
|
||||
))
|
||||
group.points[0].tasks.append(ActivateICLSCommand(
|
||||
icls, unit_id=group.units[0].id
|
||||
))
|
||||
|
||||
def add_runway_data(self, brc: int, atc: RadioFrequency,
|
||||
tacan: TacanChannel, callsign: str, icls: int) -> None:
|
||||
# TODO: Make unit name usable.
|
||||
# This relies on one control point mapping exactly
|
||||
# to one LHA, carrier, or other usable "runway".
|
||||
# This isn't wholly true, since the DD escorts of
|
||||
# the carrier group are valid for helicopters, but
|
||||
# they aren't exposed as such to the game. Should
|
||||
# clean this up so that's possible. We can't use the
|
||||
# unit name since it's an arbitrary ID.
|
||||
self.runways[self.control_point.name] = RunwayData(
|
||||
self.control_point.name,
|
||||
brc,
|
||||
"N/A",
|
||||
atc=atc,
|
||||
tacan=tacan,
|
||||
tacan_callsign=callsign,
|
||||
icls=icls,
|
||||
)
|
||||
|
||||
|
||||
class CarrierGenerator(GenericCarrierGenerator):
|
||||
"""Generator for CV(N) groups."""
|
||||
|
||||
def get_carrier_type(self, group: Group) -> UnitType:
|
||||
unit_type = super().get_carrier_type(group)
|
||||
if self.game.settings.supercarrier:
|
||||
unit_type = db.upgrade_to_supercarrier(unit_type,
|
||||
self.control_point.name)
|
||||
return unit_type
|
||||
|
||||
def tacan_callsign(self) -> str:
|
||||
# TODO: Assign these properly.
|
||||
return random.choice([
|
||||
"STE",
|
||||
"CVN",
|
||||
"CVH",
|
||||
"CCV",
|
||||
"ACC",
|
||||
"ARC",
|
||||
"GER",
|
||||
"ABR",
|
||||
"LIN",
|
||||
"TRU",
|
||||
])
|
||||
|
||||
|
||||
class LhaGenerator(GenericCarrierGenerator):
|
||||
"""Generator for LHA groups."""
|
||||
|
||||
def tacan_callsign(self) -> str:
|
||||
# TODO: Assign these properly.
|
||||
return random.choice([
|
||||
"LHD",
|
||||
"LHA",
|
||||
"LHB",
|
||||
"LHC",
|
||||
"LHD",
|
||||
"LDS",
|
||||
])
|
||||
|
||||
|
||||
class ShipObjectGenerator(GenericGroundObjectGenerator):
|
||||
"""Generator for non-carrier naval groups."""
|
||||
|
||||
def generate(self) -> None:
|
||||
if self.game.position_culled(self.ground_object.position):
|
||||
return
|
||||
|
||||
for group in self.ground_object.groups:
|
||||
if not group.units:
|
||||
logging.warning(f"Found empty group in {self.ground_object}")
|
||||
continue
|
||||
|
||||
unit_type = unit_type_from_name(group.units[0].type)
|
||||
if unit_type is None:
|
||||
raise RuntimeError(
|
||||
f"Unrecognized unit type: {group.units[0].type}")
|
||||
|
||||
self.generate_group(group, unit_type)
|
||||
|
||||
def generate_group(self, group_def: Group, unit_type: UnitType):
|
||||
group = self.m.ship_group(self.country, group_def.name, unit_type,
|
||||
position=group_def.position,
|
||||
heading=group_def.units[0].heading)
|
||||
group.units[0].name = self.m.string(group_def.units[0].name)
|
||||
# TODO: Skipping the first unit looks like copy pasta from the carrier.
|
||||
for unit in group_def.units[1:]:
|
||||
unit_type = unit_type_from_name(unit.type)
|
||||
ship = Ship(self.m.next_unit_id(),
|
||||
self.m.string(unit.name), unit_type)
|
||||
ship.position.x = unit.position.x
|
||||
ship.position.y = unit.position.y
|
||||
ship.heading = unit.heading
|
||||
group.add_unit(ship)
|
||||
self.set_alarm_state(group)
|
||||
|
||||
|
||||
class GroundObjectsGenerator:
|
||||
"""Creates DCS groups and statics for the theater during mission generation.
|
||||
|
||||
Most of the work of group/static generation is delegated to the other
|
||||
generator classes. This class is responsible for finding each of the
|
||||
locations for spawning ground objects, determining their types, and creating
|
||||
the appropriate generators.
|
||||
"""
|
||||
FARP_CAPACITY = 4
|
||||
|
||||
def __init__(self, mission: Mission, conflict: Conflict, game,
|
||||
@@ -61,188 +388,34 @@ class GroundObjectsGenerator:
|
||||
)
|
||||
|
||||
def generate(self):
|
||||
|
||||
for cp in self.game.theater.controlpoints:
|
||||
|
||||
if cp.captured:
|
||||
country = self.game.player_country
|
||||
country_name = self.game.player_country
|
||||
else:
|
||||
country = self.game.enemy_country
|
||||
side = self.m.country(country)
|
||||
country_name = self.game.enemy_country
|
||||
country = self.m.country(country_name)
|
||||
|
||||
for ground_object in cp.ground_objects:
|
||||
if ground_object.dcs_identifier == "AA":
|
||||
|
||||
if self.game.position_culled(ground_object.position):
|
||||
continue
|
||||
|
||||
for g in ground_object.groups:
|
||||
if len(g.units) > 0:
|
||||
utype = unit_type_from_name(g.units[0].type)
|
||||
|
||||
if not ground_object.sea_object:
|
||||
vg = self.m.vehicle_group(side, g.name, utype, position=g.position, heading=g.units[0].heading)
|
||||
vg.units[0].name = self.m.string(g.units[0].name)
|
||||
vg.units[0].player_can_drive = True
|
||||
for i, u in enumerate(g.units):
|
||||
if i > 0:
|
||||
vehicle = Vehicle(self.m.next_unit_id(), self.m.string(u.name), u.type)
|
||||
vehicle.position.x = u.position.x
|
||||
vehicle.position.y = u.position.y
|
||||
vehicle.heading = u.heading
|
||||
vehicle.player_can_drive = True
|
||||
vg.add_unit(vehicle)
|
||||
|
||||
if hasattr(utype, 'eplrs'):
|
||||
if utype.eplrs:
|
||||
vg.points[0].tasks.append(EPLRS(vg.id))
|
||||
else:
|
||||
vg = self.m.ship_group(side, g.name, utype, position=g.position,
|
||||
heading=g.units[0].heading)
|
||||
vg.units[0].name = self.m.string(g.units[0].name)
|
||||
for i, u in enumerate(g.units):
|
||||
utype = unit_type_from_name(u.type)
|
||||
if i > 0:
|
||||
ship = Ship(self.m.next_unit_id(), self.m.string(u.name), utype)
|
||||
ship.position.x = u.position.x
|
||||
ship.position.y = u.position.y
|
||||
ship.heading = u.heading
|
||||
vg.add_unit(ship)
|
||||
|
||||
if self.game.settings.perf_red_alert_state:
|
||||
vg.points[0].tasks.append(OptAlarmState(2))
|
||||
else:
|
||||
vg.points[0].tasks.append(OptAlarmState(1))
|
||||
|
||||
|
||||
elif ground_object.dcs_identifier in ["CARRIER", "LHA"]:
|
||||
for g in ground_object.groups:
|
||||
if len(g.units) > 0:
|
||||
|
||||
utype = unit_type_from_name(g.units[0].type)
|
||||
if ground_object.dcs_identifier == "CARRIER" and self.game.settings.supercarrier == True:
|
||||
utype = db.upgrade_to_supercarrier(utype, cp.name)
|
||||
|
||||
sg = self.m.ship_group(side, g.name, utype, position=g.position, heading=g.units[0].heading)
|
||||
atc_channel = self.radio_registry.alloc_uhf()
|
||||
sg.set_frequency(atc_channel.hertz)
|
||||
sg.units[0].name = self.m.string(g.units[0].name)
|
||||
|
||||
for i, u in enumerate(g.units):
|
||||
if i > 0:
|
||||
ship = Ship(self.m.next_unit_id(), self.m.string(u.name), unit_type_from_name(u.type))
|
||||
ship.position.x = u.position.x
|
||||
ship.position.y = u.position.y
|
||||
ship.heading = u.heading
|
||||
# TODO: Verify.
|
||||
ship.set_frequency(atc_channel.hertz)
|
||||
sg.add_unit(ship)
|
||||
|
||||
# Find carrier direction (In the wind)
|
||||
found_carrier_destination = False
|
||||
attempt = 0
|
||||
brc = self.m.weather.wind_at_ground.direction + 180
|
||||
while not found_carrier_destination and attempt < 5:
|
||||
point = sg.points[0].position.point_from_heading(brc, 100000-attempt*20000)
|
||||
if self.game.theater.is_in_sea(point):
|
||||
found_carrier_destination = True
|
||||
sg.add_waypoint(point)
|
||||
else:
|
||||
attempt = attempt + 1
|
||||
|
||||
# Set UP TACAN and ICLS
|
||||
tacan = self.tacan_registry.alloc_for_band(TacanBand.X)
|
||||
icls_channel = next(self.icls_alloc)
|
||||
# TODO: Assign these properly.
|
||||
if ground_object.dcs_identifier == "CARRIER":
|
||||
tacan_callsign = random.choice([
|
||||
"STE",
|
||||
"CVN",
|
||||
"CVH",
|
||||
"CCV",
|
||||
"ACC",
|
||||
"ARC",
|
||||
"GER",
|
||||
"ABR",
|
||||
"LIN",
|
||||
"TRU",
|
||||
])
|
||||
else:
|
||||
tacan_callsign = random.choice([
|
||||
"LHD",
|
||||
"LHA",
|
||||
"LHB",
|
||||
"LHC",
|
||||
"LHD",
|
||||
"LDS",
|
||||
])
|
||||
sg.points[0].tasks.append(ActivateBeaconCommand(
|
||||
channel=tacan.number,
|
||||
modechannel=tacan.band.value,
|
||||
callsign=tacan_callsign,
|
||||
unit_id=sg.units[0].id,
|
||||
aa=False
|
||||
))
|
||||
sg.points[0].tasks.append(ActivateICLSCommand(
|
||||
icls_channel,
|
||||
unit_id=sg.units[0].id
|
||||
))
|
||||
# TODO: Make unit name usable.
|
||||
# This relies on one control point mapping exactly
|
||||
# to one LHA, carrier, or other usable "runway".
|
||||
# This isn't wholly true, since the DD escorts of
|
||||
# the carrier group are valid for helicopters, but
|
||||
# they aren't exposed as such to the game. Should
|
||||
# clean this up so that's possible. We can't use the
|
||||
# unit name since it's an arbitrary ID.
|
||||
self.runways[cp.name] = RunwayData(
|
||||
cp.name,
|
||||
brc,
|
||||
"N/A",
|
||||
atc=atc_channel,
|
||||
tacan=tacan,
|
||||
tacan_callsign=tacan_callsign,
|
||||
icls=icls_channel,
|
||||
)
|
||||
|
||||
if isinstance(ground_object, BuildingGroundObject):
|
||||
generator = BuildingSiteGenerator(ground_object, country,
|
||||
self.game, self.m)
|
||||
elif isinstance(ground_object, CarrierGroundObject):
|
||||
generator = CarrierGenerator(ground_object, cp, country,
|
||||
self.game, self.m,
|
||||
self.radio_registry,
|
||||
self.tacan_registry,
|
||||
self.icls_alloc, self.runways)
|
||||
elif isinstance(ground_object, LhaGroundObject):
|
||||
generator = CarrierGenerator(ground_object, cp, country,
|
||||
self.game, self.m,
|
||||
self.radio_registry,
|
||||
self.tacan_registry,
|
||||
self.icls_alloc, self.runways)
|
||||
elif isinstance(ground_object, ShipGroundObject):
|
||||
generator = ShipObjectGenerator(ground_object, country,
|
||||
self.game, self.m)
|
||||
else:
|
||||
|
||||
if self.game.position_culled(ground_object.position):
|
||||
continue
|
||||
|
||||
static_type = None
|
||||
if ground_object.dcs_identifier in warehouse_map:
|
||||
static_type = warehouse_map[ground_object.dcs_identifier]
|
||||
elif ground_object.dcs_identifier in fortification_map:
|
||||
static_type = fortification_map[ground_object.dcs_identifier]
|
||||
elif ground_object.dcs_identifier in FORTIFICATION_UNITS_ID:
|
||||
for f in FORTIFICATION_UNITS:
|
||||
if f.id == ground_object.dcs_identifier:
|
||||
unit_type = f
|
||||
break
|
||||
else:
|
||||
print("Didn't find {} in static _map(s)!".format(ground_object.dcs_identifier))
|
||||
continue
|
||||
|
||||
if static_type is None:
|
||||
if not ground_object.is_dead:
|
||||
group = self.m.vehicle_group(
|
||||
country=side,
|
||||
name=ground_object.string_identifier,
|
||||
_type=unit_type,
|
||||
position=ground_object.position,
|
||||
heading=ground_object.heading,
|
||||
)
|
||||
logging.info("generated {}object identifier {} with mission id {}".format(
|
||||
"dead " if ground_object.is_dead else "", group.name, group.id))
|
||||
else:
|
||||
group = self.m.static_group(
|
||||
country=side,
|
||||
name=ground_object.string_identifier,
|
||||
_type=static_type,
|
||||
position=ground_object.position,
|
||||
heading=ground_object.heading,
|
||||
dead=ground_object.is_dead,
|
||||
)
|
||||
|
||||
logging.info("generated {}object identifier {} with mission id {}".format("dead " if ground_object.is_dead else "", group.name, group.id))
|
||||
generator = GenericGroundObjectGenerator(ground_object,
|
||||
country, self.game,
|
||||
self.m)
|
||||
generator.generate()
|
||||
|
||||
@@ -26,7 +26,7 @@ import datetime
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from typing import Dict, List, Optional, Tuple, TYPE_CHECKING
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from dcs.mission import Mission
|
||||
@@ -42,7 +42,8 @@ from .flights.flight import FlightWaypoint, FlightWaypointType
|
||||
from .radios import RadioFrequency
|
||||
from .runways import RunwayData
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
class KneeboardPageWriter:
|
||||
"""Creates kneeboard images."""
|
||||
|
||||
@@ -157,10 +158,10 @@ class FlightPlanBuilder:
|
||||
self._format_time(waypoint.waypoint.departure_time),
|
||||
])
|
||||
|
||||
def _format_time(self, time: Optional[int]) -> str:
|
||||
def _format_time(self, time: Optional[datetime.timedelta]) -> str:
|
||||
if time is None:
|
||||
return ""
|
||||
local_time = self.start_time + datetime.timedelta(seconds=time)
|
||||
local_time = self.start_time + time
|
||||
return local_time.strftime(f"%H:%M:%S")
|
||||
|
||||
def _waypoint_distance(self, waypoint: FlightWaypoint) -> str:
|
||||
@@ -189,7 +190,7 @@ class FlightPlanBuilder:
|
||||
distance = meter_to_nm(self.last_waypoint.position.distance_to_point(
|
||||
waypoint.position
|
||||
))
|
||||
duration = (waypoint.tot - last_time) / 3600
|
||||
duration = (waypoint.tot - last_time).total_seconds() / 3600
|
||||
return f"{int(distance / duration)} kt"
|
||||
|
||||
def build(self) -> List[List[str]]:
|
||||
@@ -310,8 +311,8 @@ class BriefingPage(KneeboardPage):
|
||||
class KneeboardGenerator(MissionInfoGenerator):
|
||||
"""Creates kneeboard pages for each client flight in the mission."""
|
||||
|
||||
def __init__(self, mission: Mission) -> None:
|
||||
super().__init__(mission)
|
||||
def __init__(self, mission: Mission, game: "Game") -> None:
|
||||
super().__init__(mission, game)
|
||||
|
||||
def generate(self) -> None:
|
||||
"""Generates a kneeboard per client flight."""
|
||||
|
||||
22
gen/locations/preset_control_point_locations.py
Normal file
22
gen/locations/preset_control_point_locations.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from typing import List
|
||||
|
||||
from gen.locations.preset_locations import PresetLocation
|
||||
|
||||
|
||||
@dataclass
|
||||
class PresetControlPointLocations:
|
||||
"""A repository of preset locations for a given control point"""
|
||||
|
||||
# List of possible ashore locations to generate objects (Represented in miz file by an APC_AAV_7)
|
||||
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)
|
||||
offshore_locations: List[PresetLocation] = field(default_factory=list)
|
||||
|
||||
# Possible antiship missiles sites locations (Represented in miz file by Iranian Silkworm missiles)
|
||||
antiship_locations: List[PresetLocation] = field(default_factory=list)
|
||||
|
||||
# List of possible powerplants locations (Represented in miz file by static Workshop A object, USA)
|
||||
powerplant_locations: List[PresetLocation] = field(default_factory=list)
|
||||
59
gen/locations/preset_location_finder.py
Normal file
59
gen/locations/preset_location_finder.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from dcs import Mission, ships
|
||||
from dcs.vehicles import MissilesSS
|
||||
|
||||
from gen.locations.preset_control_point_locations import PresetControlPointLocations
|
||||
from gen.locations.preset_locations import PresetLocation
|
||||
|
||||
|
||||
class PresetLocationFinder:
|
||||
|
||||
@staticmethod
|
||||
def compute_possible_locations(terrain_name: str, cp_name: str) -> PresetControlPointLocations:
|
||||
"""
|
||||
Extract the list of preset locations from miz data
|
||||
:param terrain_name: Terrain/Map name
|
||||
:param cp_name: Control Point / Airbase name
|
||||
:return:
|
||||
"""
|
||||
|
||||
miz_file = Path("./resources/mizdata/", terrain_name.lower(), cp_name + ".miz")
|
||||
|
||||
offshore_locations: List[PresetLocation] = []
|
||||
ashore_locations: List[PresetLocation] = []
|
||||
powerplants_locations: List[PresetLocation] = []
|
||||
antiship_locations: List[PresetLocation] = []
|
||||
|
||||
if miz_file.exists():
|
||||
m = Mission()
|
||||
m.load_file(miz_file.absolute())
|
||||
|
||||
for vehicle_group in m.country("USA").vehicle_group:
|
||||
if len(vehicle_group.units) > 0:
|
||||
ashore_locations.append(PresetLocation(vehicle_group.position,
|
||||
vehicle_group.units[0].heading,
|
||||
vehicle_group.name))
|
||||
|
||||
for ship_group in m.country("USA").ship_group:
|
||||
if len(ship_group.units) > 0 and ship_group.units[0].type == ships.Oliver_Hazzard_Perry_class.id:
|
||||
offshore_locations.append(PresetLocation(ship_group.position,
|
||||
ship_group.units[0].heading,
|
||||
ship_group.name))
|
||||
|
||||
for static_group in m.country("USA").static_group:
|
||||
if len(static_group.units) > 0:
|
||||
powerplants_locations.append(PresetLocation(static_group.position,
|
||||
static_group.units[0].heading,
|
||||
static_group.name))
|
||||
|
||||
if m.country("Iran") is not None:
|
||||
for vehicle_group in m.country("Iran").vehicle_group:
|
||||
if len(vehicle_group.units) > 0 and vehicle_group.units[0].type == MissilesSS.SS_N_2_Silkworm.id:
|
||||
antiship_locations.append(PresetLocation(vehicle_group.position,
|
||||
vehicle_group.units[0].heading,
|
||||
vehicle_group.name))
|
||||
|
||||
return PresetControlPointLocations(ashore_locations, offshore_locations,
|
||||
antiship_locations, powerplants_locations)
|
||||
15
gen/locations/preset_locations.py
Normal file
15
gen/locations/preset_locations.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from dcs import Point
|
||||
|
||||
|
||||
@dataclass
|
||||
class PresetLocation:
|
||||
"""A preset location"""
|
||||
position: Point
|
||||
heading: int
|
||||
id: str
|
||||
|
||||
def __str__(self):
|
||||
return "-" * 10 + "X: {}\n Y: {}\nHdg: {}°\nId: {}".format(self.position.x, self.position.y, self.heading,
|
||||
self.id) + "-" * 10
|
||||
@@ -1,10 +1,12 @@
|
||||
import logging
|
||||
import random
|
||||
from game import db
|
||||
from gen.missiles.scud_site import ScudGenerator
|
||||
from gen.missiles.v1_group import V1GroupGenerator
|
||||
|
||||
MISSILES_MAP = {
|
||||
"V1GroupGenerator": V1GroupGenerator,
|
||||
"ScudGenerator": ScudGenerator
|
||||
}
|
||||
|
||||
|
||||
|
||||
30
gen/missiles/scud_site.py
Normal file
30
gen/missiles/scud_site.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import random
|
||||
|
||||
from dcs.vehicles import Unarmed, MissilesSS, AirDefence
|
||||
|
||||
from gen.sam.group_generator import GroupGenerator
|
||||
|
||||
|
||||
class ScudGenerator(GroupGenerator):
|
||||
|
||||
def __init__(self, game, ground_object, faction):
|
||||
super(ScudGenerator, self).__init__(game, ground_object)
|
||||
self.faction = faction
|
||||
|
||||
def generate(self):
|
||||
|
||||
# Scuds
|
||||
self.add_unit(MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M, "V1#0", self.position.x, self.position.y + random.randint(1, 8), self.heading)
|
||||
self.add_unit(MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M, "V1#1", self.position.x + 50, self.position.y + random.randint(1, 8), self.heading)
|
||||
self.add_unit(MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M, "V1#2", self.position.x + 100, self.position.y + random.randint(1, 8), self.heading)
|
||||
|
||||
# Commander
|
||||
self.add_unit(Unarmed.Transport_UAZ_469, "Kubel#0", self.position.x - 35, self.position.y - 20,
|
||||
self.heading)
|
||||
|
||||
# Shorad
|
||||
self.add_unit(AirDefence.SPAAA_ZSU_23_4_Shilka, "SHILKA#0", self.position.x - 55, self.position.y - 38,
|
||||
self.heading)
|
||||
|
||||
self.add_unit(AirDefence.SAM_SA_9_Strela_1_9P31, "STRELA#0",
|
||||
self.position.x + 200, self.position.y + 15, 90)
|
||||
@@ -126,6 +126,25 @@ RADIOS: List[Radio] = [
|
||||
|
||||
Radio("R&S M3AR VHF", MHz(120), MHz(174), step=MHz(1)),
|
||||
Radio("R&S M3AR UHF", MHz(225), MHz(400), step=MHz(1)),
|
||||
|
||||
# MiG-15bis
|
||||
Radio("RSI-6K HF", MHz(3, 750), MHz(5), step=kHz(25)),
|
||||
|
||||
# MiG-19P
|
||||
Radio("RSIU-4V", MHz(100), MHz(150), step=MHz(1)),
|
||||
|
||||
# MiG-21bis
|
||||
Radio("RSIU-5V", MHz(118), MHz(140), step=MHz(1)),
|
||||
|
||||
# Ka-50
|
||||
# Note: Also capable of 100MHz-150MHz, but we can't model gaps.
|
||||
Radio("R-800L1", MHz(220), MHz(400), step=kHz(25)),
|
||||
Radio("R-828", MHz(20), MHz(60), step=kHz(25)),
|
||||
|
||||
# UH-1H
|
||||
Radio("AN/ARC-51BX", MHz(225), MHz(400), step=kHz(50)),
|
||||
Radio("AN/ARC-131", MHz(30), MHz(76), step=kHz(50)),
|
||||
Radio("AN/ARC-134", MHz(116), MHz(150), step=kHz(25)),
|
||||
]
|
||||
|
||||
|
||||
|
||||
29
gen/sam/aaa_flak18.py
Normal file
29
gen/sam/aaa_flak18.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import random
|
||||
|
||||
from dcs.vehicles import AirDefence, Unarmed
|
||||
|
||||
from gen.sam.group_generator import GroupGenerator
|
||||
|
||||
|
||||
class Flak18Generator(GroupGenerator):
|
||||
"""
|
||||
This generate a German flak artillery group using only free units, thus not requiring the WW2 asset pack
|
||||
"""
|
||||
|
||||
name = "WW2 Flak Site"
|
||||
price = 40
|
||||
|
||||
def generate(self):
|
||||
|
||||
spacing = random.randint(30, 60)
|
||||
index = 0
|
||||
|
||||
for i in range(3):
|
||||
for j in range(2):
|
||||
index = index + 1
|
||||
self.add_unit(AirDefence.AAA_8_8cm_Flak_18, "AAA#" + str(index),
|
||||
self.position.x + spacing * i + random.randint(1, 5),
|
||||
self.position.y + spacing * j + random.randint(1, 5), self.heading)
|
||||
|
||||
# Add a commander truck
|
||||
self.add_unit(Unarmed.Blitz_3_6_6700A, "Blitz#", self.position.x - 35, self.position.y - 20, self.heading)
|
||||
34
gen/sam/aaa_ww2_ally_flak.py
Normal file
34
gen/sam/aaa_ww2_ally_flak.py
Normal file
@@ -0,0 +1,34 @@
|
||||
import random
|
||||
|
||||
from dcs.vehicles import AirDefence, Unarmed, Armor
|
||||
|
||||
from gen.sam.group_generator import GroupGenerator
|
||||
|
||||
|
||||
class AllyWW2FlakGenerator(GroupGenerator):
|
||||
"""
|
||||
This generate an ally flak artillery group
|
||||
"""
|
||||
|
||||
name = "WW2 Ally Flak Site"
|
||||
price = 140
|
||||
|
||||
def generate(self):
|
||||
|
||||
positions = self.get_circular_position(4, launcher_distance=50, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.AA_gun_QF_3_7, "AA#" + str(i), position[0], position[1], position[2])
|
||||
|
||||
positions = self.get_circular_position(8, launcher_distance=100, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.AAA_M1_37mm, "AA#" + str(4 + i), position[0], position[1], position[2])
|
||||
|
||||
positions = self.get_circular_position(8, launcher_distance=150, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.AAA_M45_Quadmount, "AA#" + str(12 + i), position[0], position[1], position[2])
|
||||
|
||||
# Add a commander truck
|
||||
self.add_unit(Unarmed.Willys_MB, "CMD#1", self.position.x, self.position.y - 20, random.randint(0, 360))
|
||||
self.add_unit(Armor.M30_Cargo_Carrier, "LOG#1", self.position.x, self.position.y + 20, random.randint(0, 360))
|
||||
self.add_unit(Armor.M4_Tractor, "LOG#2", self.position.x + 20, self.position.y, random.randint(0, 360))
|
||||
self.add_unit(Unarmed.Bedford_MWD, "LOG#3", self.position.x - 20, self.position.y, random.randint(0, 360))
|
||||
72
gen/sam/cold_war_flak.py
Normal file
72
gen/sam/cold_war_flak.py
Normal file
@@ -0,0 +1,72 @@
|
||||
import random
|
||||
|
||||
from dcs.vehicles import AirDefence, Unarmed
|
||||
|
||||
from gen.sam.group_generator import GroupGenerator
|
||||
|
||||
|
||||
class EarlyColdWarFlakGenerator(GroupGenerator):
|
||||
"""
|
||||
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.
|
||||
|
||||
This does not include search lights and telemeter computer (Kdo.G 40) because these are paid units only available in WW2 asset pack
|
||||
"""
|
||||
|
||||
name = "Early Cold War Flak Site"
|
||||
price = 58
|
||||
|
||||
def generate(self):
|
||||
|
||||
spacing = random.randint(30, 60)
|
||||
index = 0
|
||||
|
||||
# Long range guns
|
||||
for i in range(3):
|
||||
for j in range(2):
|
||||
index = index + 1
|
||||
self.add_unit(AirDefence.AAA_8_8cm_Flak_18, "AAA#" + str(index),
|
||||
self.position.x + spacing * i + random.randint(1, 5),
|
||||
self.position.y + spacing * j + random.randint(1, 5), self.heading)
|
||||
|
||||
# Short range guns
|
||||
self.add_unit(AirDefence.AAA_Bofors_40mm, "SHO#1",
|
||||
self.position.x - 40, self.position.y - 40, self.heading + 180),
|
||||
self.add_unit(AirDefence.AAA_Bofors_40mm, "SHO#1",
|
||||
self.position.x + spacing * 2 + 40, self.position.y + spacing + 40, self.heading),
|
||||
|
||||
# Add a truck
|
||||
self.add_unit(Unarmed.Transport_KAMAZ_43101, "Truck#", self.position.x - 60, self.position.y - 20, self.heading)
|
||||
|
||||
|
||||
class ColdWarFlakGenerator(GroupGenerator):
|
||||
"""
|
||||
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 site is also fitted with a P-19 radar for early detection.
|
||||
"""
|
||||
|
||||
name = "Cold War Flak Site"
|
||||
price = 72
|
||||
|
||||
def generate(self):
|
||||
|
||||
spacing = random.randint(30, 60)
|
||||
index = 0
|
||||
|
||||
# Long range guns
|
||||
for i in range(3):
|
||||
for j in range(2):
|
||||
index = index + 1
|
||||
self.add_unit(AirDefence.AAA_8_8cm_Flak_18, "AAA#" + str(index),
|
||||
self.position.x + spacing * i + random.randint(1, 5),
|
||||
self.position.y + spacing * j + random.randint(1, 5), self.heading)
|
||||
|
||||
# Short range guns
|
||||
self.add_unit(AirDefence.AAA_ZU_23_Closed, "SHO#1",
|
||||
self.position.x - 40, self.position.y - 40, self.heading + 180),
|
||||
self.add_unit(AirDefence.AAA_ZU_23_Closed, "SHO#1",
|
||||
self.position.x + spacing * 2 + 40, self.position.y + spacing + 40, self.heading),
|
||||
|
||||
# Add a P19 Radar for EWR
|
||||
self.add_unit(AirDefence.SAM_SR_P_19, "SR#0", self.position.x - 60, self.position.y - 20, self.heading)
|
||||
98
gen/sam/ewrs.py
Normal file
98
gen/sam/ewrs.py
Normal file
@@ -0,0 +1,98 @@
|
||||
from dcs.vehicles import AirDefence
|
||||
from dcs.unittype import VehicleType
|
||||
|
||||
from gen.sam.group_generator import GroupGenerator
|
||||
|
||||
|
||||
class EwrGenerator(GroupGenerator):
|
||||
@property
|
||||
def unit_type(self) -> VehicleType:
|
||||
raise NotImplementedError
|
||||
|
||||
def generate(self) -> None:
|
||||
self.add_unit(self.unit_type, "EWR", self.position.x, self.position.y,
|
||||
self.heading)
|
||||
|
||||
|
||||
class BoxSpringGenerator(EwrGenerator):
|
||||
"""1L13 "Box Spring" EWR."""
|
||||
|
||||
unit_type = AirDefence.EWR_1L13
|
||||
|
||||
|
||||
class TallRackGenerator(EwrGenerator):
|
||||
"""55G6 "Tall Rack" EWR."""
|
||||
|
||||
unit_type = AirDefence.EWR_55G6
|
||||
|
||||
|
||||
class DogEarGenerator(EwrGenerator):
|
||||
"""9S80M1 "Dog Ear" EWR.
|
||||
|
||||
This is the SA-8 search radar, but used as an early warning radar.
|
||||
"""
|
||||
|
||||
unit_type = AirDefence.CP_9S80M1_Sborka
|
||||
|
||||
|
||||
class RolandEwrGenerator(EwrGenerator):
|
||||
"""Roland EWR.
|
||||
|
||||
This is the Roland search radar, but used as an early warning radar.
|
||||
"""
|
||||
|
||||
unit_type = AirDefence.SAM_Roland_EWR
|
||||
|
||||
|
||||
class FlatFaceGenerator(EwrGenerator):
|
||||
"""P-19 "Flat Face" EWR.
|
||||
|
||||
This is the SA-3 search radar, but used as an early warning radar.
|
||||
"""
|
||||
|
||||
unit_type = AirDefence.SAM_SR_P_19
|
||||
|
||||
|
||||
class PatriotEwrGenerator(EwrGenerator):
|
||||
"""Patriot EWR.
|
||||
|
||||
This is the Patriot search/track radar, but used as an early warning radar.
|
||||
"""
|
||||
|
||||
unit_type = AirDefence.SAM_Patriot_STR_AN_MPQ_53
|
||||
|
||||
|
||||
class BigBirdGenerator(EwrGenerator):
|
||||
"""64H6E "Big Bird" EWR.
|
||||
|
||||
This is the SA-10 track radar, but used as an early warning radar.
|
||||
"""
|
||||
|
||||
unit_type = AirDefence.SAM_SA_10_S_300PS_SR_64H6E
|
||||
|
||||
|
||||
class SnowDriftGenerator(EwrGenerator):
|
||||
"""9S18M1 "Snow Drift" EWR.
|
||||
|
||||
This is the SA-11 search radar, but used as an early warning radar.
|
||||
"""
|
||||
|
||||
unit_type = AirDefence.SAM_SA_11_Buk_SR_9S18M1
|
||||
|
||||
|
||||
class StraightFlushGenerator(EwrGenerator):
|
||||
"""1S91 "Straight Flush" EWR.
|
||||
|
||||
This is the SA-6 search/track radar, but used as an early warning radar.
|
||||
"""
|
||||
|
||||
unit_type = AirDefence.SAM_SA_6_Kub_STR_9S91
|
||||
|
||||
|
||||
class HawkEwrGenerator(EwrGenerator):
|
||||
"""Hawk EWR.
|
||||
|
||||
This is the Hawk search radar, but used as an early warning radar.
|
||||
"""
|
||||
|
||||
unit_type = AirDefence.SAM_Hawk_SR_AN_MPQ_50
|
||||
39
gen/sam/freya_ewr.py
Normal file
39
gen/sam/freya_ewr.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import random
|
||||
|
||||
from dcs.vehicles import AirDefence, Unarmed, Infantry
|
||||
|
||||
from gen.sam.group_generator import GroupGenerator
|
||||
|
||||
|
||||
class FreyaGenerator(GroupGenerator):
|
||||
"""
|
||||
This generate a German flak artillery group using only free units, thus not requiring the WW2 asset pack
|
||||
"""
|
||||
|
||||
name = "Freya EWR Site"
|
||||
price = 60
|
||||
|
||||
def generate(self):
|
||||
|
||||
# TODO : would be better with the Concrete structure that is supposed to protect it
|
||||
self.add_unit(AirDefence.EWR_FuMG_401_Freya_LZ, "EWR#1", self.position.x, self.position.y, self.heading)
|
||||
|
||||
positions = self.get_circular_position(4, launcher_distance=50, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.AAA_Flak_Vierling_38, "AA#" + str(i), position[0], position[1], position[2])
|
||||
|
||||
positions = self.get_circular_position(4, launcher_distance=100, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.AAA_8_8cm_Flak_18, "AA#" + str(4+i), position[0], position[1], position[2])
|
||||
|
||||
# Command/Logi
|
||||
self.add_unit(Unarmed.Kübelwagen_82, "Kubel#1", self.position.x - 20, self.position.y - 20, self.heading)
|
||||
self.add_unit(Unarmed.Sd_Kfz_7, "Sdkfz#1", self.position.x + 20, self.position.y + 22, self.heading)
|
||||
self.add_unit(Unarmed.Sd_Kfz_2, "Sdkfz#2", self.position.x - 22, self.position.y + 20, self.heading)
|
||||
|
||||
# Maschinensatz_33 and Kdo.g 40 Telemeter
|
||||
self.add_unit(AirDefence.Maschinensatz_33, "Energy#1", self.position.x + 20, self.position.y - 20, self.heading)
|
||||
self.add_unit(AirDefence.AAA_Kdo_G_40, "Telemeter#1", self.position.x + 20, self.position.y - 10, self.heading)
|
||||
self.add_unit(Infantry.Infantry_Mauser_98, "Inf#1", self.position.x + 20, self.position.y - 14, self.heading)
|
||||
self.add_unit(Infantry.Infantry_Mauser_98, "Inf#2", self.position.x + 20, self.position.y - 22, self.heading)
|
||||
self.add_unit(Infantry.Infantry_Mauser_98, "Inf#3", self.position.x + 20, self.position.y - 24, self.heading + 45)
|
||||
@@ -1,19 +1,15 @@
|
||||
import random
|
||||
from abc import ABC
|
||||
|
||||
from dcs.vehicles import AirDefence
|
||||
from game import db
|
||||
from game import Game
|
||||
from gen.sam.group_generator import GroupGenerator
|
||||
from theater.theatergroundobject import SamGroundObject
|
||||
|
||||
|
||||
class GenericSamGroupGenerator(GroupGenerator):
|
||||
class GenericSamGroupGenerator(GroupGenerator, ABC):
|
||||
"""
|
||||
This is the base for all SAM group generators
|
||||
"""
|
||||
|
||||
@property
|
||||
def groupNamePrefix(self) -> str:
|
||||
# prefix the SAM site for use with the Skynet IADS plugin
|
||||
if self.faction == self.game.player_name: # this is the player faction
|
||||
return "BLUE SAM "
|
||||
else:
|
||||
return "RED SAM "
|
||||
def __init__(self, game: Game, ground_object: SamGroundObject) -> None:
|
||||
ground_object.skynet_capable = True
|
||||
super().__init__(game, ground_object)
|
||||
|
||||
@@ -1,39 +1,47 @@
|
||||
from __future__ import annotations
|
||||
import math
|
||||
import random
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from dcs import unitgroup
|
||||
from dcs.point import PointAction
|
||||
from dcs.unit import Vehicle
|
||||
from dcs.unit import Vehicle, Ship
|
||||
from dcs.unittype import VehicleType
|
||||
|
||||
from game.factions.faction import Faction
|
||||
from theater.theatergroundobject import TheaterGroundObject
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.game import Game
|
||||
|
||||
|
||||
class GroupGenerator():
|
||||
# TODO: Generate a group description rather than a pydcs group.
|
||||
# It appears that all of this work gets redone at miz generation time (see
|
||||
# groundobjectsgen for an example). We can do less work and include the data we
|
||||
# care about in the format we want if we just generate our own group description
|
||||
# types rather than pydcs groups.
|
||||
class GroupGenerator:
|
||||
|
||||
def __init__(self, game, ground_object, faction = None): # faction is not mandatory because some subclasses do not use it
|
||||
def __init__(self, game: Game, ground_object: TheaterGroundObject) -> None:
|
||||
self.game = game
|
||||
self.go = ground_object
|
||||
self.position = ground_object.position
|
||||
self.heading = random.randint(0, 359)
|
||||
self.faction = faction
|
||||
self.vg = unitgroup.VehicleGroup(self.game.next_group_id(), self.groupNamePrefix + self.go.group_identifier)
|
||||
self.vg = unitgroup.VehicleGroup(self.game.next_group_id(),
|
||||
self.go.group_name)
|
||||
wp = self.vg.add_waypoint(self.position, PointAction.OffRoad, 0)
|
||||
wp.ETA_locked = True
|
||||
|
||||
@property
|
||||
def groupNamePrefix(self) -> str:
|
||||
return ""
|
||||
|
||||
def generate(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_generated_group(self):
|
||||
def get_generated_group(self) -> unitgroup.VehicleGroup:
|
||||
return self.vg
|
||||
|
||||
def add_unit(self, unit_type, name, pos_x, pos_y, heading):
|
||||
|
||||
nn = "cgroup|" + str(self.go.cp_id) + '|' + str(self.go.group_id) + '|' + str(self.go.group_identifier) + "|" + name
|
||||
|
||||
def add_unit(self, unit_type: VehicleType, name: str, pos_x: float,
|
||||
pos_y: float, heading: int) -> Vehicle:
|
||||
unit = Vehicle(self.game.next_unit_id(),
|
||||
nn, unit_type.id)
|
||||
f"{self.go.group_name}|{name}", unit_type.id)
|
||||
unit.position.x = pos_x
|
||||
unit.position.y = pos_y
|
||||
unit.heading = heading
|
||||
@@ -75,3 +83,25 @@ class GroupGenerator():
|
||||
current_offset += outer_offset
|
||||
return positions
|
||||
|
||||
|
||||
class ShipGroupGenerator(GroupGenerator):
|
||||
"""Abstract class for other ship generator classes"""
|
||||
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
|
||||
self.game = game
|
||||
self.go = ground_object
|
||||
self.position = ground_object.position
|
||||
self.heading = random.randint(0, 359)
|
||||
self.faction = faction
|
||||
self.vg = unitgroup.ShipGroup(self.game.next_group_id(),
|
||||
self.go.group_name)
|
||||
wp = self.vg.add_waypoint(self.position, 0)
|
||||
wp.ETA_locked = True
|
||||
|
||||
def add_unit(self, unit_type, name, pos_x, pos_y, heading) -> Ship:
|
||||
unit = Ship(self.game.next_unit_id(),
|
||||
f"{self.go.group_name}|{name}", unit_type)
|
||||
unit.position.x = pos_x
|
||||
unit.position.y = pos_y
|
||||
unit.heading = heading
|
||||
self.vg.add_unit(unit)
|
||||
return unit
|
||||
|
||||
@@ -1,13 +1,30 @@
|
||||
import random
|
||||
from typing import List, Type
|
||||
from typing import List, Optional, Type
|
||||
|
||||
from dcs.unittype import UnitType
|
||||
from dcs.vehicles import AirDefence
|
||||
from dcs.unitgroup import VehicleGroup
|
||||
|
||||
from game import db
|
||||
from game import Game, db
|
||||
from gen.sam.aaa_bofors import BoforsGenerator
|
||||
from gen.sam.aaa_flak import FlakGenerator
|
||||
from gen.sam.aaa_flak18 import Flak18Generator
|
||||
from gen.sam.aaa_ww2_ally_flak import AllyWW2FlakGenerator
|
||||
from gen.sam.aaa_zu23_insurgent import ZU23InsurgentGenerator
|
||||
from gen.sam.cold_war_flak import EarlyColdWarFlakGenerator, ColdWarFlakGenerator
|
||||
|
||||
|
||||
from gen.sam.ewrs import (
|
||||
BigBirdGenerator,
|
||||
BoxSpringGenerator,
|
||||
DogEarGenerator,
|
||||
FlatFaceGenerator,
|
||||
HawkEwrGenerator,
|
||||
PatriotEwrGenerator,
|
||||
RolandEwrGenerator,
|
||||
SnowDriftGenerator,
|
||||
StraightFlushGenerator,
|
||||
TallRackGenerator,
|
||||
)
|
||||
from gen.sam.group_generator import GroupGenerator
|
||||
from gen.sam.sam_avenger import AvengerGenerator
|
||||
from gen.sam.sam_chaparral import ChaparralGenerator
|
||||
@@ -33,6 +50,9 @@ from gen.sam.sam_zsu23 import ZSU23Generator
|
||||
from gen.sam.sam_zu23 import ZU23Generator
|
||||
from gen.sam.sam_zu23_ural import ZU23UralGenerator
|
||||
from gen.sam.sam_zu23_ural_insurgent import ZU23UralInsurgentGenerator
|
||||
from gen.sam.freya_ewr import FreyaGenerator
|
||||
from theater import TheaterGroundObject
|
||||
from theater.theatergroundobject import SamGroundObject
|
||||
|
||||
SAM_MAP = {
|
||||
"HawkGenerator": HawkGenerator,
|
||||
@@ -61,7 +81,12 @@ SAM_MAP = {
|
||||
"SA13Generator": SA13Generator,
|
||||
"SA15Generator": SA15Generator,
|
||||
"SA19Generator": SA19Generator,
|
||||
"HQ7Generator": HQ7Generator
|
||||
"HQ7Generator": HQ7Generator,
|
||||
"Flak18Generator": Flak18Generator,
|
||||
"ColdWarFlakGenerator": ColdWarFlakGenerator,
|
||||
"EarlyColdWarFlakGenerator": EarlyColdWarFlakGenerator,
|
||||
"FreyaGenerator": FreyaGenerator,
|
||||
"AllyWW2FlakGenerator": AllyWW2FlakGenerator
|
||||
}
|
||||
|
||||
SAM_PRICES = {
|
||||
@@ -98,32 +123,74 @@ SAM_PRICES = {
|
||||
AirDefence.HQ_7_Self_Propelled_LN: 35
|
||||
}
|
||||
|
||||
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_sams_generator(faction: str) -> List[Type[GroupGenerator]]:
|
||||
"""
|
||||
Return the list of possible SAM generator for the given faction
|
||||
:param faction: Faction name to search units for
|
||||
"""
|
||||
return [SAM_MAP[s] for s in db.FACTIONS[faction].sams if s in SAM_MAP.keys()]
|
||||
return [SAM_MAP[s] for s in db.FACTIONS[faction].sams if s in SAM_MAP]
|
||||
|
||||
def generate_anti_air_group(game, parent_cp, ground_object, faction:str):
|
||||
|
||||
def get_faction_possible_ewrs_generator(faction: str) -> List[Type[GroupGenerator]]:
|
||||
"""
|
||||
Return the list of possible SAM generator for the given faction
|
||||
:param faction: Faction name to search units for
|
||||
"""
|
||||
return [EWR_MAP[s] for s in db.FACTIONS[faction].ewrs if s in EWR_MAP]
|
||||
|
||||
|
||||
def generate_anti_air_group(game: Game, ground_object: TheaterGroundObject,
|
||||
faction: str) -> Optional[VehicleGroup]:
|
||||
"""
|
||||
This generate a SAM group
|
||||
:param parentCp: The parent control point
|
||||
:param ground_object: The ground object which will own the sam group
|
||||
:param country: Owner country
|
||||
:return: Nothing, but put the group reference inside the ground object
|
||||
:param game: The Game.
|
||||
:param ground_object: The ground object which will own the sam group.
|
||||
:param faction: Owner faction.
|
||||
:return: The generated group, or None if one could not be generated.
|
||||
"""
|
||||
possible_sams_generators = get_faction_possible_sams_generator(faction)
|
||||
if len(possible_sams_generators) > 0:
|
||||
sam_generator_class = random.choice(possible_sams_generators)
|
||||
generator = sam_generator_class(game, ground_object, faction)
|
||||
generator = sam_generator_class(game, ground_object)
|
||||
generator.generate()
|
||||
return generator.get_generated_group()
|
||||
return None
|
||||
|
||||
|
||||
def generate_shorad_group(game, parent_cp, ground_object, faction_name: str):
|
||||
def generate_ewr_group(game: Game, ground_object: TheaterGroundObject,
|
||||
faction: str) -> 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
|
||||
|
||||
|
||||
def generate_shorad_group(game: Game, ground_object: SamGroundObject,
|
||||
faction_name: str) -> Optional[VehicleGroup]:
|
||||
faction = db.FACTIONS[faction_name]
|
||||
|
||||
if len(faction.shorads) > 0:
|
||||
@@ -132,9 +199,4 @@ def generate_shorad_group(game, parent_cp, ground_object, faction_name: str):
|
||||
generator.generate()
|
||||
return generator.get_generated_group()
|
||||
else:
|
||||
return generate_anti_air_group(game, parent_cp, ground_object, faction_name)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return generate_anti_air_group(game, ground_object, faction_name)
|
||||
|
||||
@@ -28,6 +28,6 @@ class PatriotGenerator(GenericSamGroupGenerator):
|
||||
|
||||
# Short range protection for high value site
|
||||
num_launchers = random.randint(3, 4)
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=300, coverage=360)
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=200, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.AAA_Vulcan_M163, "SPAAA#" + str(i), position[0], position[1], position[2])
|
||||
@@ -31,7 +31,7 @@ class SA10Generator(GenericSamGroupGenerator):
|
||||
|
||||
# 2 different launcher type (C & D)
|
||||
num_launchers = random.randint(6, 8)
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=360)
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=100, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
if i%2 == 0:
|
||||
self.add_unit(AirDefence.SAM_SA_10_S_300PS_LN_5P85C, "LN#" + str(i), position[0], position[1], position[2])
|
||||
@@ -41,12 +41,12 @@ class SA10Generator(GenericSamGroupGenerator):
|
||||
# Then let's add short range protection to this high value site
|
||||
# Sa-13 Strela are great for that
|
||||
num_launchers = random.randint(2, 4)
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=300, coverage=360)
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=140, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.SAM_SA_13_Strela_10M3_9A35M3, "IR#" + str(i), position[0], position[1], position[2])
|
||||
|
||||
# And even some AA
|
||||
num_launchers = random.randint(6, 8)
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=350, coverage=360)
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=210, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.SPAAA_ZSU_23_4_Shilka, "AA#" + str(i), position[0], position[1], position[2])
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dcs.action import MarkToAll
|
||||
from dcs.condition import TimeAfter
|
||||
from dcs.mission import Mission
|
||||
@@ -5,7 +7,7 @@ from dcs.task import Option
|
||||
from dcs.translation import String
|
||||
from dcs.triggers import Event, TriggerOnce
|
||||
from dcs.unit import Skill
|
||||
|
||||
from dcs.unitgroup import FlyingGroup
|
||||
from .conflictgen import Conflict
|
||||
|
||||
PUSH_TRIGGER_SIZE = 3000
|
||||
@@ -73,8 +75,9 @@ class TriggersGenerator:
|
||||
continue
|
||||
|
||||
for country in coalition.countries.values():
|
||||
for plane_group in country.plane_group:
|
||||
for plane_unit in plane_group.units:
|
||||
flying_groups = country.plane_group + country.helicopter_group # type: FlyingGroup
|
||||
for flying_group in flying_groups:
|
||||
for plane_unit in flying_group.units:
|
||||
if plane_unit.skill != Skill.Client and plane_unit.skill != Skill.Player:
|
||||
plane_unit.skill = Skill(skill_level[0])
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
from .luaplugin import LuaPlugin
|
||||
from .manager import LuaPluginManager
|
||||
@@ -1,208 +0,0 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtWidgets import QCheckBox, QGridLayout, QGroupBox, QLabel
|
||||
|
||||
|
||||
class LuaPluginWorkOrder:
|
||||
|
||||
def __init__(self, parent, filename: str, mnemonic: str,
|
||||
disable: bool) -> None:
|
||||
self.filename = filename
|
||||
self.mnemonic = mnemonic
|
||||
self.disable = disable
|
||||
self.parent = parent
|
||||
|
||||
def work(self, operation):
|
||||
if self.disable:
|
||||
operation.bypass_plugin_script(self.mnemonic)
|
||||
else:
|
||||
operation.inject_plugin_script(self.parent.mnemonic, self.filename,
|
||||
self.mnemonic)
|
||||
|
||||
class LuaPluginSpecificOption:
|
||||
|
||||
def __init__(self, parent, mnemonic: str, nameInUI: str,
|
||||
defaultValue: bool) -> None:
|
||||
self.mnemonic = mnemonic
|
||||
self.nameInUI = nameInUI
|
||||
self.defaultValue = defaultValue
|
||||
self.parent = parent
|
||||
|
||||
class LuaPlugin:
|
||||
NAME_IN_SETTINGS_BASE:str = "plugins."
|
||||
|
||||
def __init__(self, jsonFilename: str) -> None:
|
||||
self.mnemonic: Optional[str] = None
|
||||
self.skipUI: bool = False
|
||||
self.nameInUI: Optional[str] = None
|
||||
self.nameInSettings: Optional[str] = None
|
||||
self.defaultValue: bool = False
|
||||
self.specificOptions: List[LuaPluginSpecificOption] = []
|
||||
self.scriptsWorkOrders: List[LuaPluginWorkOrder] = []
|
||||
self.configurationWorkOrders: List[LuaPluginWorkOrder] = []
|
||||
self.initFromJson(jsonFilename)
|
||||
self.enabled = self.defaultValue
|
||||
self.settings = None
|
||||
|
||||
def initFromJson(self, jsonFilename:str):
|
||||
jsonFile:Path = Path(jsonFilename)
|
||||
if jsonFile.exists():
|
||||
jsonData = json.loads(jsonFile.read_text())
|
||||
self.mnemonic = jsonData.get("mnemonic")
|
||||
self.skipUI = jsonData.get("skipUI", False)
|
||||
self.nameInUI = jsonData.get("nameInUI")
|
||||
assert self.mnemonic is not None
|
||||
self.nameInSettings = LuaPlugin.NAME_IN_SETTINGS_BASE + self.mnemonic
|
||||
self.defaultValue = jsonData.get("defaultValue", False)
|
||||
self.specificOptions = []
|
||||
for jsonSpecificOption in jsonData.get("specificOptions"):
|
||||
mnemonic = jsonSpecificOption.get("mnemonic")
|
||||
nameInUI = jsonSpecificOption.get("nameInUI", mnemonic)
|
||||
defaultValue = jsonSpecificOption.get("defaultValue")
|
||||
self.specificOptions.append(LuaPluginSpecificOption(self, mnemonic, nameInUI, defaultValue))
|
||||
self.scriptsWorkOrders = []
|
||||
for jsonWorkOrder in jsonData.get("scriptsWorkOrders"):
|
||||
file = jsonWorkOrder.get("file")
|
||||
mnemonic = jsonWorkOrder.get("mnemonic")
|
||||
disable = jsonWorkOrder.get("disable", False)
|
||||
self.scriptsWorkOrders.append(LuaPluginWorkOrder(self, file, mnemonic, disable))
|
||||
self.configurationWorkOrders = []
|
||||
for jsonWorkOrder in jsonData.get("configurationWorkOrders"):
|
||||
file = jsonWorkOrder.get("file")
|
||||
mnemonic = jsonWorkOrder.get("mnemonic")
|
||||
disable = jsonWorkOrder.get("disable", False)
|
||||
self.configurationWorkOrders.append(LuaPluginWorkOrder(self, file, mnemonic, disable))
|
||||
|
||||
def setupUI(self, settingsWindow, row:int):
|
||||
# set the game settings
|
||||
self.setSettings(settingsWindow.game.settings)
|
||||
|
||||
if not self.skipUI:
|
||||
assert self.nameInSettings is not None
|
||||
assert self.settings is not None
|
||||
|
||||
# create the plugin choice checkbox interface
|
||||
self.uiWidget: QCheckBox = QCheckBox()
|
||||
self.uiWidget.setChecked(self.isEnabled())
|
||||
self.uiWidget.toggled.connect(lambda: self.applySetting(settingsWindow))
|
||||
|
||||
settingsWindow.pluginsGroupLayout.addWidget(QLabel(self.nameInUI), row, 0)
|
||||
settingsWindow.pluginsGroupLayout.addWidget(self.uiWidget, row, 1, Qt.AlignRight)
|
||||
|
||||
# if needed, create the plugin options special page
|
||||
if settingsWindow.pluginsOptionsPageLayout and self.specificOptions != None:
|
||||
self.optionsGroup: QGroupBox = QGroupBox(self.nameInUI)
|
||||
optionsGroupLayout = QGridLayout();
|
||||
optionsGroupLayout.setAlignment(Qt.AlignTop)
|
||||
self.optionsGroup.setLayout(optionsGroupLayout)
|
||||
settingsWindow.pluginsOptionsPageLayout.addWidget(self.optionsGroup)
|
||||
|
||||
# browse each option in the specific options list
|
||||
row = 0
|
||||
for specificOption in self.specificOptions:
|
||||
assert specificOption.mnemonic is not None
|
||||
nameInSettings = self.nameInSettings + "." + specificOption.mnemonic
|
||||
if not nameInSettings in self.settings.plugins:
|
||||
self.settings.plugins[nameInSettings] = specificOption.defaultValue
|
||||
|
||||
specificOption.uiWidget = QCheckBox()
|
||||
specificOption.uiWidget.setChecked(self.settings.plugins[nameInSettings])
|
||||
#specificOption.uiWidget.setEnabled(False)
|
||||
specificOption.uiWidget.toggled.connect(lambda: self.applySetting(settingsWindow))
|
||||
|
||||
optionsGroupLayout.addWidget(QLabel(specificOption.nameInUI), row, 0)
|
||||
optionsGroupLayout.addWidget(specificOption.uiWidget, row, 1, Qt.AlignRight)
|
||||
|
||||
row += 1
|
||||
|
||||
# disable or enable the UI in the plugins special page
|
||||
self.enableOptionsGroup()
|
||||
|
||||
def enableOptionsGroup(self):
|
||||
if self.optionsGroup:
|
||||
self.optionsGroup.setEnabled(self.isEnabled())
|
||||
|
||||
def setSettings(self, settings):
|
||||
self.settings = settings
|
||||
|
||||
# ensure the setting exist
|
||||
if not self.nameInSettings in self.settings.plugins:
|
||||
self.settings.plugins[self.nameInSettings] = self.defaultValue
|
||||
|
||||
# do the same for each option in the specific options list
|
||||
for specificOption in self.specificOptions:
|
||||
nameInSettings = self.nameInSettings + "." + specificOption.mnemonic
|
||||
if not nameInSettings in self.settings.plugins:
|
||||
self.settings.plugins[nameInSettings] = specificOption.defaultValue
|
||||
|
||||
def applySetting(self, settingsWindow):
|
||||
# apply the main setting
|
||||
self.settings.plugins[self.nameInSettings] = self.uiWidget.isChecked()
|
||||
self.enabled = self.settings.plugins[self.nameInSettings]
|
||||
|
||||
# do the same for each option in the specific options list
|
||||
for specificOption in self.specificOptions:
|
||||
nameInSettings = self.nameInSettings + "." + specificOption.mnemonic
|
||||
self.settings.plugins[nameInSettings] = specificOption.uiWidget.isChecked()
|
||||
|
||||
# disable or enable the UI in the plugins special page
|
||||
self.enableOptionsGroup()
|
||||
|
||||
def injectScripts(self, operation):
|
||||
# set the game settings
|
||||
self.setSettings(operation.game.settings)
|
||||
|
||||
# execute the work order
|
||||
if self.scriptsWorkOrders != None:
|
||||
for workOrder in self.scriptsWorkOrders:
|
||||
workOrder.work(operation)
|
||||
|
||||
# serves for subclasses
|
||||
return self.isEnabled()
|
||||
|
||||
def injectConfiguration(self, operation):
|
||||
# set the game settings
|
||||
self.setSettings(operation.game.settings)
|
||||
|
||||
# inject the plugin options
|
||||
if len(self.specificOptions) > 0:
|
||||
defineAllOptions = ""
|
||||
for specificOption in self.specificOptions:
|
||||
nameInSettings = self.nameInSettings + "." + specificOption.mnemonic
|
||||
value = "true" if self.settings.plugins[nameInSettings] else "false"
|
||||
defineAllOptions += f" dcsLiberation.plugins.{self.mnemonic}.{specificOption.mnemonic} = {value} \n"
|
||||
|
||||
|
||||
lua = f"-- {self.mnemonic} plugin configuration.\n"
|
||||
lua += "\n"
|
||||
lua += "if dcsLiberation then\n"
|
||||
lua += " if not dcsLiberation.plugins then \n"
|
||||
lua += " dcsLiberation.plugins = {}\n"
|
||||
lua += " end\n"
|
||||
lua += f" dcsLiberation.plugins.{self.mnemonic} = {{}}\n"
|
||||
lua += defineAllOptions
|
||||
lua += "end"
|
||||
|
||||
operation.inject_lua_trigger(lua, f"{self.mnemonic} plugin configuration")
|
||||
|
||||
# execute the work order
|
||||
if self.configurationWorkOrders != None:
|
||||
for workOrder in self.configurationWorkOrders:
|
||||
workOrder.work(operation)
|
||||
|
||||
# serves for subclasses
|
||||
return self.isEnabled()
|
||||
|
||||
def isEnabled(self) -> bool:
|
||||
if not self.settings:
|
||||
return False
|
||||
|
||||
self.setSettings(self.settings) # create the necessary settings keys if needed
|
||||
|
||||
return self.settings != None and self.settings.plugins[self.nameInSettings]
|
||||
|
||||
def hasUI(self) -> bool:
|
||||
return not self.skipUI
|
||||
@@ -1,43 +0,0 @@
|
||||
from .luaplugin import LuaPlugin
|
||||
from typing import List
|
||||
import glob
|
||||
from pathlib import Path
|
||||
import json
|
||||
import logging
|
||||
|
||||
|
||||
class LuaPluginManager():
|
||||
PLUGINS_RESOURCE_PATH = Path("resources/plugins")
|
||||
PLUGINS_LIST_FILENAME = "plugins.json"
|
||||
PLUGINS_JSON_FILENAME = "plugin.json"
|
||||
|
||||
__plugins = None
|
||||
def __init__(self):
|
||||
if not LuaPluginManager.__plugins:
|
||||
LuaPluginManager.__plugins= []
|
||||
jsonFile:Path = Path(LuaPluginManager.PLUGINS_RESOURCE_PATH, LuaPluginManager.PLUGINS_LIST_FILENAME)
|
||||
if jsonFile.exists():
|
||||
logging.info(f"Reading plugins list from {jsonFile}")
|
||||
|
||||
jsonData = json.loads(jsonFile.read_text())
|
||||
for plugin in jsonData:
|
||||
jsonPluginFolder = Path(LuaPluginManager.PLUGINS_RESOURCE_PATH, plugin)
|
||||
jsonPluginFile = Path(jsonPluginFolder, LuaPluginManager.PLUGINS_JSON_FILENAME)
|
||||
if jsonPluginFile.exists():
|
||||
logging.info(f"Reading plugin {plugin} from {jsonPluginFile}")
|
||||
plugin = LuaPlugin(jsonPluginFile)
|
||||
LuaPluginManager.__plugins.append(plugin)
|
||||
else:
|
||||
logging.error(f"Missing configuration file {jsonPluginFile} for plugin {plugin}")
|
||||
else:
|
||||
logging.error(f"Missing plugins list file {jsonFile}")
|
||||
|
||||
def getPlugins(self):
|
||||
return LuaPluginManager.__plugins
|
||||
|
||||
def getPlugin(self, pluginName):
|
||||
for plugin in LuaPluginManager.__plugins:
|
||||
if plugin.mnemonic == pluginName:
|
||||
return plugin
|
||||
|
||||
return None
|
||||
2
pydcs
2
pydcs
Submodule pydcs updated: c12733a471...2883be31c2
169
pydcs_extensions/highdigitsams/highdigitsams.py
Normal file
169
pydcs_extensions/highdigitsams/highdigitsams.py
Normal file
@@ -0,0 +1,169 @@
|
||||
from dcs import unittype
|
||||
|
||||
|
||||
class SAM_SA_20_S_300PMU1_TR_30N6E(unittype.VehicleType):
|
||||
id = "S-300PMU1 40B6M tr"
|
||||
name = "SAM SA-20 S-300PMU1 TR 30N6E"
|
||||
detection_range = 160000
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_20_S_300PMU1_TR_30N6E_truck(unittype.VehicleType):
|
||||
id = "S-300PMU1 30N6E tr"
|
||||
name = "SAM SA-20 S-300PMU1 TR 30N6E(truck)"
|
||||
detection_range = 160000
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_20_S_300PMU1_SR_5N66E(unittype.VehicleType):
|
||||
id = "S-300PMU1 40B6MD sr"
|
||||
name = "SAM SA-20 S-300PMU1 SR 5N66E"
|
||||
detection_range = 120000
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_20_S_300PMU1_SR_64N6E(unittype.VehicleType):
|
||||
id = "S-300PMU1 64N6E sr"
|
||||
name = "SAM SA-20 S-300PMU1 SR 64N6E"
|
||||
detection_range = 300000
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_23_S_300VM_9S15M2_SR(unittype.VehicleType):
|
||||
id = "S-300VM 9S15M2 sr"
|
||||
name = "SAM SA-23 S-300VM 9S15M2 SR"
|
||||
detection_range = 320000
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_23_S_300VM_9S19M2_SR(unittype.VehicleType):
|
||||
id = "S-300VM 9S19M2 sr"
|
||||
name = "SAM SA-23 S-300VM 9S19M2 SR"
|
||||
detection_range = 310000
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_23_S_300VM_9S32ME_TR(unittype.VehicleType):
|
||||
id = "S-300VM 9S32ME tr"
|
||||
name = "SAM SA-23 S-300VM 9S32ME TR"
|
||||
detection_range = 230000
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_20_S_300PMU1_LN_5P85CE(unittype.VehicleType):
|
||||
id = "S-300PMU1 5P85CE ln"
|
||||
name = "SAM SA-20 S-300PMU1 LN 5P85CE"
|
||||
detection_range = 0
|
||||
threat_range = 150000
|
||||
air_weapon_dist = 150000
|
||||
|
||||
|
||||
class SAM_SA_20_S_300PMU1_LN_5P85DE(unittype.VehicleType):
|
||||
id = "S-300PMU1 5P85DE ln"
|
||||
name = "SAM SA-20 S-300PMU1 LN 5P85DE"
|
||||
detection_range = 0
|
||||
threat_range = 150000
|
||||
air_weapon_dist = 150000
|
||||
|
||||
|
||||
class SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE(unittype.VehicleType):
|
||||
id = "S-300PS 5P85CE ln"
|
||||
name = "SAM SA-10 (5V55RUD) S-300PS LN 5P85CE"
|
||||
detection_range = 0
|
||||
threat_range = 90000
|
||||
air_weapon_dist = 90000
|
||||
|
||||
|
||||
class SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE(unittype.VehicleType):
|
||||
id = "S-300PS 5P85DE ln"
|
||||
name = "SAM SA-10 (5V55RUD) S-300PS LN 5P85DE"
|
||||
detection_range = 0
|
||||
threat_range = 90000
|
||||
air_weapon_dist = 90000
|
||||
|
||||
|
||||
class SAM_SA_23_S_300VM_9A83ME_LN(unittype.VehicleType):
|
||||
id = "S-300VM 9A83ME ln"
|
||||
name = "SAM SA-23 S-300VM 9A83ME LN"
|
||||
detection_range = 0
|
||||
threat_range = 90000
|
||||
air_weapon_dist = 90000
|
||||
|
||||
|
||||
class SAM_SA_23_S_300VM_9A82ME_LN(unittype.VehicleType):
|
||||
id = "S-300VM 9A82ME ln"
|
||||
name = "SAM SA-23 S-300VM 9A82ME LN"
|
||||
detection_range = 0
|
||||
threat_range = 200000
|
||||
air_weapon_dist = 200000
|
||||
|
||||
|
||||
class SAM_SA_17_Buk_M1_2_LN_9A310M1_2(unittype.VehicleType):
|
||||
id = "SA-17 Buk M1-2 LN 9A310M1-2"
|
||||
name = "SAM SA-17 Buk M1-2 LN 9A310M1-2"
|
||||
detection_range = 120000
|
||||
threat_range = 50000
|
||||
air_weapon_dist = 50000
|
||||
|
||||
|
||||
class SAM_SA_2__V759__LN_SM_90(unittype.VehicleType):
|
||||
id = "S_75M_Volhov_V759"
|
||||
name = "SAM SA-2 (V759) LN SM-90"
|
||||
detection_range = 0
|
||||
threat_range = 50000
|
||||
air_weapon_dist = 50000
|
||||
|
||||
|
||||
class SAM_HQ_2_LN_SM_90(unittype.VehicleType):
|
||||
id = "HQ_2_Guideline_LN"
|
||||
name = "SAM HQ-2 LN SM-90"
|
||||
detection_range = 0
|
||||
threat_range = 50000
|
||||
air_weapon_dist = 50000
|
||||
|
||||
|
||||
class SAM_SA_3__V_601P__LN_5P73(unittype.VehicleType):
|
||||
id = "5p73 V-601P ln"
|
||||
name = "SAM SA-3 (V-601P) LN 5P73"
|
||||
detection_range = 0
|
||||
threat_range = 18000
|
||||
air_weapon_dist = 18000
|
||||
|
||||
|
||||
class SAM_SA_20_S_300PMU1_CP_54K6(unittype.VehicleType):
|
||||
id = "S-300PMU1 54K6 cp"
|
||||
name = "SAM SA-20 S-300PMU1 CP 54K6"
|
||||
detection_range = 0
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_23_S_300VM_9S457ME_CP(unittype.VehicleType):
|
||||
id = "S-300VM 9S457ME cp"
|
||||
name = "SAM SA-23 S-300VM 9S457ME CP"
|
||||
detection_range = 0
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_24_Igla_S_manpad(unittype.VehicleType):
|
||||
id = "SA-24 Igla-S manpad"
|
||||
name = "SAM SA-24 Igla-S manpad"
|
||||
detection_range = 5000
|
||||
threat_range = 6000
|
||||
air_weapon_dist = 6000
|
||||
|
||||
|
||||
class SAM_SA_14_Strela_3_manpad(unittype.VehicleType):
|
||||
id = "SA-14 Strela-3 manpad"
|
||||
name = "SAM SA-14 Strela-3 manpad"
|
||||
detection_range = 5000
|
||||
threat_range = 4500
|
||||
air_weapon_dist = 4500
|
||||
@@ -1,4 +1,5 @@
|
||||
from pydcs_extensions.a4ec.a4ec import A_4E_C
|
||||
from pydcs_extensions.highdigitsams import highdigitsams
|
||||
from pydcs_extensions.mb339.mb339 import MB_339PAN
|
||||
from pydcs_extensions.rafale.rafale import Rafale_M, Rafale_A_S
|
||||
from pydcs_extensions.su57.su57 import Su_57
|
||||
@@ -39,5 +40,26 @@ MODDED_VEHICLES = [
|
||||
frenchpack.DIM__TOYOTA_BLUE,
|
||||
frenchpack.DIM__TOYOTA_GREEN,
|
||||
frenchpack.DIM__TOYOTA_DESERT,
|
||||
frenchpack.DIM__KAMIKAZE
|
||||
frenchpack.DIM__KAMIKAZE,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E_truck,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_SR_5N66E,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_SR_64N6E,
|
||||
highdigitsams.SAM_SA_23_S_300VM_9S15M2_SR,
|
||||
highdigitsams.SAM_SA_23_S_300VM_9S19M2_SR,
|
||||
highdigitsams.SAM_SA_23_S_300VM_9S32ME_TR,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85CE,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85DE,
|
||||
highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE,
|
||||
highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE,
|
||||
highdigitsams.SAM_SA_23_S_300VM_9A83ME_LN,
|
||||
highdigitsams.SAM_SA_23_S_300VM_9A82ME_LN,
|
||||
highdigitsams.SAM_SA_17_Buk_M1_2_LN_9A310M1_2,
|
||||
highdigitsams.SAM_SA_2__V759__LN_SM_90,
|
||||
highdigitsams.SAM_HQ_2_LN_SM_90,
|
||||
highdigitsams.SAM_SA_3__V_601P__LN_5P73,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_CP_54K6,
|
||||
highdigitsams.SAM_SA_23_S_300VM_9S457ME_CP,
|
||||
highdigitsams.SAM_SA_24_Igla_S_manpad,
|
||||
highdigitsams.SAM_SA_14_Strela_3_manpad
|
||||
]
|
||||
@@ -34,12 +34,13 @@ class Dialog:
|
||||
cls.game_model = game_model
|
||||
|
||||
@classmethod
|
||||
def open_new_package_dialog(cls, mission_target: MissionTarget):
|
||||
def open_new_package_dialog(cls, mission_target: MissionTarget, parent=None):
|
||||
"""Opens the dialog to create a new package with the given target."""
|
||||
cls.new_package_dialog = QNewPackageDialog(
|
||||
cls.game_model,
|
||||
cls.game_model.ato_model,
|
||||
mission_target
|
||||
mission_target,
|
||||
parent=parent
|
||||
)
|
||||
cls.new_package_dialog.show()
|
||||
|
||||
@@ -55,11 +56,12 @@ class Dialog:
|
||||
|
||||
@classmethod
|
||||
def open_edit_flight_dialog(cls, package_model: PackageModel,
|
||||
flight: Flight) -> None:
|
||||
flight: Flight, parent=None) -> None:
|
||||
"""Opens the dialog to edit the given flight."""
|
||||
cls.edit_flight_dialog = QEditFlightDialog(
|
||||
cls.game_model,
|
||||
package_model.package,
|
||||
flight
|
||||
flight,
|
||||
parent=parent
|
||||
)
|
||||
cls.edit_flight_dialog.show()
|
||||
|
||||
@@ -7,7 +7,7 @@ from PySide2 import QtWidgets
|
||||
from PySide2.QtGui import QPixmap
|
||||
from PySide2.QtWidgets import QApplication, QSplashScreen
|
||||
|
||||
from game import persistency
|
||||
from game import db, persistency, VERSION
|
||||
from qt_ui import (
|
||||
liberation_install,
|
||||
liberation_theme,
|
||||
@@ -20,9 +20,11 @@ from qt_ui.windows.preferences.QLiberationFirstStartWindow import \
|
||||
QLiberationFirstStartWindow
|
||||
|
||||
# Logging setup
|
||||
logging_config.init_logging(uiconstants.VERSION_STRING)
|
||||
logging_config.init_logging(VERSION)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Load eagerly to catch errors early.
|
||||
db.FACTIONS.initialize()
|
||||
|
||||
os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" # Potential fix for 4K screens
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
@@ -125,7 +125,8 @@ class PackageModel(QAbstractListModel):
|
||||
count = flight.count
|
||||
name = db.unit_type_name(flight.unit_type)
|
||||
estimator = TotEstimator(self.package)
|
||||
delay = datetime.timedelta(seconds=estimator.mission_start_time(flight))
|
||||
delay = datetime.timedelta(
|
||||
seconds=int(estimator.mission_start_time(flight).total_seconds()))
|
||||
origin = flight.from_cp.name
|
||||
return f"[{task}] {count} x {name} from {origin} in {delay}"
|
||||
|
||||
@@ -162,7 +163,7 @@ class PackageModel(QAbstractListModel):
|
||||
"""Returns the flight located at the given index."""
|
||||
return self.package.flights[index.row()]
|
||||
|
||||
def update_tot(self, tot: int) -> None:
|
||||
def update_tot(self, tot: datetime.timedelta) -> None:
|
||||
self.package.time_over_target = tot
|
||||
self.layoutChanged.emit()
|
||||
|
||||
@@ -216,6 +217,8 @@ class AtoModel(QAbstractListModel):
|
||||
self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
|
||||
self.ato.add_package(package)
|
||||
self.endInsertRows()
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.client_slots_changed.emit()
|
||||
|
||||
def delete_package_at_index(self, index: QModelIndex) -> None:
|
||||
"""Removes the package at the given index from the ATO."""
|
||||
@@ -230,6 +233,8 @@ class AtoModel(QAbstractListModel):
|
||||
for flight in package.flights:
|
||||
self.game.aircraft_inventory.return_from_flight(flight)
|
||||
self.endRemoveRows()
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.client_slots_changed.emit()
|
||||
|
||||
def package_at_index(self, index: QModelIndex) -> Package:
|
||||
"""Returns the package at the given index."""
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import os
|
||||
from typing import Dict
|
||||
from pathlib import Path
|
||||
|
||||
from PySide2.QtGui import QColor, QFont, QPixmap
|
||||
|
||||
from theater.theatergroundobject import CATEGORY_MAP
|
||||
from .liberation_theme import get_theme_icons
|
||||
|
||||
VERSION_STRING = "2.2.0-preview"
|
||||
|
||||
URLS : Dict[str, str] = {
|
||||
"Manual": "https://github.com/khopa/dcs_liberation/wiki",
|
||||
|
||||
@@ -48,12 +48,14 @@ class QTopPanel(QFrame):
|
||||
self.passTurnButton.setIcon(CONST.ICONS["PassTurn"])
|
||||
self.passTurnButton.setProperty("style", "btn-primary")
|
||||
self.passTurnButton.clicked.connect(self.passTurn)
|
||||
if not self.game:
|
||||
self.passTurnButton.setEnabled(False)
|
||||
|
||||
self.proceedButton = QPushButton("Take off")
|
||||
self.proceedButton.setIcon(CONST.ICONS["Proceed"])
|
||||
self.proceedButton.setProperty("style", "start-button")
|
||||
self.proceedButton.clicked.connect(self.launch_mission)
|
||||
if self.game and self.game.turn == 0:
|
||||
if not self.game or self.game.turn == 0:
|
||||
self.proceedButton.setEnabled(False)
|
||||
|
||||
self.factionsInfos = QFactionsInfos(self.game)
|
||||
@@ -101,6 +103,8 @@ class QTopPanel(QFrame):
|
||||
self.budgetBox.setGame(game)
|
||||
self.factionsInfos.setGame(game)
|
||||
|
||||
self.passTurnButton.setEnabled(True)
|
||||
|
||||
if game and game.turn == 0:
|
||||
self.proceedButton.setEnabled(False)
|
||||
else:
|
||||
@@ -126,7 +130,7 @@ class QTopPanel(QFrame):
|
||||
continue
|
||||
estimator = TotEstimator(package)
|
||||
for flight in package.flights:
|
||||
if estimator.mission_start_time(flight) < 0:
|
||||
if estimator.mission_start_time(flight).total_seconds() < 0:
|
||||
packages.append(package)
|
||||
break
|
||||
return packages
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""Widgets for displaying air tasking orders."""
|
||||
import datetime
|
||||
import logging
|
||||
from contextlib import contextmanager
|
||||
from typing import ContextManager, Optional
|
||||
@@ -22,6 +21,7 @@ from PySide2.QtWidgets import (
|
||||
QAction,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QListView,
|
||||
QMenu,
|
||||
QPushButton,
|
||||
@@ -64,7 +64,7 @@ class FlightDelegate(QStyledItemDelegate):
|
||||
count = flight.count
|
||||
name = db.unit_type_name(flight.unit_type)
|
||||
estimator = TotEstimator(self.package)
|
||||
delay = datetime.timedelta(seconds=estimator.mission_start_time(flight))
|
||||
delay = estimator.mission_start_time(flight)
|
||||
return f"[{task}] {count} x {name} in {delay}"
|
||||
|
||||
def second_row_text(self, index: QModelIndex) -> str:
|
||||
@@ -194,7 +194,8 @@ class QFlightList(QListView):
|
||||
def edit_flight(self, index: QModelIndex) -> None:
|
||||
from qt_ui.dialogs import Dialog
|
||||
Dialog.open_edit_flight_dialog(
|
||||
self.package_model, self.package_model.flight_at_index(index)
|
||||
self.package_model, self.package_model.flight_at_index(index),
|
||||
parent=self.window()
|
||||
)
|
||||
|
||||
def delete_flight(self, index: QModelIndex) -> None:
|
||||
@@ -235,6 +236,12 @@ class QFlightPanel(QGroupBox):
|
||||
self.vbox = QVBoxLayout()
|
||||
self.setLayout(self.vbox)
|
||||
|
||||
self.tip = QLabel(
|
||||
"To add flights to a package, edit the package by double clicking "
|
||||
"it or pressing the edit button."
|
||||
)
|
||||
self.vbox.addWidget(self.tip)
|
||||
|
||||
self.flight_list = QFlightList(game_model, package_model)
|
||||
self.vbox.addWidget(self.flight_list)
|
||||
|
||||
@@ -328,10 +335,7 @@ class PackageDelegate(QStyledItemDelegate):
|
||||
|
||||
def right_text(self, index: QModelIndex) -> str:
|
||||
package = self.package(index)
|
||||
if package.time_over_target is None:
|
||||
return ""
|
||||
tot = datetime.timedelta(seconds=package.time_over_target)
|
||||
return f"TOT T+{tot}"
|
||||
return f"TOT T+{package.time_over_target}"
|
||||
|
||||
def paint(self, painter: QPainter, option: QStyleOptionViewItem,
|
||||
index: QModelIndex) -> None:
|
||||
@@ -440,6 +444,13 @@ class QPackagePanel(QGroupBox):
|
||||
self.vbox = QVBoxLayout()
|
||||
self.setLayout(self.vbox)
|
||||
|
||||
self.tip = QLabel(
|
||||
"To create a new package, right click the mission target on the "
|
||||
"map. To target airbase objectives, use\n"
|
||||
"the attack button in the airbase view."
|
||||
)
|
||||
self.vbox.addWidget(self.tip)
|
||||
|
||||
self.package_list = QPackageList(self.ato_model)
|
||||
self.vbox.addWidget(self.package_list)
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ class QFlightTypeComboBox(QComboBox):
|
||||
yield from self.ENEMY_AIRBASE_MISSIONS
|
||||
elif isinstance(self.target, TheaterGroundObject):
|
||||
# TODO: Filter more based on the category.
|
||||
friendly = self.target.parent_control_point(self.theater).captured
|
||||
friendly = self.target.control_point.captured
|
||||
if friendly:
|
||||
yield from self.FRIENDLY_GROUND_OBJECT_MISSIONS
|
||||
else:
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"""Combo box for selecting a departure airfield."""
|
||||
from typing import Iterable
|
||||
|
||||
from PySide2.QtCore import Signal
|
||||
from PySide2.QtWidgets import QComboBox
|
||||
|
||||
from dcs.planes import PlaneType
|
||||
|
||||
from game.inventory import GlobalAircraftInventory
|
||||
from theater.controlpoint import ControlPoint
|
||||
|
||||
@@ -15,6 +16,8 @@ class QOriginAirfieldSelector(QComboBox):
|
||||
that have unassigned inventory of the given aircraft type.
|
||||
"""
|
||||
|
||||
availability_changed = Signal(int)
|
||||
|
||||
def __init__(self, global_inventory: GlobalAircraftInventory,
|
||||
origins: Iterable[ControlPoint],
|
||||
aircraft: PlaneType) -> None:
|
||||
@@ -23,6 +26,7 @@ class QOriginAirfieldSelector(QComboBox):
|
||||
self.origins = list(origins)
|
||||
self.aircraft = aircraft
|
||||
self.rebuild_selector()
|
||||
self.currentIndexChanged.connect(self.index_changed)
|
||||
|
||||
def change_aircraft(self, aircraft: PlaneType) -> None:
|
||||
if self.aircraft == aircraft:
|
||||
@@ -43,5 +47,14 @@ class QOriginAirfieldSelector(QComboBox):
|
||||
@property
|
||||
def available(self) -> int:
|
||||
origin = self.currentData()
|
||||
if origin is None:
|
||||
return 0
|
||||
inventory = self.global_inventory.for_control_point(origin)
|
||||
return inventory.available(self.aircraft)
|
||||
|
||||
def index_changed(self, index: int) -> None:
|
||||
origin = self.itemData(index)
|
||||
if origin is None:
|
||||
return
|
||||
inventory = self.global_inventory.for_control_point(origin)
|
||||
self.availability_changed.emit(inventory.available(self.aircraft))
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from PySide2.QtCore import QSortFilterProxyModel, Qt, QModelIndex
|
||||
from PySide2.QtGui import QStandardItem, QStandardItemModel
|
||||
from PySide2.QtWidgets import QComboBox, QCompleter
|
||||
|
||||
from game import Game
|
||||
from gen import Conflict, FlightWaypointType
|
||||
from gen.flights.flight import FlightWaypoint, PredefinedWaypointCategory
|
||||
from gen import BuildingGroundObject, Conflict, FlightWaypointType
|
||||
from gen.flights.flight import FlightWaypoint
|
||||
from qt_ui.widgets.combos.QFilteredComboBox import QFilteredComboBox
|
||||
from theater import ControlPointType
|
||||
|
||||
@@ -45,7 +44,6 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
|
||||
i = 0
|
||||
|
||||
def add_model_item(i, model, name, wpt):
|
||||
print(name)
|
||||
item = QStandardItem(name)
|
||||
model.setItem(i, 0, item)
|
||||
self.wpts.append(wpt)
|
||||
@@ -66,15 +64,13 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
|
||||
wpt.alt_type = "RADIO"
|
||||
wpt.pretty_name = wpt.name
|
||||
wpt.description = "Frontline"
|
||||
wpt.data = [cp, ecp]
|
||||
wpt.category = PredefinedWaypointCategory.FRONTLINE
|
||||
i = add_model_item(i, model, wpt.pretty_name, wpt)
|
||||
|
||||
if self.include_targets:
|
||||
for cp in self.game.theater.controlpoints:
|
||||
if (self.include_enemy and not cp.captured) or (self.include_friendly and cp.captured):
|
||||
for ground_object in cp.ground_objects:
|
||||
if not ground_object.is_dead and not ground_object.dcs_identifier == "AA":
|
||||
if not ground_object.is_dead and not isinstance(ground_object, BuildingGroundObject):
|
||||
wpt = FlightWaypoint(
|
||||
FlightWaypointType.CUSTOM,
|
||||
ground_object.position.x,
|
||||
@@ -82,17 +78,14 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
|
||||
0
|
||||
)
|
||||
wpt.alt_type = "RADIO"
|
||||
wpt.name = wpt.name = "[" + str(ground_object.obj_name) + "] : " + ground_object.category + " #" + str(ground_object.object_id)
|
||||
wpt.name = ground_object.waypoint_name
|
||||
wpt.pretty_name = wpt.name
|
||||
wpt.obj_name = ground_object.obj_name
|
||||
wpt.targets.append(ground_object)
|
||||
wpt.data = ground_object
|
||||
if cp.captured:
|
||||
wpt.description = "Friendly Building"
|
||||
wpt.category = PredefinedWaypointCategory.ALLY_BUILDING
|
||||
else:
|
||||
wpt.description = "Enemy Building"
|
||||
wpt.category = PredefinedWaypointCategory.ENEMY_BUILDING
|
||||
i = add_model_item(i, model, wpt.pretty_name, wpt)
|
||||
|
||||
if self.include_units:
|
||||
@@ -112,15 +105,12 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
|
||||
wpt.name = wpt.name = "[" + str(ground_object.obj_name) + "] : " + u.type + " #" + str(j)
|
||||
wpt.pretty_name = wpt.name
|
||||
wpt.targets.append(u)
|
||||
wpt.data = u
|
||||
wpt.obj_name = ground_object.obj_name
|
||||
wpt.waypoint_type = FlightWaypointType.CUSTOM
|
||||
if cp.captured:
|
||||
wpt.description = "Friendly unit : " + u.type
|
||||
wpt.category = PredefinedWaypointCategory.ALLY_UNIT
|
||||
else:
|
||||
wpt.description = "Enemy unit : " + u.type
|
||||
wpt.category = PredefinedWaypointCategory.ENEMY_UNIT
|
||||
i = add_model_item(i, model, wpt.pretty_name, wpt)
|
||||
|
||||
if self.include_airbases:
|
||||
@@ -134,13 +124,10 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
|
||||
)
|
||||
wpt.alt_type = "RADIO"
|
||||
wpt.name = cp.name
|
||||
wpt.data = cp
|
||||
if cp.captured:
|
||||
wpt.description = "Position of " + cp.name + " [Friendly Airbase]"
|
||||
wpt.category = PredefinedWaypointCategory.ALLY_CP
|
||||
else:
|
||||
wpt.description = "Position of " + cp.name + " [Enemy Airbase]"
|
||||
wpt.category = PredefinedWaypointCategory.ENEMY_CP
|
||||
|
||||
if cp.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP:
|
||||
wpt.pretty_name = cp.name + " (Aircraft Carrier Group)"
|
||||
|
||||
@@ -26,13 +26,11 @@ from dcs.mapping import point_from_heading
|
||||
|
||||
import qt_ui.uiconstants as CONST
|
||||
from game import Game, db
|
||||
from game.data.aaa_db import AAA_UNITS
|
||||
from game.data.radar_db import UNITS_WITH_RADAR
|
||||
from game.utils import meter_to_feet
|
||||
from game.weather import TimeOfDay
|
||||
from gen import Conflict, PackageWaypointTiming
|
||||
from gen.ato import Package
|
||||
from gen import Conflict
|
||||
from gen.flights.flight import Flight, FlightWaypoint, FlightWaypointType
|
||||
from gen.flights.flightplan import FlightPlan
|
||||
from qt_ui.displayoptions import DisplayOptions
|
||||
from qt_ui.models import GameModel
|
||||
from qt_ui.widgets.map.QFrontLine import QFrontLine
|
||||
@@ -41,6 +39,11 @@ from qt_ui.widgets.map.QMapControlPoint import QMapControlPoint
|
||||
from qt_ui.widgets.map.QMapGroundObject import QMapGroundObject
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from theater import ControlPoint, FrontLine
|
||||
from theater.theatergroundobject import (
|
||||
EwrGroundObject,
|
||||
MissileSiteGroundObject,
|
||||
TheaterGroundObject,
|
||||
)
|
||||
|
||||
|
||||
class QLiberationMap(QGraphicsView):
|
||||
@@ -121,8 +124,8 @@ class QLiberationMap(QGraphicsView):
|
||||
|
||||
def setGame(self, game: Optional[Game]):
|
||||
self.game = game
|
||||
logging.debug("Reloading Map Canvas")
|
||||
if self.game is not None:
|
||||
logging.debug("Reloading Map Canvas")
|
||||
self.reload_scene()
|
||||
|
||||
"""
|
||||
@@ -163,6 +166,28 @@ class QLiberationMap(QGraphicsView):
|
||||
self.reload_scene()
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def aa_ranges(ground_object: TheaterGroundObject) -> Tuple[int, int]:
|
||||
detection_range = 0
|
||||
threat_range = 0
|
||||
for g in ground_object.groups:
|
||||
for u in g.units:
|
||||
unit = db.unit_type_from_name(u.type)
|
||||
if unit is None:
|
||||
logging.error(f"Unknown unit type {u.type}")
|
||||
continue
|
||||
|
||||
# Some units in pydcs have detection_range and threat_range
|
||||
# defined, but explicitly set to None.
|
||||
unit_detection_range = getattr(unit, "detection_range", None)
|
||||
if unit_detection_range is not None:
|
||||
detection_range = max(detection_range, unit_detection_range)
|
||||
|
||||
unit_threat_range = getattr(unit, "threat_range", None)
|
||||
if unit_threat_range is not None:
|
||||
threat_range = max(threat_range, unit_threat_range)
|
||||
|
||||
return detection_range, threat_range
|
||||
|
||||
def reload_scene(self):
|
||||
scene = self.scene()
|
||||
@@ -215,41 +240,34 @@ class QLiberationMap(QGraphicsView):
|
||||
buildings = self.game.theater.find_ground_objects_by_obj_name(ground_object.obj_name)
|
||||
scene.addItem(QMapGroundObject(self, go_pos[0], go_pos[1], 14, 12, cp, ground_object, self.game, buildings))
|
||||
|
||||
is_aa = ground_object.category == "aa"
|
||||
is_missile = isinstance(ground_object, MissileSiteGroundObject)
|
||||
is_aa = ground_object.category == "aa" and not is_missile
|
||||
is_ewr = isinstance(ground_object, EwrGroundObject)
|
||||
is_display_type = is_aa or is_ewr
|
||||
should_display = ((DisplayOptions.sam_ranges and cp.captured)
|
||||
or
|
||||
(DisplayOptions.enemy_sam_ranges and not cp.captured))
|
||||
|
||||
if is_aa and should_display:
|
||||
threat_range = 0
|
||||
detection_range = 0
|
||||
can_fire = False
|
||||
if ground_object.groups:
|
||||
for g in ground_object.groups:
|
||||
for u in g.units:
|
||||
unit = db.unit_type_from_name(u.type)
|
||||
if unit in UNITS_WITH_RADAR or unit in AAA_UNITS:
|
||||
can_fire = True
|
||||
if unit.detection_range > detection_range:
|
||||
detection_range = unit.detection_range
|
||||
if unit.threat_range > threat_range:
|
||||
threat_range = unit.threat_range
|
||||
if can_fire:
|
||||
if is_display_type and should_display:
|
||||
detection_range, threat_range = self.aa_ranges(
|
||||
ground_object
|
||||
)
|
||||
if threat_range:
|
||||
threat_pos = self._transform_point(Point(ground_object.position.x+threat_range,
|
||||
ground_object.position.y+threat_range))
|
||||
detection_pos = self._transform_point(Point(ground_object.position.x+detection_range,
|
||||
ground_object.position.y+detection_range))
|
||||
threat_radius = Point(*go_pos).distance_to_point(Point(*threat_pos))
|
||||
detection_radius = Point(*go_pos).distance_to_point(Point(*detection_pos))
|
||||
|
||||
# Add detection range circle
|
||||
if DisplayOptions.detection_range:
|
||||
scene.addEllipse(go_pos[0] - detection_radius/2 + 7, go_pos[1] - detection_radius/2 + 6,
|
||||
detection_radius, detection_radius, self.detection_pen(cp.captured))
|
||||
|
||||
# Add threat range circle
|
||||
scene.addEllipse(go_pos[0] - threat_radius / 2 + 7, go_pos[1] - threat_radius / 2 + 6,
|
||||
threat_radius, threat_radius, self.threat_pen(cp.captured))
|
||||
if detection_range:
|
||||
# Add detection range circle
|
||||
detection_pos = self._transform_point(Point(ground_object.position.x+detection_range,
|
||||
ground_object.position.y+detection_range))
|
||||
detection_radius = Point(*go_pos).distance_to_point(Point(*detection_pos))
|
||||
if DisplayOptions.detection_range:
|
||||
scene.addEllipse(go_pos[0] - detection_radius/2 + 7, go_pos[1] - detection_radius/2 + 6,
|
||||
detection_radius, detection_radius, self.detection_pen(cp.captured))
|
||||
added_objects.append(ground_object.obj_name)
|
||||
|
||||
for cp in self.game.theater.enemy_points():
|
||||
@@ -294,11 +312,10 @@ class QLiberationMap(QGraphicsView):
|
||||
selected = (p_idx, f_idx) == self.selected_flight
|
||||
if DisplayOptions.flight_paths.only_selected and not selected:
|
||||
continue
|
||||
self.draw_flight_plan(scene, package_model.package, flight,
|
||||
selected)
|
||||
self.draw_flight_plan(scene, flight, selected)
|
||||
|
||||
def draw_flight_plan(self, scene: QGraphicsScene, package: Package,
|
||||
flight: Flight, selected: bool) -> None:
|
||||
def draw_flight_plan(self, scene: QGraphicsScene, flight: Flight,
|
||||
selected: bool) -> None:
|
||||
is_player = flight.from_cp.captured
|
||||
pos = self._transform_point(flight.from_cp.position)
|
||||
|
||||
@@ -310,7 +327,7 @@ class QLiberationMap(QGraphicsView):
|
||||
FlightWaypointType.TARGET_POINT,
|
||||
FlightWaypointType.TARGET_SHIP,
|
||||
)
|
||||
for idx, point in enumerate(flight.points):
|
||||
for idx, point in enumerate(flight.flight_plan.waypoints[1:]):
|
||||
new_pos = self._transform_point(Point(point.x, point.y))
|
||||
self.draw_flight_path(scene, prev_pos, new_pos, is_player,
|
||||
selected)
|
||||
@@ -321,8 +338,8 @@ class QLiberationMap(QGraphicsView):
|
||||
# Don't draw dozens of targets over each other.
|
||||
continue
|
||||
drew_target = True
|
||||
self.draw_waypoint_info(scene, idx + 1, point, new_pos, package,
|
||||
flight)
|
||||
self.draw_waypoint_info(scene, idx + 1, point, new_pos,
|
||||
flight.flight_plan)
|
||||
prev_pos = tuple(new_pos)
|
||||
self.draw_flight_path(scene, prev_pos, pos, is_player, selected)
|
||||
|
||||
@@ -337,21 +354,21 @@ class QLiberationMap(QGraphicsView):
|
||||
|
||||
def draw_waypoint_info(self, scene: QGraphicsScene, number: int,
|
||||
waypoint: FlightWaypoint, position: Tuple[int, int],
|
||||
package: Package, flight: Flight) -> None:
|
||||
timing = PackageWaypointTiming.for_package(package)
|
||||
flight_plan: FlightPlan) -> None:
|
||||
|
||||
altitude = meter_to_feet(waypoint.alt)
|
||||
altitude_type = "AGL" if waypoint.alt_type == "RADIO" else "MSL"
|
||||
|
||||
prefix = "TOT"
|
||||
time = timing.tot_for_waypoint(flight, waypoint)
|
||||
time = flight_plan.tot_for_waypoint(waypoint)
|
||||
if time is None:
|
||||
prefix = "Depart"
|
||||
time = timing.depart_time_for_waypoint(waypoint, flight)
|
||||
time = flight_plan.depart_time_for_waypoint(waypoint)
|
||||
if time is None:
|
||||
tot = ""
|
||||
else:
|
||||
tot = f"{prefix} T+{datetime.timedelta(seconds=time)}"
|
||||
time = datetime.timedelta(seconds=int(time.total_seconds()))
|
||||
tot = f"{prefix} T+{time}"
|
||||
|
||||
pen = QPen(QColor("black"), 0.3)
|
||||
brush = QColor("white")
|
||||
|
||||
@@ -90,3 +90,10 @@ class QMapControlPoint(QMapObject):
|
||||
# Reinitialized ground planners and the like.
|
||||
self.game_model.game.initialize_turn()
|
||||
GameUpdateSignal.get_instance().updateGame(self.game_model.game)
|
||||
|
||||
def open_new_package_dialog(self) -> None:
|
||||
"""Extends the default packagedialog to redirect to base menu for red air base."""
|
||||
if not self.control_point.captured:
|
||||
self.on_click()
|
||||
else:
|
||||
super(QMapControlPoint, self).open_new_package_dialog()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
import sys
|
||||
import traceback
|
||||
import webbrowser
|
||||
from typing import Optional, Union
|
||||
from typing import Optional
|
||||
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtGui import QCloseEvent, QIcon
|
||||
@@ -10,14 +10,14 @@ from PySide2.QtWidgets import (
|
||||
QActionGroup, QDesktopWidget,
|
||||
QFileDialog,
|
||||
QMainWindow,
|
||||
QMenu, QMessageBox,
|
||||
QMessageBox,
|
||||
QSplitter,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
import qt_ui.uiconstants as CONST
|
||||
from game import Game, persistency
|
||||
from game import Game, VERSION, persistency
|
||||
from qt_ui.dialogs import Dialog
|
||||
from qt_ui.displayoptions import DisplayGroup, DisplayOptions, DisplayRule
|
||||
from qt_ui.models import GameModel
|
||||
@@ -41,13 +41,12 @@ class QLiberationWindow(QMainWindow):
|
||||
self.game: Optional[Game] = None
|
||||
self.game_model = GameModel()
|
||||
Dialog.set_game(self.game_model)
|
||||
self.ato_panel = None
|
||||
self.info_panel = None
|
||||
self.liberation_map = None
|
||||
self.setGame(persistency.restore_game())
|
||||
self.ato_panel = QAirTaskingOrderPanel(self.game_model)
|
||||
self.info_panel = QInfoPanel(self.game)
|
||||
self.liberation_map = QLiberationMap(self.game_model)
|
||||
|
||||
self.setGeometry(300, 100, 270, 100)
|
||||
self.setWindowTitle("DCS Liberation - v" + CONST.VERSION_STRING)
|
||||
self.setWindowTitle(f"DCS Liberation - v{VERSION}")
|
||||
self.setWindowIcon(QIcon("./resources/icon.png"))
|
||||
self.statusBar().showMessage('Ready')
|
||||
|
||||
@@ -56,24 +55,24 @@ class QLiberationWindow(QMainWindow):
|
||||
self.initMenuBar()
|
||||
self.initToolbar()
|
||||
self.connectSignals()
|
||||
self.onGameGenerated(self.game)
|
||||
|
||||
screen = QDesktopWidget().screenGeometry()
|
||||
self.setGeometry(0, 0, screen.width(), screen.height())
|
||||
self.setWindowState(Qt.WindowMaximized)
|
||||
|
||||
def initUi(self):
|
||||
self.ato_panel = QAirTaskingOrderPanel(self.game_model)
|
||||
self.liberation_map = QLiberationMap(self.game_model)
|
||||
self.info_panel = QInfoPanel(self.game)
|
||||
self.onGameGenerated(persistency.restore_game())
|
||||
|
||||
def initUi(self):
|
||||
hbox = QSplitter(Qt.Horizontal)
|
||||
vbox = QSplitter(Qt.Vertical)
|
||||
hbox.addWidget(self.ato_panel)
|
||||
hbox.addWidget(vbox)
|
||||
vbox.addWidget(self.liberation_map)
|
||||
vbox.addWidget(self.info_panel)
|
||||
hbox.setSizes([100, 600])
|
||||
|
||||
# Will make the ATO sidebar as small as necessary to fit the content. In
|
||||
# practice this means it is sized by the hints in the panel.
|
||||
hbox.setSizes([1, 10000000])
|
||||
vbox.setSizes([600, 100])
|
||||
|
||||
vbox = QVBoxLayout()
|
||||
@@ -191,8 +190,7 @@ class QLiberationWindow(QMainWindow):
|
||||
filter="*.liberation")
|
||||
if file is not None:
|
||||
game = persistency.load_game(file[0])
|
||||
self.setGame(game)
|
||||
GameUpdateSignal.get_instance().updateGame(self.game)
|
||||
GameUpdateSignal.get_instance().updateGame(game)
|
||||
|
||||
def saveGame(self):
|
||||
logging.info("Saving game")
|
||||
@@ -215,26 +213,40 @@ class QLiberationWindow(QMainWindow):
|
||||
GameUpdateSignal.get_instance().updateGame(self.game)
|
||||
|
||||
def setGame(self, game: Optional[Game]):
|
||||
if game is not None:
|
||||
game.on_load()
|
||||
self.game = game
|
||||
if self.info_panel is not None:
|
||||
self.info_panel.setGame(game)
|
||||
self.game_model.set(self.game)
|
||||
if self.liberation_map is not None:
|
||||
self.liberation_map.setGame(game)
|
||||
try:
|
||||
if game is not None:
|
||||
game.on_load()
|
||||
self.game = game
|
||||
if self.info_panel is not None:
|
||||
self.info_panel.setGame(game)
|
||||
self.game_model.set(self.game)
|
||||
if self.liberation_map is not None:
|
||||
self.liberation_map.setGame(game)
|
||||
except AttributeError:
|
||||
logging.exception("Incompatible save game")
|
||||
QMessageBox.critical(
|
||||
self,
|
||||
"Could not load save game",
|
||||
"The save game you have loaded is incompatible with this "
|
||||
"version of DCS Liberation.\n"
|
||||
"\n"
|
||||
f"{traceback.format_exc()}",
|
||||
QMessageBox.Ok
|
||||
)
|
||||
GameUpdateSignal.get_instance().updateGame(None)
|
||||
|
||||
def showAboutDialog(self):
|
||||
text = "<h3>DCS Liberation " + CONST.VERSION_STRING + "</h3>" + \
|
||||
text = "<h3>DCS Liberation " + VERSION + "</h3>" + \
|
||||
"<b>Source code :</b> https://github.com/khopa/dcs_liberation" + \
|
||||
"<h4>Authors</h4>" + \
|
||||
"<p>DCS Liberation was originally developed by <b>shdwp</b>, DCS Liberation 2.0 is a partial rewrite based on this work by <b>Khopa</b>." \
|
||||
"<h4>Contributors</h4>" + \
|
||||
"shdwp, Khopa, ColonelPanic, Wrycu, calvinmorrow, JohanAberg, Deus, root0fall, Captain Cody, steveveepee, pedromagueija, parithon, bwRavencl, davidp57" + \
|
||||
"shdwp, Khopa, ColonelPanic, Roach, Wrycu, calvinmorrow, JohanAberg, Deus, root0fall, Captain Cody, steveveepee, pedromagueija, parithon, bwRavencl, davidp57" + \
|
||||
"<h4>Special Thanks :</h4>" \
|
||||
"<b>rp-</b> <i>for the pydcs framework</i><br/>"\
|
||||
"<b>Grimes (mrSkortch)</b> & <b>Speed</b> <i>for the MIST framework</i><br/>"\
|
||||
"<b>Ciribob </b> <i>for the JTACAutoLase.lua script</i><br/>"
|
||||
"<b>Ciribob </b> <i>for the JTACAutoLase.lua script</i><br/>"\
|
||||
"<b>Walder </b> <i>for the Skynet-IADS script</i><br/>"
|
||||
about = QMessageBox()
|
||||
about.setWindowTitle("About DCS Liberation")
|
||||
about.setIcon(QMessageBox.Icon.Information)
|
||||
|
||||
@@ -13,8 +13,9 @@ from PySide2.QtWidgets import (
|
||||
QLabel,
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
QTextEdit,
|
||||
QTextBrowser,
|
||||
)
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
|
||||
from game.debriefing import Debriefing, wait_for_debriefing
|
||||
from game.game import Event, Game, logging
|
||||
@@ -65,27 +66,21 @@ class QWaitingForMissionResultWindow(QDialog):
|
||||
self.layout.addWidget(header, 0, 0)
|
||||
|
||||
self.gridLayout = QGridLayout()
|
||||
TEXT = "" + \
|
||||
"<b>You are clear for takeoff</b>" + \
|
||||
"" + \
|
||||
"<h2>For Singleplayer :</h2>\n" + \
|
||||
"In DCS, open the Mission Editor, and load the file : \n" + \
|
||||
"<i>liberation_nextturn</i>\n" + \
|
||||
"<p>Then once the mission is loaded in ME, in menu \"Flight\",\n" + \
|
||||
"click on FLY Mission to launch.</p>\n" + \
|
||||
"" + \
|
||||
"<h2>For Multiplayer :</h2>" + \
|
||||
"In DCS, open the Mission Editor, and load the file : " + \
|
||||
"<i>liberation_nextturn</i>" + \
|
||||
"<p>Click on File/Save. Then exit the mission editor, and go to Multiplayer.</p>" + \
|
||||
"<p>Then host a server with the mission, and tell your friends to join !</p>" + \
|
||||
"<i>(The step in the mission editor is important, and fix a game breaking bug.)</i>" + \
|
||||
"<h2>Finishing</h2>" + \
|
||||
"<p>Once you have played the mission, click on the \"Accept Results\" button.</p>" + \
|
||||
"<p>If DCS Liberation does not detect mission end, use the manually submit button, and choose the state.json file.</p>"
|
||||
|
||||
self.instructions_text = QTextEdit(TEXT)
|
||||
self.instructions_text.setReadOnly(True)
|
||||
jinja = Environment(
|
||||
loader=FileSystemLoader("resources/ui/templates"),
|
||||
autoescape=select_autoescape(
|
||||
disabled_extensions=("",),
|
||||
default_for_string=True,
|
||||
default=True,
|
||||
),
|
||||
trim_blocks=True,
|
||||
lstrip_blocks=True,
|
||||
)
|
||||
self.instructions_text = QTextBrowser()
|
||||
self.instructions_text.setHtml(
|
||||
jinja.get_template("mission_start_EN.j2").render())
|
||||
self.instructions_text.setOpenExternalLinks(True)
|
||||
self.gridLayout.addWidget(self.instructions_text, 1, 0)
|
||||
|
||||
progress = QLabel("")
|
||||
|
||||
@@ -16,11 +16,11 @@ class QBaseMenuTabs(QTabWidget):
|
||||
if cp:
|
||||
|
||||
if not cp.captured:
|
||||
self.intel = QIntelInfo(cp, game_model.game)
|
||||
self.addTab(self.intel, "Intel")
|
||||
if not cp.is_carrier:
|
||||
self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
|
||||
self.addTab(self.base_defenses_hq, "Base Defenses")
|
||||
self.intel = QIntelInfo(cp, game_model.game)
|
||||
self.addTab(self.intel, "Intel")
|
||||
else:
|
||||
if cp.has_runway():
|
||||
self.airfield_command = QAirfieldCommand(cp, game_model)
|
||||
|
||||
@@ -6,6 +6,7 @@ from PySide2.QtWidgets import (
|
||||
QGridLayout,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QMessageBox,
|
||||
QScrollArea,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
@@ -88,7 +89,21 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
|
||||
super().buy(unit_type)
|
||||
self.hangar_status.update_label(self.total_units, self.cp.available_aircraft_slots)
|
||||
|
||||
def sell(self, unit_type):
|
||||
def sell(self, unit_type: UnitType):
|
||||
# Don't need to remove aircraft from the inventory if we're canceling
|
||||
# orders.
|
||||
if self.deliveryEvent.units.get(unit_type, 0) <= 0:
|
||||
global_inventory = self.game_model.game.aircraft_inventory
|
||||
inventory = global_inventory.for_control_point(self.cp)
|
||||
try:
|
||||
inventory.remove_aircraft(unit_type, 1)
|
||||
except ValueError:
|
||||
QMessageBox.critical(
|
||||
self, "Could not sell aircraft",
|
||||
f"Attempted to sell one {unit_type.id} at {self.cp.name} "
|
||||
"but none are available. Are all aircraft currently "
|
||||
"assigned to a mission?", QMessageBox.Ok)
|
||||
return
|
||||
super().sell(unit_type)
|
||||
self.hangar_status.update_label(self.total_units, self.cp.available_aircraft_slots)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtWidgets import QGridLayout, QLabel, QGroupBox, QPushButton, QVBoxLayout
|
||||
|
||||
from qt_ui.dialogs import Dialog
|
||||
from qt_ui.uiconstants import VEHICLES_ICONS
|
||||
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
|
||||
from theater import ControlPoint, TheaterGroundObject
|
||||
@@ -23,13 +24,20 @@ class QBaseDefenseGroupInfo(QGroupBox):
|
||||
def init_ui(self):
|
||||
|
||||
self.buildLayout()
|
||||
manage_button = QPushButton("Manage")
|
||||
manage_button.setProperty("style", "btn-success")
|
||||
manage_button.setMaximumWidth(180)
|
||||
manage_button.clicked.connect(self.onManage)
|
||||
|
||||
self.main_layout.addLayout(self.unit_layout)
|
||||
self.main_layout.addWidget(manage_button, 0, Qt.AlignLeft)
|
||||
if not self.cp.captured and not self.ground_object.is_dead:
|
||||
attack_button = QPushButton("Attack")
|
||||
attack_button.setProperty("style", "btn-danger")
|
||||
attack_button.setMaximumWidth(180)
|
||||
attack_button.clicked.connect(self.onAttack)
|
||||
self.main_layout.addWidget(attack_button, 0, Qt.AlignLeft)
|
||||
|
||||
if self.cp.captured:
|
||||
manage_button = QPushButton("Manage")
|
||||
manage_button.setProperty("style", "btn-success")
|
||||
manage_button.setMaximumWidth(180)
|
||||
manage_button.clicked.connect(self.onManage)
|
||||
self.main_layout.addWidget(manage_button, 0, Qt.AlignLeft)
|
||||
|
||||
self.setLayout(self.main_layout)
|
||||
|
||||
@@ -66,6 +74,9 @@ class QBaseDefenseGroupInfo(QGroupBox):
|
||||
|
||||
|
||||
self.setLayout(self.main_layout)
|
||||
|
||||
def onAttack(self):
|
||||
Dialog.open_new_package_dialog(self.ground_object, parent=self.window())
|
||||
|
||||
def onManage(self):
|
||||
self.edition_menu = QGroundObjectMenu(self.window(), self.ground_object, self.buildings, self.cp, self.game)
|
||||
|
||||
@@ -14,8 +14,8 @@ class QGroundForcesStrategySelector(QComboBox):
|
||||
self.cp.stances[enemy_cp.id] = CombatStance.DEFENSIVE
|
||||
|
||||
for i, stance in enumerate(CombatStance):
|
||||
self.addItem(stance.name, userData=stance.value)
|
||||
if self.cp.stances[enemy_cp.id] == stance.value:
|
||||
self.addItem(stance.name, userData=stance)
|
||||
if self.cp.stances[enemy_cp.id] == stance:
|
||||
self.setCurrentIndex(i)
|
||||
|
||||
self.currentTextChanged.connect(self.on_change)
|
||||
|
||||
@@ -15,8 +15,8 @@ from qt_ui.windows.mission.flight.QFlightPlanner import QFlightPlanner
|
||||
class QEditFlightDialog(QDialog):
|
||||
"""Dialog window for editing flight plans and loadouts."""
|
||||
|
||||
def __init__(self, game_model: GameModel, package: Package, flight: Flight) -> None:
|
||||
super().__init__()
|
||||
def __init__(self, game_model: GameModel, package: Package, flight: Flight, parent=None) -> None:
|
||||
super().__init__(parent=parent)
|
||||
|
||||
self.game_model = game_model
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class QFlightItem(QStandardItem):
|
||||
self.setIcon(icon)
|
||||
self.setEditable(False)
|
||||
estimator = TotEstimator(self.package)
|
||||
delay = datetime.timedelta(seconds=estimator.mission_start_time(flight))
|
||||
delay = estimator.mission_start_time(flight)
|
||||
self.setText("["+str(self.flight.flight_type.name[:6])+"] "
|
||||
+ str(self.flight.count) + " x " + db.unit_type_name(self.flight.unit_type)
|
||||
+ " in " + str(delay))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Dialogs for creating and editing ATO packages."""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from typing import Optional
|
||||
|
||||
from PySide2.QtCore import QItemSelection, QTime, Signal
|
||||
@@ -35,8 +36,8 @@ class QPackageDialog(QDialog):
|
||||
#: Emitted when a change is made to the package.
|
||||
package_changed = Signal()
|
||||
|
||||
def __init__(self, game_model: GameModel, model: PackageModel) -> None:
|
||||
super().__init__()
|
||||
def __init__(self, game_model: GameModel, model: PackageModel, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.game_model = game_model
|
||||
self.package_model = model
|
||||
self.add_flight_dialog: Optional[QFlightCreator] = None
|
||||
@@ -78,7 +79,7 @@ class QPackageDialog(QDialog):
|
||||
self.tot_spinner.timeChanged.connect(self.save_tot)
|
||||
self.tot_column.addWidget(self.tot_spinner)
|
||||
|
||||
self.reset_tot_button = QPushButton("Reset TOT")
|
||||
self.reset_tot_button = QPushButton("ASAP")
|
||||
self.reset_tot_button.setToolTip(
|
||||
"Sets the package TOT to the earliest time that all flights can "
|
||||
"arrive at the target."
|
||||
@@ -118,7 +119,7 @@ class QPackageDialog(QDialog):
|
||||
return self.game_model.game
|
||||
|
||||
def tot_qtime(self) -> QTime:
|
||||
delay = self.package_model.package.time_over_target
|
||||
delay = int(self.package_model.package.time_over_target.total_seconds())
|
||||
hours = delay // 3600
|
||||
minutes = delay // 60 % 60
|
||||
seconds = delay % 60
|
||||
@@ -137,11 +138,11 @@ class QPackageDialog(QDialog):
|
||||
def save_tot(self) -> None:
|
||||
time = self.tot_spinner.time()
|
||||
seconds = time.hour() * 3600 + time.minute() * 60 + time.second()
|
||||
self.package_model.update_tot(seconds)
|
||||
self.package_model.update_tot(timedelta(seconds=seconds))
|
||||
|
||||
def reset_tot(self) -> None:
|
||||
if not list(self.package_model.flights):
|
||||
self.package_model.update_tot(0)
|
||||
self.package_model.update_tot(timedelta())
|
||||
else:
|
||||
self.package_model.update_tot(
|
||||
TotEstimator(self.package_model.package).earliest_tot())
|
||||
@@ -155,7 +156,8 @@ class QPackageDialog(QDialog):
|
||||
def on_add_flight(self) -> None:
|
||||
"""Opens the new flight dialog."""
|
||||
self.add_flight_dialog = QFlightCreator(self.game,
|
||||
self.package_model.package)
|
||||
self.package_model.package,
|
||||
parent=self.window())
|
||||
self.add_flight_dialog.created.connect(self.add_flight)
|
||||
self.add_flight_dialog.show()
|
||||
|
||||
@@ -188,8 +190,8 @@ class QNewPackageDialog(QPackageDialog):
|
||||
"""
|
||||
|
||||
def __init__(self, game_model: GameModel, model: AtoModel,
|
||||
target: MissionTarget) -> None:
|
||||
super().__init__(game_model, PackageModel(Package(target)))
|
||||
target: MissionTarget, parent=None) -> None:
|
||||
super().__init__(game_model, PackageModel(Package(target)), parent=parent)
|
||||
self.ato_model = model
|
||||
|
||||
self.save_button = QPushButton("Save")
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import Optional
|
||||
from PySide2.QtCore import Qt, Signal
|
||||
from PySide2.QtWidgets import (
|
||||
QDialog,
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
)
|
||||
@@ -23,8 +24,8 @@ from theater import ControlPoint
|
||||
class QFlightCreator(QDialog):
|
||||
created = Signal(Flight)
|
||||
|
||||
def __init__(self, game: Game, package: Package) -> None:
|
||||
super().__init__()
|
||||
def __init__(self, game: Game, package: Package, parent=None) -> None:
|
||||
super().__init__(parent=parent)
|
||||
|
||||
self.game = game
|
||||
self.package = package
|
||||
@@ -53,11 +54,11 @@ class QFlightCreator(QDialog):
|
||||
[cp for cp in game.theater.controlpoints if cp.captured],
|
||||
self.aircraft_selector.currentData()
|
||||
)
|
||||
self.aircraft_selector.currentIndexChanged.connect(self.update_max_size)
|
||||
self.airfield_selector.availability_changed.connect(self.update_max_size)
|
||||
layout.addLayout(QLabeledWidget("Airfield:", self.airfield_selector))
|
||||
|
||||
self.flight_size_spinner = QFlightSizeSpinner()
|
||||
self.update_max_size()
|
||||
self.update_max_size(self.airfield_selector.available)
|
||||
layout.addLayout(QLabeledWidget("Size:", self.flight_size_spinner))
|
||||
|
||||
self.client_slots_spinner = QFlightSizeSpinner(
|
||||
@@ -90,12 +91,15 @@ class QFlightCreator(QDialog):
|
||||
return f"{origin.name} has no {aircraft.id} available."
|
||||
if size > available:
|
||||
return f"{origin.name} has only {available} {aircraft.id} available."
|
||||
if size <= 0:
|
||||
return f"Flight must have at least one aircraft."
|
||||
return None
|
||||
|
||||
def create_flight(self) -> None:
|
||||
error = self.verify_form()
|
||||
if error is not None:
|
||||
self.error_box("Could not create flight", error)
|
||||
QMessageBox.critical(self, "Could not create flight", error,
|
||||
QMessageBox.Ok)
|
||||
return
|
||||
|
||||
task = self.task_selector.currentData()
|
||||
@@ -108,7 +112,6 @@ class QFlightCreator(QDialog):
|
||||
else:
|
||||
start_type = "Warm"
|
||||
flight = Flight(self.package, aircraft, size, origin, task, start_type)
|
||||
flight.scheduled_in = self.package.delay
|
||||
flight.client_count = self.client_slots_spinner.value()
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
@@ -119,7 +122,8 @@ class QFlightCreator(QDialog):
|
||||
new_aircraft = self.aircraft_selector.itemData(index)
|
||||
self.airfield_selector.change_aircraft(new_aircraft)
|
||||
|
||||
def update_max_size(self) -> None:
|
||||
self.flight_size_spinner.setMaximum(
|
||||
min(self.airfield_selector.available, 4)
|
||||
)
|
||||
def update_max_size(self, available: int) -> None:
|
||||
self.flight_size_spinner.setMaximum(min(available, 4))
|
||||
if self.flight_size_spinner.maximum() >= 2:
|
||||
if self.flight_size_spinner.value() < 2:
|
||||
self.flight_size_spinner.setValue(2)
|
||||
|
||||
@@ -19,7 +19,7 @@ class QFlightDepartureDisplay(QGroupBox):
|
||||
layout.addLayout(departure_row)
|
||||
|
||||
estimator = TotEstimator(package)
|
||||
delay = datetime.timedelta(seconds=estimator.mission_start_time(flight))
|
||||
delay = estimator.mission_start_time(flight)
|
||||
|
||||
departure_row.addWidget(QLabel(
|
||||
f"Departing from <b>{flight.from_cp.name}</b>"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import logging
|
||||
|
||||
from PySide2.QtCore import Signal
|
||||
from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QSpinBox, QGridLayout
|
||||
|
||||
@@ -10,30 +12,27 @@ class QFlightSlotEditor(QGroupBox):
|
||||
super(QFlightSlotEditor, self).__init__("Slots")
|
||||
self.flight = flight
|
||||
self.game = game
|
||||
inventory = self.game.aircraft_inventory.for_control_point(
|
||||
self.inventory = self.game.aircraft_inventory.for_control_point(
|
||||
flight.from_cp
|
||||
)
|
||||
self.available = inventory.all_aircraft
|
||||
if self.flight.unit_type not in self.available:
|
||||
max = self.flight.count
|
||||
else:
|
||||
max = self.flight.count + self.available[self.flight.unit_type]
|
||||
if max > 4:
|
||||
max = 4
|
||||
available = self.inventory.available(self.flight.unit_type)
|
||||
max_count = self.flight.count + available
|
||||
if max_count > 4:
|
||||
max_count = 4
|
||||
|
||||
layout = QGridLayout()
|
||||
|
||||
self.aircraft_count = QLabel("Aircraft count :")
|
||||
self.aircraft_count_spinner = QSpinBox()
|
||||
self.aircraft_count_spinner.setMinimum(1)
|
||||
self.aircraft_count_spinner.setMaximum(max)
|
||||
self.aircraft_count_spinner.setMaximum(max_count)
|
||||
self.aircraft_count_spinner.setValue(flight.count)
|
||||
self.aircraft_count_spinner.valueChanged.connect(self._changed_aircraft_count)
|
||||
|
||||
self.client_count = QLabel("Client slots count :")
|
||||
self.client_count_spinner = QSpinBox()
|
||||
self.client_count_spinner.setMinimum(0)
|
||||
self.client_count_spinner.setMaximum(max)
|
||||
self.client_count_spinner.setMaximum(max_count)
|
||||
self.client_count_spinner.setValue(flight.client_count)
|
||||
self.client_count_spinner.valueChanged.connect(self._changed_client_count)
|
||||
|
||||
@@ -50,9 +49,23 @@ class QFlightSlotEditor(QGroupBox):
|
||||
self.setLayout(layout)
|
||||
|
||||
def _changed_aircraft_count(self):
|
||||
self.game.aircraft_inventory.return_from_flight(self.flight)
|
||||
old_count = self.flight.count
|
||||
self.flight.count = int(self.aircraft_count_spinner.value())
|
||||
try:
|
||||
self.game.aircraft_inventory.claim_for_flight(self.flight)
|
||||
except ValueError:
|
||||
# The UI should have prevented this, but if we ran out of aircraft
|
||||
# then roll back the inventory change.
|
||||
difference = self.flight.count - old_count
|
||||
available = self.inventory.available(self.flight.unit_type)
|
||||
logging.error(
|
||||
f"Could not add {difference} additional aircraft to "
|
||||
f"{self.flight} because {self.flight.from_cp} has only "
|
||||
f"{available} {self.flight.unit_type} remaining")
|
||||
self.flight.count = old_count
|
||||
self.game.aircraft_inventory.claim_for_flight(self.flight)
|
||||
self.changed.emit()
|
||||
# TODO check if enough aircraft are available
|
||||
|
||||
def _changed_client_count(self):
|
||||
self.flight.client_count = int(self.client_count_spinner.value())
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import datetime
|
||||
import itertools
|
||||
from datetime import timedelta
|
||||
|
||||
from PySide2.QtCore import QItemSelectionModel, QPoint
|
||||
from PySide2.QtGui import QStandardItem, QStandardItemModel
|
||||
from PySide2.QtWidgets import QHeaderView, QTableView
|
||||
|
||||
from game.utils import meter_to_feet
|
||||
from gen.aircraft import PackageWaypointTiming
|
||||
from gen.ato import Package
|
||||
from gen.flights.flight import Flight, FlightWaypoint
|
||||
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointItem import \
|
||||
@@ -43,8 +42,6 @@ class QFlightWaypointList(QTableView):
|
||||
|
||||
self.model.setHorizontalHeaderLabels(["Name", "Alt", "TOT/DEPART"])
|
||||
|
||||
timing = PackageWaypointTiming.for_package(self.package)
|
||||
|
||||
# The first waypoint is set up by pydcs at mission generation time, so
|
||||
# we need to add that waypoint manually.
|
||||
takeoff = FlightWaypoint(self.flight.from_cp.position.x,
|
||||
@@ -55,13 +52,12 @@ class QFlightWaypointList(QTableView):
|
||||
|
||||
waypoints = itertools.chain([takeoff], self.flight.points)
|
||||
for row, waypoint in enumerate(waypoints):
|
||||
self.add_waypoint_row(row, self.flight, waypoint, timing)
|
||||
self.add_waypoint_row(row, self.flight, waypoint)
|
||||
self.selectionModel().setCurrentIndex(self.indexAt(QPoint(1, 1)),
|
||||
QItemSelectionModel.Select)
|
||||
|
||||
def add_waypoint_row(self, row: int, flight: Flight,
|
||||
waypoint: FlightWaypoint,
|
||||
timing: PackageWaypointTiming) -> None:
|
||||
waypoint: FlightWaypoint) -> None:
|
||||
self.model.insertRow(self.model.rowCount())
|
||||
|
||||
self.model.setItem(row, 0, QWaypointItem(waypoint, row))
|
||||
@@ -72,18 +68,19 @@ class QFlightWaypointList(QTableView):
|
||||
altitude_item.setEditable(False)
|
||||
self.model.setItem(row, 1, altitude_item)
|
||||
|
||||
tot = self.tot_text(flight, waypoint, timing)
|
||||
tot = self.tot_text(flight, waypoint)
|
||||
tot_item = QStandardItem(tot)
|
||||
tot_item.setEditable(False)
|
||||
self.model.setItem(row, 2, tot_item)
|
||||
|
||||
def tot_text(self, flight: Flight, waypoint: FlightWaypoint,
|
||||
timing: PackageWaypointTiming) -> str:
|
||||
@staticmethod
|
||||
def tot_text(flight: Flight, waypoint: FlightWaypoint) -> str:
|
||||
prefix = ""
|
||||
time = timing.tot_for_waypoint(flight, waypoint)
|
||||
time = flight.flight_plan.tot_for_waypoint(waypoint)
|
||||
if time is None:
|
||||
prefix = "Depart "
|
||||
time = timing.depart_time_for_waypoint(waypoint, self.flight)
|
||||
time = flight.flight_plan.depart_time_for_waypoint(waypoint)
|
||||
if time is None:
|
||||
return ""
|
||||
return f"{prefix}T+{datetime.timedelta(seconds=time)}"
|
||||
time = timedelta(seconds=int(time.total_seconds()))
|
||||
return f"{prefix}T+{time}"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import List, Optional
|
||||
from typing import Iterable, List, Optional
|
||||
|
||||
from PySide2.QtCore import Signal
|
||||
from PySide2.QtWidgets import (
|
||||
@@ -12,13 +12,18 @@ from PySide2.QtWidgets import (
|
||||
|
||||
from game import Game
|
||||
from gen.ato import Package
|
||||
from gen.flights.flight import Flight, FlightType
|
||||
from gen.flights.flightplan import FlightPlanBuilder
|
||||
from gen.flights.flight import Flight, FlightType, FlightWaypoint
|
||||
from gen.flights.flightplan import (
|
||||
CustomFlightPlan,
|
||||
FlightPlanBuilder,
|
||||
StrikeFlightPlan,
|
||||
)
|
||||
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointList import \
|
||||
QFlightWaypointList
|
||||
from qt_ui.windows.mission.flight.waypoints.QPredefinedWaypointSelectionWindow import \
|
||||
from qt_ui.windows.mission.flight.waypoints \
|
||||
.QPredefinedWaypointSelectionWindow import \
|
||||
QPredefinedWaypointSelectionWindow
|
||||
from theater import ControlPoint, FrontLine
|
||||
from theater import FrontLine
|
||||
|
||||
|
||||
class QFlightWaypointTab(QFrame):
|
||||
@@ -33,8 +38,6 @@ class QFlightWaypointTab(QFrame):
|
||||
self.planner = FlightPlanBuilder(self.game, package, is_player=True)
|
||||
|
||||
self.flight_waypoint_list: Optional[QFlightWaypointList] = None
|
||||
self.ascend_waypoint: Optional[QPushButton] = None
|
||||
self.descend_waypoint: Optional[QPushButton] = None
|
||||
self.rtb_waypoint: Optional[QPushButton] = None
|
||||
self.delete_selected: Optional[QPushButton] = None
|
||||
self.open_fast_waypoint_button: Optional[QPushButton] = None
|
||||
@@ -59,6 +62,7 @@ class QFlightWaypointTab(QFrame):
|
||||
recreate_types = [
|
||||
FlightType.CAS,
|
||||
FlightType.CAP,
|
||||
FlightType.DEAD,
|
||||
FlightType.ESCORT,
|
||||
FlightType.SEAD,
|
||||
FlightType.STRIKE
|
||||
@@ -76,14 +80,6 @@ class QFlightWaypointTab(QFrame):
|
||||
rlayout.addWidget(QLabel("<strong>Advanced : </strong>"))
|
||||
rlayout.addWidget(QLabel("<small>Do not use for AI flights</small>"))
|
||||
|
||||
self.ascend_waypoint = QPushButton("Add Ascend Waypoint")
|
||||
self.ascend_waypoint.clicked.connect(self.on_ascend_waypoint)
|
||||
rlayout.addWidget(self.ascend_waypoint)
|
||||
|
||||
self.descend_waypoint = QPushButton("Add Descend Waypoint")
|
||||
self.descend_waypoint.clicked.connect(self.on_descend_waypoint)
|
||||
rlayout.addWidget(self.descend_waypoint)
|
||||
|
||||
self.rtb_waypoint = QPushButton("Add RTB Waypoint")
|
||||
self.rtb_waypoint.clicked.connect(self.on_rtb_waypoint)
|
||||
rlayout.addWidget(self.rtb_waypoint)
|
||||
@@ -101,35 +97,51 @@ class QFlightWaypointTab(QFrame):
|
||||
def on_delete_waypoint(self):
|
||||
wpt = self.flight_waypoint_list.selectionModel().currentIndex().row()
|
||||
if wpt > 0:
|
||||
del self.flight.points[wpt-1]
|
||||
self.delete_waypoint(self.flight.flight_plan.waypoints[wpt])
|
||||
self.flight_waypoint_list.update_list()
|
||||
self.on_change()
|
||||
|
||||
def delete_waypoint(self, waypoint: FlightWaypoint) -> None:
|
||||
# Need to degrade to a custom flight plan and remove the waypoint.
|
||||
# If the waypoint is a target waypoint and is not the last target
|
||||
# waypoint, we don't need to degrade.
|
||||
if isinstance(self.flight.flight_plan, StrikeFlightPlan):
|
||||
is_target = waypoint in self.flight.flight_plan.targets
|
||||
if is_target and len(self.flight.flight_plan.targets) > 1:
|
||||
self.flight.flight_plan.targets.remove(waypoint)
|
||||
return
|
||||
|
||||
self.degrade_to_custom_flight_plan()
|
||||
self.flight.flight_plan.waypoints.remove(waypoint)
|
||||
|
||||
def on_fast_waypoint(self):
|
||||
self.subwindow = QPredefinedWaypointSelectionWindow(self.game, self.flight, self.flight_waypoint_list)
|
||||
self.subwindow.finished.connect(self.on_change)
|
||||
self.subwindow.waypoints_added.connect(self.on_waypoints_added)
|
||||
self.subwindow.show()
|
||||
|
||||
def on_ascend_waypoint(self):
|
||||
ascend = self.planner.generate_ascend_point(self.flight,
|
||||
self.flight.from_cp)
|
||||
self.flight.points.append(ascend)
|
||||
def on_waypoints_added(self, waypoints: Iterable[FlightWaypoint]) -> None:
|
||||
if not waypoints:
|
||||
return
|
||||
self.degrade_to_custom_flight_plan()
|
||||
self.flight.flight_plan.waypoints.extend(waypoints)
|
||||
self.flight_waypoint_list.update_list()
|
||||
self.on_change()
|
||||
|
||||
def on_rtb_waypoint(self):
|
||||
rtb = self.planner.generate_rtb_waypoint(self.flight,
|
||||
self.flight.from_cp)
|
||||
self.flight.points.append(rtb)
|
||||
self.degrade_to_custom_flight_plan()
|
||||
self.flight.flight_plan.waypoints.append(rtb)
|
||||
self.flight_waypoint_list.update_list()
|
||||
self.on_change()
|
||||
|
||||
def on_descend_waypoint(self):
|
||||
descend = self.planner.generate_descend_point(self.flight,
|
||||
self.flight.from_cp)
|
||||
self.flight.points.append(descend)
|
||||
self.flight_waypoint_list.update_list()
|
||||
self.on_change()
|
||||
def degrade_to_custom_flight_plan(self) -> None:
|
||||
if not isinstance(self.flight.flight_plan, CustomFlightPlan):
|
||||
self.flight.flight_plan = CustomFlightPlan(
|
||||
package=self.flight.package,
|
||||
flight=self.flight,
|
||||
custom_waypoints=self.flight.flight_plan.waypoints
|
||||
)
|
||||
|
||||
def confirm_recreate(self, task: FlightType) -> None:
|
||||
result = QMessageBox.question(
|
||||
@@ -149,7 +161,7 @@ class QFlightWaypointTab(QFrame):
|
||||
if task == FlightType.CAP:
|
||||
if isinstance(self.package.target, FrontLine):
|
||||
task = FlightType.TARCAP
|
||||
elif isinstance(self.package.target, ControlPoint):
|
||||
else:
|
||||
task = FlightType.BARCAP
|
||||
self.flight.flight_type = task
|
||||
self.planner.populate_flight_plan(self.flight)
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtWidgets import QDialog, QLabel, QHBoxLayout, QVBoxLayout, QPushButton, QCheckBox
|
||||
from PySide2.QtCore import Qt, Signal
|
||||
from PySide2.QtWidgets import (
|
||||
QCheckBox,
|
||||
QDialog,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
from game import Game
|
||||
from gen.flights.flight import Flight
|
||||
from qt_ui.uiconstants import EVENT_ICONS
|
||||
from qt_ui.widgets.combos.QPredefinedWaypointSelectionComboBox import QPredefinedWaypointSelectionComboBox
|
||||
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointInfoBox import QFlightWaypointInfoBox
|
||||
from qt_ui.widgets.combos.QPredefinedWaypointSelectionComboBox import \
|
||||
QPredefinedWaypointSelectionComboBox
|
||||
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointInfoBox import \
|
||||
QFlightWaypointInfoBox
|
||||
|
||||
PREDEFINED_WAYPOINT_CATEGORIES = [
|
||||
"Frontline (CAS AREA)",
|
||||
@@ -17,6 +26,8 @@ PREDEFINED_WAYPOINT_CATEGORIES = [
|
||||
|
||||
class QPredefinedWaypointSelectionWindow(QDialog):
|
||||
|
||||
# List of FlightWaypoint
|
||||
waypoints_added = Signal(list)
|
||||
|
||||
def __init__(self, game: Game, flight: Flight, flight_waypoint_list):
|
||||
super(QPredefinedWaypointSelectionWindow, self).__init__()
|
||||
@@ -44,7 +55,6 @@ class QPredefinedWaypointSelectionWindow(QDialog):
|
||||
|
||||
self.init_ui()
|
||||
self.on_select_wpt_changed()
|
||||
print("DONE")
|
||||
|
||||
|
||||
def init_ui(self):
|
||||
@@ -77,12 +87,5 @@ class QPredefinedWaypointSelectionWindow(QDialog):
|
||||
self.add_button.setDisabled(False)
|
||||
|
||||
def add_waypoint(self):
|
||||
|
||||
for wpt in self.selected_waypoints:
|
||||
self.flight.points.append(wpt)
|
||||
|
||||
self.flight_waypoint_list.update_list()
|
||||
self.waypoints_added.emit(self.selected_waypoints)
|
||||
self.close()
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ class Campaign:
|
||||
name: str
|
||||
icon_name: str
|
||||
authors: str
|
||||
description: str
|
||||
theater: ConflictTheater
|
||||
|
||||
@classmethod
|
||||
@@ -29,7 +30,7 @@ class Campaign:
|
||||
|
||||
sanitized_theater = data["theater"].replace(" ", "")
|
||||
return cls(data["name"], f"Terrain_{sanitized_theater}", data.get("authors", "???"),
|
||||
ConflictTheater.from_json(data))
|
||||
data.get("description", ""), ConflictTheater.from_json(data))
|
||||
|
||||
|
||||
def load_campaigns() -> List[Campaign]:
|
||||
|
||||
@@ -1,25 +1,32 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
from PySide2 import QtGui, QtWidgets
|
||||
from PySide2.QtCore import QItemSelectionModel, QPoint, Qt
|
||||
from PySide2.QtWidgets import QVBoxLayout
|
||||
from dcs.task import CAP, CAS
|
||||
from PySide2.QtWidgets import QVBoxLayout, QTextEdit
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
|
||||
import qt_ui.uiconstants as CONST
|
||||
from game import Game, db
|
||||
from game import db
|
||||
from game.settings import Settings
|
||||
from gen import namegen
|
||||
from qt_ui.windows.newgame.QCampaignList import (
|
||||
Campaign,
|
||||
QCampaignList,
|
||||
load_campaigns,
|
||||
)
|
||||
from theater import ConflictTheater, start_generator
|
||||
from theater.start_generator import GameGenerator
|
||||
|
||||
jinja_env = Environment(
|
||||
loader=FileSystemLoader("resources/ui/templates"),
|
||||
autoescape=select_autoescape(
|
||||
disabled_extensions=("",),
|
||||
default_for_string=True,
|
||||
default=True,
|
||||
),
|
||||
trim_blocks=True,
|
||||
lstrip_blocks=True,
|
||||
)
|
||||
|
||||
class NewGameWizard(QtWidgets.QWizard):
|
||||
def __init__(self, parent=None):
|
||||
@@ -41,7 +48,6 @@ class NewGameWizard(QtWidgets.QWizard):
|
||||
self.generatedGame = None
|
||||
|
||||
def accept(self):
|
||||
|
||||
logging.info("New Game Wizard accept")
|
||||
logging.info("======================")
|
||||
|
||||
@@ -56,7 +62,9 @@ class NewGameWizard(QtWidgets.QWizard):
|
||||
|
||||
timePeriod = db.TIME_PERIODS[list(db.TIME_PERIODS.keys())[self.field("timePeriod")]]
|
||||
midGame = self.field("midGame")
|
||||
multiplier = self.field("multiplier")
|
||||
# QSlider forces integers, so we use 1 to 50 and divide by 10 to give
|
||||
# 0.1 to 5.0.
|
||||
multiplier = self.field("multiplier") / 10
|
||||
no_carrier = self.field("no_carrier")
|
||||
no_lha = self.field("no_lha")
|
||||
supercarrier = self.field("supercarrier")
|
||||
@@ -76,39 +84,13 @@ class NewGameWizard(QtWidgets.QWizard):
|
||||
settings.do_not_generate_player_navy = no_player_navy
|
||||
settings.do_not_generate_enemy_navy = no_enemy_navy
|
||||
|
||||
self.generatedGame = self.start_new_game(player_name, enemy_name, conflictTheater, midGame, multiplier,
|
||||
timePeriod, settings, starting_money)
|
||||
generator = GameGenerator(player_name, enemy_name, conflictTheater,
|
||||
settings, timePeriod, starting_money,
|
||||
multiplier, midGame)
|
||||
self.generatedGame = generator.generate()
|
||||
|
||||
super(NewGameWizard, self).accept()
|
||||
|
||||
def start_new_game(self, player_name: str, enemy_name: str, conflictTheater: ConflictTheater,
|
||||
midgame: bool, multiplier: float, period: datetime, settings:Settings, starting_money: int):
|
||||
|
||||
# Reset name generator
|
||||
namegen.reset()
|
||||
start_generator.prepare_theater(conflictTheater, settings, midgame)
|
||||
|
||||
print("-- Starting New Game Generator")
|
||||
print("Enemy name : " + enemy_name)
|
||||
print("Player name : " + player_name)
|
||||
print("Midgame : " + str(midgame))
|
||||
start_generator.generate_initial_units(conflictTheater, enemy_name, True, multiplier)
|
||||
|
||||
print("-- Initial units generated")
|
||||
game = Game(player_name=player_name,
|
||||
enemy_name=enemy_name,
|
||||
theater=conflictTheater,
|
||||
start_date=period,
|
||||
settings=settings)
|
||||
|
||||
print("-- Game Object generated")
|
||||
start_generator.generate_groundobjects(conflictTheater, game)
|
||||
game.budget = starting_money
|
||||
game.settings.multiplier = multiplier
|
||||
game.settings.sams = True
|
||||
game.settings.version = CONST.VERSION_STRING
|
||||
return game
|
||||
|
||||
|
||||
class IntroPage(QtWidgets.QWizardPage):
|
||||
def __init__(self, parent=None):
|
||||
@@ -140,7 +122,9 @@ class FactionSelection(QtWidgets.QWizardPage):
|
||||
|
||||
# Factions selection
|
||||
self.factionsGroup = QtWidgets.QGroupBox("Factions")
|
||||
self.factionsGroupLayout = QtWidgets.QGridLayout()
|
||||
self.factionsGroupLayout = QtWidgets.QHBoxLayout()
|
||||
self.blueGroupLayout = QtWidgets.QGridLayout()
|
||||
self.redGroupLayout = QtWidgets.QGridLayout()
|
||||
|
||||
blueFaction = QtWidgets.QLabel("<b>Player Faction :</b>")
|
||||
self.blueFactionSelect = QtWidgets.QComboBox()
|
||||
@@ -152,6 +136,13 @@ class FactionSelection(QtWidgets.QWizardPage):
|
||||
self.redFactionSelect = QtWidgets.QComboBox()
|
||||
redFaction.setBuddy(self.redFactionSelect)
|
||||
|
||||
# Faction description
|
||||
self.blueFactionDescription = QTextEdit("")
|
||||
self.blueFactionDescription.setReadOnly(True)
|
||||
|
||||
self.redFactionDescription = QTextEdit("")
|
||||
self.redFactionDescription.setReadOnly(True)
|
||||
|
||||
# Setup default selected factions
|
||||
for i, r in enumerate(db.FACTIONS):
|
||||
self.redFactionSelect.addItem(r)
|
||||
@@ -160,20 +151,16 @@ class FactionSelection(QtWidgets.QWizardPage):
|
||||
if r == "USA 2005":
|
||||
self.blueFactionSelect.setCurrentIndex(i)
|
||||
|
||||
self.blueSideRecap = QtWidgets.QLabel("")
|
||||
self.blueSideRecap.setFont(CONST.FONT_PRIMARY_I)
|
||||
self.blueSideRecap.setWordWrap(True)
|
||||
self.blueGroupLayout.addWidget(blueFaction, 0, 0)
|
||||
self.blueGroupLayout.addWidget(self.blueFactionSelect, 0, 1)
|
||||
self.blueGroupLayout.addWidget(self.blueFactionDescription, 1, 0, 1, 2)
|
||||
|
||||
self.redSideRecap = QtWidgets.QLabel("")
|
||||
self.redSideRecap.setFont(CONST.FONT_PRIMARY_I)
|
||||
self.redSideRecap.setWordWrap(True)
|
||||
self.redGroupLayout.addWidget(redFaction, 0, 0)
|
||||
self.redGroupLayout.addWidget(self.redFactionSelect, 0, 1)
|
||||
self.redGroupLayout.addWidget(self.redFactionDescription, 1, 0, 1, 2)
|
||||
|
||||
self.factionsGroupLayout.addWidget(blueFaction, 0, 0)
|
||||
self.factionsGroupLayout.addWidget(self.blueFactionSelect, 0, 1)
|
||||
self.factionsGroupLayout.addWidget(self.blueSideRecap, 1, 0, 1, 2)
|
||||
self.factionsGroupLayout.addWidget(redFaction, 2, 0)
|
||||
self.factionsGroupLayout.addWidget(self.redFactionSelect, 2, 1)
|
||||
self.factionsGroupLayout.addWidget(self.redSideRecap, 3, 0, 1, 2)
|
||||
self.factionsGroupLayout.addLayout(self.blueGroupLayout)
|
||||
self.factionsGroupLayout.addLayout(self.redGroupLayout)
|
||||
self.factionsGroup.setLayout(self.factionsGroupLayout)
|
||||
|
||||
# Create required mod layout
|
||||
@@ -199,39 +186,34 @@ class FactionSelection(QtWidgets.QWizardPage):
|
||||
|
||||
def updateUnitRecap(self):
|
||||
|
||||
self.requiredMods.setText("<ul>")
|
||||
|
||||
red_faction = db.FACTIONS[self.redFactionSelect.currentText()]
|
||||
blue_faction = db.FACTIONS[self.blueFactionSelect.currentText()]
|
||||
|
||||
red_units = red_faction.aircrafts
|
||||
blue_units = blue_faction.aircrafts
|
||||
template = jinja_env.get_template("factiontemplate_EN.j2")
|
||||
|
||||
blue_txt = ""
|
||||
for u in blue_units:
|
||||
if u in db.UNIT_BY_TASK[CAP] or u in db.UNIT_BY_TASK[CAS]:
|
||||
blue_txt = blue_txt + u.id + ", "
|
||||
blue_txt = blue_txt + "\n"
|
||||
self.blueSideRecap.setText(blue_txt)
|
||||
blue_faction_txt = template.render({"faction": blue_faction})
|
||||
red_faction_txt = template.render({"faction": red_faction})
|
||||
|
||||
red_txt = ""
|
||||
for u in red_units:
|
||||
if u in db.UNIT_BY_TASK[CAP] or u in db.UNIT_BY_TASK[CAS]:
|
||||
red_txt = red_txt + u.id + ", "
|
||||
red_txt = red_txt + "\n"
|
||||
self.redSideRecap.setText(red_txt)
|
||||
self.blueFactionDescription.setText(blue_faction_txt)
|
||||
self.redFactionDescription.setText(red_faction_txt)
|
||||
|
||||
# Compute mod requirements txt
|
||||
self.requiredMods.setText("<ul>")
|
||||
has_mod = False
|
||||
if len(red_faction.requirements.keys()) > 0:
|
||||
has_mod = True
|
||||
for mod in red_faction.requirements.keys():
|
||||
self.requiredMods.setText(self.requiredMods.text() + "\n<li>" + mod + ": <a href=\""+red_faction.requirements[mod]+"\">" + red_faction.requirements[mod] + "</a></li>")
|
||||
self.requiredMods.setText(
|
||||
self.requiredMods.text() + "\n<li>" + mod + ": <a href=\"" + red_faction.requirements[mod] + "\">" +
|
||||
red_faction.requirements[mod] + "</a></li>")
|
||||
|
||||
if len(blue_faction.requirements.keys()) > 0:
|
||||
has_mod = True
|
||||
for mod in blue_faction.requirements.keys():
|
||||
if not "requirements" in red_faction.keys() or mod not in red_faction.requirements.keys():
|
||||
self.requiredMods.setText(self.requiredMods.text() + "\n<li>" + mod + ": <a href=\""+blue_faction.requirements[mod]+"\">" + blue_faction.requirements[mod] + "</a></li>")
|
||||
if mod not in red_faction.requirements.keys():
|
||||
self.requiredMods.setText(
|
||||
self.requiredMods.text() + "\n<li>" + mod + ": <a href=\"" + blue_faction.requirements[
|
||||
mod] + "\">" + blue_faction.requirements[mod] + "</a></li>")
|
||||
|
||||
if has_mod:
|
||||
self.requiredMods.setText(self.requiredMods.text() + "</ul>\n\n")
|
||||
@@ -255,10 +237,16 @@ class TheaterConfiguration(QtWidgets.QWizardPage):
|
||||
campaignList = QCampaignList(campaigns)
|
||||
self.registerField("selectedCampaign", campaignList)
|
||||
|
||||
# Faction description
|
||||
self.campaignMapDescription = QTextEdit("")
|
||||
self.campaignMapDescription.setReadOnly(True)
|
||||
|
||||
def on_campaign_selected():
|
||||
template = jinja_env.get_template("campaigntemplate_EN.j2")
|
||||
index = campaignList.selectionModel().currentIndex().row()
|
||||
campaign = campaignList.campaigns[index]
|
||||
self.setField("selectedCampaign", campaign)
|
||||
self.campaignMapDescription.setText(template.render({"campaign": campaign}))
|
||||
|
||||
campaignList.selectionModel().setCurrentIndex(campaignList.indexAt(QPoint(1, 1)), QItemSelectionModel.Rows)
|
||||
campaignList.selectionModel().selectionChanged.connect(on_campaign_selected)
|
||||
@@ -294,8 +282,9 @@ class TheaterConfiguration(QtWidgets.QWizardPage):
|
||||
layout = QtWidgets.QGridLayout()
|
||||
layout.setColumnMinimumWidth(0, 20)
|
||||
layout.addWidget(campaignList, 0, 0, 3, 1)
|
||||
layout.addWidget(mapSettingsGroup, 0, 1, 1, 1)
|
||||
layout.addWidget(timeGroup, 1, 1, 1, 1)
|
||||
layout.addWidget(self.campaignMapDescription, 0, 1, 1, 1)
|
||||
layout.addWidget(mapSettingsGroup, 1, 1, 1, 1)
|
||||
layout.addWidget(timeGroup, 2, 1, 1, 1)
|
||||
self.setLayout(layout)
|
||||
|
||||
|
||||
@@ -337,6 +326,44 @@ class BudgetInputs(QtWidgets.QGridLayout):
|
||||
self.addWidget(self.starting_money, 1, 1)
|
||||
|
||||
|
||||
class ForceMultiplierSpinner(QtWidgets.QSpinBox):
|
||||
def __init__(self, minimum: Optional[int] = None,
|
||||
maximum: Optional[int] = None,
|
||||
initial: Optional[int] = None) -> None:
|
||||
super().__init__()
|
||||
|
||||
if minimum is not None:
|
||||
self.setMinimum(minimum)
|
||||
if maximum is not None:
|
||||
self.setMaximum(maximum)
|
||||
if initial is not None:
|
||||
self.setValue(initial)
|
||||
|
||||
def textFromValue(self, val: int) -> str:
|
||||
return f"X {val / 10:.1f}"
|
||||
|
||||
|
||||
class ForceMultiplierInputs(QtWidgets.QGridLayout):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.addWidget(QtWidgets.QLabel("Enemy forces multiplier"), 0, 0)
|
||||
|
||||
minimum = 1
|
||||
maximum = 50
|
||||
initial = 10
|
||||
|
||||
slider = QtWidgets.QSlider(Qt.Horizontal)
|
||||
slider.setMinimum(minimum)
|
||||
slider.setMaximum(maximum)
|
||||
slider.setValue(initial)
|
||||
self.multiplier = ForceMultiplierSpinner(minimum, maximum, initial)
|
||||
slider.valueChanged.connect(lambda x: self.multiplier.setValue(x))
|
||||
self.multiplier.valueChanged.connect(lambda x: slider.setValue(x))
|
||||
|
||||
self.addWidget(slider, 1, 0)
|
||||
self.addWidget(self.multiplier, 1, 1)
|
||||
|
||||
|
||||
class MiscOptions(QtWidgets.QWizardPage):
|
||||
def __init__(self, parent=None):
|
||||
super(MiscOptions, self).__init__(parent)
|
||||
@@ -347,14 +374,12 @@ class MiscOptions(QtWidgets.QWizardPage):
|
||||
QtGui.QPixmap('./resources/ui/wizard/logo1.png'))
|
||||
|
||||
midGame = QtWidgets.QCheckBox()
|
||||
multiplier = QtWidgets.QSpinBox()
|
||||
multiplier.setEnabled(False)
|
||||
multiplier.setMinimum(1)
|
||||
multiplier.setMaximum(5)
|
||||
multiplier_inputs = ForceMultiplierInputs()
|
||||
self.registerField('multiplier', multiplier_inputs.multiplier)
|
||||
|
||||
|
||||
miscSettingsGroup = QtWidgets.QGroupBox("Misc Settings")
|
||||
self.registerField('midGame', midGame)
|
||||
self.registerField('multiplier', multiplier)
|
||||
|
||||
# Campaign settings
|
||||
generatorSettingsGroup = QtWidgets.QGroupBox("Generator Settings")
|
||||
@@ -364,7 +389,7 @@ class MiscOptions(QtWidgets.QWizardPage):
|
||||
self.registerField('no_lha', no_lha)
|
||||
supercarrier = QtWidgets.QCheckBox()
|
||||
self.registerField('supercarrier', supercarrier)
|
||||
no_player_navy= QtWidgets.QCheckBox()
|
||||
no_player_navy = QtWidgets.QCheckBox()
|
||||
self.registerField('no_player_navy', no_player_navy)
|
||||
no_enemy_navy = QtWidgets.QCheckBox()
|
||||
self.registerField('no_enemy_navy', no_enemy_navy)
|
||||
@@ -372,8 +397,7 @@ class MiscOptions(QtWidgets.QWizardPage):
|
||||
layout = QtWidgets.QGridLayout()
|
||||
layout.addWidget(QtWidgets.QLabel("Start at mid game"), 1, 0)
|
||||
layout.addWidget(midGame, 1, 1)
|
||||
layout.addWidget(QtWidgets.QLabel("Ennemy forces multiplier [Disabled for Now]"), 2, 0)
|
||||
layout.addWidget(multiplier, 2, 1)
|
||||
layout.addLayout(multiplier_inputs, 2, 0)
|
||||
miscSettingsGroup.setLayout(layout)
|
||||
|
||||
generatorLayout = QtWidgets.QGridLayout()
|
||||
|
||||
@@ -26,7 +26,8 @@ from game.infos.information import Information
|
||||
from qt_ui.widgets.QLabeledWidget import QLabeledWidget
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from qt_ui.windows.finances.QFinancesMenu import QHorizontalSeparationLine
|
||||
from plugin import LuaPluginManager
|
||||
from qt_ui.windows.settings.plugins import PluginOptionsPage, PluginsPage
|
||||
|
||||
|
||||
class CheatSettingsBox(QGroupBox):
|
||||
def __init__(self, game: Game, apply_settings: Callable[[], None]) -> None:
|
||||
@@ -97,21 +98,21 @@ class QSettingsWindow(QDialog):
|
||||
self.categoryModel.appendRow(cheat)
|
||||
self.right_layout.addWidget(self.cheatPage)
|
||||
|
||||
self.initPluginsLayout()
|
||||
if self.pluginsPage:
|
||||
plugins = QStandardItem("LUA Plugins")
|
||||
plugins.setIcon(CONST.ICONS["Plugins"])
|
||||
plugins.setEditable(False)
|
||||
plugins.setSelectable(True)
|
||||
self.categoryModel.appendRow(plugins)
|
||||
self.right_layout.addWidget(self.pluginsPage)
|
||||
if self.pluginsOptionsPage:
|
||||
pluginsOptions = QStandardItem("LUA Plugins Options")
|
||||
pluginsOptions.setIcon(CONST.ICONS["PluginsOptions"])
|
||||
pluginsOptions.setEditable(False)
|
||||
pluginsOptions.setSelectable(True)
|
||||
self.categoryModel.appendRow(pluginsOptions)
|
||||
self.right_layout.addWidget(self.pluginsOptionsPage)
|
||||
self.pluginsPage = PluginsPage()
|
||||
plugins = QStandardItem("LUA Plugins")
|
||||
plugins.setIcon(CONST.ICONS["Plugins"])
|
||||
plugins.setEditable(False)
|
||||
plugins.setSelectable(True)
|
||||
self.categoryModel.appendRow(plugins)
|
||||
self.right_layout.addWidget(self.pluginsPage)
|
||||
|
||||
self.pluginsOptionsPage = PluginOptionsPage()
|
||||
pluginsOptions = QStandardItem("LUA Plugins Options")
|
||||
pluginsOptions.setIcon(CONST.ICONS["PluginsOptions"])
|
||||
pluginsOptions.setEditable(False)
|
||||
pluginsOptions.setSelectable(True)
|
||||
self.categoryModel.appendRow(pluginsOptions)
|
||||
self.right_layout.addWidget(self.pluginsOptionsPage)
|
||||
|
||||
self.categoryList.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
self.categoryList.setModel(self.categoryModel)
|
||||
@@ -205,7 +206,7 @@ class QSettingsWindow(QDialog):
|
||||
self.generatorPage.setLayout(self.generatorLayout)
|
||||
|
||||
self.gameplay = QGroupBox("Gameplay")
|
||||
self.gameplayLayout = QGridLayout();
|
||||
self.gameplayLayout = QGridLayout()
|
||||
self.gameplayLayout.setAlignment(Qt.AlignTop)
|
||||
self.gameplay.setLayout(self.gameplayLayout)
|
||||
|
||||
@@ -217,10 +218,23 @@ class QSettingsWindow(QDialog):
|
||||
self.generate_marks.setChecked(self.game.settings.generate_marks)
|
||||
self.generate_marks.toggled.connect(self.applySettings)
|
||||
|
||||
self.never_delay_players = QCheckBox()
|
||||
self.never_delay_players.setChecked(
|
||||
self.game.settings.never_delay_player_flights)
|
||||
self.never_delay_players.toggled.connect(self.applySettings)
|
||||
self.never_delay_players.setToolTip(
|
||||
"When checked, player flights with a delayed start time will be "
|
||||
"spawned immediately. AI wingmen may begin startup immediately."
|
||||
)
|
||||
|
||||
self.gameplayLayout.addWidget(QLabel("Use Supercarrier Module"), 0, 0)
|
||||
self.gameplayLayout.addWidget(self.supercarrier, 0, 1, Qt.AlignRight)
|
||||
self.gameplayLayout.addWidget(QLabel("Put Objective Markers on Map"), 1, 0)
|
||||
self.gameplayLayout.addWidget(self.generate_marks, 1, 1, Qt.AlignRight)
|
||||
self.gameplayLayout.addWidget(
|
||||
QLabel("Never delay player flights"), 2, 0)
|
||||
self.gameplayLayout.addWidget(self.never_delay_players, 2, 1,
|
||||
Qt.AlignRight)
|
||||
|
||||
self.performance = QGroupBox("Performance")
|
||||
self.performanceLayout = QGridLayout()
|
||||
@@ -317,34 +331,6 @@ class QSettingsWindow(QDialog):
|
||||
self.moneyCheatBoxLayout.addWidget(btn, i/2, i%2)
|
||||
self.cheatLayout.addWidget(self.moneyCheatBox, stretch=1)
|
||||
|
||||
def initPluginsLayout(self):
|
||||
uiPrepared = False
|
||||
row:int = 0
|
||||
for plugin in LuaPluginManager().getPlugins():
|
||||
if plugin.hasUI():
|
||||
if not uiPrepared:
|
||||
uiPrepared = True
|
||||
|
||||
self.pluginsOptionsPage = QWidget()
|
||||
self.pluginsOptionsPageLayout = QVBoxLayout()
|
||||
self.pluginsOptionsPageLayout.setAlignment(Qt.AlignTop)
|
||||
self.pluginsOptionsPage.setLayout(self.pluginsOptionsPageLayout)
|
||||
|
||||
self.pluginsPage = QWidget()
|
||||
self.pluginsPageLayout = QVBoxLayout()
|
||||
self.pluginsPageLayout.setAlignment(Qt.AlignTop)
|
||||
self.pluginsPage.setLayout(self.pluginsPageLayout)
|
||||
|
||||
self.pluginsGroup = QGroupBox("Plugins")
|
||||
self.pluginsGroupLayout = QGridLayout();
|
||||
self.pluginsGroupLayout.setAlignment(Qt.AlignTop)
|
||||
self.pluginsGroup.setLayout(self.pluginsGroupLayout)
|
||||
|
||||
self.pluginsPageLayout.addWidget(self.pluginsGroup)
|
||||
|
||||
plugin.setupUI(self, row)
|
||||
row = row + 1
|
||||
|
||||
def cheatLambda(self, amount):
|
||||
return lambda: self.cheatMoney(amount)
|
||||
|
||||
@@ -366,6 +352,7 @@ class QSettingsWindow(QDialog):
|
||||
self.game.settings.map_coalition_visibility = self.mapVisibiitySelection.currentData()
|
||||
self.game.settings.external_views_allowed = self.ext_views.isChecked()
|
||||
self.game.settings.generate_marks = self.generate_marks.isChecked()
|
||||
self.game.settings.never_delay_player_flights = self.never_delay_players.isChecked()
|
||||
|
||||
self.game.settings.supercarrier = self.supercarrier.isChecked()
|
||||
|
||||
|
||||
71
qt_ui/windows/settings/plugins.py
Normal file
71
qt_ui/windows/settings/plugins.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtWidgets import (
|
||||
QCheckBox,
|
||||
QGridLayout,
|
||||
QGroupBox,
|
||||
QLabel, QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from game.plugins import LuaPlugin, LuaPluginManager
|
||||
|
||||
|
||||
class PluginsBox(QGroupBox):
|
||||
def __init__(self) -> None:
|
||||
super().__init__("Plugins")
|
||||
|
||||
layout = QGridLayout()
|
||||
layout.setAlignment(Qt.AlignTop)
|
||||
self.setLayout(layout)
|
||||
|
||||
for row, plugin in enumerate(LuaPluginManager.plugins()):
|
||||
if not plugin.show_in_ui:
|
||||
continue
|
||||
|
||||
layout.addWidget(QLabel(plugin.name), row, 0)
|
||||
|
||||
checkbox = QCheckBox()
|
||||
checkbox.setChecked(plugin.enabled)
|
||||
checkbox.toggled.connect(plugin.set_enabled)
|
||||
layout.addWidget(checkbox, row, 1)
|
||||
|
||||
|
||||
class PluginsPage(QWidget):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
layout = QVBoxLayout()
|
||||
layout.setAlignment(Qt.AlignTop)
|
||||
self.setLayout(layout)
|
||||
|
||||
layout.addWidget(PluginsBox())
|
||||
|
||||
|
||||
class PluginOptionsBox(QGroupBox):
|
||||
def __init__(self, plugin: LuaPlugin) -> None:
|
||||
super().__init__(plugin.name)
|
||||
|
||||
layout = QGridLayout()
|
||||
layout.setAlignment(Qt.AlignTop)
|
||||
self.setLayout(layout)
|
||||
|
||||
for row, option in enumerate(plugin.options):
|
||||
layout.addWidget(QLabel(option.name), row, 0)
|
||||
|
||||
checkbox = QCheckBox()
|
||||
checkbox.setChecked(option.enabled)
|
||||
checkbox.toggled.connect(option.set_enabled)
|
||||
layout.addWidget(checkbox, row, 1)
|
||||
|
||||
|
||||
class PluginOptionsPage(QWidget):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
layout = QVBoxLayout()
|
||||
layout.setAlignment(Qt.AlignTop)
|
||||
self.setLayout(layout)
|
||||
|
||||
for plugin in LuaPluginManager.plugins():
|
||||
if plugin.options:
|
||||
layout.addWidget(PluginOptionsBox(plugin))
|
||||
@@ -6,4 +6,5 @@ Pillow~=7.2.0
|
||||
tabulate~=0.8.7
|
||||
|
||||
mypy==0.782
|
||||
mypy-extensions==0.4.3
|
||||
mypy-extensions==0.4.3
|
||||
jinja2>=2.11.2
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user