mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Compare commits
71 Commits
verify-cha
...
develop-3.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6866ac245 | ||
|
|
aeb043e863 | ||
|
|
e33a4ec0c3 | ||
|
|
6b310a10de | ||
|
|
9433138c20 | ||
|
|
a3d61ea286 | ||
|
|
3a11068259 | ||
|
|
f5d4c04b75 | ||
|
|
04fa65107e | ||
|
|
abc0c19bd1 | ||
|
|
0d31de344d | ||
|
|
28c0ae7802 | ||
|
|
c0bef399ad | ||
|
|
21681a8240 | ||
|
|
bc0ac0cbfc | ||
|
|
7f508f0b2c | ||
|
|
ef9e957ef2 | ||
|
|
7d08e1ee2c | ||
|
|
97ac6a0612 | ||
|
|
83c311c853 | ||
|
|
df97110546 | ||
|
|
f570cfc12e | ||
|
|
70b4f75f25 | ||
|
|
1ac95438ce | ||
|
|
73ca6606e1 | ||
|
|
7a04cf5905 | ||
|
|
64c426653c | ||
|
|
a8960c9bbe | ||
|
|
72282845e8 | ||
|
|
e64aff4e91 | ||
|
|
e192e54c90 | ||
|
|
39b0599b7b | ||
|
|
45b40e4aa3 | ||
|
|
9887a8ff83 | ||
|
|
8d3556aa4b | ||
|
|
a59c01bcfe | ||
|
|
fb72962f74 | ||
|
|
ed1dacfe7c | ||
|
|
794de0fcbb | ||
|
|
9d71b2e727 | ||
|
|
5b8f626651 | ||
|
|
461f4b82a9 | ||
|
|
15653d0628 | ||
|
|
dffc631b87 | ||
|
|
17efb48b2e | ||
|
|
7e85825d2b | ||
|
|
798591b980 | ||
|
|
4a52af298c | ||
|
|
fe886a754e | ||
|
|
0220fa4ff6 | ||
|
|
e7336d8608 | ||
|
|
d77a174ac1 | ||
|
|
6348317893 | ||
|
|
a516cd2f80 | ||
|
|
e1aa3e9d0e | ||
|
|
d1d1acf6e0 | ||
|
|
a0e5a707fb | ||
|
|
4555a4968d | ||
|
|
ae34e4749b | ||
|
|
635eee9590 | ||
|
|
f0558c4c1e | ||
|
|
637ca8fbca | ||
|
|
e4e65df976 | ||
|
|
29579a2aec | ||
|
|
e32b43cffb | ||
|
|
de2e5f861b | ||
|
|
b27a7fc71b | ||
|
|
5861ce6146 | ||
|
|
c732ed556f | ||
|
|
be1a75e520 | ||
|
|
c41d10c581 |
32
.github/workflows/verify_changelog_change.yml
vendored
32
.github/workflows/verify_changelog_change.yml
vendored
@@ -1,32 +0,0 @@
|
||||
name: Verify Changelog.md update
|
||||
|
||||
# Controls when the workflow will run
|
||||
on:
|
||||
# Triggers the workflow on pull request events but only for the develop branch
|
||||
pull_request:
|
||||
branches: [ develop ]
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
# This workflow contains a single job called "confirm"
|
||||
confirm-changelog-update:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
name: Verify that the PR contains an update to changelog.md
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v8.4
|
||||
with:
|
||||
files: |
|
||||
changelog.md
|
||||
|
||||
- name: Fail if no change to changelog.md
|
||||
if: steps.changed-files.outputs.any_changed != 'true'
|
||||
run: exit 1
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,7 +12,6 @@ resources/tools/a.miz
|
||||
# User-specific stuff
|
||||
.idea/
|
||||
.env
|
||||
env/
|
||||
|
||||
/kneeboards
|
||||
/liberation_preferences.json
|
||||
|
||||
4
.gitmodules
vendored
Normal file
4
.gitmodules
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
[submodule "pydcs"]
|
||||
path = pydcs
|
||||
url = https://github.com/pydcs/dcs
|
||||
branch = master
|
||||
@@ -14,7 +14,7 @@
|
||||
DCS Liberation is a [DCS World](https://www.digitalcombatsimulator.com/en/products/world/) turn based single-player or co-op dynamic campaign.
|
||||
It is an external program that generates full and complex DCS missions and manage a persistent combat environment.
|
||||
|
||||

|
||||

|
||||
|
||||
## Downloads
|
||||
|
||||
|
||||
56
changelog.md
56
changelog.md
@@ -1,66 +1,12 @@
|
||||
# 5.0.0
|
||||
|
||||
Saves from 3.x are not compatible with 5.0.
|
||||
# 3.1.0
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
## Fixes
|
||||
|
||||
# 4.0.1
|
||||
|
||||
Saves from 4.0.0 are compatible with 4.0.1.
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
## Fixes
|
||||
|
||||
# 4.0.0
|
||||
|
||||
Saves from 3.x are not compatible with 4.0.
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Engine]** Support for DCS 2.7.2.7910.1 and newer, including Cyprus, F-16 JDAMs, and the Hind.
|
||||
* **[Campaign]** Squadrons now (optionally, off by default) have a maximum size and killed pilots replenish at a limited rate.
|
||||
* **[Campaign]** Added an option to disable levelling up of AI pilots.
|
||||
* **[Campaign]** Added Russian Intervention 2015 campaign on Syria, for a small and somewhat realistic Russian COIN scenario.
|
||||
* **[Campaign]** Added Operation Atilla campaign on Syria, for a reasonably large invasion of Cyprus scenario.
|
||||
* **[Campaign AI]** AI will plan Tanker flights.
|
||||
* **[Campaign AI]** Removed max distance for AEW&C auto planning.
|
||||
* **[Economy]** Adjusted prices for aircraft to balance out some price inconsistencies.
|
||||
* **[Factions]** Added more tankers to factions.
|
||||
* **[Flight Planner]** Added ability to plan Tankers.
|
||||
* **[Modding]** Campaign format version is now 7.0 to account for DCS map changes that made scenery strike targets incompatible with existing campaigns.
|
||||
* **[Mods]** Added support for the Gripen mod.
|
||||
* **[Mods]** Removes MB-339PAN support, as the mod is now deprecated and no longer works with DCS 2.7+.
|
||||
* **[Mission Generation]** Added support for "Neutral Dot" label options.
|
||||
* **[New Game Wizard]** Mods are now selected via checkboxes in the new game wizard, not as separate factions.
|
||||
* **[UI]** Ctrl click and shift click now buy or sell 5 or 10 units respectively.
|
||||
* **[UI]** Multiple waypoints can now be deleted simultaneously if multiple waypoints are selected.
|
||||
* **[UI]** Carriers and LHAs now match the colour of airfields, and their destination icons are translucent.
|
||||
* **[UI]** Updated intel box text for first turn.
|
||||
* **[UI]** Base Capture Cheat is now usable at all bases and can also be used to transfer player-owned bases to OPFOR.
|
||||
* **[UI]** Pass Turn button is relabled as "Begin Campaign" on Turn 0.
|
||||
* **[UI]** Added a ruler to the map.
|
||||
* **[UI]** Liberation now saves games to `<DCS user directory>/Liberation/Saves` by default to declutter the main directory.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Campaign AI]** Fix procurement for factions that lack some unit types.
|
||||
* **[Campaign AI]** Fix auto purchase of aircraft for factions that have no transport aircraft.
|
||||
* **[Campaign AI]** Fix refunding of pending aircraft purchases when a side has no factory available.
|
||||
* **[Mission Generation]** Fixed problem with mission load when control point name contained an apostrophe.
|
||||
* **[Mission Generation]** Fixed EWR group names so they contribute to Skynet again.
|
||||
* **[Mission Generation]** Fixed duplicate name error when generating convoys and cargo ships when creating manual transfers after loading a game.
|
||||
* **[Mission Generation]** Fixed empty convoys not being disbanded when all units are killed/removed.
|
||||
* **[Mission Generation]** Fixed player losing frontline progress when skipping from turn 0 to turn 1.
|
||||
* **[Mission Generation]** Fixed issue where frontline would only search to the right for valid locations.
|
||||
* **[UI]** Made non-interactive map elements less obstructive.
|
||||
* **[UI]** Added support for Neutral Dot difficulty label
|
||||
* **[UI]** Clear skies at night no longer described as "Sunny" by the weather widget.
|
||||
* **[UI]** Removed ability to buy (useless) ground units at carriers and LHAs.
|
||||
* **[UI]** Fixed enable/disable of buy/sell buttons.
|
||||
* **[UI]** EWRs now appear in the custom waypoint list.
|
||||
|
||||
# 3.0.0
|
||||
|
||||
|
||||
22
game/data/aaa_db.py
Normal file
22
game/data/aaa_db.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
AAA_UNITS = [
|
||||
AirDefence.SPAAA_Gepard,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
|
||||
AirDefence.SPAAA_Vulcan_M163,
|
||||
AirDefence.AAA_ZU_23_Closed_Emplacement,
|
||||
AirDefence.AAA_ZU_23_Emplacement,
|
||||
AirDefence.SPAAA_ZU_23_2_Mounted_Ural_375,
|
||||
AirDefence.AAA_ZU_23_Insurgent_Closed_Emplacement,
|
||||
AirDefence.SPAAA_ZU_23_2_Insurgent_Mounted_Ural_375,
|
||||
AirDefence.AAA_ZU_23_Insurgent_Emplacement,
|
||||
AirDefence.AAA_8_8cm_Flak_18,
|
||||
AirDefence.AAA_Flak_38_20mm,
|
||||
AirDefence.AAA_8_8cm_Flak_36,
|
||||
AirDefence.AAA_8_8cm_Flak_37,
|
||||
AirDefence.AAA_Flak_Vierling_38_Quad_20mm,
|
||||
AirDefence.AAA_SP_Kdo_G_40,
|
||||
AirDefence.AAA_8_8cm_Flak_41,
|
||||
AirDefence.AAA_Bofors_40mm,
|
||||
AirDefence.AAA_S_60_57mm,
|
||||
]
|
||||
@@ -4,35 +4,35 @@ from dcs.vehicles import AirDefence
|
||||
|
||||
class AlicCodes:
|
||||
CODES = {
|
||||
AirDefence._1L13_EWR.id: 101,
|
||||
AirDefence._55G6_EWR.id: 102,
|
||||
AirDefence.S_300PS_40B6MD_sr.id: 103,
|
||||
AirDefence.S_300PS_64H6E_sr.id: 104,
|
||||
AirDefence.SA_11_Buk_SR_9S18M1.id: 107,
|
||||
AirDefence.Kub_1S91_str.id: 108,
|
||||
AirDefence.Dog_Ear_radar.id: 109,
|
||||
AirDefence.S_300PS_40B6M_tr.id: 110,
|
||||
AirDefence.SA_11_Buk_LN_9A310M1.id: 115,
|
||||
AirDefence.Osa_9A33_ln.id: 117,
|
||||
AirDefence.Strela_10M3.id: 118,
|
||||
AirDefence.Tor_9A331.id: 119,
|
||||
AirDefence._2S6_Tunguska.id: 120,
|
||||
AirDefence.ZSU_23_4_Shilka.id: 121,
|
||||
AirDefence.P_19_s_125_sr.id: 122,
|
||||
AirDefence.Snr_s_125_tr.id: 123,
|
||||
AirDefence.Rapier_fsa_blindfire_radar.id: 124,
|
||||
AirDefence.Rapier_fsa_launcher.id: 125,
|
||||
AirDefence.SNR_75V.id: 126,
|
||||
AirDefence.HQ_7_LN_SP.id: 127,
|
||||
AirDefence.HQ_7_STR_SP.id: 128,
|
||||
AirDefence.Roland_ADS.id: 201,
|
||||
AirDefence.Patriot_str.id: 202,
|
||||
AirDefence.Hawk_sr.id: 203,
|
||||
AirDefence.Hawk_tr.id: 204,
|
||||
AirDefence.Roland_Radar.id: 205,
|
||||
AirDefence.Hawk_cwar.id: 206,
|
||||
AirDefence.Gepard.id: 207,
|
||||
AirDefence.Vulcan.id: 208,
|
||||
AirDefence.EWR_1L13.id: 101,
|
||||
AirDefence.EWR_55G6.id: 102,
|
||||
AirDefence.SAM_SA_10_S_300_Grumble_Clam_Shell_SR.id: 103,
|
||||
AirDefence.SAM_SA_10_S_300_Grumble_Big_Bird_SR.id: 104,
|
||||
AirDefence.SAM_SA_11_Buk_Gadfly_Snow_Drift_SR.id: 107,
|
||||
AirDefence.SAM_SA_6_Kub_Straight_Flush_STR.id: 108,
|
||||
AirDefence.MCC_SR_Sborka_Dog_Ear_SR.id: 109,
|
||||
AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR.id: 110,
|
||||
AirDefence.SAM_SA_11_Buk_Gadfly_Fire_Dome_TEL.id: 115,
|
||||
AirDefence.SAM_SA_8_Osa_Gecko_TEL.id: 117,
|
||||
AirDefence.SAM_SA_13_Strela_10M3_Gopher_TEL.id: 118,
|
||||
AirDefence.SAM_SA_15_Tor_Gauntlet.id: 119,
|
||||
AirDefence.SAM_SA_19_Tunguska_Grison.id: 120,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish.id: 121,
|
||||
AirDefence.SAM_P19_Flat_Face_SR__SA_2_3.id: 122,
|
||||
AirDefence.SAM_SA_3_S_125_Low_Blow_TR.id: 123,
|
||||
AirDefence.SAM_Rapier_Blindfire_TR.id: 124,
|
||||
AirDefence.SAM_Rapier_LN.id: 125,
|
||||
AirDefence.SAM_SA_2_S_75_Fan_Song_TR.id: 126,
|
||||
AirDefence.HQ_7_Self_Propelled_LN.id: 127,
|
||||
AirDefence.HQ_7_Self_Propelled_STR.id: 128,
|
||||
AirDefence.SAM_Roland_ADS.id: 201,
|
||||
AirDefence.SAM_Patriot_STR.id: 202,
|
||||
AirDefence.SAM_Hawk_SR__AN_MPQ_50.id: 203,
|
||||
AirDefence.SAM_Hawk_TR__AN_MPQ_46.id: 204,
|
||||
AirDefence.SAM_Roland_EWR.id: 205,
|
||||
AirDefence.SAM_Hawk_CWAR_AN_MPQ_55.id: 206,
|
||||
AirDefence.SPAAA_Gepard.id: 207,
|
||||
AirDefence.SPAAA_Vulcan_M163.id: 208,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
||||
51
game/data/cap_capabilities_db.py
Normal file
51
game/data/cap_capabilities_db.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from dcs.planes import (
|
||||
Bf_109K_4,
|
||||
C_101CC,
|
||||
FW_190A8,
|
||||
FW_190D9,
|
||||
F_5E_3,
|
||||
F_86F_Sabre,
|
||||
I_16,
|
||||
L_39ZA,
|
||||
MiG_15bis,
|
||||
MiG_19P,
|
||||
MiG_21Bis,
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
P_51D,
|
||||
P_51D_30_NA,
|
||||
SpitfireLFMkIX,
|
||||
SpitfireLFMkIXCW,
|
||||
)
|
||||
|
||||
from pydcs_extensions.a4ec.a4ec import A_4E_C
|
||||
|
||||
"""
|
||||
This list contains the aircraft that do not use the guns as the last resort weapons, but as a main weapon
|
||||
They'll RTB when they don't have gun ammo left
|
||||
"""
|
||||
GUNFIGHTERS = [
|
||||
# Cold War
|
||||
MiG_15bis,
|
||||
MiG_19P,
|
||||
MiG_21Bis,
|
||||
F_86F_Sabre,
|
||||
A_4E_C,
|
||||
F_5E_3,
|
||||
# Trainers
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
# WW2
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
I_16,
|
||||
]
|
||||
@@ -1,17 +1,239 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import unique, Enum
|
||||
from typing import Type
|
||||
|
||||
from dcs.vehicles import AirDefence, Infantry, Unarmed, Artillery, Armor
|
||||
from dcs.unittype import VehicleType
|
||||
|
||||
from pydcs_extensions.frenchpack import frenchpack
|
||||
|
||||
|
||||
@unique
|
||||
class GroundUnitClass(Enum):
|
||||
Tank = "Tank"
|
||||
Atgm = "ATGM"
|
||||
Ifv = "IFV"
|
||||
Apc = "APC"
|
||||
Artillery = "Artillery"
|
||||
Logistics = "Logistics"
|
||||
Recon = "Recon"
|
||||
Infantry = "Infantry"
|
||||
Shorads = "SHORADS"
|
||||
Manpads = "MANPADS"
|
||||
Tank = (
|
||||
"Tank",
|
||||
(
|
||||
Armor.MBT_T_55,
|
||||
Armor.MBT_T_72B,
|
||||
Armor.MBT_T_72B3,
|
||||
Armor.MBT_T_80U,
|
||||
Armor.MBT_T_90,
|
||||
Armor.MBT_Leopard_2A4,
|
||||
Armor.MBT_Leopard_2A4_Trs,
|
||||
Armor.MBT_Leopard_2A5,
|
||||
Armor.MBT_Leopard_2A6M,
|
||||
Armor.MBT_Leopard_1A3,
|
||||
Armor.MBT_Leclerc,
|
||||
Armor.MBT_Challenger_II,
|
||||
Armor.MBT_Chieftain_Mk_3,
|
||||
Armor.MBT_M1A2_Abrams,
|
||||
Armor.MBT_M60A3_Patton,
|
||||
Armor.MBT_Merkava_IV,
|
||||
Armor.ZTZ_96B,
|
||||
# WW2
|
||||
# Axis
|
||||
Armor.Tk_PzIV_H,
|
||||
Armor.SPG_Sturmpanzer_IV_Brummbar,
|
||||
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
|
||||
Armor.HT_Pz_Kpfw_VI_Tiger_I,
|
||||
Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II,
|
||||
# Allies
|
||||
Armor.Tk_M4_Sherman,
|
||||
Armor.CT_Centaur_IV,
|
||||
Armor.CT_Cromwell_IV,
|
||||
Armor.HIT_Churchill_VII,
|
||||
# Mods
|
||||
frenchpack.DIM__TOYOTA_BLUE,
|
||||
frenchpack.DIM__TOYOTA_GREEN,
|
||||
frenchpack.DIM__TOYOTA_DESERT,
|
||||
frenchpack.DIM__KAMIKAZE,
|
||||
frenchpack.AMX_30B2,
|
||||
frenchpack.Leclerc_Serie_XXI,
|
||||
),
|
||||
)
|
||||
|
||||
Atgm = (
|
||||
"ATGM",
|
||||
(
|
||||
Armor.ATGM_HMMWV,
|
||||
Armor.ATGM_VAB_Mephisto,
|
||||
Armor.ATGM_Stryker,
|
||||
Armor.IFV_BMP_2,
|
||||
# WW2 (Tank Destroyers)
|
||||
# Axxis
|
||||
Armor.SPG_StuG_III_Ausf__G,
|
||||
Armor.SPG_StuG_IV,
|
||||
Armor.SPG_Jagdpanzer_IV,
|
||||
Armor.SPG_Jagdpanther_G1,
|
||||
Armor.SPG_Sd_Kfz_184_Elefant,
|
||||
# Allies
|
||||
Armor.SPG_M10_GMC,
|
||||
Armor.MT_M4A4_Sherman_Firefly,
|
||||
# Mods
|
||||
frenchpack.VBAE_CRAB_MMP,
|
||||
frenchpack.VAB_MEPHISTO,
|
||||
frenchpack.TRM_2000_PAMELA,
|
||||
),
|
||||
)
|
||||
|
||||
Ifv = (
|
||||
"IFV",
|
||||
(
|
||||
Armor.IFV_BMP_3,
|
||||
Armor.IFV_BMP_2,
|
||||
Armor.IFV_BMP_1,
|
||||
Armor.IFV_Marder,
|
||||
Armor.IFV_Warrior,
|
||||
Armor.SPG_Stryker_MGS,
|
||||
Armor.IFV_M2A2_Bradley,
|
||||
Armor.IFV_BMD_1,
|
||||
Armor.ZBD_04A,
|
||||
# Mods
|
||||
frenchpack.VBAE_CRAB,
|
||||
frenchpack.VAB_T20_13,
|
||||
),
|
||||
)
|
||||
|
||||
Apc = (
|
||||
"APC",
|
||||
(
|
||||
Armor.IFV_M1126_Stryker_ICV,
|
||||
Armor.APC_M113,
|
||||
Armor.APC_BTR_80,
|
||||
Armor.IFV_BTR_82A,
|
||||
Armor.APC_MTLB,
|
||||
Armor.APC_AAV_7_Amphibious,
|
||||
Armor.APC_TPz_Fuchs,
|
||||
Armor.APC_BTR_RD,
|
||||
# WW2
|
||||
Armor.APC_M2A1_Halftrack,
|
||||
Armor.APC_Sd_Kfz_251_Halftrack,
|
||||
# Mods
|
||||
frenchpack.VAB__50,
|
||||
frenchpack.VBL__50,
|
||||
frenchpack.VBL_AANF1,
|
||||
),
|
||||
)
|
||||
|
||||
Artillery = (
|
||||
"Artillery",
|
||||
(
|
||||
Artillery.Grad_MRL_FDDM__FC,
|
||||
Artillery.MLRS_9A52_Smerch_HE_300mm,
|
||||
Artillery.SPH_2S1_Gvozdika_122mm,
|
||||
Artillery.SPH_2S3_Akatsia_152mm,
|
||||
Artillery.MLRS_BM_21_Grad_122mm,
|
||||
Artillery.MLRS_9K57_Uragan_BM_27_220mm,
|
||||
Artillery.SPH_M109_Paladin_155mm,
|
||||
Artillery.MLRS_M270_227mm,
|
||||
Artillery.SPM_2S9_Nona_120mm_M,
|
||||
Artillery.SPH_Dana_vz77_152mm,
|
||||
Artillery.SPH_T155_Firtina_155mm,
|
||||
Artillery.PLZ_05,
|
||||
Artillery.SPH_2S19_Msta_152mm,
|
||||
Artillery.MLRS_9A52_Smerch_CM_300mm,
|
||||
# WW2
|
||||
Artillery.SPG_M12_GMC_155mm,
|
||||
),
|
||||
)
|
||||
|
||||
Logistics = (
|
||||
"Logistics",
|
||||
(
|
||||
Unarmed.Carrier_M30_Cargo,
|
||||
Unarmed.Truck_M818_6x6,
|
||||
Unarmed.Truck_KAMAZ_43101,
|
||||
Unarmed.Truck_Ural_375,
|
||||
Unarmed.Truck_GAZ_66,
|
||||
Unarmed.Truck_GAZ_3307,
|
||||
Unarmed.Truck_GAZ_3308,
|
||||
Unarmed.Truck_Ural_4320_31_Arm_d,
|
||||
Unarmed.Truck_Ural_4320T,
|
||||
Unarmed.Truck_Opel_Blitz,
|
||||
Unarmed.LUV_Kubelwagen_82,
|
||||
Unarmed.Carrier_Sd_Kfz_7_Tractor,
|
||||
Unarmed.LUV_Kettenrad,
|
||||
Unarmed.Car_Willys_Jeep,
|
||||
Unarmed.LUV_Land_Rover_109,
|
||||
Unarmed.Truck_Land_Rover_101_FC,
|
||||
# Mods
|
||||
frenchpack.VBL,
|
||||
frenchpack.VAB,
|
||||
),
|
||||
)
|
||||
|
||||
Recon = (
|
||||
"Recon",
|
||||
(
|
||||
Armor.Scout_HMMWV,
|
||||
Armor.Scout_Cobra,
|
||||
Armor.LT_PT_76,
|
||||
Armor.IFV_LAV_25,
|
||||
Armor.Scout_BRDM_2,
|
||||
# WW2
|
||||
Armor.LT_Mk_VII_Tetrarch,
|
||||
Armor.IFV_Sd_Kfz_234_2_Puma,
|
||||
Armor.Car_M8_Greyhound_Armored,
|
||||
Armor.Car_Daimler_Armored,
|
||||
# Mods
|
||||
frenchpack.ERC_90,
|
||||
frenchpack.AMX_10RCR,
|
||||
frenchpack.AMX_10RCR_SEPAR,
|
||||
),
|
||||
)
|
||||
|
||||
Infantry = (
|
||||
"Infantry",
|
||||
(
|
||||
Infantry.Insurgent_AK_74,
|
||||
Infantry.Infantry_AK_74,
|
||||
Infantry.Infantry_M1_Garand,
|
||||
Infantry.Infantry_Mauser_98,
|
||||
Infantry.Infantry_SMLE_No_4_Mk_1,
|
||||
Infantry.Infantry_M4_Georgia,
|
||||
Infantry.Infantry_AK_74_Rus,
|
||||
Infantry.Paratrooper_AKS,
|
||||
Infantry.Paratrooper_RPG_16,
|
||||
Infantry.Infantry_M249,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Infantry_RPG,
|
||||
),
|
||||
)
|
||||
|
||||
Shorads = (
|
||||
"SHORADS",
|
||||
(
|
||||
AirDefence.SPAAA_ZU_23_2_Mounted_Ural_375,
|
||||
AirDefence.SPAAA_ZU_23_2_Insurgent_Mounted_Ural_375,
|
||||
AirDefence.SPAAA_ZSU_57_2,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
|
||||
AirDefence.SAM_SA_8_Osa_Gecko_TEL,
|
||||
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL,
|
||||
AirDefence.SAM_SA_13_Strela_10M3_Gopher_TEL,
|
||||
AirDefence.SAM_SA_15_Tor_Gauntlet,
|
||||
AirDefence.SAM_SA_19_Tunguska_Grison,
|
||||
AirDefence.SPAAA_Gepard,
|
||||
AirDefence.SPAAA_Vulcan_M163,
|
||||
AirDefence.SAM_Linebacker___Bradley_M6,
|
||||
AirDefence.SAM_Chaparral_M48,
|
||||
AirDefence.SAM_Avenger__Stinger,
|
||||
AirDefence.SAM_Roland_ADS,
|
||||
AirDefence.HQ_7_Self_Propelled_LN,
|
||||
AirDefence.AAA_8_8cm_Flak_18,
|
||||
AirDefence.AAA_8_8cm_Flak_36,
|
||||
AirDefence.AAA_8_8cm_Flak_37,
|
||||
AirDefence.AAA_8_8cm_Flak_41,
|
||||
AirDefence.AAA_Bofors_40mm,
|
||||
AirDefence.AAA_S_60_57mm,
|
||||
AirDefence.AAA_M1_37mm,
|
||||
AirDefence.AAA_QF_3_7,
|
||||
),
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self, class_name: str, unit_list: tuple[Type[VehicleType], ...]
|
||||
) -> None:
|
||||
self.class_name = class_name
|
||||
self.unit_list = unit_list
|
||||
|
||||
def __contains__(self, unit_type: Type[VehicleType]) -> bool:
|
||||
return unit_type in self.unit_list
|
||||
|
||||
@@ -1,108 +1,108 @@
|
||||
from dcs.ships import (
|
||||
PIOTR,
|
||||
MOSCOW,
|
||||
VINSON,
|
||||
CVN_71,
|
||||
CVN_72,
|
||||
CVN_73,
|
||||
Stennis,
|
||||
KUZNECOW,
|
||||
CV_1143_5,
|
||||
NEUSTRASH,
|
||||
ALBATROS,
|
||||
REZKY,
|
||||
MOLNIYA,
|
||||
LHA_Tarawa,
|
||||
PERRY,
|
||||
TICONDEROG,
|
||||
Type_052B,
|
||||
Type_052C,
|
||||
Type_054A,
|
||||
USS_Arleigh_Burke_IIa,
|
||||
Battlecruiser_1144_2_Pyotr_Velikiy,
|
||||
Cruiser_1164_Moskva,
|
||||
CVN_70_Carl_Vinson,
|
||||
CVN_71_Theodore_Roosevelt,
|
||||
CVN_72_Abraham_Lincoln,
|
||||
CVN_73_George_Washington,
|
||||
CVN_74_John_C__Stennis,
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
CV_1143_5_Admiral_Kuznetsov_2017,
|
||||
Frigate_11540_Neustrashimy,
|
||||
Corvette_1124_4_Grisha,
|
||||
Frigate_1135M_Rezky,
|
||||
Corvette_1241_1_Molniya,
|
||||
LHA_1_Tarawa,
|
||||
FFG_Oliver_Hazzard_Perry,
|
||||
CG_Ticonderoga,
|
||||
Type_052B_Destroyer,
|
||||
Type_052C_Destroyer,
|
||||
Type_054A_Frigate,
|
||||
DDG_Arleigh_Burke_IIa,
|
||||
)
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
TELARS = {
|
||||
AirDefence._2S6_Tunguska,
|
||||
AirDefence.SA_11_Buk_SR_9S18M1,
|
||||
AirDefence.Osa_9A33_ln,
|
||||
AirDefence.Tor_9A331,
|
||||
AirDefence.Roland_ADS,
|
||||
AirDefence.SAM_SA_19_Tunguska_Grison,
|
||||
AirDefence.SAM_SA_11_Buk_Gadfly_Fire_Dome_TEL,
|
||||
AirDefence.SAM_SA_8_Osa_Gecko_TEL,
|
||||
AirDefence.SAM_SA_15_Tor_Gauntlet,
|
||||
AirDefence.SAM_Roland_ADS,
|
||||
}
|
||||
|
||||
TRACK_RADARS = {
|
||||
AirDefence.Kub_1S91_str,
|
||||
AirDefence.Snr_s_125_tr,
|
||||
AirDefence.S_300PS_40B6M_tr,
|
||||
AirDefence.Hawk_tr,
|
||||
AirDefence.Patriot_str,
|
||||
AirDefence.SNR_75V,
|
||||
AirDefence.Rapier_fsa_blindfire_radar,
|
||||
AirDefence.HQ_7_STR_SP,
|
||||
AirDefence.SAM_SA_6_Kub_Straight_Flush_STR,
|
||||
AirDefence.SAM_SA_3_S_125_Low_Blow_TR,
|
||||
AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR,
|
||||
AirDefence.SAM_Hawk_TR__AN_MPQ_46,
|
||||
AirDefence.SAM_Patriot_STR,
|
||||
AirDefence.SAM_SA_2_S_75_Fan_Song_TR,
|
||||
AirDefence.SAM_Rapier_Blindfire_TR,
|
||||
AirDefence.HQ_7_Self_Propelled_STR,
|
||||
}
|
||||
|
||||
LAUNCHER_TRACKER_PAIRS = {
|
||||
AirDefence.Kub_2P25_ln: AirDefence.Kub_1S91_str,
|
||||
AirDefence._5p73_s_125_ln: AirDefence.Snr_s_125_tr,
|
||||
AirDefence.S_300PS_5P85C_ln: AirDefence.S_300PS_40B6M_tr,
|
||||
AirDefence.S_300PS_5P85D_ln: AirDefence.S_300PS_40B6M_tr,
|
||||
AirDefence.Hawk_ln: AirDefence.Hawk_tr,
|
||||
AirDefence.Patriot_ln: AirDefence.Patriot_str,
|
||||
AirDefence.S_75M_Volhov: AirDefence.SNR_75V,
|
||||
AirDefence.Rapier_fsa_launcher: AirDefence.Rapier_fsa_blindfire_radar,
|
||||
AirDefence.HQ_7_LN_SP: AirDefence.HQ_7_STR_SP,
|
||||
AirDefence.SAM_SA_6_Kub_Gainful_TEL: AirDefence.SAM_SA_6_Kub_Straight_Flush_STR,
|
||||
AirDefence.SAM_SA_3_S_125_Goa_LN: AirDefence.SAM_SA_3_S_125_Low_Blow_TR,
|
||||
AirDefence.SAM_SA_10_S_300_Grumble_TEL_D: AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR,
|
||||
AirDefence.SAM_SA_10_S_300_Grumble_TEL_C: AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR,
|
||||
AirDefence.SAM_Hawk_LN_M192: AirDefence.SAM_Hawk_TR__AN_MPQ_46,
|
||||
AirDefence.SAM_Patriot_LN: AirDefence.SAM_Patriot_STR,
|
||||
AirDefence.SAM_SA_2_S_75_Guideline_LN: AirDefence.SAM_SA_2_S_75_Fan_Song_TR,
|
||||
AirDefence.SAM_Rapier_LN: AirDefence.SAM_Rapier_Blindfire_TR,
|
||||
AirDefence.HQ_7_Self_Propelled_LN: AirDefence.HQ_7_Self_Propelled_STR,
|
||||
}
|
||||
|
||||
UNITS_WITH_RADAR = {
|
||||
# Radars
|
||||
AirDefence._2S6_Tunguska,
|
||||
AirDefence.SA_11_Buk_LN_9A310M1,
|
||||
AirDefence.Osa_9A33_ln,
|
||||
AirDefence.Tor_9A331,
|
||||
AirDefence.Gepard,
|
||||
AirDefence.Vulcan,
|
||||
AirDefence.Roland_ADS,
|
||||
AirDefence.ZSU_23_4_Shilka,
|
||||
AirDefence._1L13_EWR,
|
||||
AirDefence.Kub_1S91_str,
|
||||
AirDefence.S_300PS_40B6M_tr,
|
||||
AirDefence.S_300PS_40B6MD_sr,
|
||||
AirDefence._55G6_EWR,
|
||||
AirDefence.S_300PS_64H6E_sr,
|
||||
AirDefence.SA_11_Buk_SR_9S18M1,
|
||||
AirDefence.Dog_Ear_radar,
|
||||
AirDefence.Hawk_tr,
|
||||
AirDefence.Hawk_sr,
|
||||
AirDefence.Patriot_str,
|
||||
AirDefence.Hawk_cwar,
|
||||
AirDefence.P_19_s_125_sr,
|
||||
AirDefence.Roland_Radar,
|
||||
AirDefence.Snr_s_125_tr,
|
||||
AirDefence.SNR_75V,
|
||||
AirDefence.Rapier_fsa_blindfire_radar,
|
||||
AirDefence.HQ_7_LN_SP,
|
||||
AirDefence.HQ_7_STR_SP,
|
||||
AirDefence.FuMG_401,
|
||||
AirDefence.FuSe_65,
|
||||
AirDefence.SAM_SA_19_Tunguska_Grison,
|
||||
AirDefence.SAM_SA_11_Buk_Gadfly_Fire_Dome_TEL,
|
||||
AirDefence.SAM_SA_8_Osa_Gecko_TEL,
|
||||
AirDefence.SAM_SA_15_Tor_Gauntlet,
|
||||
AirDefence.SPAAA_Gepard,
|
||||
AirDefence.SPAAA_Vulcan_M163,
|
||||
AirDefence.SAM_Roland_ADS,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
|
||||
AirDefence.EWR_1L13,
|
||||
AirDefence.SAM_SA_6_Kub_Straight_Flush_STR,
|
||||
AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR,
|
||||
AirDefence.SAM_SA_10_S_300_Grumble_Clam_Shell_SR,
|
||||
AirDefence.EWR_55G6,
|
||||
AirDefence.SAM_SA_10_S_300_Grumble_Big_Bird_SR,
|
||||
AirDefence.SAM_SA_11_Buk_Gadfly_Snow_Drift_SR,
|
||||
AirDefence.MCC_SR_Sborka_Dog_Ear_SR,
|
||||
AirDefence.SAM_Hawk_TR__AN_MPQ_46,
|
||||
AirDefence.SAM_Hawk_SR__AN_MPQ_50,
|
||||
AirDefence.SAM_Patriot_STR,
|
||||
AirDefence.SAM_Hawk_CWAR_AN_MPQ_55,
|
||||
AirDefence.SAM_P19_Flat_Face_SR__SA_2_3,
|
||||
AirDefence.SAM_Roland_EWR,
|
||||
AirDefence.SAM_SA_3_S_125_Low_Blow_TR,
|
||||
AirDefence.SAM_SA_2_S_75_Fan_Song_TR,
|
||||
AirDefence.SAM_Rapier_Blindfire_TR,
|
||||
AirDefence.HQ_7_Self_Propelled_LN,
|
||||
AirDefence.HQ_7_Self_Propelled_STR,
|
||||
AirDefence.EWR_FuMG_401_Freya_LZ,
|
||||
AirDefence.EWR_FuSe_65_Würzburg_Riese,
|
||||
# Ships
|
||||
VINSON,
|
||||
PERRY,
|
||||
TICONDEROG,
|
||||
ALBATROS,
|
||||
KUZNECOW,
|
||||
MOLNIYA,
|
||||
MOSCOW,
|
||||
NEUSTRASH,
|
||||
PIOTR,
|
||||
REZKY,
|
||||
CV_1143_5,
|
||||
Stennis,
|
||||
CVN_71,
|
||||
CVN_72,
|
||||
CVN_73,
|
||||
USS_Arleigh_Burke_IIa,
|
||||
LHA_Tarawa,
|
||||
Type_052B,
|
||||
Type_054A,
|
||||
Type_052C,
|
||||
CVN_70_Carl_Vinson,
|
||||
FFG_Oliver_Hazzard_Perry,
|
||||
CG_Ticonderoga,
|
||||
Corvette_1124_4_Grisha,
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
Corvette_1241_1_Molniya,
|
||||
Cruiser_1164_Moskva,
|
||||
Frigate_11540_Neustrashimy,
|
||||
Battlecruiser_1144_2_Pyotr_Velikiy,
|
||||
Frigate_1135M_Rezky,
|
||||
CV_1143_5_Admiral_Kuznetsov_2017,
|
||||
CVN_74_John_C__Stennis,
|
||||
CVN_71_Theodore_Roosevelt,
|
||||
CVN_72_Abraham_Lincoln,
|
||||
CVN_73_George_Washington,
|
||||
DDG_Arleigh_Burke_IIa,
|
||||
LHA_1_Tarawa,
|
||||
Type_052B_Destroyer,
|
||||
Type_054A_Frigate,
|
||||
Type_052C_Destroyer,
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@ import datetime
|
||||
import inspect
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, Iterator, Optional, Set, Tuple, Union, cast
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Iterator, Optional, Set, Tuple, Type, Union, cast
|
||||
|
||||
from dcs.unitgroup import FlyingGroup
|
||||
from dcs.unittype import FlyingType
|
||||
from dcs.weapons_data import Weapons, weapon_ids
|
||||
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
|
||||
PydcsWeapon = Dict[str, Union[int, str]]
|
||||
PydcsWeaponAssignment = Tuple[int, PydcsWeapon]
|
||||
@@ -21,8 +21,8 @@ class Weapon:
|
||||
"""Wraps a pydcs weapon dict in a hashable type."""
|
||||
|
||||
cls_id: str
|
||||
name: str = field(compare=False)
|
||||
weight: int = field(compare=False)
|
||||
name: str
|
||||
weight: int
|
||||
|
||||
def available_on(self, date: datetime.date) -> bool:
|
||||
introduction_year = WEAPON_INTRODUCTION_YEARS.get(self)
|
||||
@@ -97,12 +97,12 @@ class Pylon:
|
||||
yield weapon
|
||||
|
||||
@classmethod
|
||||
def for_aircraft(cls, aircraft: AircraftType, number: int) -> Pylon:
|
||||
def for_aircraft(cls, aircraft: Type[FlyingType], number: int) -> Pylon:
|
||||
# In pydcs these are all arbitrary inner classes of the aircraft type.
|
||||
# The only way to identify them is by their name.
|
||||
pylons = [
|
||||
v
|
||||
for v in aircraft.dcs_unit_type.__dict__.values()
|
||||
for v in aircraft.__dict__.values()
|
||||
if inspect.isclass(v) and v.__name__.startswith("Pylon")
|
||||
]
|
||||
|
||||
@@ -121,8 +121,8 @@ class Pylon:
|
||||
return cls(number, allowed)
|
||||
|
||||
@classmethod
|
||||
def iter_pylons(cls, aircraft: AircraftType) -> Iterator[Pylon]:
|
||||
for pylon in sorted(list(aircraft.dcs_unit_type.pylons)):
|
||||
def iter_pylons(cls, aircraft: Type[FlyingType]) -> Iterator[Pylon]:
|
||||
for pylon in sorted(list(aircraft.pylons)):
|
||||
yield cls.for_aircraft(aircraft, pylon)
|
||||
|
||||
|
||||
|
||||
1347
game/db.py
1347
game/db.py
File diff suppressed because it is too large
Load Diff
@@ -1,248 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
from pathlib import Path
|
||||
from typing import ClassVar, Type, Iterator, TYPE_CHECKING, Optional, Any
|
||||
|
||||
import yaml
|
||||
from dcs.helicopters import helicopter_map
|
||||
from dcs.planes import plane_map
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game.dcs.unittype import UnitType
|
||||
from game.radio.channels import (
|
||||
ChannelNamer,
|
||||
RadioChannelAllocator,
|
||||
CommonRadioChannelAllocator,
|
||||
HueyChannelNamer,
|
||||
SCR522ChannelNamer,
|
||||
ViggenChannelNamer,
|
||||
ViperChannelNamer,
|
||||
TomcatChannelNamer,
|
||||
MirageChannelNamer,
|
||||
SingleRadioChannelNamer,
|
||||
FarmerRadioChannelAllocator,
|
||||
SCR522RadioChannelAllocator,
|
||||
ViggenRadioChannelAllocator,
|
||||
NoOpChannelAllocator,
|
||||
)
|
||||
from game.utils import Distance, Speed, feet, kph, knots
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gen.aircraft import FlightData
|
||||
from gen import AirSupport, RadioFrequency, RadioRegistry
|
||||
from gen.radios import Radio
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RadioConfig:
|
||||
inter_flight: Optional[Radio]
|
||||
intra_flight: Optional[Radio]
|
||||
channel_allocator: Optional[RadioChannelAllocator]
|
||||
channel_namer: Type[ChannelNamer]
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data: dict[str, Any]) -> RadioConfig:
|
||||
return RadioConfig(
|
||||
cls.make_radio(data.get("inter_flight", None)),
|
||||
cls.make_radio(data.get("intra_flight", None)),
|
||||
cls.make_allocator(data.get("channels", {})),
|
||||
cls.make_namer(data.get("channels", {})),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def make_radio(cls, name: Optional[str]) -> Optional[Radio]:
|
||||
from gen.radios import get_radio
|
||||
|
||||
if name is None:
|
||||
return None
|
||||
return get_radio(name)
|
||||
|
||||
@classmethod
|
||||
def make_allocator(cls, data: dict[str, Any]) -> Optional[RadioChannelAllocator]:
|
||||
try:
|
||||
alloc_type = data["type"]
|
||||
except KeyError:
|
||||
return None
|
||||
allocator_type: Type[RadioChannelAllocator] = {
|
||||
"SCR-522": SCR522RadioChannelAllocator,
|
||||
"common": CommonRadioChannelAllocator,
|
||||
"farmer": FarmerRadioChannelAllocator,
|
||||
"noop": NoOpChannelAllocator,
|
||||
"viggen": ViggenRadioChannelAllocator,
|
||||
}[alloc_type]
|
||||
return allocator_type.from_cfg(data)
|
||||
|
||||
@classmethod
|
||||
def make_namer(cls, config: dict[str, Any]) -> Type[ChannelNamer]:
|
||||
return {
|
||||
"SCR-522": SCR522ChannelNamer,
|
||||
"default": ChannelNamer,
|
||||
"huey": HueyChannelNamer,
|
||||
"mirage": MirageChannelNamer,
|
||||
"single": SingleRadioChannelNamer,
|
||||
"tomcat": TomcatChannelNamer,
|
||||
"viggen": ViggenChannelNamer,
|
||||
"viper": ViperChannelNamer,
|
||||
}[config.get("namer", "default")]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PatrolConfig:
|
||||
altitude: Optional[Distance]
|
||||
speed: Optional[Speed]
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data: dict[str, Any]) -> PatrolConfig:
|
||||
altitude = data.get("altitude", None)
|
||||
speed = data.get("altitude", None)
|
||||
return PatrolConfig(
|
||||
feet(altitude) if altitude is not None else None,
|
||||
knots(speed) if speed is not None else None,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AircraftType(UnitType[FlyingType]):
|
||||
carrier_capable: bool
|
||||
lha_capable: bool
|
||||
always_keeps_gun: bool
|
||||
|
||||
# If true, the aircraft does not use the guns as the last resort weapons, but as a main weapon.
|
||||
# It'll RTB when it doesn't have gun ammo left.
|
||||
gunfighter: bool
|
||||
|
||||
max_group_size: int
|
||||
patrol_altitude: Optional[Distance]
|
||||
patrol_speed: Optional[Speed]
|
||||
intra_flight_radio: Optional[Radio]
|
||||
channel_allocator: Optional[RadioChannelAllocator]
|
||||
channel_namer: Type[ChannelNamer]
|
||||
|
||||
_by_name: ClassVar[dict[str, AircraftType]] = {}
|
||||
_by_unit_type: ClassVar[dict[Type[FlyingType], list[AircraftType]]] = defaultdict(
|
||||
list
|
||||
)
|
||||
_loaded: ClassVar[bool] = False
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def dcs_id(self) -> str:
|
||||
return self.dcs_unit_type.id
|
||||
|
||||
@property
|
||||
def flyable(self) -> bool:
|
||||
return self.dcs_unit_type.flyable
|
||||
|
||||
@cached_property
|
||||
def max_speed(self) -> Speed:
|
||||
return kph(self.dcs_unit_type.max_speed)
|
||||
|
||||
def alloc_flight_radio(self, radio_registry: RadioRegistry) -> RadioFrequency:
|
||||
from gen.radios import ChannelInUseError, MHz
|
||||
|
||||
if self.intra_flight_radio is not None:
|
||||
return radio_registry.alloc_for_radio(self.intra_flight_radio)
|
||||
|
||||
freq = MHz(self.dcs_unit_type.radio_frequency)
|
||||
try:
|
||||
radio_registry.reserve(freq)
|
||||
except ChannelInUseError:
|
||||
pass
|
||||
return freq
|
||||
|
||||
def assign_channels_for_flight(
|
||||
self, flight: FlightData, air_support: AirSupport
|
||||
) -> None:
|
||||
if self.channel_allocator is not None:
|
||||
self.channel_allocator.assign_channels_for_flight(flight, air_support)
|
||||
|
||||
def channel_name(self, radio_id: int, channel_id: int) -> str:
|
||||
return self.channel_namer.channel_name(radio_id, channel_id)
|
||||
|
||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||
# Update any existing models with new data on load.
|
||||
updated = AircraftType.named(state["name"])
|
||||
state.update(updated.__dict__)
|
||||
self.__dict__.update(state)
|
||||
|
||||
@classmethod
|
||||
def register(cls, aircraft_type: AircraftType) -> None:
|
||||
cls._by_name[aircraft_type.name] = aircraft_type
|
||||
cls._by_unit_type[aircraft_type.dcs_unit_type].append(aircraft_type)
|
||||
|
||||
@classmethod
|
||||
def named(cls, name: str) -> AircraftType:
|
||||
if not cls._loaded:
|
||||
cls._load_all()
|
||||
return cls._by_name[name]
|
||||
|
||||
@classmethod
|
||||
def for_dcs_type(cls, dcs_unit_type: Type[FlyingType]) -> Iterator[AircraftType]:
|
||||
if not cls._loaded:
|
||||
cls._load_all()
|
||||
yield from cls._by_unit_type[dcs_unit_type]
|
||||
|
||||
@staticmethod
|
||||
def _each_unit_type() -> Iterator[Type[FlyingType]]:
|
||||
yield from helicopter_map.values()
|
||||
yield from plane_map.values()
|
||||
|
||||
@classmethod
|
||||
def _load_all(cls) -> None:
|
||||
for unit_type in cls._each_unit_type():
|
||||
for data in cls._each_variant_of(unit_type):
|
||||
cls.register(data)
|
||||
cls._loaded = True
|
||||
|
||||
@classmethod
|
||||
def _each_variant_of(cls, aircraft: Type[FlyingType]) -> Iterator[AircraftType]:
|
||||
data_path = Path("resources/units/aircraft") / f"{aircraft.id}.yaml"
|
||||
if not data_path.exists():
|
||||
logging.warning(f"No data for {aircraft.id}; it will not be available")
|
||||
return
|
||||
|
||||
with data_path.open() as data_file:
|
||||
data = yaml.safe_load(data_file)
|
||||
|
||||
try:
|
||||
price = data["price"]
|
||||
except KeyError as ex:
|
||||
raise KeyError(f"Missing required price field: {data_path}") from ex
|
||||
|
||||
radio_config = RadioConfig.from_data(data.get("radios", {}))
|
||||
patrol_config = PatrolConfig.from_data(data.get("patrol", {}))
|
||||
|
||||
try:
|
||||
introduction = data["introduced"]
|
||||
if introduction is None:
|
||||
introduction = "N/A"
|
||||
except KeyError:
|
||||
introduction = "No data."
|
||||
|
||||
for variant in data.get("variants", [aircraft.id]):
|
||||
yield AircraftType(
|
||||
dcs_unit_type=aircraft,
|
||||
name=variant,
|
||||
description=data.get("description", "No data."),
|
||||
year_introduced=introduction,
|
||||
country_of_origin=data.get("origin", "No data."),
|
||||
manufacturer=data.get("manufacturer", "No data."),
|
||||
role=data.get("role", "No data."),
|
||||
price=price,
|
||||
carrier_capable=data.get("carrier_capable", False),
|
||||
lha_capable=data.get("lha_capable", False),
|
||||
always_keeps_gun=data.get("always_keeps_gun", False),
|
||||
gunfighter=data.get("gunfighter", False),
|
||||
max_group_size=data.get("max_group_size", aircraft.group_size_max),
|
||||
patrol_altitude=patrol_config.altitude,
|
||||
patrol_speed=patrol_config.speed,
|
||||
intra_flight_radio=radio_config.intra_flight,
|
||||
channel_allocator=radio_config.channel_allocator,
|
||||
channel_namer=radio_config.channel_namer,
|
||||
)
|
||||
@@ -1,97 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Type, Optional, ClassVar, Iterator
|
||||
|
||||
import yaml
|
||||
from dcs.unittype import VehicleType
|
||||
from dcs.vehicles import vehicle_map
|
||||
|
||||
from game.data.groundunitclass import GroundUnitClass
|
||||
from game.dcs.unittype import UnitType
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GroundUnitType(UnitType[VehicleType]):
|
||||
unit_class: Optional[GroundUnitClass]
|
||||
spawn_weight: int
|
||||
|
||||
_by_name: ClassVar[dict[str, GroundUnitType]] = {}
|
||||
_by_unit_type: ClassVar[
|
||||
dict[Type[VehicleType], list[GroundUnitType]]
|
||||
] = defaultdict(list)
|
||||
_loaded: ClassVar[bool] = False
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def dcs_id(self) -> str:
|
||||
return self.dcs_unit_type.id
|
||||
|
||||
@classmethod
|
||||
def register(cls, aircraft_type: GroundUnitType) -> None:
|
||||
cls._by_name[aircraft_type.name] = aircraft_type
|
||||
cls._by_unit_type[aircraft_type.dcs_unit_type].append(aircraft_type)
|
||||
|
||||
@classmethod
|
||||
def named(cls, name: str) -> GroundUnitType:
|
||||
if not cls._loaded:
|
||||
cls._load_all()
|
||||
return cls._by_name[name]
|
||||
|
||||
@classmethod
|
||||
def for_dcs_type(cls, dcs_unit_type: Type[VehicleType]) -> Iterator[GroundUnitType]:
|
||||
if not cls._loaded:
|
||||
cls._load_all()
|
||||
yield from cls._by_unit_type[dcs_unit_type]
|
||||
|
||||
@staticmethod
|
||||
def _each_unit_type() -> Iterator[Type[VehicleType]]:
|
||||
yield from vehicle_map.values()
|
||||
|
||||
@classmethod
|
||||
def _load_all(cls) -> None:
|
||||
for unit_type in cls._each_unit_type():
|
||||
for data in cls._each_variant_of(unit_type):
|
||||
cls.register(data)
|
||||
cls._loaded = True
|
||||
|
||||
@classmethod
|
||||
def _each_variant_of(cls, vehicle: Type[VehicleType]) -> Iterator[GroundUnitType]:
|
||||
data_path = Path("resources/units/ground_units") / f"{vehicle.id}.yaml"
|
||||
if not data_path.exists():
|
||||
logging.warning(f"No data for {vehicle.id}; it will not be available")
|
||||
return
|
||||
|
||||
with data_path.open() as data_file:
|
||||
data = yaml.safe_load(data_file)
|
||||
|
||||
try:
|
||||
introduction = data["introduced"]
|
||||
if introduction is None:
|
||||
introduction = "N/A"
|
||||
except KeyError:
|
||||
introduction = "No data."
|
||||
|
||||
class_name = data.get("class")
|
||||
unit_class: Optional[GroundUnitClass] = None
|
||||
if class_name is not None:
|
||||
unit_class = GroundUnitClass(class_name)
|
||||
|
||||
for variant in data.get("variants", [vehicle.id]):
|
||||
yield GroundUnitType(
|
||||
dcs_unit_type=vehicle,
|
||||
unit_class=unit_class,
|
||||
spawn_weight=data.get("spawn_weight", 0),
|
||||
name=variant,
|
||||
description=data.get("description", "No data."),
|
||||
year_introduced=introduction,
|
||||
country_of_origin=data.get("origin", "No data."),
|
||||
manufacturer=data.get("manufacturer", "No data."),
|
||||
role=data.get("role", "No data."),
|
||||
price=data.get("price", 1),
|
||||
)
|
||||
@@ -1,26 +0,0 @@
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
from typing import TypeVar, Generic, Type
|
||||
|
||||
from dcs.unittype import UnitType as DcsUnitType
|
||||
|
||||
DcsUnitTypeT = TypeVar("DcsUnitTypeT", bound=DcsUnitType)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class UnitType(Generic[DcsUnitTypeT]):
|
||||
dcs_unit_type: Type[DcsUnitTypeT]
|
||||
name: str
|
||||
description: str
|
||||
year_introduced: str
|
||||
country_of_origin: str
|
||||
manufacturer: str
|
||||
role: str
|
||||
price: int
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
@cached_property
|
||||
def eplrs_capable(self) -> bool:
|
||||
return getattr(self.dcs_unit_type, "eplrs", False)
|
||||
@@ -14,12 +14,13 @@ from typing import (
|
||||
Dict,
|
||||
Iterator,
|
||||
List,
|
||||
Type,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
from dcs.unittype import FlyingType, UnitType
|
||||
|
||||
from game import db
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.theater import Airfield, ControlPoint
|
||||
from game.transfers import CargoShip
|
||||
from game.unitmap import (
|
||||
@@ -48,8 +49,8 @@ class AirLosses:
|
||||
def losses(self) -> Iterator[FlyingUnit]:
|
||||
return itertools.chain(self.player, self.enemy)
|
||||
|
||||
def by_type(self, player: bool) -> Dict[AircraftType, int]:
|
||||
losses_by_type: Dict[AircraftType, int] = defaultdict(int)
|
||||
def by_type(self, player: bool) -> Dict[Type[FlyingType], int]:
|
||||
losses_by_type: Dict[Type[FlyingType], int] = defaultdict(int)
|
||||
losses = self.player if player else self.enemy
|
||||
for loss in losses:
|
||||
losses_by_type[loss.flight.unit_type] += 1
|
||||
@@ -181,8 +182,8 @@ class Debriefing:
|
||||
def casualty_count(self, control_point: ControlPoint) -> int:
|
||||
return len([x for x in self.front_line_losses if x.origin == control_point])
|
||||
|
||||
def front_line_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]:
|
||||
losses_by_type: dict[GroundUnitType, int] = defaultdict(int)
|
||||
def front_line_losses_by_type(self, player: bool) -> Dict[Type[UnitType], int]:
|
||||
losses_by_type: Dict[Type[UnitType], int] = defaultdict(int)
|
||||
if player:
|
||||
losses = self.ground_losses.player_front_line
|
||||
else:
|
||||
@@ -191,8 +192,8 @@ class Debriefing:
|
||||
losses_by_type[loss.unit_type] += 1
|
||||
return losses_by_type
|
||||
|
||||
def convoy_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]:
|
||||
losses_by_type: dict[GroundUnitType, int] = defaultdict(int)
|
||||
def convoy_losses_by_type(self, player: bool) -> Dict[Type[UnitType], int]:
|
||||
losses_by_type: Dict[Type[UnitType], int] = defaultdict(int)
|
||||
if player:
|
||||
losses = self.ground_losses.player_convoy
|
||||
else:
|
||||
@@ -201,8 +202,8 @@ class Debriefing:
|
||||
losses_by_type[loss.unit_type] += 1
|
||||
return losses_by_type
|
||||
|
||||
def cargo_ship_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]:
|
||||
losses_by_type: dict[GroundUnitType, int] = defaultdict(int)
|
||||
def cargo_ship_losses_by_type(self, player: bool) -> Dict[Type[UnitType], int]:
|
||||
losses_by_type: Dict[Type[UnitType], int] = defaultdict(int)
|
||||
if player:
|
||||
ships = self.ground_losses.player_cargo_ships
|
||||
else:
|
||||
@@ -212,8 +213,8 @@ class Debriefing:
|
||||
losses_by_type[unit_type] += count
|
||||
return losses_by_type
|
||||
|
||||
def airlift_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]:
|
||||
losses_by_type: dict[GroundUnitType, int] = defaultdict(int)
|
||||
def airlift_losses_by_type(self, player: bool) -> Dict[Type[UnitType], int]:
|
||||
losses_by_type: Dict[Type[UnitType], int] = defaultdict(int)
|
||||
if player:
|
||||
losses = self.ground_losses.player_airlifts
|
||||
else:
|
||||
@@ -382,21 +383,15 @@ class PollDebriefingFileThread(threading.Thread):
|
||||
else:
|
||||
last_modified = 0
|
||||
while not self.stopped():
|
||||
try:
|
||||
if (
|
||||
os.path.isfile("state.json")
|
||||
and os.path.getmtime("state.json") > last_modified
|
||||
):
|
||||
with open("state.json", "r") as json_file:
|
||||
json_data = json.load(json_file)
|
||||
debriefing = Debriefing(json_data, self.game, self.unit_map)
|
||||
self.callback(debriefing)
|
||||
break
|
||||
except json.JSONDecodeError:
|
||||
logging.exception(
|
||||
"Failed to decode state.json. Probably attempted read while DCS "
|
||||
"was still writing the file. Will retry in 5 seconds."
|
||||
)
|
||||
if (
|
||||
os.path.isfile("state.json")
|
||||
and os.path.getmtime("state.json") > last_modified
|
||||
):
|
||||
with open("state.json", "r") as json_file:
|
||||
json_data = json.load(json_file)
|
||||
debriefing = Debriefing(json_data, self.game, self.unit_map)
|
||||
self.callback(debriefing)
|
||||
break
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ from game.operation.operation import Operation
|
||||
from game.theater import ControlPoint
|
||||
from gen import AirTaskingOrder
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
from ..dcs.groundunittype import GroundUnitType
|
||||
from ..unitmap import UnitMap
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -54,7 +53,7 @@ class Event:
|
||||
|
||||
@property
|
||||
def is_player_attacking(self) -> bool:
|
||||
return self.attacker_name == self.game.player_faction.name
|
||||
return self.attacker_name == self.game.player_name
|
||||
|
||||
@property
|
||||
def tasks(self) -> List[Type[Task]]:
|
||||
@@ -123,7 +122,7 @@ class Event:
|
||||
|
||||
def commit_air_losses(self, debriefing: Debriefing) -> None:
|
||||
for loss in debriefing.air_losses.losses:
|
||||
if loss.pilot is not None and (
|
||||
if (
|
||||
not loss.pilot.player
|
||||
or not self.game.settings.invulnerable_player_pilots
|
||||
):
|
||||
@@ -435,12 +434,12 @@ class Event:
|
||||
moved_units[frontline_unit] = int(count * move_factor)
|
||||
total_units_redeployed = total_units_redeployed + int(count * move_factor)
|
||||
|
||||
destination.base.commission_units(moved_units)
|
||||
destination.base.commision_units(moved_units)
|
||||
source.base.commit_losses(moved_units)
|
||||
|
||||
# Also transfer pending deliveries.
|
||||
for unit_type, count in source.pending_unit_deliveries.units.items():
|
||||
if not isinstance(unit_type, GroundUnitType):
|
||||
if not issubclass(unit_type, VehicleType):
|
||||
continue
|
||||
if count <= 0:
|
||||
# Don't transfer *sales*...
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
from __future__ import annotations
|
||||
from game.data.groundunitclass import GroundUnitClass
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, Dict, Type, List, Any, Iterator
|
||||
from typing import Optional, Dict, Type, List, Any, cast
|
||||
|
||||
import dcs
|
||||
from dcs.countries import country_dict
|
||||
from dcs.unittype import ShipType, UnitType
|
||||
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,
|
||||
@@ -21,9 +23,7 @@ from game.data.doctrine import (
|
||||
COLDWAR_DOCTRINE,
|
||||
WWII_DOCTRINE,
|
||||
)
|
||||
from game.data.groundunitclass import GroundUnitClass
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from pydcs_extensions.mod_units import MODDED_VEHICLES, MODDED_AIRPLANES
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -45,25 +45,25 @@ class Faction:
|
||||
description: str = field(default="")
|
||||
|
||||
# Available aircraft
|
||||
aircrafts: List[AircraftType] = field(default_factory=list)
|
||||
aircrafts: List[Type[FlyingType]] = field(default_factory=list)
|
||||
|
||||
# Available awacs aircraft
|
||||
awacs: List[AircraftType] = field(default_factory=list)
|
||||
awacs: List[Type[FlyingType]] = field(default_factory=list)
|
||||
|
||||
# Available tanker aircraft
|
||||
tankers: List[AircraftType] = field(default_factory=list)
|
||||
tankers: List[Type[FlyingType]] = field(default_factory=list)
|
||||
|
||||
# Available frontline units
|
||||
frontline_units: List[GroundUnitType] = field(default_factory=list)
|
||||
frontline_units: List[Type[VehicleType]] = field(default_factory=list)
|
||||
|
||||
# Available artillery units
|
||||
artillery_units: List[GroundUnitType] = field(default_factory=list)
|
||||
artillery_units: List[Type[VehicleType]] = field(default_factory=list)
|
||||
|
||||
# Infantry units used
|
||||
infantry_units: List[GroundUnitType] = field(default_factory=list)
|
||||
infantry_units: List[Type[VehicleType]] = field(default_factory=list)
|
||||
|
||||
# Logistics units used
|
||||
logistics_units: List[GroundUnitType] = field(default_factory=list)
|
||||
logistics_units: List[Type[VehicleType]] = field(default_factory=list)
|
||||
|
||||
# Possible SAMS site generators for this faction
|
||||
air_defenses: List[str] = field(default_factory=list)
|
||||
@@ -114,7 +114,7 @@ class Faction:
|
||||
has_jtac: bool = field(default=False)
|
||||
|
||||
# Unit to use as JTAC for this faction
|
||||
jtac_unit: Optional[AircraftType] = field(default=None)
|
||||
jtac_unit: Optional[Type[FlyingType]] = field(default=None)
|
||||
|
||||
# doctrine
|
||||
doctrine: Doctrine = field(default=MODERN_DOCTRINE)
|
||||
@@ -123,7 +123,7 @@ class Faction:
|
||||
building_set: List[str] = field(default_factory=list)
|
||||
|
||||
# List of default livery overrides
|
||||
liveries_overrides: Dict[AircraftType, List[str]] = field(default_factory=dict)
|
||||
liveries_overrides: Dict[Type[UnitType], List[str]] = field(default_factory=dict)
|
||||
|
||||
#: Set to True if the faction should force the "Unrestricted satnav" option
|
||||
#: for the mission. This option enables GPS for capable aircraft regardless
|
||||
@@ -134,11 +134,15 @@ class Faction:
|
||||
#: both will use it.
|
||||
unrestricted_satnav: bool = False
|
||||
|
||||
def has_access_to_unittype(self, unit_class: GroundUnitClass) -> bool:
|
||||
for vehicle in itertools.chain(self.frontline_units, self.artillery_units):
|
||||
if vehicle.unit_class is unit_class:
|
||||
def has_access_to_unittype(self, unitclass: GroundUnitClass) -> bool:
|
||||
has_access = False
|
||||
for vehicle in unitclass.unit_list:
|
||||
if vehicle in self.frontline_units:
|
||||
return True
|
||||
return False
|
||||
if vehicle in self.artillery_units:
|
||||
return True
|
||||
|
||||
return has_access
|
||||
|
||||
@classmethod
|
||||
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
|
||||
@@ -159,26 +163,16 @@ class Faction:
|
||||
faction.authors = json.get("authors", "")
|
||||
faction.description = json.get("description", "")
|
||||
|
||||
faction.aircrafts = [AircraftType.named(n) for n in json.get("aircrafts", [])]
|
||||
faction.awacs = [AircraftType.named(n) for n in json.get("awacs", [])]
|
||||
faction.tankers = [AircraftType.named(n) for n in json.get("tankers", [])]
|
||||
faction.aircrafts = load_all_aircraft(json.get("aircrafts", []))
|
||||
faction.awacs = load_all_aircraft(json.get("awacs", []))
|
||||
faction.tankers = load_all_aircraft(json.get("tankers", []))
|
||||
|
||||
faction.aircrafts = list(
|
||||
set(faction.aircrafts + faction.awacs + faction.tankers)
|
||||
)
|
||||
faction.aircrafts = list(set(faction.aircrafts + faction.awacs))
|
||||
|
||||
faction.frontline_units = [
|
||||
GroundUnitType.named(n) for n in json.get("frontline_units", [])
|
||||
]
|
||||
faction.artillery_units = [
|
||||
GroundUnitType.named(n) for n in json.get("artillery_units", [])
|
||||
]
|
||||
faction.infantry_units = [
|
||||
GroundUnitType.named(n) for n in json.get("infantry_units", [])
|
||||
]
|
||||
faction.logistics_units = [
|
||||
GroundUnitType.named(n) for n in json.get("logistics_units", [])
|
||||
]
|
||||
faction.frontline_units = load_all_vehicles(json.get("frontline_units", []))
|
||||
faction.artillery_units = load_all_vehicles(json.get("artillery_units", []))
|
||||
faction.infantry_units = load_all_vehicles(json.get("infantry_units", []))
|
||||
faction.logistics_units = load_all_vehicles(json.get("logistics_units", []))
|
||||
|
||||
faction.ewrs = json.get("ewrs", [])
|
||||
|
||||
@@ -202,7 +196,7 @@ class Faction:
|
||||
faction.has_jtac = json.get("has_jtac", False)
|
||||
jtac_name = json.get("jtac_unit", None)
|
||||
if jtac_name is not None:
|
||||
faction.jtac_unit = AircraftType.named(jtac_name)
|
||||
faction.jtac_unit = load_aircraft(jtac_name)
|
||||
else:
|
||||
faction.jtac_unit = None
|
||||
faction.navy_group_count = int(json.get("navy_group_count", 1))
|
||||
@@ -236,110 +230,87 @@ class Faction:
|
||||
# Load liveries override
|
||||
faction.liveries_overrides = {}
|
||||
liveries_overrides = json.get("liveries_overrides", {})
|
||||
for name, livery in liveries_overrides.items():
|
||||
aircraft = AircraftType.named(name)
|
||||
faction.liveries_overrides[aircraft] = [s.lower() for s in livery]
|
||||
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]
|
||||
|
||||
faction.unrestricted_satnav = json.get("unrestricted_satnav", False)
|
||||
|
||||
return faction
|
||||
|
||||
@property
|
||||
def ground_units(self) -> Iterator[GroundUnitType]:
|
||||
yield from self.artillery_units
|
||||
yield from self.frontline_units
|
||||
yield from self.logistics_units
|
||||
def units(self) -> List[Type[UnitType]]:
|
||||
return (
|
||||
self.infantry_units
|
||||
+ self.aircrafts
|
||||
+ self.awacs
|
||||
+ self.artillery_units
|
||||
+ self.frontline_units
|
||||
+ self.tankers
|
||||
+ self.logistics_units
|
||||
)
|
||||
|
||||
def infantry_with_class(
|
||||
self, unit_class: GroundUnitClass
|
||||
) -> Iterator[GroundUnitType]:
|
||||
for unit in self.infantry_units:
|
||||
if unit.unit_class is unit_class:
|
||||
yield unit
|
||||
|
||||
def apply_mod_settings(self, mod_settings) -> Faction:
|
||||
# aircraft
|
||||
if not mod_settings.a4_skyhawk:
|
||||
self.remove_aircraft("A-4E-C")
|
||||
if not mod_settings.hercules:
|
||||
self.remove_aircraft("Hercules")
|
||||
if not mod_settings.f22_raptor:
|
||||
self.remove_aircraft("F-22A")
|
||||
if not mod_settings.jas39_gripen:
|
||||
self.remove_aircraft("JAS39Gripen")
|
||||
self.remove_aircraft("JAS39Gripen_AG")
|
||||
if not mod_settings.su57_felon:
|
||||
self.remove_aircraft("Su-57")
|
||||
# frenchpack
|
||||
if not mod_settings.frenchpack:
|
||||
self.remove_vehicle("AMX10RCR")
|
||||
self.remove_vehicle("SEPAR")
|
||||
self.remove_vehicle("ERC")
|
||||
self.remove_vehicle("M120")
|
||||
self.remove_vehicle("AA20")
|
||||
self.remove_vehicle("TRM2000")
|
||||
self.remove_vehicle("TRM2000_Citerne")
|
||||
self.remove_vehicle("TRM2000_AA20")
|
||||
self.remove_vehicle("TRMMISTRAL")
|
||||
self.remove_vehicle("VABH")
|
||||
self.remove_vehicle("VAB_RADIO")
|
||||
self.remove_vehicle("VAB_50")
|
||||
self.remove_vehicle("VIB_VBR")
|
||||
self.remove_vehicle("VAB_HOT")
|
||||
self.remove_vehicle("VAB_MORTIER")
|
||||
self.remove_vehicle("VBL50")
|
||||
self.remove_vehicle("VBLANF1")
|
||||
self.remove_vehicle("VBL-radio")
|
||||
self.remove_vehicle("VBAE")
|
||||
self.remove_vehicle("VBAE_MMP")
|
||||
self.remove_vehicle("AMX-30B2")
|
||||
self.remove_vehicle("Tracma")
|
||||
self.remove_vehicle("JTACFP")
|
||||
self.remove_vehicle("SHERIDAN")
|
||||
self.remove_vehicle("Leclerc_XXI")
|
||||
self.remove_vehicle("Toyota_bleu")
|
||||
self.remove_vehicle("Toyota_vert")
|
||||
self.remove_vehicle("Toyota_desert")
|
||||
self.remove_vehicle("Kamikaze")
|
||||
self.remove_vehicle("AMX1375")
|
||||
self.remove_vehicle("AMX1390")
|
||||
self.remove_vehicle("VBCI")
|
||||
self.remove_vehicle("T62")
|
||||
self.remove_vehicle("T64BV")
|
||||
self.remove_vehicle("T72M")
|
||||
self.remove_vehicle("KORNET")
|
||||
# high digit sams
|
||||
if not mod_settings.high_digit_sams:
|
||||
self.remove_air_defenses("SA10BGenerator")
|
||||
self.remove_air_defenses("SA12Generator")
|
||||
self.remove_air_defenses("SA20Generator")
|
||||
self.remove_air_defenses("SA20BGenerator")
|
||||
self.remove_air_defenses("SA23Generator")
|
||||
self.remove_air_defenses("SA17Generator")
|
||||
self.remove_air_defenses("KS19Generator")
|
||||
return self
|
||||
def unit_loader(unit: str, class_repository: List[Any]) -> Optional[Type[UnitType]]:
|
||||
"""
|
||||
Find unit by name
|
||||
:param unit: Unit name as string
|
||||
:param class_repository: Repository of classes (Either a module, a class, or a list of classes)
|
||||
:return: The unit as a PyDCS type
|
||||
"""
|
||||
if unit is None:
|
||||
return None
|
||||
elif unit in plane_map.keys():
|
||||
return plane_map[unit]
|
||||
else:
|
||||
for mother_class in class_repository:
|
||||
if getattr(mother_class, unit, None) is not None:
|
||||
return getattr(mother_class, unit)
|
||||
if type(mother_class) is list:
|
||||
for m in mother_class:
|
||||
if m.__name__ == unit:
|
||||
return m
|
||||
logging.error(f"FACTION ERROR : Unable to find {unit} in pydcs")
|
||||
return None
|
||||
|
||||
def remove_aircraft(self, name):
|
||||
for i in self.aircrafts:
|
||||
if i.dcs_unit_type.id == name:
|
||||
self.aircrafts.remove(i)
|
||||
|
||||
def remove_air_defenses(self, name):
|
||||
for i in self.air_defenses:
|
||||
if i == name:
|
||||
self.air_defenses.remove(i)
|
||||
def load_aircraft(name: str) -> Optional[Type[FlyingType]]:
|
||||
return cast(
|
||||
Optional[FlyingType],
|
||||
unit_loader(name, [dcs.planes, dcs.helicopters, MODDED_AIRPLANES]),
|
||||
)
|
||||
|
||||
def remove_vehicle(self, name):
|
||||
for i in self.frontline_units:
|
||||
if i.dcs_unit_type.id == name:
|
||||
self.frontline_units.remove(i)
|
||||
|
||||
def load_all_aircraft(data) -> List[Type[FlyingType]]:
|
||||
items = []
|
||||
for name in data:
|
||||
item = load_aircraft(name)
|
||||
if item is not None:
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
|
||||
def load_vehicle(name: str) -> Optional[Type[VehicleType]]:
|
||||
return cast(
|
||||
Optional[FlyingType],
|
||||
unit_loader(
|
||||
name, [Infantry, Unarmed, Armor, AirDefence, Artillery, MODDED_VEHICLES]
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def load_all_vehicles(data) -> List[Type[VehicleType]]:
|
||||
items = []
|
||||
for name in data:
|
||||
item = load_vehicle(name)
|
||||
if item is not None:
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
|
||||
def load_ship(name: str) -> Optional[Type[ShipType]]:
|
||||
if (ship := getattr(dcs.ships, name, None)) is not None:
|
||||
return ship
|
||||
logging.error(f"FACTION ERROR : Unable to find {name} in dcs.ships")
|
||||
return None
|
||||
return cast(Optional[FlyingType], unit_loader(name, [dcs.ships]))
|
||||
|
||||
|
||||
def load_all_ships(data) -> List[Type[ShipType]]:
|
||||
|
||||
58
game/game.py
58
game/game.py
@@ -1,24 +1,22 @@
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
import itertools
|
||||
import logging
|
||||
import random
|
||||
import sys
|
||||
from datetime import date, datetime, timedelta
|
||||
from enum import Enum
|
||||
from typing import Any, List
|
||||
from typing import Any, Dict, List, Iterator
|
||||
|
||||
from dcs.action import Coalition
|
||||
from dcs.mapping import Point
|
||||
from dcs.task import CAP, CAS, PinpointStrike
|
||||
from dcs.vehicles import AirDefence
|
||||
from pydcs_extensions.a4ec.a4ec import A_4E_C
|
||||
from faker import Faker
|
||||
|
||||
from game import db
|
||||
from game.inventory import GlobalAircraftInventory
|
||||
from game.models.game_stats import GameStats
|
||||
from game.plugins import LuaPluginManager
|
||||
from gen import aircraft, naming
|
||||
from game.theater.theatergroundobject import MissileSiteGroundObject
|
||||
from gen.ato import AirTaskingOrder
|
||||
from gen.conflictgen import Conflict
|
||||
from gen.flights.ai_flight_planner import CoalitionMissionPlanner
|
||||
@@ -36,7 +34,7 @@ from .navmesh import NavMesh
|
||||
from .procurement import AircraftProcurementRequest, ProcurementAi
|
||||
from .profiling import logged_duration
|
||||
from .settings import Settings, AutoAtoBehavior
|
||||
from .squadrons import AirWing
|
||||
from .squadrons import Pilot, AirWing
|
||||
from .theater import ConflictTheater
|
||||
from .theater.bullseye import Bullseye
|
||||
from .theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
|
||||
@@ -88,8 +86,8 @@ class TurnState(Enum):
|
||||
class Game:
|
||||
def __init__(
|
||||
self,
|
||||
player_faction: Faction,
|
||||
enemy_faction: Faction,
|
||||
player_name: str,
|
||||
enemy_name: str,
|
||||
theater: ConflictTheater,
|
||||
start_date: datetime,
|
||||
settings: Settings,
|
||||
@@ -99,10 +97,10 @@ class Game:
|
||||
self.settings = settings
|
||||
self.events: List[Event] = []
|
||||
self.theater = theater
|
||||
self.player_faction = player_faction
|
||||
self.player_country = player_faction.country
|
||||
self.enemy_faction = enemy_faction
|
||||
self.enemy_country = enemy_faction.country
|
||||
self.player_name = player_name
|
||||
self.player_country = db.FACTIONS[player_name].country
|
||||
self.enemy_name = enemy_name
|
||||
self.enemy_country = db.FACTIONS[enemy_name].country
|
||||
# pass_turn() will be called when initialization is complete which will
|
||||
# increment this to turn 0 before it reaches the player.
|
||||
self.turn = -1
|
||||
@@ -110,7 +108,7 @@ class Game:
|
||||
self.date = date(start_date.year, start_date.month, start_date.day)
|
||||
self.game_stats = GameStats()
|
||||
self.game_stats.update(self)
|
||||
self.ground_planners: dict[int, GroundPlanner] = {}
|
||||
self.ground_planners: Dict[int, GroundPlanner] = {}
|
||||
self.informations = []
|
||||
self.informations.append(Information("Game Start", "-" * 40, 0))
|
||||
# Culling Zones are for areas around points of interest that contain things we may not wish to cull.
|
||||
@@ -121,7 +119,6 @@ class Game:
|
||||
self.enemy_budget = enemy_budget
|
||||
self.current_unit_id = 0
|
||||
self.current_group_id = 0
|
||||
self.name_generator = naming.namegen
|
||||
|
||||
self.conditions = self.generate_conditions()
|
||||
|
||||
@@ -151,7 +148,7 @@ class Game:
|
||||
|
||||
self.on_load(game_still_initializing=True)
|
||||
|
||||
def __getstate__(self) -> dict[str, Any]:
|
||||
def __getstate__(self) -> Dict[str, Any]:
|
||||
state = self.__dict__.copy()
|
||||
# Avoid persisting any volatile types that can be deterministically
|
||||
# recomputed on load for the sake of save compatibility.
|
||||
@@ -163,7 +160,7 @@ class Game:
|
||||
del state["red_faker"]
|
||||
return state
|
||||
|
||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||
def __setstate__(self, state: Dict[str, Any]) -> None:
|
||||
self.__dict__.update(state)
|
||||
# Regenerate any state that was not persisted.
|
||||
self.on_load()
|
||||
@@ -203,6 +200,14 @@ class Game:
|
||||
else:
|
||||
self.enemy_country = "Russia"
|
||||
|
||||
@property
|
||||
def player_faction(self) -> Faction:
|
||||
return db.FACTIONS[self.player_name]
|
||||
|
||||
@property
|
||||
def enemy_faction(self) -> Faction:
|
||||
return db.FACTIONS[self.enemy_name]
|
||||
|
||||
def faction_for(self, player: bool) -> Faction:
|
||||
if player:
|
||||
return self.player_faction
|
||||
@@ -242,8 +247,8 @@ class Game:
|
||||
player_cp,
|
||||
enemy_cp,
|
||||
enemy_cp.position,
|
||||
self.player_faction.name,
|
||||
self.enemy_faction.name,
|
||||
self.player_name,
|
||||
self.enemy_name,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -289,20 +294,12 @@ class Game:
|
||||
return (
|
||||
event
|
||||
and event.attacker_name
|
||||
and event.attacker_name == self.player_faction.name
|
||||
and event.attacker_name == self.player_name
|
||||
)
|
||||
else:
|
||||
raise RuntimeError(f"{event} was passed when an Event type was expected")
|
||||
|
||||
def on_load(self, game_still_initializing: bool = False) -> None:
|
||||
if not hasattr(self, "name_generator"):
|
||||
self.name_generator = naming.namegen
|
||||
# Hack: Replace the global name generator state with the state from the save
|
||||
# game.
|
||||
#
|
||||
# We need to persist this state so that names generated after game load don't
|
||||
# conflict with those generated before exit.
|
||||
naming.namegen = self.name_generator
|
||||
LuaPluginManager.load_settings(self.settings)
|
||||
ObjectiveDistanceCache.set_theater(self.theater)
|
||||
self.compute_conflicts_position()
|
||||
@@ -330,18 +327,15 @@ class Game:
|
||||
# one hop ahead. ControlPoint.process_turn handles unit deliveries.
|
||||
self.transfers.perform_transfers()
|
||||
|
||||
# Needs to happen *before* planning transfers so we don't cancel them.
|
||||
# Needs to happen *before* planning transfers so we don't cancel the
|
||||
self.reset_ato()
|
||||
for control_point in self.theater.controlpoints:
|
||||
control_point.process_turn(self)
|
||||
|
||||
self.blue_air_wing.replenish()
|
||||
self.red_air_wing.replenish()
|
||||
|
||||
if not skipped:
|
||||
if not skipped and self.turn > 1:
|
||||
for cp in self.theater.player_points():
|
||||
cp.base.affect_strength(+PLAYER_BASE_STRENGTH_RECOVERY)
|
||||
elif self.turn > 1:
|
||||
else:
|
||||
for cp in self.theater.player_points():
|
||||
if not cp.is_carrier and not cp.is_lha:
|
||||
cp.base.affect_strength(-PLAYER_BASE_STRENGTH_RECOVERY)
|
||||
|
||||
@@ -6,7 +6,6 @@ from typing import Dict, Iterable, Iterator, Set, Tuple, TYPE_CHECKING, Type
|
||||
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -18,9 +17,9 @@ class ControlPointAircraftInventory:
|
||||
|
||||
def __init__(self, control_point: ControlPoint) -> None:
|
||||
self.control_point = control_point
|
||||
self.inventory: Dict[AircraftType, int] = defaultdict(int)
|
||||
self.inventory: Dict[Type[FlyingType], int] = defaultdict(int)
|
||||
|
||||
def add_aircraft(self, aircraft: AircraftType, count: int) -> None:
|
||||
def add_aircraft(self, aircraft: Type[FlyingType], count: int) -> None:
|
||||
"""Adds aircraft to the inventory.
|
||||
|
||||
Args:
|
||||
@@ -29,7 +28,7 @@ class ControlPointAircraftInventory:
|
||||
"""
|
||||
self.inventory[aircraft] += count
|
||||
|
||||
def remove_aircraft(self, aircraft: AircraftType, count: int) -> None:
|
||||
def remove_aircraft(self, aircraft: Type[FlyingType], count: int) -> None:
|
||||
"""Removes aircraft from the inventory.
|
||||
|
||||
Args:
|
||||
@@ -43,12 +42,12 @@ class ControlPointAircraftInventory:
|
||||
available = self.inventory[aircraft]
|
||||
if available < count:
|
||||
raise ValueError(
|
||||
f"Cannot remove {count} {aircraft} from "
|
||||
f"Cannot remove {count} {aircraft.id} from "
|
||||
f"{self.control_point.name}. Only have {available}."
|
||||
)
|
||||
self.inventory[aircraft] -= count
|
||||
|
||||
def available(self, aircraft: AircraftType) -> int:
|
||||
def available(self, aircraft: Type[FlyingType]) -> int:
|
||||
"""Returns the number of available aircraft of the given type.
|
||||
|
||||
Args:
|
||||
@@ -60,14 +59,14 @@ class ControlPointAircraftInventory:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def types_available(self) -> Iterator[AircraftType]:
|
||||
def types_available(self) -> Iterator[Type[FlyingType]]:
|
||||
"""Iterates over all available aircraft types."""
|
||||
for aircraft, count in self.inventory.items():
|
||||
if count > 0:
|
||||
yield aircraft
|
||||
|
||||
@property
|
||||
def all_aircraft(self) -> Iterator[Tuple[AircraftType, int]]:
|
||||
def all_aircraft(self) -> Iterator[Tuple[Type[FlyingType], int]]:
|
||||
"""Iterates over all available aircraft types, including amounts."""
|
||||
for aircraft, count in self.inventory.items():
|
||||
if count > 0:
|
||||
@@ -108,9 +107,9 @@ class GlobalAircraftInventory:
|
||||
return self.inventories[control_point]
|
||||
|
||||
@property
|
||||
def available_types_for_player(self) -> Iterator[AircraftType]:
|
||||
def available_types_for_player(self) -> Iterator[Type[FlyingType]]:
|
||||
"""Iterates over all aircraft types available to the player."""
|
||||
seen: Set[AircraftType] = set()
|
||||
seen: Set[Type[FlyingType]] = set()
|
||||
for control_point, inventory in self.inventories.items():
|
||||
if control_point.captured:
|
||||
for aircraft in inventory.types_available:
|
||||
|
||||
@@ -17,7 +17,7 @@ from dcs.triggers import TriggerStart
|
||||
from game.plugins import LuaPluginManager
|
||||
from game.theater.theatergroundobject import TheaterGroundObject
|
||||
from gen import Conflict, FlightType, VisualGenerator
|
||||
from gen.aircraft import AircraftConflictGenerator, FlightData
|
||||
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
|
||||
@@ -77,8 +77,8 @@ class Operation:
|
||||
yield Conflict(
|
||||
cls.game.theater,
|
||||
frontline,
|
||||
cls.game.player_faction.name,
|
||||
cls.game.enemy_faction.name,
|
||||
cls.game.player_name,
|
||||
cls.game.enemy_name,
|
||||
cls.game.player_country,
|
||||
cls.game.enemy_country,
|
||||
frontline.position,
|
||||
@@ -95,8 +95,8 @@ class Operation:
|
||||
return Conflict(
|
||||
cls.game.theater,
|
||||
FrontLine(player_cp, enemy_cp),
|
||||
cls.game.player_faction.name,
|
||||
cls.game.enemy_faction.name,
|
||||
cls.game.player_name,
|
||||
cls.game.enemy_name,
|
||||
cls.game.player_country,
|
||||
cls.game.enemy_country,
|
||||
mid_point,
|
||||
@@ -215,7 +215,23 @@ class Operation:
|
||||
for flight in flights:
|
||||
if not flight.client_units:
|
||||
continue
|
||||
flight.aircraft_type.assign_channels_for_flight(flight, air_support)
|
||||
cls.assign_channels_to_flight(flight, air_support)
|
||||
|
||||
@staticmethod
|
||||
def assign_channels_to_flight(flight: FlightData, air_support: AirSupport) -> None:
|
||||
"""Assigns preset radio channels for a client flight."""
|
||||
airframe = flight.aircraft_type
|
||||
|
||||
try:
|
||||
aircraft_data = AIRCRAFT_DATA[airframe.id]
|
||||
except KeyError:
|
||||
logging.warning(f"No aircraft data for {airframe.id}")
|
||||
return
|
||||
|
||||
if aircraft_data.channel_allocator is not None:
|
||||
aircraft_data.channel_allocator.assign_channels_for_flight(
|
||||
flight, air_support
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _create_tacan_registry(
|
||||
@@ -359,7 +375,6 @@ class Operation:
|
||||
cls.game.settings,
|
||||
cls.game,
|
||||
cls.radio_registry,
|
||||
cls.tacan_registry,
|
||||
cls.unit_map,
|
||||
air_support=cls.airsupportgen.air_support,
|
||||
)
|
||||
@@ -389,8 +404,8 @@ class Operation:
|
||||
player_cp = front_line.blue_cp
|
||||
enemy_cp = front_line.red_cp
|
||||
conflict = Conflict.frontline_cas_conflict(
|
||||
cls.game.player_faction.name,
|
||||
cls.game.enemy_faction.name,
|
||||
cls.game.player_name,
|
||||
cls.game.enemy_name,
|
||||
cls.current_mission.country(cls.game.player_country),
|
||||
cls.current_mission.country(cls.game.enemy_country),
|
||||
front_line,
|
||||
|
||||
@@ -2,18 +2,16 @@ import logging
|
||||
import os
|
||||
import pickle
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
_dcs_saved_game_folder: Optional[str] = None
|
||||
_file_abs_path = None
|
||||
|
||||
|
||||
def setup(user_folder: str):
|
||||
global _dcs_saved_game_folder
|
||||
_dcs_saved_game_folder = user_folder
|
||||
if not save_dir().exists():
|
||||
save_dir().mkdir(parents=True)
|
||||
_file_abs_path = os.path.join(base_path(), "default.liberation")
|
||||
|
||||
|
||||
def base_path() -> str:
|
||||
@@ -22,20 +20,16 @@ def base_path() -> str:
|
||||
return _dcs_saved_game_folder
|
||||
|
||||
|
||||
def save_dir() -> Path:
|
||||
return Path(base_path()) / "Liberation" / "Saves"
|
||||
|
||||
|
||||
def _temporary_save_file() -> str:
|
||||
return str(save_dir() / "tmpsave.liberation")
|
||||
return os.path.join(base_path(), "tmpsave.liberation")
|
||||
|
||||
|
||||
def _autosave_path() -> str:
|
||||
return str(save_dir() / "autosave.liberation")
|
||||
return os.path.join(base_path(), "autosave.liberation")
|
||||
|
||||
|
||||
def mission_path_for(name: str) -> str:
|
||||
return os.path.join(base_path(), "Missions", name)
|
||||
return os.path.join(base_path(), "Missions", "{}".format(name))
|
||||
|
||||
|
||||
def load_game(path):
|
||||
|
||||
@@ -3,12 +3,12 @@ from __future__ import annotations
|
||||
import math
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterator, List, Optional, TYPE_CHECKING, Tuple
|
||||
from typing import Iterator, List, Optional, TYPE_CHECKING, Tuple, Type
|
||||
|
||||
from dcs.unittype import FlyingType, VehicleType
|
||||
|
||||
from game import db
|
||||
from game.data.groundunitclass import GroundUnitClass
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.factions.faction import Faction
|
||||
from game.theater import ControlPoint, MissionTarget
|
||||
from game.utils import Distance
|
||||
@@ -125,7 +125,7 @@ class ProcurementAi:
|
||||
if available % 2 == 0:
|
||||
continue
|
||||
inventory.remove_aircraft(aircraft, 1)
|
||||
total += aircraft.price
|
||||
total += db.PRICES[aircraft]
|
||||
return total
|
||||
|
||||
def repair_runways(self, budget: float) -> float:
|
||||
@@ -147,17 +147,17 @@ class ProcurementAi:
|
||||
|
||||
def affordable_ground_unit_of_class(
|
||||
self, budget: float, unit_class: GroundUnitClass
|
||||
) -> Optional[GroundUnitType]:
|
||||
) -> Optional[Type[VehicleType]]:
|
||||
faction_units = set(self.faction.frontline_units) | set(
|
||||
self.faction.artillery_units
|
||||
)
|
||||
of_class = {u for u in faction_units if u.unit_class is unit_class}
|
||||
of_class = set(unit_class.unit_list) & faction_units
|
||||
|
||||
# faction has no access to needed unit type, take a random unit
|
||||
if not of_class:
|
||||
of_class = faction_units
|
||||
|
||||
affordable_units = [u for u in of_class if u.price <= budget]
|
||||
affordable_units = [u for u in of_class if db.PRICES[u] <= budget]
|
||||
if not affordable_units:
|
||||
return None
|
||||
return random.choice(affordable_units)
|
||||
@@ -179,7 +179,7 @@ class ProcurementAi:
|
||||
# Can't afford any more units.
|
||||
break
|
||||
|
||||
budget -= unit.price
|
||||
budget -= db.PRICES[unit]
|
||||
cp.pending_unit_deliveries.order({unit: 1})
|
||||
|
||||
return budget
|
||||
@@ -215,12 +215,12 @@ class ProcurementAi:
|
||||
airbase: ControlPoint,
|
||||
number: int,
|
||||
max_price: float,
|
||||
) -> Optional[AircraftType]:
|
||||
best_choice: Optional[AircraftType] = None
|
||||
) -> Optional[Type[FlyingType]]:
|
||||
best_choice: Optional[Type[FlyingType]] = None
|
||||
for unit in aircraft_for_task(task):
|
||||
if unit not in self.faction.aircrafts:
|
||||
continue
|
||||
if unit.price * number > max_price:
|
||||
if db.PRICES[unit] * number > max_price:
|
||||
continue
|
||||
if not airbase.can_operate(unit):
|
||||
continue
|
||||
@@ -241,7 +241,7 @@ class ProcurementAi:
|
||||
|
||||
def affordable_aircraft_for(
|
||||
self, request: AircraftProcurementRequest, airbase: ControlPoint, budget: float
|
||||
) -> Optional[AircraftType]:
|
||||
) -> Optional[Type[FlyingType]]:
|
||||
return self._affordable_aircraft_for_task(
|
||||
request.task_capability, airbase, request.number, budget
|
||||
)
|
||||
@@ -259,7 +259,7 @@ class ProcurementAi:
|
||||
# able to operate expensive aircraft.
|
||||
continue
|
||||
|
||||
budget -= unit.price * request.number
|
||||
budget -= db.PRICES[unit] * request.number
|
||||
airbase.pending_unit_deliveries.order({unit: request.number})
|
||||
return budget, True
|
||||
return budget, False
|
||||
@@ -360,9 +360,9 @@ class ProcurementAi:
|
||||
class_cost = 0
|
||||
total_cost = 0
|
||||
for unit_type, count in allocations.all.items():
|
||||
cost = unit_type.price * count
|
||||
cost = db.PRICES[unit_type] * count
|
||||
total_cost += cost
|
||||
if unit_type.unit_class is unit_class:
|
||||
if unit_type in unit_class:
|
||||
class_cost += cost
|
||||
if not total_cost:
|
||||
return 0
|
||||
|
||||
@@ -1,298 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Any, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gen import FlightData, AirSupport
|
||||
|
||||
|
||||
class RadioChannelAllocator:
|
||||
"""Base class for radio channel allocators."""
|
||||
|
||||
def assign_channels_for_flight(
|
||||
self, flight: FlightData, air_support: AirSupport
|
||||
) -> None:
|
||||
"""Assigns mission frequencies to preset channels for the flight."""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def from_cfg(cls, cfg: dict[str, Any]) -> RadioChannelAllocator:
|
||||
return cls()
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CommonRadioChannelAllocator(RadioChannelAllocator):
|
||||
"""Radio channel allocator suitable for most aircraft.
|
||||
|
||||
Most of the aircraft with preset channels available have one or more radios
|
||||
with 20 or more channels available (typically per-radio, but this is not the
|
||||
case for the JF-17).
|
||||
"""
|
||||
|
||||
#: Index of the radio used for intra-flight communications. Matches the
|
||||
#: index of the panel_radio field of the pydcs.dcs.planes object.
|
||||
inter_flight_radio_index: Optional[int]
|
||||
|
||||
#: Index of the radio used for intra-flight communications. Matches the
|
||||
#: index of the panel_radio field of the pydcs.dcs.planes object.
|
||||
intra_flight_radio_index: Optional[int]
|
||||
|
||||
def assign_channels_for_flight(
|
||||
self, flight: FlightData, air_support: AirSupport
|
||||
) -> None:
|
||||
if self.intra_flight_radio_index is not None:
|
||||
flight.assign_channel(
|
||||
self.intra_flight_radio_index, 1, flight.intra_flight_channel
|
||||
)
|
||||
|
||||
if self.inter_flight_radio_index is None:
|
||||
return
|
||||
|
||||
# For cases where the inter-flight and intra-flight radios share presets
|
||||
# (the JF-17 only has one set of channels, even though it can use two
|
||||
# channels simultaneously), start assigning inter-flight channels at 2.
|
||||
radio_id = self.inter_flight_radio_index
|
||||
if self.intra_flight_radio_index == radio_id:
|
||||
first_channel = 2
|
||||
else:
|
||||
first_channel = 1
|
||||
|
||||
last_channel = flight.num_radio_channels(radio_id)
|
||||
channel_alloc = iter(range(first_channel, last_channel + 1))
|
||||
|
||||
if flight.departure.atc is not None:
|
||||
flight.assign_channel(radio_id, next(channel_alloc), flight.departure.atc)
|
||||
|
||||
# TODO: If there ever are multiple AWACS, limit to mission relevant.
|
||||
for awacs in air_support.awacs:
|
||||
flight.assign_channel(radio_id, next(channel_alloc), awacs.freq)
|
||||
|
||||
if flight.arrival != flight.departure and flight.arrival.atc is not None:
|
||||
flight.assign_channel(radio_id, next(channel_alloc), flight.arrival.atc)
|
||||
|
||||
try:
|
||||
# TODO: Skip incompatible tankers.
|
||||
for tanker in air_support.tankers:
|
||||
flight.assign_channel(radio_id, next(channel_alloc), tanker.freq)
|
||||
|
||||
if flight.divert is not None and flight.divert.atc is not None:
|
||||
flight.assign_channel(radio_id, next(channel_alloc), flight.divert.atc)
|
||||
except StopIteration:
|
||||
# Any remaining channels are nice-to-haves, but not necessary for
|
||||
# the few aircraft with a small number of channels available.
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def from_cfg(cls, cfg: dict[str, Any]) -> CommonRadioChannelAllocator:
|
||||
return CommonRadioChannelAllocator(
|
||||
inter_flight_radio_index=cfg["inter_flight_radio_index"],
|
||||
intra_flight_radio_index=cfg["intra_flight_radio_index"],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
return "common"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class NoOpChannelAllocator(RadioChannelAllocator):
|
||||
"""Channel allocator for aircraft that don't support preset channels."""
|
||||
|
||||
def assign_channels_for_flight(
|
||||
self, flight: FlightData, air_support: AirSupport
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
return "noop"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FarmerRadioChannelAllocator(RadioChannelAllocator):
|
||||
"""Preset channel allocator for the MiG-19P."""
|
||||
|
||||
def assign_channels_for_flight(
|
||||
self, flight: FlightData, air_support: AirSupport
|
||||
) -> None:
|
||||
# The Farmer only has 6 preset channels. It also only has a VHF radio,
|
||||
# and currently our ATC data and AWACS are only in the UHF band.
|
||||
radio_id = 1
|
||||
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
|
||||
# TODO: Assign 4-6 to VHF frequencies of departure, arrival, and divert.
|
||||
# TODO: Assign 2 and 3 to AWACS if it is VHF.
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
return "farmer"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ViggenRadioChannelAllocator(RadioChannelAllocator):
|
||||
"""Preset channel allocator for the AJS37."""
|
||||
|
||||
def assign_channels_for_flight(
|
||||
self, flight: FlightData, air_support: AirSupport
|
||||
) -> None:
|
||||
# The Viggen's preset channels are handled differently from other
|
||||
# aircraft. The aircraft automatically configures channels for every
|
||||
# allied flight in the game (including AWACS) and for every airfield. As
|
||||
# such, we don't need to allocate any of those. There are seven presets
|
||||
# we can modify, however: three channels for the main radio intended for
|
||||
# communication with wingmen, and four emergency channels for the backup
|
||||
# radio. We'll set the first channel of the main radio to the
|
||||
# intra-flight channel, and the first three emergency channels to each
|
||||
# of the flight plan's airfields. The fourth emergency channel is always
|
||||
# the guard channel.
|
||||
radio_id = 1
|
||||
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
|
||||
if flight.departure.atc is not None:
|
||||
flight.assign_channel(radio_id, 4, flight.departure.atc)
|
||||
if flight.arrival.atc is not None:
|
||||
flight.assign_channel(radio_id, 5, flight.arrival.atc)
|
||||
# TODO: Assign divert to 6 when we support divert airfields.
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
return "viggen"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SCR522RadioChannelAllocator(RadioChannelAllocator):
|
||||
"""Preset channel allocator for the SCR522 WW2 radios. (4 channels)"""
|
||||
|
||||
def assign_channels_for_flight(
|
||||
self, flight: FlightData, air_support: AirSupport
|
||||
) -> None:
|
||||
radio_id = 1
|
||||
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
|
||||
if flight.departure.atc is not None:
|
||||
flight.assign_channel(radio_id, 2, flight.departure.atc)
|
||||
if flight.arrival.atc is not None:
|
||||
flight.assign_channel(radio_id, 3, flight.arrival.atc)
|
||||
|
||||
# TODO : Some GCI on Channel 4 ?
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
return "SCR-522"
|
||||
|
||||
|
||||
class ChannelNamer:
|
||||
"""Base class allowing channel name customization per-aircraft.
|
||||
|
||||
Most aircraft will want to customize this behavior, but the default is
|
||||
reasonable for any aircraft with numbered radios.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def channel_name(radio_id: int, channel_id: int) -> str:
|
||||
"""Returns the name of the channel for the given radio and channel."""
|
||||
return f"COMM{radio_id} Ch {channel_id}"
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
return "default"
|
||||
|
||||
|
||||
class SingleRadioChannelNamer(ChannelNamer):
|
||||
"""Channel namer for the aircraft with only a single radio.
|
||||
|
||||
Aircraft like the MiG-19P and the MiG-21bis only have a single radio, so
|
||||
it's not necessary for us to name the radio when naming the channel.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def channel_name(radio_id: int, channel_id: int) -> str:
|
||||
return f"Ch {channel_id}"
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
return "single"
|
||||
|
||||
|
||||
class HueyChannelNamer(ChannelNamer):
|
||||
"""Channel namer for the UH-1H."""
|
||||
|
||||
@staticmethod
|
||||
def channel_name(radio_id: int, channel_id: int) -> str:
|
||||
return f"COM3 Ch {channel_id}"
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
return "huey"
|
||||
|
||||
|
||||
class MirageChannelNamer(ChannelNamer):
|
||||
"""Channel namer for the M-2000."""
|
||||
|
||||
@staticmethod
|
||||
def channel_name(radio_id: int, channel_id: int) -> str:
|
||||
radio_name = ["V/UHF", "UHF"][radio_id - 1]
|
||||
return f"{radio_name} Ch {channel_id}"
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
return "mirage"
|
||||
|
||||
|
||||
class TomcatChannelNamer(ChannelNamer):
|
||||
"""Channel namer for the F-14."""
|
||||
|
||||
@staticmethod
|
||||
def channel_name(radio_id: int, channel_id: int) -> str:
|
||||
radio_name = ["UHF", "VHF/UHF"][radio_id - 1]
|
||||
return f"{radio_name} Ch {channel_id}"
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
return "tomcat"
|
||||
|
||||
|
||||
class ViggenChannelNamer(ChannelNamer):
|
||||
"""Channel namer for the AJS37."""
|
||||
|
||||
@staticmethod
|
||||
def channel_name(radio_id: int, channel_id: int) -> str:
|
||||
if channel_id >= 4:
|
||||
channel_letter = "EFGH"[channel_id - 4]
|
||||
return f"FR 24 {channel_letter}"
|
||||
return f"FR 22 Special {channel_id}"
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
return "viggen"
|
||||
|
||||
|
||||
class ViperChannelNamer(ChannelNamer):
|
||||
"""Channel namer for the F-16."""
|
||||
|
||||
@staticmethod
|
||||
def channel_name(radio_id: int, channel_id: int) -> str:
|
||||
return f"COM{radio_id} Ch {channel_id}"
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
return "viper"
|
||||
|
||||
|
||||
class SCR522ChannelNamer(ChannelNamer):
|
||||
"""
|
||||
Channel namer for P-51 & P-47D
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def channel_name(radio_id: int, channel_id: int) -> str:
|
||||
if channel_id > 3:
|
||||
return "?"
|
||||
else:
|
||||
return f"Button " + "ABCD"[channel_id - 1]
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
return "SCR-522"
|
||||
@@ -20,7 +20,6 @@ class Settings:
|
||||
# Difficulty settings
|
||||
player_skill: str = "Good"
|
||||
enemy_skill: str = "Average"
|
||||
ai_pilot_levelling: bool = True
|
||||
enemy_vehicle_skill: str = "Average"
|
||||
map_coalition_visibility: ForcedOptions.Views = ForcedOptions.Views.All
|
||||
labels: str = "Full"
|
||||
@@ -34,18 +33,6 @@ class Settings:
|
||||
player_income_multiplier: float = 1.0
|
||||
enemy_income_multiplier: float = 1.0
|
||||
|
||||
#: Feature flag for squadron limits.
|
||||
enable_squadron_pilot_limits: bool = False
|
||||
|
||||
#: The maximum number of pilots a squadron can have at one time. Changing this after
|
||||
#: the campaign has started will have no immediate effect; pilots already in the
|
||||
#: squadron will not be removed if the limit is lowered and pilots will not be
|
||||
#: immediately created if the limit is raised.
|
||||
squadron_pilot_limit: int = 12
|
||||
|
||||
#: The number of pilots a squadron can replace per turn.
|
||||
squadron_replenishment_rate: int = 4
|
||||
|
||||
default_start_type: str = "Cold"
|
||||
|
||||
# Mission specific
|
||||
@@ -57,7 +44,6 @@ class Settings:
|
||||
automate_aircraft_reinforcements: bool = False
|
||||
restrict_weapons_by_date: bool = False
|
||||
disable_legacy_aewc: bool = True
|
||||
disable_legacy_tanker: bool = True
|
||||
generate_dark_kneeboard: bool = False
|
||||
invulnerable_player_pilots: bool = True
|
||||
auto_ato_behavior: AutoAtoBehavior = AutoAtoBehavior.Default
|
||||
|
||||
@@ -8,6 +8,7 @@ from dataclasses import dataclass, field
|
||||
from enum import unique, Enum
|
||||
from pathlib import Path
|
||||
from typing import (
|
||||
Type,
|
||||
Tuple,
|
||||
TYPE_CHECKING,
|
||||
Optional,
|
||||
@@ -16,9 +17,10 @@ from typing import (
|
||||
)
|
||||
|
||||
import yaml
|
||||
from dcs.unittype import FlyingType
|
||||
from faker import Faker
|
||||
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.db import flying_type_from_name
|
||||
from game.settings import AutoAtoBehavior
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -77,20 +79,11 @@ class Squadron:
|
||||
nickname: Optional[str]
|
||||
country: str
|
||||
role: str
|
||||
aircraft: AircraftType
|
||||
aircraft: Type[FlyingType]
|
||||
livery: Optional[str]
|
||||
mission_types: tuple[FlightType, ...]
|
||||
|
||||
#: The pool of pilots that have not yet been assigned to the squadron. This only
|
||||
#: happens when a preset squadron defines more preset pilots than the squadron limit
|
||||
#: allows. This pool will be consumed before random pilots are generated.
|
||||
pilot_pool: list[Pilot]
|
||||
|
||||
current_roster: list[Pilot] = field(default_factory=list, init=False, hash=False)
|
||||
available_pilots: list[Pilot] = field(
|
||||
default_factory=list, init=False, hash=False, compare=False
|
||||
)
|
||||
|
||||
pilots: list[Pilot]
|
||||
available_pilots: list[Pilot] = field(init=False, hash=False, compare=False)
|
||||
auto_assignable_mission_types: set[FlightType] = field(
|
||||
init=False, hash=False, compare=False
|
||||
)
|
||||
@@ -102,9 +95,7 @@ class Squadron:
|
||||
player: bool
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if any(p.status is not PilotStatus.Active for p in self.pilot_pool):
|
||||
raise ValueError("Squadrons can only be created with active pilots.")
|
||||
self._recruit_pilots(self.game.settings.squadron_pilot_limit)
|
||||
self.available_pilots = list(self.active_pilots)
|
||||
self.auto_assignable_mission_types = set(self.mission_types)
|
||||
|
||||
def __str__(self) -> str:
|
||||
@@ -112,19 +103,12 @@ class Squadron:
|
||||
return self.name
|
||||
return f'{self.name} "{self.nickname}"'
|
||||
|
||||
@property
|
||||
def pilot_limits_enabled(self) -> bool:
|
||||
return self.game.settings.enable_squadron_pilot_limits
|
||||
|
||||
def claim_new_pilot_if_allowed(self) -> Optional[Pilot]:
|
||||
if self.pilot_limits_enabled:
|
||||
return None
|
||||
self._recruit_pilots(1)
|
||||
return self.available_pilots.pop()
|
||||
|
||||
def claim_available_pilot(self) -> Optional[Pilot]:
|
||||
# No pilots available, so the preference is irrelevant. Create a new pilot and
|
||||
# return it.
|
||||
if not self.available_pilots:
|
||||
return self.claim_new_pilot_if_allowed()
|
||||
self.enlist_new_pilots(1)
|
||||
return self.available_pilots.pop()
|
||||
|
||||
# For opfor, so player/AI option is irrelevant.
|
||||
if not self.player:
|
||||
@@ -145,12 +129,11 @@ class Squadron:
|
||||
# No pilot was found that matched the user's preference.
|
||||
#
|
||||
# If they chose to *never* assign players and only players remain in the pool,
|
||||
# we cannot fill the slot with the available pilots.
|
||||
# we cannot fill the slot with the available pilots. Recruit a new one.
|
||||
#
|
||||
# If they only *prefer* players and we're out of players, just return an AI
|
||||
# pilot.
|
||||
# If they prefer players and we're out of players, just return an AI pilot.
|
||||
if not prefer_players:
|
||||
return self.claim_new_pilot_if_allowed()
|
||||
self.enlist_new_pilots(1)
|
||||
return self.available_pilots.pop()
|
||||
|
||||
def claim_pilot(self, pilot: Pilot) -> None:
|
||||
@@ -170,48 +153,23 @@ class Squadron:
|
||||
# repopulating the same size flight from the same squadron.
|
||||
self.available_pilots.extend(reversed(pilots))
|
||||
|
||||
def _recruit_pilots(self, count: int) -> None:
|
||||
new_pilots = self.pilot_pool[:count]
|
||||
self.pilot_pool = self.pilot_pool[count:]
|
||||
count -= len(new_pilots)
|
||||
new_pilots.extend([Pilot(self.faker.name()) for _ in range(count)])
|
||||
self.current_roster.extend(new_pilots)
|
||||
def enlist_new_pilots(self, count: int) -> None:
|
||||
new_pilots = [Pilot(self.faker.name()) for _ in range(count)]
|
||||
self.pilots.extend(new_pilots)
|
||||
self.available_pilots.extend(new_pilots)
|
||||
|
||||
def replenish_lost_pilots(self) -> None:
|
||||
if not self.pilot_limits_enabled:
|
||||
return
|
||||
|
||||
replenish_count = min(
|
||||
self.game.settings.squadron_replenishment_rate,
|
||||
self._number_of_unfilled_pilot_slots,
|
||||
)
|
||||
if replenish_count > 0:
|
||||
self._recruit_pilots(replenish_count)
|
||||
|
||||
def return_all_pilots(self) -> None:
|
||||
self.available_pilots = list(self.active_pilots)
|
||||
|
||||
@staticmethod
|
||||
def send_on_leave(pilot: Pilot) -> None:
|
||||
pilot.send_on_leave()
|
||||
|
||||
def return_from_leave(self, pilot: Pilot):
|
||||
if not self.has_unfilled_pilot_slots:
|
||||
raise RuntimeError(
|
||||
f"Cannot return {pilot} from leave because {self} is full"
|
||||
)
|
||||
pilot.return_from_leave()
|
||||
|
||||
@property
|
||||
def faker(self) -> Faker:
|
||||
return self.game.faker_for(self.player)
|
||||
|
||||
def _pilots_with_status(self, status: PilotStatus) -> list[Pilot]:
|
||||
return [p for p in self.current_roster if p.status == status]
|
||||
return [p for p in self.pilots if p.status == status]
|
||||
|
||||
def _pilots_without_status(self, status: PilotStatus) -> list[Pilot]:
|
||||
return [p for p in self.current_roster if p.status != status]
|
||||
return [p for p in self.pilots if p.status != status]
|
||||
|
||||
@property
|
||||
def active_pilots(self) -> list[Pilot]:
|
||||
@@ -222,47 +180,27 @@ class Squadron:
|
||||
return self._pilots_with_status(PilotStatus.OnLeave)
|
||||
|
||||
@property
|
||||
def number_of_pilots_including_inactive(self) -> int:
|
||||
return len(self.current_roster)
|
||||
def number_of_pilots_including_dead(self) -> int:
|
||||
return len(self.pilots)
|
||||
|
||||
@property
|
||||
def _number_of_unfilled_pilot_slots(self) -> int:
|
||||
return self.game.settings.squadron_pilot_limit - len(self.active_pilots)
|
||||
|
||||
@property
|
||||
def number_of_available_pilots(self) -> int:
|
||||
return len(self.available_pilots)
|
||||
|
||||
def can_provide_pilots(self, count: int) -> bool:
|
||||
return not self.pilot_limits_enabled or self.number_of_available_pilots >= count
|
||||
|
||||
@property
|
||||
def has_available_pilots(self) -> bool:
|
||||
return not self.pilot_limits_enabled or bool(self.available_pilots)
|
||||
|
||||
@property
|
||||
def has_unfilled_pilot_slots(self) -> bool:
|
||||
return not self.pilot_limits_enabled or self._number_of_unfilled_pilot_slots > 0
|
||||
|
||||
def can_auto_assign(self, task: FlightType) -> bool:
|
||||
return task in self.auto_assignable_mission_types
|
||||
def number_of_living_pilots(self) -> int:
|
||||
return len(self._pilots_without_status(PilotStatus.Dead))
|
||||
|
||||
def pilot_at_index(self, index: int) -> Pilot:
|
||||
return self.current_roster[index]
|
||||
return self.pilots[index]
|
||||
|
||||
@classmethod
|
||||
def from_yaml(cls, path: Path, game: Game, player: bool) -> Squadron:
|
||||
from gen.flights.ai_flight_planner_db import tasks_for_aircraft
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
with path.open(encoding="utf8") as squadron_file:
|
||||
with path.open() as squadron_file:
|
||||
data = yaml.safe_load(squadron_file)
|
||||
|
||||
name = data["aircraft"]
|
||||
try:
|
||||
unit_type = AircraftType.named(name)
|
||||
except KeyError as ex:
|
||||
raise KeyError(f"Could not find any aircraft named {name}") from ex
|
||||
unit_type = flying_type_from_name(data["aircraft"])
|
||||
if unit_type is None:
|
||||
raise KeyError(f"Could not find any aircraft with the ID {unit_type}")
|
||||
|
||||
pilots = [Pilot(n, player=False) for n in data.get("pilots", [])]
|
||||
pilots.extend([Pilot(n, player=True) for n in data.get("players", [])])
|
||||
@@ -285,7 +223,7 @@ class Squadron:
|
||||
aircraft=unit_type,
|
||||
livery=data.get("livery"),
|
||||
mission_types=tuple(mission_types),
|
||||
pilot_pool=pilots,
|
||||
pilots=pilots,
|
||||
game=game,
|
||||
player=player,
|
||||
)
|
||||
@@ -309,8 +247,8 @@ class SquadronLoader:
|
||||
yield Path(persistency.base_path()) / "Liberation/Squadrons"
|
||||
yield Path("resources/squadrons")
|
||||
|
||||
def load(self) -> dict[AircraftType, list[Squadron]]:
|
||||
squadrons: dict[AircraftType, list[Squadron]] = defaultdict(list)
|
||||
def load(self) -> dict[Type[FlyingType], list[Squadron]]:
|
||||
squadrons: dict[Type[FlyingType], list[Squadron]] = defaultdict(list)
|
||||
country = self.game.country_for(self.player)
|
||||
faction = self.game.faction_for(self.player)
|
||||
any_country = country.startswith("Combined Joint Task Forces ")
|
||||
@@ -375,35 +313,21 @@ class AirWing:
|
||||
aircraft=aircraft,
|
||||
livery=None,
|
||||
mission_types=tuple(tasks_for_aircraft(aircraft)),
|
||||
pilot_pool=[],
|
||||
pilots=[],
|
||||
game=game,
|
||||
player=player,
|
||||
)
|
||||
]
|
||||
|
||||
def squadrons_for(self, aircraft: AircraftType) -> Sequence[Squadron]:
|
||||
def squadrons_for(self, aircraft: Type[FlyingType]) -> Sequence[Squadron]:
|
||||
return self.squadrons[aircraft]
|
||||
|
||||
def can_auto_plan(self, task: FlightType) -> bool:
|
||||
try:
|
||||
next(self.auto_assignable_for_task(task))
|
||||
return True
|
||||
except StopIteration:
|
||||
return False
|
||||
|
||||
def auto_assignable_for_task(self, task: FlightType) -> Iterator[Squadron]:
|
||||
def squadrons_for_task(self, task: FlightType) -> Iterator[Squadron]:
|
||||
for squadron in self.iter_squadrons():
|
||||
if squadron.can_auto_assign(task):
|
||||
if task in squadron.mission_types:
|
||||
yield squadron
|
||||
|
||||
def auto_assignable_for_task_with_type(
|
||||
self, aircraft: AircraftType, task: FlightType
|
||||
) -> Iterator[Squadron]:
|
||||
for squadron in self.squadrons_for(aircraft):
|
||||
if squadron.can_auto_assign(task) and squadron.has_available_pilots:
|
||||
yield squadron
|
||||
|
||||
def squadron_for(self, aircraft: AircraftType) -> Squadron:
|
||||
def squadron_for(self, aircraft: Type[FlyingType]) -> Squadron:
|
||||
return self.squadrons_for(aircraft)[0]
|
||||
|
||||
def iter_squadrons(self) -> Iterator[Squadron]:
|
||||
@@ -412,10 +336,6 @@ class AirWing:
|
||||
def squadron_at_index(self, index: int) -> Squadron:
|
||||
return list(self.iter_squadrons())[index]
|
||||
|
||||
def replenish(self) -> None:
|
||||
for squadron in self.iter_squadrons():
|
||||
squadron.replenish_lost_pilots()
|
||||
|
||||
def reset(self) -> None:
|
||||
for squadron in self.iter_squadrons():
|
||||
squadron.return_all_pilots()
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
import itertools
|
||||
import logging
|
||||
from typing import Any
|
||||
import math
|
||||
import typing
|
||||
from typing import Dict, Type
|
||||
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.dcs.unittype import UnitType
|
||||
from dcs.task import AWACS, CAP, CAS, Embarking, PinpointStrike, Task, Transport
|
||||
from dcs.unittype import FlyingType, UnitType, VehicleType
|
||||
from dcs.vehicles import AirDefence, Armor
|
||||
|
||||
from game import db
|
||||
from game.db import PRICES
|
||||
|
||||
STRENGTH_AA_ASSEMBLE_MIN = 0.2
|
||||
PLANES_SCRAMBLE_MIN_BASE = 2
|
||||
PLANES_SCRAMBLE_MAX_BASE = 8
|
||||
PLANES_SCRAMBLE_FACTOR = 0.3
|
||||
|
||||
BASE_MAX_STRENGTH = 1
|
||||
BASE_MIN_STRENGTH = 0
|
||||
@@ -12,8 +22,11 @@ BASE_MIN_STRENGTH = 0
|
||||
|
||||
class Base:
|
||||
def __init__(self):
|
||||
self.aircraft: dict[AircraftType, int] = {}
|
||||
self.armor: dict[GroundUnitType, int] = {}
|
||||
self.aircraft: Dict[Type[FlyingType], int] = {}
|
||||
self.armor: Dict[Type[VehicleType], int] = {}
|
||||
# TODO: Appears unused.
|
||||
self.aa: Dict[AirDefence, int] = {}
|
||||
self.commision_points: Dict[Type, float] = {}
|
||||
self.strength = 1
|
||||
|
||||
@property
|
||||
@@ -28,52 +41,145 @@ class Base:
|
||||
def total_armor_value(self) -> int:
|
||||
total = 0
|
||||
for unit_type, count in self.armor.items():
|
||||
total += unit_type.price * count
|
||||
try:
|
||||
total += PRICES[unit_type] * count
|
||||
except KeyError:
|
||||
logging.exception(f"No price found for {unit_type.id}")
|
||||
return total
|
||||
|
||||
def total_units_of_type(self, unit_type: UnitType) -> int:
|
||||
@property
|
||||
def total_aa(self) -> int:
|
||||
return sum(self.aa.values())
|
||||
|
||||
def total_units(self, task: Task) -> int:
|
||||
return sum(
|
||||
[
|
||||
c
|
||||
for t, c in itertools.chain(self.aircraft.items(), self.armor.items())
|
||||
for t, c in itertools.chain(
|
||||
self.aircraft.items(), self.armor.items(), self.aa.items()
|
||||
)
|
||||
if t in db.UNIT_BY_TASK[task]
|
||||
]
|
||||
)
|
||||
|
||||
def total_units_of_type(self, unit_type) -> int:
|
||||
return sum(
|
||||
[
|
||||
c
|
||||
for t, c in itertools.chain(
|
||||
self.aircraft.items(), self.armor.items(), self.aa.items()
|
||||
)
|
||||
if t == unit_type
|
||||
]
|
||||
)
|
||||
|
||||
def commission_units(self, units: dict[Any, int]):
|
||||
@property
|
||||
def all_units(self):
|
||||
return itertools.chain(
|
||||
self.aircraft.items(), self.armor.items(), self.aa.items()
|
||||
)
|
||||
|
||||
def _find_best_unit(
|
||||
self, available_units: Dict[UnitType, int], for_type: Task, count: int
|
||||
) -> Dict[UnitType, int]:
|
||||
if count <= 0:
|
||||
logging.warning("{}: no units for {}".format(self, for_type))
|
||||
return {}
|
||||
|
||||
sorted_units = [
|
||||
key for key in available_units if key in db.UNIT_BY_TASK[for_type]
|
||||
]
|
||||
sorted_units.sort(key=lambda x: db.PRICES[x], reverse=True)
|
||||
|
||||
result: Dict[UnitType, int] = {}
|
||||
for unit_type in sorted_units:
|
||||
existing_count = available_units[unit_type] # type: int
|
||||
if not existing_count:
|
||||
continue
|
||||
|
||||
if count <= 0:
|
||||
break
|
||||
|
||||
result_unit_count = min(count, existing_count)
|
||||
count -= result_unit_count
|
||||
|
||||
assert result_unit_count > 0
|
||||
result[unit_type] = result.get(unit_type, 0) + result_unit_count
|
||||
|
||||
logging.info("{} for {} ({}): {}".format(self, for_type, count, result))
|
||||
return result
|
||||
|
||||
def _find_best_planes(
|
||||
self, for_type: Task, count: int
|
||||
) -> typing.Dict[FlyingType, int]:
|
||||
return self._find_best_unit(self.aircraft, for_type, count)
|
||||
|
||||
def _find_best_armor(self, for_type: Task, count: int) -> typing.Dict[Armor, int]:
|
||||
return self._find_best_unit(self.armor, for_type, count)
|
||||
|
||||
def append_commision_points(self, for_type, points: float) -> int:
|
||||
self.commision_points[for_type] = (
|
||||
self.commision_points.get(for_type, 0) + points
|
||||
)
|
||||
points = self.commision_points[for_type]
|
||||
if points >= 1:
|
||||
self.commision_points[for_type] = points - math.floor(points)
|
||||
return int(math.floor(points))
|
||||
|
||||
return 0
|
||||
|
||||
def filter_units(self, applicable_units: typing.Collection):
|
||||
self.aircraft = {
|
||||
k: v for k, v in self.aircraft.items() if k in applicable_units
|
||||
}
|
||||
self.armor = {k: v for k, v in self.armor.items() if k in applicable_units}
|
||||
|
||||
def commision_units(self, units: typing.Dict[typing.Any, int]):
|
||||
|
||||
for unit_type, unit_count in units.items():
|
||||
if unit_count <= 0:
|
||||
continue
|
||||
|
||||
target_dict: dict[Any, int]
|
||||
if isinstance(unit_type, AircraftType):
|
||||
for_task = db.unit_task(unit_type)
|
||||
|
||||
target_dict = None
|
||||
if (
|
||||
for_task == AWACS
|
||||
or for_task == CAS
|
||||
or for_task == CAP
|
||||
or for_task == Embarking
|
||||
or for_task == Transport
|
||||
):
|
||||
target_dict = self.aircraft
|
||||
elif isinstance(unit_type, GroundUnitType):
|
||||
elif for_task == PinpointStrike:
|
||||
target_dict = self.armor
|
||||
elif for_task == AirDefence:
|
||||
target_dict = self.aa
|
||||
|
||||
if target_dict is not None:
|
||||
target_dict[unit_type] = target_dict.get(unit_type, 0) + unit_count
|
||||
else:
|
||||
logging.error(f"Unexpected unit type of {unit_type}")
|
||||
return
|
||||
logging.error("Unable to determine target dict for " + str(unit_type))
|
||||
|
||||
target_dict[unit_type] = target_dict.get(unit_type, 0) + unit_count
|
||||
def commit_losses(self, units_lost: typing.Dict[typing.Any, int]):
|
||||
|
||||
def commit_losses(self, units_lost: dict[Any, int]):
|
||||
for unit_type, count in units_lost.items():
|
||||
target_dict: dict[Any, int]
|
||||
|
||||
if unit_type in self.aircraft:
|
||||
target_dict = self.aircraft
|
||||
target_array = self.aircraft
|
||||
elif unit_type in self.armor:
|
||||
target_dict = self.armor
|
||||
target_array = self.armor
|
||||
else:
|
||||
print("Base didn't find event type {}".format(unit_type))
|
||||
continue
|
||||
|
||||
if unit_type not in target_dict:
|
||||
if unit_type not in target_array:
|
||||
print("Base didn't find event type {}".format(unit_type))
|
||||
continue
|
||||
|
||||
target_dict[unit_type] = max(target_dict[unit_type] - count, 0)
|
||||
if target_dict[unit_type] == 0:
|
||||
del target_dict[unit_type]
|
||||
target_array[unit_type] = max(target_array[unit_type] - count, 0)
|
||||
if target_array[unit_type] == 0:
|
||||
del target_array[unit_type]
|
||||
|
||||
def affect_strength(self, amount):
|
||||
self.strength += amount
|
||||
@@ -84,3 +190,55 @@ class Base:
|
||||
|
||||
def set_strength_to_minimum(self) -> None:
|
||||
self.strength = BASE_MIN_STRENGTH
|
||||
|
||||
def scramble_count(self, multiplier: float, task: Task = None) -> int:
|
||||
if task:
|
||||
count = sum(
|
||||
[v for k, v in self.aircraft.items() if db.unit_task(k) == task]
|
||||
)
|
||||
else:
|
||||
count = self.total_aircraft
|
||||
|
||||
count = int(math.ceil(count * PLANES_SCRAMBLE_FACTOR * self.strength))
|
||||
return min(
|
||||
min(
|
||||
max(count, PLANES_SCRAMBLE_MIN_BASE),
|
||||
int(PLANES_SCRAMBLE_MAX_BASE * multiplier),
|
||||
),
|
||||
count,
|
||||
)
|
||||
|
||||
def assemble_count(self):
|
||||
return int(self.total_armor * 0.5)
|
||||
|
||||
def assemble_aa_count(self) -> int:
|
||||
# previous logic removed because we always want the full air defense capabilities.
|
||||
return self.total_aa
|
||||
|
||||
def scramble_sweep(self, multiplier: float) -> typing.Dict[FlyingType, int]:
|
||||
return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP))
|
||||
|
||||
def scramble_last_defense(self):
|
||||
# return as many CAP-capable aircraft as we can since this is the last defense of the base
|
||||
# (but not more than 20 - that's just nuts)
|
||||
return self._find_best_planes(CAP, min(self.total_aircraft, 20))
|
||||
|
||||
def scramble_cas(self, multiplier: float) -> typing.Dict[FlyingType, int]:
|
||||
return self._find_best_planes(CAS, self.scramble_count(multiplier, CAS))
|
||||
|
||||
def scramble_interceptors(self, multiplier: float) -> typing.Dict[FlyingType, int]:
|
||||
return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP))
|
||||
|
||||
def assemble_attack(self) -> typing.Dict[Armor, int]:
|
||||
return self._find_best_armor(PinpointStrike, self.assemble_count())
|
||||
|
||||
def assemble_defense(self) -> typing.Dict[Armor, int]:
|
||||
count = int(self.total_armor * min(self.strength + 0.5, 1))
|
||||
return self._find_best_armor(PinpointStrike, count)
|
||||
|
||||
def assemble_aa(self, count=None) -> typing.Dict[AirDefence, int]:
|
||||
return self._find_best_unit(
|
||||
self.aa,
|
||||
AirDefence,
|
||||
count and min(count, self.total_aa) or self.assemble_aa_count(),
|
||||
)
|
||||
|
||||
@@ -16,10 +16,10 @@ from dcs.country import Country
|
||||
from dcs.mapping import Point
|
||||
from dcs.planes import F_15C
|
||||
from dcs.ships import (
|
||||
HandyWind,
|
||||
Stennis,
|
||||
USS_Arleigh_Burke_IIa,
|
||||
LHA_Tarawa,
|
||||
Bulker_Handy_Wind,
|
||||
CVN_74_John_C__Stennis,
|
||||
DDG_Arleigh_Burke_IIa,
|
||||
LHA_1_Tarawa,
|
||||
)
|
||||
from dcs.statics import Fortification, Warehouse
|
||||
from dcs.terrain import (
|
||||
@@ -77,53 +77,53 @@ class MizCampaignLoader:
|
||||
|
||||
OFF_MAP_UNIT_TYPE = F_15C.id
|
||||
|
||||
CV_UNIT_TYPE = Stennis.id
|
||||
LHA_UNIT_TYPE = LHA_Tarawa.id
|
||||
FRONT_LINE_UNIT_TYPE = Armor.M_113.id
|
||||
SHIPPING_LANE_UNIT_TYPE = HandyWind.id
|
||||
CV_UNIT_TYPE = CVN_74_John_C__Stennis.id
|
||||
LHA_UNIT_TYPE = LHA_1_Tarawa.id
|
||||
FRONT_LINE_UNIT_TYPE = Armor.APC_M113.id
|
||||
SHIPPING_LANE_UNIT_TYPE = Bulker_Handy_Wind.id
|
||||
|
||||
FOB_UNIT_TYPE = Unarmed.SKP_11.id
|
||||
FOB_UNIT_TYPE = Unarmed.Truck_SKP_11_Mobile_ATC.id
|
||||
FARP_HELIPAD = "SINGLE_HELIPAD"
|
||||
|
||||
OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id
|
||||
SHIP_UNIT_TYPE = USS_Arleigh_Burke_IIa.id
|
||||
MISSILE_SITE_UNIT_TYPE = MissilesSS.Scud_B.id
|
||||
COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.Hy_launcher.id
|
||||
SHIP_UNIT_TYPE = DDG_Arleigh_Burke_IIa.id
|
||||
MISSILE_SITE_UNIT_TYPE = MissilesSS.SSM_SS_1C_Scud_B.id
|
||||
COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.AShM_SS_N_2_Silkworm.id
|
||||
|
||||
# Multiple options for air defenses so campaign designers can more accurately see
|
||||
# the coverage of their IADS for the expected type.
|
||||
LONG_RANGE_SAM_UNIT_TYPES = {
|
||||
AirDefence.Patriot_ln.id,
|
||||
AirDefence.S_300PS_5P85C_ln.id,
|
||||
AirDefence.S_300PS_5P85D_ln.id,
|
||||
AirDefence.SAM_Patriot_LN.id,
|
||||
AirDefence.SAM_SA_10_S_300_Grumble_TEL_C.id,
|
||||
AirDefence.SAM_SA_10_S_300_Grumble_TEL_D.id,
|
||||
}
|
||||
|
||||
MEDIUM_RANGE_SAM_UNIT_TYPES = {
|
||||
AirDefence.Hawk_ln.id,
|
||||
AirDefence.S_75M_Volhov.id,
|
||||
AirDefence._5p73_s_125_ln.id,
|
||||
AirDefence.SAM_Hawk_LN_M192.id,
|
||||
AirDefence.SAM_SA_2_S_75_Guideline_LN.id,
|
||||
AirDefence.SAM_SA_3_S_125_Goa_LN.id,
|
||||
}
|
||||
|
||||
SHORT_RANGE_SAM_UNIT_TYPES = {
|
||||
AirDefence.M1097_Avenger.id,
|
||||
AirDefence.Rapier_fsa_launcher.id,
|
||||
AirDefence._2S6_Tunguska.id,
|
||||
AirDefence.Strela_1_9P31.id,
|
||||
AirDefence.SAM_Avenger__Stinger.id,
|
||||
AirDefence.SAM_Rapier_LN.id,
|
||||
AirDefence.SAM_SA_19_Tunguska_Grison.id,
|
||||
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL.id,
|
||||
}
|
||||
|
||||
AAA_UNIT_TYPES = {
|
||||
AirDefence.Flak18.id,
|
||||
AirDefence.Vulcan.id,
|
||||
AirDefence.ZSU_23_4_Shilka.id,
|
||||
AirDefence.AAA_8_8cm_Flak_18.id,
|
||||
AirDefence.SPAAA_Vulcan_M163.id,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish.id,
|
||||
}
|
||||
|
||||
EWR_UNIT_TYPE = AirDefence._1L13_EWR.id
|
||||
EWR_UNIT_TYPE = AirDefence.EWR_1L13.id
|
||||
|
||||
ARMOR_GROUP_UNIT_TYPE = Armor.M_1_Abrams.id
|
||||
ARMOR_GROUP_UNIT_TYPE = Armor.MBT_M1A2_Abrams.id
|
||||
|
||||
FACTORY_UNIT_TYPE = Fortification.Workshop_A.id
|
||||
|
||||
AMMUNITION_DEPOT_UNIT_TYPE = Warehouse._Ammunition_depot.id
|
||||
AMMUNITION_DEPOT_UNIT_TYPE = Warehouse.Ammunition_depot.id
|
||||
|
||||
STRIKE_TARGET_UNIT_TYPE = Fortification.Tech_combine.id
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ from typing import (
|
||||
Optional,
|
||||
Set,
|
||||
TYPE_CHECKING,
|
||||
Type,
|
||||
Union,
|
||||
Sequence,
|
||||
Iterable,
|
||||
@@ -24,13 +25,14 @@ from typing import (
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.ships import (
|
||||
Stennis,
|
||||
KUZNECOW,
|
||||
LHA_Tarawa,
|
||||
Type_071,
|
||||
CVN_74_John_C__Stennis,
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
LHA_1_Tarawa,
|
||||
Type_071_Amphibious_Transport_Dock,
|
||||
)
|
||||
from dcs.terrain.terrain import Airport, ParkingSlot
|
||||
from dcs.unit import Unit
|
||||
from dcs.unittype import FlyingType, VehicleType
|
||||
|
||||
from game import db
|
||||
from game.point_with_heading import PointWithHeading
|
||||
@@ -44,8 +46,7 @@ from .theatergroundobject import (
|
||||
GenericCarrierGroundObject,
|
||||
TheaterGroundObject,
|
||||
)
|
||||
from ..dcs.aircrafttype import AircraftType
|
||||
from ..dcs.groundunittype import GroundUnitType
|
||||
from ..db import PRICES
|
||||
from ..utils import nautical_miles
|
||||
from ..weather import Conditions
|
||||
|
||||
@@ -124,19 +125,19 @@ class PresetLocations:
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AircraftAllocations:
|
||||
present: dict[AircraftType, int]
|
||||
ordered: dict[AircraftType, int]
|
||||
transferring: dict[AircraftType, int]
|
||||
present: dict[Type[FlyingType], int]
|
||||
ordered: dict[Type[FlyingType], int]
|
||||
transferring: dict[Type[FlyingType], int]
|
||||
|
||||
@property
|
||||
def total_value(self) -> int:
|
||||
total: int = 0
|
||||
for unit_type, count in self.present.items():
|
||||
total += unit_type.price * count
|
||||
total += PRICES[unit_type] * count
|
||||
for unit_type, count in self.ordered.items():
|
||||
total += unit_type.price * count
|
||||
total += PRICES[unit_type] * count
|
||||
for unit_type, count in self.transferring.items():
|
||||
total += unit_type.price * count
|
||||
total += PRICES[unit_type] * count
|
||||
|
||||
return total
|
||||
|
||||
@@ -159,13 +160,13 @@ class AircraftAllocations:
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GroundUnitAllocations:
|
||||
present: dict[GroundUnitType, int]
|
||||
ordered: dict[GroundUnitType, int]
|
||||
transferring: dict[GroundUnitType, int]
|
||||
present: dict[Type[VehicleType], int]
|
||||
ordered: dict[Type[VehicleType], int]
|
||||
transferring: dict[Type[VehicleType], int]
|
||||
|
||||
@property
|
||||
def all(self) -> dict[GroundUnitType, int]:
|
||||
combined: dict[GroundUnitType, int] = defaultdict(int)
|
||||
def all(self) -> dict[Type[VehicleType], int]:
|
||||
combined: dict[Type[VehicleType], int] = defaultdict(int)
|
||||
for unit_type, count in itertools.chain(
|
||||
self.present.items(), self.ordered.items(), self.transferring.items()
|
||||
):
|
||||
@@ -176,11 +177,11 @@ class GroundUnitAllocations:
|
||||
def total_value(self) -> int:
|
||||
total: int = 0
|
||||
for unit_type, count in self.present.items():
|
||||
total += unit_type.price * count
|
||||
total += PRICES[unit_type] * count
|
||||
for unit_type, count in self.ordered.items():
|
||||
total += unit_type.price * count
|
||||
total += PRICES[unit_type] * count
|
||||
for unit_type, count in self.transferring.items():
|
||||
total += unit_type.price * count
|
||||
total += PRICES[unit_type] * count
|
||||
|
||||
return total
|
||||
|
||||
@@ -485,14 +486,14 @@ class ControlPoint(MissionTarget, ABC):
|
||||
for group in g.groups:
|
||||
for u in group.units:
|
||||
if db.unit_type_from_name(u.type) in [
|
||||
Stennis,
|
||||
KUZNECOW,
|
||||
CVN_74_John_C__Stennis,
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
]:
|
||||
return group.name
|
||||
elif g.dcs_identifier == "LHA":
|
||||
for group in g.groups:
|
||||
for u in group.units:
|
||||
if db.unit_type_from_name(u.type) in [LHA_Tarawa]:
|
||||
if db.unit_type_from_name(u.type) in [LHA_1_Tarawa]:
|
||||
return group.name
|
||||
return None
|
||||
|
||||
@@ -540,19 +541,27 @@ class ControlPoint(MissionTarget, ABC):
|
||||
while self.base.armor:
|
||||
unit_type, count = self.base.armor.popitem()
|
||||
for _ in range(count):
|
||||
destination.control_point.base.commission_units({unit_type: 1})
|
||||
destination.control_point.base.commision_units({unit_type: 1})
|
||||
destination = heapq.heappushpop(destinations, destination)
|
||||
|
||||
def capture_aircraft(self, game: Game, airframe: AircraftType, count: int) -> None:
|
||||
value = airframe.price * count
|
||||
def capture_aircraft(
|
||||
self, game: Game, airframe: Type[FlyingType], count: int
|
||||
) -> None:
|
||||
try:
|
||||
value = PRICES[airframe] * count
|
||||
except KeyError:
|
||||
logging.exception(f"Unknown price for {airframe.id}")
|
||||
return
|
||||
|
||||
game.adjust_budget(value, player=not self.captured)
|
||||
game.message(
|
||||
f"No valid retreat destination in range of {self.name} for {airframe}"
|
||||
f"{count} aircraft have been captured and sold for ${value}M."
|
||||
f"No valid retreat destination in range of {self.name} for "
|
||||
f"{airframe.id}. {count} aircraft have been captured and sold for "
|
||||
f"${value}M."
|
||||
)
|
||||
|
||||
def aircraft_retreat_destination(
|
||||
self, game: Game, airframe: AircraftType
|
||||
self, game: Game, airframe: Type[FlyingType]
|
||||
) -> Optional[ControlPoint]:
|
||||
closest = ObjectiveDistanceCache.get_closest_airfields(self)
|
||||
# TODO: Should be airframe dependent.
|
||||
@@ -570,17 +579,17 @@ class ControlPoint(MissionTarget, ABC):
|
||||
return None
|
||||
|
||||
def _retreat_air_units(
|
||||
self, game: Game, airframe: AircraftType, count: int
|
||||
self, game: Game, airframe: Type[FlyingType], count: int
|
||||
) -> None:
|
||||
while count:
|
||||
logging.debug(f"Retreating {count} {airframe} from {self.name}")
|
||||
logging.debug(f"Retreating {count} {airframe.id} from {self.name}")
|
||||
destination = self.aircraft_retreat_destination(game, airframe)
|
||||
if destination is None:
|
||||
self.capture_aircraft(game, airframe, count)
|
||||
return
|
||||
parking = destination.unclaimed_parking(game)
|
||||
transfer_amount = min([parking, count])
|
||||
destination.base.commission_units({airframe: transfer_amount})
|
||||
destination.base.commision_units({airframe: transfer_amount})
|
||||
count -= transfer_amount
|
||||
|
||||
def retreat_air_units(self, game: Game) -> None:
|
||||
@@ -609,16 +618,16 @@ class ControlPoint(MissionTarget, ABC):
|
||||
self.base.set_strength_to_minimum()
|
||||
|
||||
@abstractmethod
|
||||
def can_operate(self, aircraft: AircraftType) -> bool:
|
||||
def can_operate(self, aircraft: Type[FlyingType]) -> bool:
|
||||
...
|
||||
|
||||
def aircraft_transferring(self, game: Game) -> dict[AircraftType, int]:
|
||||
def aircraft_transferring(self, game: Game) -> dict[Type[FlyingType], int]:
|
||||
if self.captured:
|
||||
ato = game.blue_ato
|
||||
else:
|
||||
ato = game.red_ato
|
||||
|
||||
transferring: defaultdict[AircraftType, int] = defaultdict(int)
|
||||
transferring: defaultdict[Type[FlyingType], int] = defaultdict(int)
|
||||
for package in ato.packages:
|
||||
for flight in package.flights:
|
||||
if flight.departure == flight.arrival:
|
||||
@@ -683,7 +692,7 @@ class ControlPoint(MissionTarget, ABC):
|
||||
def allocated_aircraft(self, game: Game) -> AircraftAllocations:
|
||||
on_order = {}
|
||||
for unit_bought, count in self.pending_unit_deliveries.units.items():
|
||||
if isinstance(unit_bought, AircraftType):
|
||||
if issubclass(unit_bought, FlyingType):
|
||||
on_order[unit_bought] = count
|
||||
|
||||
return AircraftAllocations(
|
||||
@@ -695,10 +704,10 @@ class ControlPoint(MissionTarget, ABC):
|
||||
) -> GroundUnitAllocations:
|
||||
on_order = {}
|
||||
for unit_bought, count in self.pending_unit_deliveries.units.items():
|
||||
if isinstance(unit_bought, GroundUnitType):
|
||||
if issubclass(unit_bought, VehicleType):
|
||||
on_order[unit_bought] = count
|
||||
|
||||
transferring: dict[GroundUnitType, int] = defaultdict(int)
|
||||
transferring: dict[Type[VehicleType], int] = defaultdict(int)
|
||||
for transfer in transfers:
|
||||
if transfer.destination == self:
|
||||
for unit_type, count in transfer.units.items():
|
||||
@@ -779,7 +788,7 @@ class Airfield(ControlPoint):
|
||||
self.airport = airport
|
||||
self._runway_status = RunwayStatus()
|
||||
|
||||
def can_operate(self, aircraft: AircraftType) -> bool:
|
||||
def can_operate(self, aircraft: FlyingType) -> bool:
|
||||
# TODO: Allow helicopters.
|
||||
# Need to implement ground spawns so the helos don't use the runway.
|
||||
# TODO: Allow harrier.
|
||||
@@ -801,7 +810,6 @@ class Airfield(ControlPoint):
|
||||
if self.is_friendly(for_player):
|
||||
yield from [
|
||||
FlightType.AEWC,
|
||||
FlightType.REFUELING,
|
||||
# TODO: FlightType.INTERCEPTION
|
||||
# TODO: FlightType.LOGISTICS
|
||||
]
|
||||
@@ -891,10 +899,10 @@ class NavalControlPoint(ControlPoint, ABC):
|
||||
for group in self.find_main_tgo().groups:
|
||||
for u in group.units:
|
||||
if db.unit_type_from_name(u.type) in [
|
||||
Stennis,
|
||||
LHA_Tarawa,
|
||||
KUZNECOW,
|
||||
Type_071,
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
Type_071_Amphibious_Transport_Dock,
|
||||
]:
|
||||
return True
|
||||
return False
|
||||
@@ -951,10 +959,7 @@ class Carrier(NavalControlPoint):
|
||||
|
||||
yield from super().mission_types(for_player)
|
||||
if self.is_friendly(for_player):
|
||||
yield from [
|
||||
FlightType.AEWC,
|
||||
FlightType.REFUELING,
|
||||
]
|
||||
yield FlightType.AEWC
|
||||
|
||||
def capture(self, game: Game, for_player: bool) -> None:
|
||||
raise RuntimeError("Carriers cannot be captured")
|
||||
@@ -963,8 +968,8 @@ class Carrier(NavalControlPoint):
|
||||
def is_carrier(self):
|
||||
return True
|
||||
|
||||
def can_operate(self, aircraft: AircraftType) -> bool:
|
||||
return aircraft.carrier_capable
|
||||
def can_operate(self, aircraft: FlyingType) -> bool:
|
||||
return aircraft in db.CARRIER_CAPABLE
|
||||
|
||||
@property
|
||||
def total_aircraft_parking(self) -> int:
|
||||
@@ -997,8 +1002,8 @@ class Lha(NavalControlPoint):
|
||||
def is_lha(self) -> bool:
|
||||
return True
|
||||
|
||||
def can_operate(self, aircraft: AircraftType) -> bool:
|
||||
return aircraft.lha_capable
|
||||
def can_operate(self, aircraft: FlyingType) -> bool:
|
||||
return aircraft in db.LHA_CAPABLE
|
||||
|
||||
@property
|
||||
def total_aircraft_parking(self) -> int:
|
||||
@@ -1037,7 +1042,7 @@ class OffMapSpawn(ControlPoint):
|
||||
def total_aircraft_parking(self) -> int:
|
||||
return 1000
|
||||
|
||||
def can_operate(self, aircraft: AircraftType) -> bool:
|
||||
def can_operate(self, aircraft: FlyingType) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
@@ -1108,7 +1113,7 @@ class Fob(ControlPoint):
|
||||
def total_aircraft_parking(self) -> int:
|
||||
return 0
|
||||
|
||||
def can_operate(self, aircraft: AircraftType) -> bool:
|
||||
def can_operate(self, aircraft: FlyingType) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
|
||||
@@ -81,7 +81,6 @@ class FrontLine(MissionTarget):
|
||||
yield from [
|
||||
FlightType.CAS,
|
||||
FlightType.AEWC,
|
||||
FlightType.REFUELING
|
||||
# TODO: FlightType.TROOP_TRANSPORT
|
||||
# TODO: FlightType.EVAC
|
||||
]
|
||||
|
||||
@@ -78,33 +78,20 @@ class GeneratorSettings:
|
||||
no_enemy_navy: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModSettings:
|
||||
a4_skyhawk: bool = False
|
||||
f22_raptor: bool = False
|
||||
hercules: bool = False
|
||||
jas39_gripen: bool = False
|
||||
su57_felon: bool = False
|
||||
frenchpack: bool = False
|
||||
high_digit_sams: bool = False
|
||||
|
||||
|
||||
class GameGenerator:
|
||||
def __init__(
|
||||
self,
|
||||
player: Faction,
|
||||
enemy: Faction,
|
||||
player: str,
|
||||
enemy: str,
|
||||
theater: ConflictTheater,
|
||||
settings: Settings,
|
||||
generator_settings: GeneratorSettings,
|
||||
mod_settings: ModSettings,
|
||||
) -> None:
|
||||
self.player = player
|
||||
self.enemy = enemy
|
||||
self.theater = theater
|
||||
self.settings = settings
|
||||
self.generator_settings = generator_settings
|
||||
self.mod_settings = mod_settings
|
||||
|
||||
def generate(self) -> Game:
|
||||
with logged_duration("TGO population"):
|
||||
@@ -112,8 +99,8 @@ class GameGenerator:
|
||||
namegen.reset()
|
||||
self.prepare_theater()
|
||||
game = Game(
|
||||
player_faction=self.player.apply_mod_settings(self.mod_settings),
|
||||
enemy_faction=self.enemy.apply_mod_settings(self.mod_settings),
|
||||
player_name=self.player,
|
||||
enemy_name=self.enemy,
|
||||
theater=self.theater,
|
||||
start_date=self.generator_settings.start_date,
|
||||
settings=self.settings,
|
||||
@@ -172,9 +159,9 @@ class ControlPointGroundObjectGenerator:
|
||||
@property
|
||||
def faction_name(self) -> str:
|
||||
if self.control_point.captured:
|
||||
return self.game.player_faction.name
|
||||
return self.game.player_name
|
||||
else:
|
||||
return self.game.enemy_faction.name
|
||||
return self.game.enemy_name
|
||||
|
||||
@property
|
||||
def faction(self) -> Faction:
|
||||
|
||||
@@ -1,34 +1,3 @@
|
||||
"""Implements support for ground unit transfers between bases.
|
||||
|
||||
Ground units can be transferred between bases via a number of transport methods, and
|
||||
doing so can take multiple turns.
|
||||
|
||||
There are a few main concepts here:
|
||||
|
||||
* A TransferOrder is a request to move units from one base to another. It is described
|
||||
by its origin, destination, current position, and contents. TransferOrders persist
|
||||
across turns, and if no Transport is available to move the units in a given turn it
|
||||
will have no Transport assigned.
|
||||
* Transports: A Transport is the planned move of a group of units for a leg of the
|
||||
journey *this turn*. A Transport has an assigned mode of transportation and has
|
||||
vehicles assigned to move the units if needed. This might be a Convoy, a CargoShip, or
|
||||
an Airlift.
|
||||
|
||||
The TransportMap (more accurately, it's subtypes) is responsible for managing the
|
||||
transports moving from A to B for the turn. Transfers that are moving between A and B
|
||||
this turn will be added to the TransportMap, which will create a new transport if needed
|
||||
or add the units to an existing transport if one exists. This allows transfers from
|
||||
A->B->C and D->B->C to share a transport between B and C.
|
||||
|
||||
AirLifts do not use TransportMap because no merging will take place between orders. It
|
||||
instead uses AirLiftPlanner to create transport packages.
|
||||
|
||||
PendingTransfers manages all the incomplete transfer orders for the game. New transfer
|
||||
orders are registered with PendingTransfers and it is responsible for allocating
|
||||
transports and processing the turn's transit actions.
|
||||
|
||||
Routing is handled by TransitNetwork.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
@@ -37,19 +6,20 @@ from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from functools import singledispatchmethod
|
||||
from typing import (
|
||||
Dict,
|
||||
Generic,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
TYPE_CHECKING,
|
||||
Type,
|
||||
TypeVar,
|
||||
Sequence,
|
||||
)
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.unittype import FlyingType, VehicleType
|
||||
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.procurement import AircraftProcurementRequest
|
||||
from game.squadrons import Squadron
|
||||
from game.theater import ControlPoint, MissionTarget
|
||||
@@ -59,7 +29,7 @@ from game.theater.transitnetwork import (
|
||||
)
|
||||
from game.utils import meters, nautical_miles
|
||||
from gen.ato import Package
|
||||
from gen.flights.ai_flight_planner_db import aircraft_for_task
|
||||
from gen.flights.ai_flight_planner_db import TRANSPORT_CAPABLE
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from gen.flights.flight import Flight, FlightType
|
||||
from gen.flights.flightplan import FlightPlanBuilder
|
||||
@@ -102,18 +72,10 @@ class TransferOrder:
|
||||
player: bool = field(init=False)
|
||||
|
||||
#: The units being transferred.
|
||||
units: dict[GroundUnitType, int]
|
||||
units: Dict[Type[VehicleType], int]
|
||||
|
||||
transport: Optional[Transport] = field(default=None)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Returns the text that should be displayed for the transfer."""
|
||||
count = self.size
|
||||
origin = self.origin.name
|
||||
destination = self.destination.name
|
||||
description = "Transfer" if self.player else "Enemy transfer"
|
||||
return f"{description} of {count} units from {origin} to {destination}"
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.position = self.origin
|
||||
self.player = self.origin.is_friendly(to_player=True)
|
||||
@@ -127,27 +89,27 @@ class TransferOrder:
|
||||
def kill_all(self) -> None:
|
||||
self.units.clear()
|
||||
|
||||
def kill_unit(self, unit_type: GroundUnitType) -> None:
|
||||
def kill_unit(self, unit_type: Type[VehicleType]) -> None:
|
||||
if unit_type not in self.units or not self.units[unit_type]:
|
||||
raise KeyError(f"{self} has no {unit_type} remaining")
|
||||
raise KeyError(f"{self.destination} has no {unit_type} remaining")
|
||||
self.units[unit_type] -= 1
|
||||
|
||||
@property
|
||||
def size(self) -> int:
|
||||
return sum(self.units.values())
|
||||
return sum(c for c in self.units.values())
|
||||
|
||||
def iter_units(self) -> Iterator[GroundUnitType]:
|
||||
def iter_units(self) -> Iterator[Type[VehicleType]]:
|
||||
for unit_type, count in self.units.items():
|
||||
for _ in range(count):
|
||||
yield unit_type
|
||||
|
||||
@property
|
||||
def completed(self) -> bool:
|
||||
return self.destination == self.position or not self.size
|
||||
return self.destination == self.position or not self.units
|
||||
|
||||
def disband_at(self, location: ControlPoint) -> None:
|
||||
logging.info(f"Units halting at {location}.")
|
||||
location.base.commission_units(self.units)
|
||||
location.base.commision_units(self.units)
|
||||
self.units.clear()
|
||||
|
||||
@property
|
||||
@@ -158,17 +120,15 @@ class TransferOrder:
|
||||
)
|
||||
return self.transport.destination
|
||||
|
||||
def find_escape_route(self) -> Optional[ControlPoint]:
|
||||
if self.transport is not None:
|
||||
return self.transport.find_escape_route()
|
||||
return None
|
||||
|
||||
def proceed(self) -> None:
|
||||
if self.transport is None:
|
||||
return
|
||||
|
||||
if not self.destination.is_friendly(self.player):
|
||||
logging.info(f"Transfer destination {self.destination} was captured.")
|
||||
if self.position.is_friendly(self.player):
|
||||
self.disband_at(self.position)
|
||||
elif (escape_route := self.find_escape_route()) is not None:
|
||||
elif (escape_route := self.transport.find_escape_route()) is not None:
|
||||
self.disband_at(escape_route)
|
||||
else:
|
||||
logging.info(
|
||||
@@ -178,9 +138,6 @@ class TransferOrder:
|
||||
self.kill_all()
|
||||
return
|
||||
|
||||
if self.transport is None:
|
||||
return
|
||||
|
||||
self.position = self.next_stop
|
||||
self.transport = None
|
||||
|
||||
@@ -199,7 +156,7 @@ class Airlift(Transport):
|
||||
self.flight = flight
|
||||
|
||||
@property
|
||||
def units(self) -> dict[GroundUnitType, int]:
|
||||
def units(self) -> Dict[Type[VehicleType], int]:
|
||||
return self.transfer.units
|
||||
|
||||
@property
|
||||
@@ -234,9 +191,9 @@ class AirliftPlanner:
|
||||
self.package = Package(target=next_stop, auto_asap=True)
|
||||
|
||||
def compatible_with_mission(
|
||||
self, unit_type: AircraftType, airfield: ControlPoint
|
||||
self, unit_type: Type[FlyingType], airfield: ControlPoint
|
||||
) -> bool:
|
||||
if unit_type not in aircraft_for_task(FlightType.TRANSPORT):
|
||||
if not unit_type in TRANSPORT_CAPABLE:
|
||||
return False
|
||||
if not self.transfer.origin.can_operate(unit_type):
|
||||
return False
|
||||
@@ -244,7 +201,7 @@ class AirliftPlanner:
|
||||
return False
|
||||
|
||||
# Cargo planes have no maximum range.
|
||||
if not unit_type.dcs_unit_type.helicopter:
|
||||
if not unit_type.helicopter:
|
||||
return True
|
||||
|
||||
# A helicopter that is transport capable and able to operate at both bases. Need
|
||||
@@ -270,46 +227,36 @@ class AirliftPlanner:
|
||||
distance_cache = ObjectiveDistanceCache.get_closest_airfields(
|
||||
self.transfer.position
|
||||
)
|
||||
air_wing = self.game.air_wing_for(self.for_player)
|
||||
for cp in distance_cache.closest_airfields:
|
||||
if cp.captured != self.for_player:
|
||||
continue
|
||||
|
||||
inventory = self.game.aircraft_inventory.for_control_point(cp)
|
||||
for unit_type, available in inventory.all_aircraft:
|
||||
squadrons = air_wing.auto_assignable_for_task_with_type(
|
||||
unit_type, FlightType.TRANSPORT
|
||||
)
|
||||
for squadron in squadrons:
|
||||
if self.compatible_with_mission(unit_type, cp):
|
||||
while (
|
||||
available
|
||||
and squadron.has_available_pilots
|
||||
and self.transfer.transport is None
|
||||
):
|
||||
flight_size = self.create_airlift_flight(
|
||||
squadron, inventory
|
||||
)
|
||||
available -= flight_size
|
||||
squadrons = [
|
||||
s
|
||||
for s in self.game.air_wing_for(self.for_player).squadrons_for(
|
||||
unit_type
|
||||
)
|
||||
if FlightType.TRANSPORT in s.auto_assignable_mission_types
|
||||
]
|
||||
if not squadrons:
|
||||
continue
|
||||
squadron = squadrons[0]
|
||||
if self.compatible_with_mission(unit_type, cp):
|
||||
while available and self.transfer.transport is None:
|
||||
flight_size = self.create_airlift_flight(squadron, inventory)
|
||||
available -= flight_size
|
||||
if self.package.flights:
|
||||
self.game.ato_for(self.for_player).add_package(self.package)
|
||||
|
||||
def create_airlift_flight(
|
||||
self, squadron: Squadron, inventory: ControlPointAircraftInventory
|
||||
) -> int:
|
||||
available_aircraft = inventory.available(squadron.aircraft)
|
||||
capacity_each = 1 if squadron.aircraft.dcs_unit_type.helicopter else 2
|
||||
available = inventory.available(squadron.aircraft)
|
||||
capacity_each = 1 if squadron.aircraft.helicopter else 2
|
||||
required = math.ceil(self.transfer.size / capacity_each)
|
||||
flight_size = min(
|
||||
required,
|
||||
available_aircraft,
|
||||
squadron.aircraft.dcs_unit_type.group_size_max,
|
||||
)
|
||||
# TODO: Use number_of_available_pilots directly once feature flag is gone.
|
||||
# The number of currently available pilots is not relevant when pilot limits
|
||||
# are disabled.
|
||||
if not squadron.can_provide_pilots(flight_size):
|
||||
flight_size = squadron.number_of_available_pilots
|
||||
flight_size = min(required, available, squadron.aircraft.group_size_max)
|
||||
capacity = flight_size * capacity_each
|
||||
|
||||
if capacity < self.transfer.size:
|
||||
@@ -361,7 +308,7 @@ class MultiGroupTransport(MissionTarget, Transport):
|
||||
transfer.transport = None
|
||||
self.transfers.remove(transfer)
|
||||
|
||||
def kill_unit(self, unit_type: GroundUnitType) -> None:
|
||||
def kill_unit(self, unit_type: Type[VehicleType]) -> None:
|
||||
for transfer in self.transfers:
|
||||
try:
|
||||
transfer.kill_unit(unit_type)
|
||||
@@ -381,21 +328,16 @@ class MultiGroupTransport(MissionTarget, Transport):
|
||||
|
||||
@property
|
||||
def size(self) -> int:
|
||||
return sum(t.size for t in self.transfers)
|
||||
return sum(sum(t.units.values()) for t in self.transfers)
|
||||
|
||||
@property
|
||||
def units(self) -> dict[GroundUnitType, int]:
|
||||
units: dict[GroundUnitType, int] = defaultdict(int)
|
||||
def units(self) -> Dict[Type[VehicleType], int]:
|
||||
units: Dict[Type[VehicleType], int] = defaultdict(int)
|
||||
for transfer in self.transfers:
|
||||
for unit_type, count in transfer.units.items():
|
||||
units[unit_type] += count
|
||||
return units
|
||||
|
||||
def iter_units(self) -> Iterator[GroundUnitType]:
|
||||
for unit_type, count in self.units.items():
|
||||
for _ in range(count):
|
||||
yield unit_type
|
||||
|
||||
@property
|
||||
def player_owned(self) -> bool:
|
||||
return self.origin.captured
|
||||
@@ -461,8 +403,8 @@ TransportType = TypeVar("TransportType", bound=MultiGroupTransport)
|
||||
class TransportMap(Generic[TransportType]):
|
||||
def __init__(self) -> None:
|
||||
# Dict of origin -> destination -> transport.
|
||||
self.transports: dict[
|
||||
ControlPoint, dict[ControlPoint, TransportType]
|
||||
self.transports: Dict[
|
||||
ControlPoint, Dict[ControlPoint, TransportType]
|
||||
] = defaultdict(dict)
|
||||
|
||||
def create_transport(
|
||||
@@ -620,7 +562,7 @@ class PendingTransfers:
|
||||
if transfer.transport is not None:
|
||||
self.cancel_transport(transfer.transport, transfer)
|
||||
self.pending_transfers.remove(transfer)
|
||||
transfer.origin.base.commission_units(transfer.units)
|
||||
transfer.origin.base.commision_units(transfer.units)
|
||||
|
||||
def perform_transfers(self) -> None:
|
||||
incomplete = []
|
||||
@@ -639,10 +581,7 @@ class PendingTransfers:
|
||||
|
||||
def order_airlift_assets(self) -> None:
|
||||
for control_point in self.game.theater.controlpoints:
|
||||
if self.game.air_wing_for(control_point.captured).can_auto_plan(
|
||||
FlightType.TRANSPORT
|
||||
):
|
||||
self.order_airlift_assets_at(control_point)
|
||||
self.order_airlift_assets_at(control_point)
|
||||
|
||||
@staticmethod
|
||||
def desired_airlift_capacity(control_point: ControlPoint) -> int:
|
||||
@@ -650,10 +589,10 @@ class PendingTransfers:
|
||||
|
||||
def current_airlift_capacity(self, control_point: ControlPoint) -> int:
|
||||
inventory = self.game.aircraft_inventory.for_control_point(control_point)
|
||||
squadrons = self.game.air_wing_for(
|
||||
control_point.captured
|
||||
).auto_assignable_for_task(FlightType.TRANSPORT)
|
||||
unit_types = {s.aircraft for s in squadrons}
|
||||
squadrons = self.game.air_wing_for(control_point.captured).squadrons_for_task(
|
||||
FlightType.TRANSPORT
|
||||
)
|
||||
unit_types = {s.aircraft for s in squadrons}.intersection(TRANSPORT_CAPABLE)
|
||||
return sum(
|
||||
count
|
||||
for unit_type, count in inventory.all_aircraft
|
||||
|
||||
@@ -3,11 +3,12 @@ from __future__ import annotations
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, TYPE_CHECKING, Any
|
||||
from typing import Dict, Optional, TYPE_CHECKING, Type
|
||||
|
||||
from dcs.unittype import UnitType, VehicleType
|
||||
|
||||
from game.theater import ControlPoint
|
||||
from .dcs.groundunittype import GroundUnitType
|
||||
from .dcs.unittype import UnitType
|
||||
from .db import PRICES
|
||||
from .theater.transitnetwork import (
|
||||
NoPathError,
|
||||
TransitNetwork,
|
||||
@@ -28,16 +29,16 @@ class PendingUnitDeliveries:
|
||||
self.destination = destination
|
||||
|
||||
# Maps unit type to order quantity.
|
||||
self.units: dict[UnitType, int] = defaultdict(int)
|
||||
self.units: Dict[Type[UnitType], int] = defaultdict(int)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Pending delivery to {self.destination}"
|
||||
|
||||
def order(self, units: dict[UnitType, int]) -> None:
|
||||
def order(self, units: Dict[Type[UnitType], int]) -> None:
|
||||
for k, v in units.items():
|
||||
self.units[k] += v
|
||||
|
||||
def sell(self, units: dict[UnitType, int]) -> None:
|
||||
def sell(self, units: Dict[Type[UnitType], int]) -> None:
|
||||
for k, v in units.items():
|
||||
self.units[k] -= v
|
||||
|
||||
@@ -45,28 +46,24 @@ class PendingUnitDeliveries:
|
||||
self.refund(game, self.units)
|
||||
self.units = defaultdict(int)
|
||||
|
||||
def refund_ground_units(self, game: Game) -> None:
|
||||
ground_units: dict[UnitType[Any], int] = {
|
||||
u: self.units[u] for u in self.units.keys() if isinstance(u, GroundUnitType)
|
||||
}
|
||||
self.refund(game, ground_units)
|
||||
for gu in ground_units.keys():
|
||||
del self.units[gu]
|
||||
|
||||
def refund(self, game: Game, units: dict[UnitType, int]) -> None:
|
||||
def refund(self, game: Game, units: Dict[Type[UnitType], int]) -> None:
|
||||
for unit_type, count in units.items():
|
||||
logging.info(f"Refunding {count} {unit_type} at {self.destination.name}")
|
||||
game.adjust_budget(
|
||||
unit_type.price * count, player=self.destination.captured
|
||||
)
|
||||
try:
|
||||
price = PRICES[unit_type]
|
||||
except KeyError:
|
||||
logging.error(f"Could not refund {unit_type.id}, price unknown")
|
||||
continue
|
||||
|
||||
def pending_orders(self, unit_type: UnitType) -> int:
|
||||
logging.info(f"Refunding {count} {unit_type.id} at {self.destination.name}")
|
||||
game.adjust_budget(price * count, player=self.destination.captured)
|
||||
|
||||
def pending_orders(self, unit_type: Type[UnitType]) -> int:
|
||||
pending_units = self.units.get(unit_type)
|
||||
if pending_units is None:
|
||||
pending_units = 0
|
||||
return pending_units
|
||||
|
||||
def available_next_turn(self, unit_type: UnitType) -> int:
|
||||
def available_next_turn(self, unit_type: Type[UnitType]) -> int:
|
||||
current_units = self.destination.base.total_units_of_type(unit_type)
|
||||
return self.pending_orders(unit_type) + current_units
|
||||
|
||||
@@ -77,16 +74,18 @@ class PendingUnitDeliveries:
|
||||
f"{self.destination.name} lost its source for ground unit "
|
||||
"reinforcements. Refunding purchase price."
|
||||
)
|
||||
self.refund_ground_units(game)
|
||||
self.refund_all(game)
|
||||
return
|
||||
|
||||
bought_units: dict[UnitType, int] = {}
|
||||
units_needing_transfer: dict[GroundUnitType, int] = {}
|
||||
sold_units: dict[UnitType, int] = {}
|
||||
bought_units: Dict[Type[UnitType], int] = {}
|
||||
units_needing_transfer: Dict[Type[VehicleType], int] = {}
|
||||
sold_units: Dict[Type[UnitType], int] = {}
|
||||
for unit_type, count in self.units.items():
|
||||
coalition = "Ally" if self.destination.captured else "Enemy"
|
||||
d: dict[Any, int]
|
||||
name = unit_type.id
|
||||
|
||||
if (
|
||||
isinstance(unit_type, GroundUnitType)
|
||||
issubclass(unit_type, VehicleType)
|
||||
and self.destination != ground_unit_source
|
||||
):
|
||||
source = ground_unit_source
|
||||
@@ -98,27 +97,22 @@ class PendingUnitDeliveries:
|
||||
if count >= 0:
|
||||
d[unit_type] = count
|
||||
game.message(
|
||||
f"{coalition} reinforcements: {unit_type} x {count} at {source}"
|
||||
f"{coalition} reinforcements: {name} x {count} at {source}"
|
||||
)
|
||||
else:
|
||||
sold_units[unit_type] = -count
|
||||
game.message(f"{coalition} sold: {unit_type} x {-count} at {source}")
|
||||
game.message(f"{coalition} sold: {name} x {-count} at {source}")
|
||||
|
||||
self.units = defaultdict(int)
|
||||
self.destination.base.commission_units(bought_units)
|
||||
self.destination.base.commision_units(bought_units)
|
||||
self.destination.base.commit_losses(sold_units)
|
||||
|
||||
if units_needing_transfer:
|
||||
if ground_unit_source is None:
|
||||
raise RuntimeError(
|
||||
f"ground unit source could not be found for {self.destination} but still tried to "
|
||||
f"transfer units to there"
|
||||
)
|
||||
ground_unit_source.base.commission_units(units_needing_transfer)
|
||||
ground_unit_source.base.commision_units(units_needing_transfer)
|
||||
self.create_transfer(game, ground_unit_source, units_needing_transfer)
|
||||
|
||||
def create_transfer(
|
||||
self, game: Game, source: ControlPoint, units: dict[GroundUnitType, int]
|
||||
self, game: Game, source: ControlPoint, units: Dict[Type[VehicleType], int]
|
||||
) -> None:
|
||||
game.transfers.new_transfer(TransferOrder(source, self.destination, units))
|
||||
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
import itertools
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Optional
|
||||
from typing import Dict, Optional, Type
|
||||
|
||||
from dcs.unit import Unit
|
||||
from dcs.unitgroup import FlyingGroup, Group, VehicleGroup
|
||||
from dcs.unittype import VehicleType
|
||||
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game import db
|
||||
from game.squadrons import Pilot
|
||||
from game.theater import Airfield, ControlPoint, TheaterGroundObject
|
||||
from game.theater.theatergroundobject import BuildingGroundObject, SceneryGroundObject
|
||||
@@ -18,12 +19,12 @@ from gen.flights.flight import Flight
|
||||
@dataclass(frozen=True)
|
||||
class FlyingUnit:
|
||||
flight: Flight
|
||||
pilot: Optional[Pilot]
|
||||
pilot: Pilot
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FrontLineUnit:
|
||||
unit_type: GroundUnitType
|
||||
unit_type: Type[VehicleType]
|
||||
origin: ControlPoint
|
||||
|
||||
|
||||
@@ -36,13 +37,13 @@ class GroundObjectUnit:
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ConvoyUnit:
|
||||
unit_type: GroundUnitType
|
||||
unit_type: Type[VehicleType]
|
||||
convoy: Convoy
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AirliftUnits:
|
||||
cargo: tuple[GroundUnitType, ...]
|
||||
cargo: tuple[Type[VehicleType], ...]
|
||||
transfer: TransferOrder
|
||||
|
||||
|
||||
@@ -69,6 +70,8 @@ class UnitMap:
|
||||
name = str(unit.name)
|
||||
if name in self.aircraft:
|
||||
raise RuntimeError(f"Duplicate unit name: {name}")
|
||||
if pilot is None:
|
||||
raise ValueError(f"{name} has no pilot assigned")
|
||||
self.aircraft[name] = FlyingUnit(flight, pilot)
|
||||
if flight.cargo is not None:
|
||||
self.add_airlift_units(group, flight.cargo)
|
||||
@@ -84,15 +87,20 @@ class UnitMap:
|
||||
def airfield(self, name: str) -> Optional[Airfield]:
|
||||
return self.airfields.get(name, None)
|
||||
|
||||
def add_front_line_units(
|
||||
self, group: Group, origin: ControlPoint, unit_type: GroundUnitType
|
||||
) -> None:
|
||||
def add_front_line_units(self, group: Group, origin: ControlPoint) -> None:
|
||||
for unit in group.units:
|
||||
# The actual name is a String (the pydcs translatable string), which
|
||||
# doesn't define __eq__.
|
||||
name = str(unit.name)
|
||||
if name in self.front_line_units:
|
||||
raise RuntimeError(f"Duplicate front line unit: {name}")
|
||||
unit_type = db.unit_type_from_name(unit.type)
|
||||
if unit_type is None:
|
||||
raise RuntimeError(f"Unknown unit type: {unit.type}")
|
||||
if not issubclass(unit_type, VehicleType):
|
||||
raise RuntimeError(
|
||||
f"{name} is a {unit_type.__name__}, expected a VehicleType"
|
||||
)
|
||||
self.front_line_units[name] = FrontLineUnit(unit_type, origin)
|
||||
|
||||
def front_line_unit(self, name: str) -> Optional[FrontLineUnit]:
|
||||
@@ -135,12 +143,19 @@ class UnitMap:
|
||||
return self.ground_object_units.get(name, None)
|
||||
|
||||
def add_convoy_units(self, group: Group, convoy: Convoy) -> None:
|
||||
for unit, unit_type in zip(group.units, convoy.iter_units()):
|
||||
for unit in group.units:
|
||||
# The actual name is a String (the pydcs translatable string), which
|
||||
# doesn't define __eq__.
|
||||
name = str(unit.name)
|
||||
if name in self.convoys:
|
||||
raise RuntimeError(f"Duplicate convoy unit: {name}")
|
||||
unit_type = db.unit_type_from_name(unit.type)
|
||||
if unit_type is None:
|
||||
raise RuntimeError(f"Unknown unit type: {unit.type}")
|
||||
if not issubclass(unit_type, VehicleType):
|
||||
raise RuntimeError(
|
||||
f"{name} is a {unit_type.__name__}, expected a VehicleType"
|
||||
)
|
||||
self.convoys[name] = ConvoyUnit(unit_type, convoy)
|
||||
|
||||
def convoy_unit(self, name: str) -> Optional[ConvoyUnit]:
|
||||
|
||||
@@ -58,10 +58,6 @@ class Distance:
|
||||
def from_nautical_miles(cls, value: float) -> Distance:
|
||||
return cls(value * NM_TO_METERS)
|
||||
|
||||
@classmethod
|
||||
def inf(cls) -> Distance:
|
||||
return cls.from_meters(math.inf)
|
||||
|
||||
def __add__(self, other: Distance) -> Distance:
|
||||
return meters(self.meters + other.meters)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ from pathlib import Path
|
||||
|
||||
|
||||
def _build_version_string() -> str:
|
||||
components = ["5.0.0"]
|
||||
components = ["3.1.0"]
|
||||
build_number_path = Path("resources/buildnumber")
|
||||
if build_number_path.exists():
|
||||
with build_number_path.open("r") as build_number_file:
|
||||
@@ -70,9 +70,9 @@ VERSION = _build_version_string()
|
||||
#: Version 4.2
|
||||
#: * Adds support for AAA objectives. Place with any of the following units (either red
|
||||
#: or blue):
|
||||
#: * Flak18,
|
||||
#: * Vulcan,
|
||||
#: * ZSU_23_4_Shilka,
|
||||
#: * AAA_8_8cm_Flak_18,
|
||||
#: * SPAAA_Vulcan_M163,
|
||||
#: * SPAAA_ZSU_23_4_Shilka_Gun_Dish,
|
||||
#:
|
||||
#: Version 5.0
|
||||
#: * Ammunition Depots objective locations are now predetermined using the "Ammunition
|
||||
@@ -87,13 +87,4 @@ VERSION = _build_version_string()
|
||||
#: Version 6.0
|
||||
#: * Random objective generation no is longer supported. Fixed objective locations were
|
||||
#: added in 4.1.
|
||||
#:
|
||||
#: Version 6.1
|
||||
#: * Support for new Syrian airfields in DCS 2.7.2.7910.1 (Cyprus update).
|
||||
#:
|
||||
#: Version 7.0
|
||||
#: * DCS 2.7.2.7910.1 (Cyprus update) changed the IDs of scenery strike targets. Any
|
||||
#: mission using map buildings as strike targets must check and potentially recreate
|
||||
#: all those objectives. This definitely affects all Syria campaigns, other maps are
|
||||
#: not yet verified.
|
||||
CAMPAIGN_FORMAT_VERSION = (7, 0)
|
||||
CAMPAIGN_FORMAT_VERSION = (6, 0)
|
||||
|
||||
614
gen/aircraft.py
614
gen/aircraft.py
@@ -12,17 +12,30 @@ from dcs.action import AITaskPush, ActivateGroup
|
||||
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,
|
||||
B_17G,
|
||||
B_52H,
|
||||
Bf_109K_4,
|
||||
C_101CC,
|
||||
C_101EB,
|
||||
FW_190A8,
|
||||
FW_190D9,
|
||||
F_14B,
|
||||
I_16,
|
||||
JF_17,
|
||||
Ju_88A4,
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
P_51D,
|
||||
P_51D_30_NA,
|
||||
PlaneType,
|
||||
SpitfireLFMkIX,
|
||||
SpitfireLFMkIXCW,
|
||||
Su_33,
|
||||
Tu_22M3,
|
||||
)
|
||||
@@ -30,7 +43,6 @@ from dcs.point import MovingPoint, PointAction
|
||||
from dcs.task import (
|
||||
AWACS,
|
||||
AWACSTaskAction,
|
||||
ActivateBeaconCommand,
|
||||
AntishipStrike,
|
||||
AttackGroup,
|
||||
Bombing,
|
||||
@@ -49,10 +61,8 @@ from dcs.task import (
|
||||
OptReactOnThreat,
|
||||
OptRestrictJettison,
|
||||
OrbitAction,
|
||||
Refueling,
|
||||
RunwayAttack,
|
||||
StartCommand,
|
||||
Tanker,
|
||||
Targets,
|
||||
Transport,
|
||||
WeaponType,
|
||||
@@ -62,14 +72,15 @@ from dcs.terrain.terrain import Airport, NoParkingSlotError
|
||||
from dcs.triggers import Event, TriggerOnce, TriggerRule
|
||||
from dcs.unit import Unit, Skill
|
||||
from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup
|
||||
from dcs.unittype import FlyingType
|
||||
from dcs.unittype import FlyingType, UnitType
|
||||
|
||||
from game import db
|
||||
from game.data.cap_capabilities_db import GUNFIGHTERS
|
||||
from game.data.weapons import Pylon
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.db import GUN_RELIANT_AIRFRAMES
|
||||
from game.factions.faction import Faction
|
||||
from game.settings import Settings
|
||||
from game.squadrons import Pilot
|
||||
from game.squadrons import Pilot, Squadron
|
||||
from game.theater.controlpoint import (
|
||||
Airfield,
|
||||
ControlPoint,
|
||||
@@ -90,17 +101,15 @@ from gen.flights.flight import (
|
||||
FlightWaypoint,
|
||||
FlightWaypointType,
|
||||
)
|
||||
from gen.radios import RadioFrequency, RadioRegistry
|
||||
from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
|
||||
from gen.runways import RunwayData
|
||||
from gen.tacan import TacanBand, TacanRegistry
|
||||
from .airsupportgen import AirSupport, AwacsInfo, TankerInfo
|
||||
from .airsupportgen import AirSupport, AwacsInfo
|
||||
from .callsigns import callsign_for_support_unit
|
||||
from .flights.flightplan import (
|
||||
AwacsFlightPlan,
|
||||
CasFlightPlan,
|
||||
LoiterFlightPlan,
|
||||
PatrollingFlightPlan,
|
||||
RefuelingFlightPlan,
|
||||
SweepFlightPlan,
|
||||
)
|
||||
from .flights.traveltime import GroundSpeed, TotEstimator
|
||||
@@ -116,6 +125,16 @@ RTB_ALTITUDE = meters(800)
|
||||
RTB_DISTANCE = 5000
|
||||
HELI_ALT = 500
|
||||
|
||||
# Note that fallback radio channels will *not* be reserved. It's possible that
|
||||
# flights using these will overlap with other channels. This is because we would
|
||||
# need to make sure we fell back to a frequency that is not used by any beacon
|
||||
# or ATC, which we don't have the information to predict. Deal with the minor
|
||||
# annoyance for now since we'll be fleshing out radio info soon enough.
|
||||
ALLIES_WW2_CHANNEL = MHz(124)
|
||||
GERMAN_WW2_CHANNEL = MHz(40)
|
||||
HELICOPTER_CHANNEL = MHz(127)
|
||||
UHF_FALLBACK_CHANNEL = MHz(251)
|
||||
|
||||
TARGET_WAYPOINTS = (
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
FlightWaypointType.TARGET_POINT,
|
||||
@@ -123,6 +142,121 @@ TARGET_WAYPOINTS = (
|
||||
)
|
||||
|
||||
|
||||
# TODO: Get radio information for all the special cases.
|
||||
def get_fallback_channel(unit_type: UnitType) -> RadioFrequency:
|
||||
if unit_type in helicopter_map.values() and unit_type != UH_1H:
|
||||
return HELICOPTER_CHANNEL
|
||||
|
||||
german_ww2_aircraft = [
|
||||
Bf_109K_4,
|
||||
FW_190A8,
|
||||
FW_190D9,
|
||||
Ju_88A4,
|
||||
]
|
||||
|
||||
if unit_type in german_ww2_aircraft:
|
||||
return GERMAN_WW2_CHANNEL
|
||||
|
||||
allied_ww2_aircraft = [
|
||||
I_16,
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
P_51D,
|
||||
P_51D_30_NA,
|
||||
SpitfireLFMkIX,
|
||||
SpitfireLFMkIXCW,
|
||||
]
|
||||
|
||||
if unit_type in allied_ww2_aircraft:
|
||||
return ALLIES_WW2_CHANNEL
|
||||
|
||||
return UHF_FALLBACK_CHANNEL
|
||||
|
||||
|
||||
class ChannelNamer:
|
||||
"""Base class allowing channel name customization per-aircraft.
|
||||
|
||||
Most aircraft will want to customize this behavior, but the default is
|
||||
reasonable for any aircraft with numbered radios.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def channel_name(radio_id: int, channel_id: int) -> str:
|
||||
"""Returns the name of the channel for the given radio and channel."""
|
||||
return f"COMM{radio_id} Ch {channel_id}"
|
||||
|
||||
|
||||
class SingleRadioChannelNamer(ChannelNamer):
|
||||
"""Channel namer for the aircraft with only a single radio.
|
||||
|
||||
Aircraft like the MiG-19P and the MiG-21bis only have a single radio, so
|
||||
it's not necessary for us to name the radio when naming the channel.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def channel_name(radio_id: int, channel_id: int) -> str:
|
||||
return f"Ch {channel_id}"
|
||||
|
||||
|
||||
class HueyChannelNamer(ChannelNamer):
|
||||
"""Channel namer for the UH-1H."""
|
||||
|
||||
@staticmethod
|
||||
def channel_name(radio_id: int, channel_id: int) -> str:
|
||||
return f"COM3 Ch {channel_id}"
|
||||
|
||||
|
||||
class MirageChannelNamer(ChannelNamer):
|
||||
"""Channel namer for the M-2000."""
|
||||
|
||||
@staticmethod
|
||||
def channel_name(radio_id: int, channel_id: int) -> str:
|
||||
radio_name = ["V/UHF", "UHF"][radio_id - 1]
|
||||
return f"{radio_name} Ch {channel_id}"
|
||||
|
||||
|
||||
class TomcatChannelNamer(ChannelNamer):
|
||||
"""Channel namer for the F-14."""
|
||||
|
||||
@staticmethod
|
||||
def channel_name(radio_id: int, channel_id: int) -> str:
|
||||
radio_name = ["UHF", "VHF/UHF"][radio_id - 1]
|
||||
return f"{radio_name} Ch {channel_id}"
|
||||
|
||||
|
||||
class ViggenChannelNamer(ChannelNamer):
|
||||
"""Channel namer for the AJS37."""
|
||||
|
||||
@staticmethod
|
||||
def channel_name(radio_id: int, channel_id: int) -> str:
|
||||
if channel_id >= 4:
|
||||
channel_letter = "EFGH"[channel_id - 4]
|
||||
return f"FR 24 {channel_letter}"
|
||||
return f"FR 22 Special {channel_id}"
|
||||
|
||||
|
||||
class ViperChannelNamer(ChannelNamer):
|
||||
"""Channel namer for the F-16."""
|
||||
|
||||
@staticmethod
|
||||
def channel_name(radio_id: int, channel_id: int) -> str:
|
||||
return f"COM{radio_id} Ch {channel_id}"
|
||||
|
||||
|
||||
class SCR522ChannelNamer(ChannelNamer):
|
||||
"""
|
||||
Channel namer for P-51 & P-47D
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def channel_name(radio_id: int, channel_id: int) -> str:
|
||||
if channel_id > 3:
|
||||
return "?"
|
||||
else:
|
||||
return f"Button " + "ABCD"[channel_id - 1]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ChannelAssignment:
|
||||
radio_id: int
|
||||
@@ -136,6 +270,9 @@ class FlightData:
|
||||
#: The package that the flight belongs to.
|
||||
package: Package
|
||||
|
||||
#: The country that the flight belongs to.
|
||||
country: str
|
||||
|
||||
flight_type: FlightType
|
||||
|
||||
#: All units in the flight.
|
||||
@@ -176,7 +313,7 @@ class FlightData:
|
||||
def __init__(
|
||||
self,
|
||||
package: Package,
|
||||
aircraft_type: AircraftType,
|
||||
country: str,
|
||||
flight_type: FlightType,
|
||||
units: List[FlyingUnit],
|
||||
size: int,
|
||||
@@ -192,7 +329,7 @@ class FlightData:
|
||||
custom_name: Optional[str],
|
||||
) -> None:
|
||||
self.package = package
|
||||
self.aircraft_type = aircraft_type
|
||||
self.country = country
|
||||
self.flight_type = flight_type
|
||||
self.units = units
|
||||
self.size = size
|
||||
@@ -214,6 +351,11 @@ class FlightData:
|
||||
"""List of playable units in the flight."""
|
||||
return [u for u in self.units if u.is_human()]
|
||||
|
||||
@property
|
||||
def aircraft_type(self) -> FlyingType:
|
||||
"""Returns the type of aircraft in this flight."""
|
||||
return self.units[0].unit_type
|
||||
|
||||
def num_radio_channels(self, radio_id: int) -> int:
|
||||
"""Returns the number of preset channels for the given radio."""
|
||||
# Note: pydcs only initializes the radio presets for client slots.
|
||||
@@ -239,6 +381,302 @@ class FlightData:
|
||||
)
|
||||
|
||||
|
||||
class RadioChannelAllocator:
|
||||
"""Base class for radio channel allocators."""
|
||||
|
||||
def assign_channels_for_flight(
|
||||
self, flight: FlightData, air_support: AirSupport
|
||||
) -> None:
|
||||
"""Assigns mission frequencies to preset channels for the flight."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CommonRadioChannelAllocator(RadioChannelAllocator):
|
||||
"""Radio channel allocator suitable for most aircraft.
|
||||
|
||||
Most of the aircraft with preset channels available have one or more radios
|
||||
with 20 or more channels available (typically per-radio, but this is not the
|
||||
case for the JF-17).
|
||||
"""
|
||||
|
||||
#: Index of the radio used for intra-flight communications. Matches the
|
||||
#: index of the panel_radio field of the pydcs.dcs.planes object.
|
||||
inter_flight_radio_index: Optional[int]
|
||||
|
||||
#: Index of the radio used for intra-flight communications. Matches the
|
||||
#: index of the panel_radio field of the pydcs.dcs.planes object.
|
||||
intra_flight_radio_index: Optional[int]
|
||||
|
||||
def assign_channels_for_flight(
|
||||
self, flight: FlightData, air_support: AirSupport
|
||||
) -> None:
|
||||
if self.intra_flight_radio_index is not None:
|
||||
flight.assign_channel(
|
||||
self.intra_flight_radio_index, 1, flight.intra_flight_channel
|
||||
)
|
||||
|
||||
if self.inter_flight_radio_index is None:
|
||||
return
|
||||
|
||||
# For cases where the inter-flight and intra-flight radios share presets
|
||||
# (the JF-17 only has one set of channels, even though it can use two
|
||||
# channels simultaneously), start assigning inter-flight channels at 2.
|
||||
radio_id = self.inter_flight_radio_index
|
||||
if self.intra_flight_radio_index == radio_id:
|
||||
first_channel = 2
|
||||
else:
|
||||
first_channel = 1
|
||||
|
||||
last_channel = flight.num_radio_channels(radio_id)
|
||||
channel_alloc = iter(range(first_channel, last_channel + 1))
|
||||
|
||||
if flight.departure.atc is not None:
|
||||
flight.assign_channel(radio_id, next(channel_alloc), flight.departure.atc)
|
||||
|
||||
# TODO: If there ever are multiple AWACS, limit to mission relevant.
|
||||
for awacs in air_support.awacs:
|
||||
flight.assign_channel(radio_id, next(channel_alloc), awacs.freq)
|
||||
|
||||
if flight.arrival != flight.departure and flight.arrival.atc is not None:
|
||||
flight.assign_channel(radio_id, next(channel_alloc), flight.arrival.atc)
|
||||
|
||||
try:
|
||||
# TODO: Skip incompatible tankers.
|
||||
for tanker in air_support.tankers:
|
||||
flight.assign_channel(radio_id, next(channel_alloc), tanker.freq)
|
||||
|
||||
if flight.divert is not None and flight.divert.atc is not None:
|
||||
flight.assign_channel(radio_id, next(channel_alloc), flight.divert.atc)
|
||||
except StopIteration:
|
||||
# Any remaining channels are nice-to-haves, but not necessary for
|
||||
# the few aircraft with a small number of channels available.
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class NoOpChannelAllocator(RadioChannelAllocator):
|
||||
"""Channel allocator for aircraft that don't support preset channels."""
|
||||
|
||||
def assign_channels_for_flight(
|
||||
self, flight: FlightData, air_support: AirSupport
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FarmerRadioChannelAllocator(RadioChannelAllocator):
|
||||
"""Preset channel allocator for the MiG-19P."""
|
||||
|
||||
def assign_channels_for_flight(
|
||||
self, flight: FlightData, air_support: AirSupport
|
||||
) -> None:
|
||||
# The Farmer only has 6 preset channels. It also only has a VHF radio,
|
||||
# and currently our ATC data and AWACS are only in the UHF band.
|
||||
radio_id = 1
|
||||
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
|
||||
# TODO: Assign 4-6 to VHF frequencies of departure, arrival, and divert.
|
||||
# TODO: Assign 2 and 3 to AWACS if it is VHF.
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ViggenRadioChannelAllocator(RadioChannelAllocator):
|
||||
"""Preset channel allocator for the AJS37."""
|
||||
|
||||
def assign_channels_for_flight(
|
||||
self, flight: FlightData, air_support: AirSupport
|
||||
) -> None:
|
||||
# The Viggen's preset channels are handled differently from other
|
||||
# aircraft. The aircraft automatically configures channels for every
|
||||
# allied flight in the game (including AWACS) and for every airfield. As
|
||||
# such, we don't need to allocate any of those. There are seven presets
|
||||
# we can modify, however: three channels for the main radio intended for
|
||||
# communication with wingmen, and four emergency channels for the backup
|
||||
# radio. We'll set the first channel of the main radio to the
|
||||
# intra-flight channel, and the first three emergency channels to each
|
||||
# of the flight plan's airfields. The fourth emergency channel is always
|
||||
# the guard channel.
|
||||
radio_id = 1
|
||||
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
|
||||
if flight.departure.atc is not None:
|
||||
flight.assign_channel(radio_id, 4, flight.departure.atc)
|
||||
if flight.arrival.atc is not None:
|
||||
flight.assign_channel(radio_id, 5, flight.arrival.atc)
|
||||
# TODO: Assign divert to 6 when we support divert airfields.
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SCR522RadioChannelAllocator(RadioChannelAllocator):
|
||||
"""Preset channel allocator for the SCR522 WW2 radios. (4 channels)"""
|
||||
|
||||
def assign_channels_for_flight(
|
||||
self, flight: FlightData, air_support: AirSupport
|
||||
) -> None:
|
||||
radio_id = 1
|
||||
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
|
||||
if flight.departure.atc is not None:
|
||||
flight.assign_channel(radio_id, 2, flight.departure.atc)
|
||||
if flight.arrival.atc is not None:
|
||||
flight.assign_channel(radio_id, 3, flight.arrival.atc)
|
||||
|
||||
# TODO : Some GCI on Channel 4 ?
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AircraftData:
|
||||
"""Additional aircraft data not exposed by pydcs."""
|
||||
|
||||
#: The type of radio used for inter-flight communications.
|
||||
inter_flight_radio: Radio
|
||||
|
||||
#: The type of radio used for intra-flight communications.
|
||||
intra_flight_radio: Radio
|
||||
|
||||
#: The radio preset channel allocator, if the aircraft supports channel
|
||||
#: presets. If the aircraft does not support preset channels, this will be
|
||||
#: None.
|
||||
channel_allocator: Optional[RadioChannelAllocator]
|
||||
|
||||
#: Defines how channels should be named when printed in the kneeboard.
|
||||
channel_namer: Type[ChannelNamer] = ChannelNamer
|
||||
|
||||
|
||||
# Indexed by the id field of the pydcs PlaneType.
|
||||
AIRCRAFT_DATA: Dict[str, AircraftData] = {
|
||||
"A-10C": AircraftData(
|
||||
inter_flight_radio=get_radio("AN/ARC-164"),
|
||||
# VHF for intraflight is not accepted anymore by DCS
|
||||
# (see https://forums.eagle.ru/showthread.php?p=4499738).
|
||||
intra_flight_radio=get_radio("AN/ARC-164"),
|
||||
channel_allocator=NoOpChannelAllocator(),
|
||||
),
|
||||
"AJS37": AircraftData(
|
||||
# The AJS37 has somewhat unique radio configuration. Two backup radio
|
||||
# (FR 24) can only operate simultaneously with the main radio in guard
|
||||
# mode. As such, we only use the main radio for both inter- and intra-
|
||||
# flight communication.
|
||||
inter_flight_radio=get_radio("FR 22"),
|
||||
intra_flight_radio=get_radio("FR 22"),
|
||||
channel_allocator=ViggenRadioChannelAllocator(),
|
||||
channel_namer=ViggenChannelNamer,
|
||||
),
|
||||
"AV8BNA": AircraftData(
|
||||
inter_flight_radio=get_radio("AN/ARC-210"),
|
||||
intra_flight_radio=get_radio("AN/ARC-210"),
|
||||
channel_allocator=CommonRadioChannelAllocator(
|
||||
inter_flight_radio_index=2, intra_flight_radio_index=1
|
||||
),
|
||||
),
|
||||
"F-14B": AircraftData(
|
||||
inter_flight_radio=get_radio("AN/ARC-159"),
|
||||
intra_flight_radio=get_radio("AN/ARC-182"),
|
||||
channel_allocator=CommonRadioChannelAllocator(
|
||||
inter_flight_radio_index=1, intra_flight_radio_index=2
|
||||
),
|
||||
channel_namer=TomcatChannelNamer,
|
||||
),
|
||||
"F-16C_50": AircraftData(
|
||||
inter_flight_radio=get_radio("AN/ARC-164"),
|
||||
intra_flight_radio=get_radio("AN/ARC-222"),
|
||||
# COM2 is the AN/ARC-222, which is the VHF radio we want to use for
|
||||
# intra-flight communication to leave COM1 open for UHF inter-flight.
|
||||
channel_allocator=CommonRadioChannelAllocator(
|
||||
inter_flight_radio_index=1, intra_flight_radio_index=2
|
||||
),
|
||||
channel_namer=ViperChannelNamer,
|
||||
),
|
||||
"FA-18C_hornet": AircraftData(
|
||||
inter_flight_radio=get_radio("AN/ARC-210"),
|
||||
intra_flight_radio=get_radio("AN/ARC-210"),
|
||||
# DCS will clobber channel 1 of the first radio compatible with the
|
||||
# flight's assigned frequency. Since the F/A-18's two radios are both
|
||||
# AN/ARC-210s, radio 1 will be compatible regardless of which frequency
|
||||
# is assigned, so we must use radio 1 for the intra-flight radio.
|
||||
channel_allocator=CommonRadioChannelAllocator(
|
||||
inter_flight_radio_index=2, intra_flight_radio_index=1
|
||||
),
|
||||
),
|
||||
"JF-17": AircraftData(
|
||||
inter_flight_radio=get_radio("R&S M3AR UHF"),
|
||||
intra_flight_radio=get_radio("R&S M3AR VHF"),
|
||||
channel_allocator=CommonRadioChannelAllocator(
|
||||
inter_flight_radio_index=1, intra_flight_radio_index=1
|
||||
),
|
||||
# Same naming pattern as the Viper, so just reuse that.
|
||||
channel_namer=ViperChannelNamer,
|
||||
),
|
||||
"Ka-50": AircraftData(
|
||||
inter_flight_radio=get_radio("R-800L1"),
|
||||
intra_flight_radio=get_radio("R-800L1"),
|
||||
# The R-800L1 doesn't have preset channels, and the other radio is for
|
||||
# communications with FAC and ground units, which don't currently have
|
||||
# radios assigned, so no channels to configure.
|
||||
channel_allocator=NoOpChannelAllocator(),
|
||||
),
|
||||
"M-2000C": AircraftData(
|
||||
inter_flight_radio=get_radio("TRT ERA 7000 V/UHF"),
|
||||
intra_flight_radio=get_radio("TRT ERA 7200 UHF"),
|
||||
channel_allocator=CommonRadioChannelAllocator(
|
||||
inter_flight_radio_index=1, intra_flight_radio_index=2
|
||||
),
|
||||
channel_namer=MirageChannelNamer,
|
||||
),
|
||||
"MiG-15bis": AircraftData(
|
||||
inter_flight_radio=get_radio("RSI-6K HF"),
|
||||
intra_flight_radio=get_radio("RSI-6K HF"),
|
||||
channel_allocator=NoOpChannelAllocator(),
|
||||
),
|
||||
"MiG-19P": AircraftData(
|
||||
inter_flight_radio=get_radio("RSIU-4V"),
|
||||
intra_flight_radio=get_radio("RSIU-4V"),
|
||||
channel_allocator=FarmerRadioChannelAllocator(),
|
||||
channel_namer=SingleRadioChannelNamer,
|
||||
),
|
||||
"MiG-21Bis": AircraftData(
|
||||
inter_flight_radio=get_radio("RSIU-5V"),
|
||||
intra_flight_radio=get_radio("RSIU-5V"),
|
||||
channel_allocator=CommonRadioChannelAllocator(
|
||||
inter_flight_radio_index=1, intra_flight_radio_index=1
|
||||
),
|
||||
channel_namer=SingleRadioChannelNamer,
|
||||
),
|
||||
"P-51D": AircraftData(
|
||||
inter_flight_radio=get_radio("SCR522"),
|
||||
intra_flight_radio=get_radio("SCR522"),
|
||||
channel_allocator=CommonRadioChannelAllocator(
|
||||
inter_flight_radio_index=1, intra_flight_radio_index=1
|
||||
),
|
||||
channel_namer=SCR522ChannelNamer,
|
||||
),
|
||||
"UH-1H": AircraftData(
|
||||
inter_flight_radio=get_radio("AN/ARC-51BX"),
|
||||
# Ideally this would use the AN/ARC-131 because that radio is supposed
|
||||
# to be used for flight comms, but DCS won't allow it as the flight's
|
||||
# frequency, nor will it allow the AN/ARC-134.
|
||||
intra_flight_radio=get_radio("AN/ARC-51BX"),
|
||||
channel_allocator=CommonRadioChannelAllocator(
|
||||
inter_flight_radio_index=1, intra_flight_radio_index=1
|
||||
),
|
||||
channel_namer=HueyChannelNamer,
|
||||
),
|
||||
"F-22A": AircraftData(
|
||||
inter_flight_radio=get_radio("SCR-522"),
|
||||
intra_flight_radio=get_radio("SCR-522"),
|
||||
channel_allocator=None,
|
||||
channel_namer=SCR522ChannelNamer,
|
||||
),
|
||||
"JAS39Gripen": AircraftData(
|
||||
inter_flight_radio=get_radio("R&S Series 6000"),
|
||||
intra_flight_radio=get_radio("R&S Series 6000"),
|
||||
channel_allocator=None,
|
||||
),
|
||||
}
|
||||
AIRCRAFT_DATA["A-10C_2"] = AIRCRAFT_DATA["A-10C"]
|
||||
AIRCRAFT_DATA["P-51D-30-NA"] = AIRCRAFT_DATA["P-51D"]
|
||||
AIRCRAFT_DATA["P-47D-30"] = AIRCRAFT_DATA["P-51D"]
|
||||
AIRCRAFT_DATA["JAS39Gripen_AG"] = AIRCRAFT_DATA["JAS39Gripen"]
|
||||
|
||||
|
||||
class AircraftConflictGenerator:
|
||||
def __init__(
|
||||
self,
|
||||
@@ -246,7 +684,6 @@ class AircraftConflictGenerator:
|
||||
settings: Settings,
|
||||
game: Game,
|
||||
radio_registry: RadioRegistry,
|
||||
tacan_registry: TacanRegistry,
|
||||
unit_map: UnitMap,
|
||||
air_support: AirSupport,
|
||||
) -> None:
|
||||
@@ -254,7 +691,6 @@ class AircraftConflictGenerator:
|
||||
self.game = game
|
||||
self.settings = settings
|
||||
self.radio_registry = radio_registry
|
||||
self.tacan_registy = tacan_registry
|
||||
self.unit_map = unit_map
|
||||
self.flights: List[FlightData] = []
|
||||
self.air_support = air_support
|
||||
@@ -274,6 +710,21 @@ class AircraftConflictGenerator:
|
||||
total += flight.client_count
|
||||
return total
|
||||
|
||||
def get_intra_flight_channel(self, airframe: UnitType) -> RadioFrequency:
|
||||
"""Allocates an intra-flight channel to a group.
|
||||
|
||||
Args:
|
||||
airframe: The type of aircraft a channel should be allocated for.
|
||||
|
||||
Returns:
|
||||
The frequency of the intra-flight channel.
|
||||
"""
|
||||
try:
|
||||
aircraft_data = AIRCRAFT_DATA[airframe.id]
|
||||
return self.radio_registry.alloc_for_radio(aircraft_data.intra_flight_radio)
|
||||
except KeyError:
|
||||
return get_fallback_channel(airframe)
|
||||
|
||||
@staticmethod
|
||||
def _start_type(start_type: str) -> StartType:
|
||||
if start_type == "Runway":
|
||||
@@ -303,10 +754,7 @@ class AircraftConflictGenerator:
|
||||
current_level = levels.index(base_skill)
|
||||
missions_for_skill_increase = 4
|
||||
increase = pilot.record.missions_flown // missions_for_skill_increase
|
||||
capped_increase = min(current_level + increase, len(levels) - 1)
|
||||
new_level = (capped_increase, current_level)[
|
||||
self.game.settings.ai_pilot_levelling
|
||||
]
|
||||
new_level = min(current_level + increase, len(levels) - 1)
|
||||
return levels[new_level]
|
||||
|
||||
def set_skill(self, unit: FlyingUnit, pilot: Optional[Pilot], blue: bool) -> None:
|
||||
@@ -376,13 +824,10 @@ class AircraftConflictGenerator:
|
||||
OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)
|
||||
)
|
||||
|
||||
if (
|
||||
flight.flight_type == FlightType.AEWC
|
||||
or flight.flight_type == FlightType.REFUELING
|
||||
):
|
||||
if flight.flight_type == FlightType.AEWC:
|
||||
channel = self.radio_registry.alloc_uhf()
|
||||
else:
|
||||
channel = flight.unit_type.alloc_flight_radio(self.radio_registry)
|
||||
channel = self.get_intra_flight_channel(unit_type)
|
||||
group.set_frequency(channel.mhz)
|
||||
|
||||
divert = None
|
||||
@@ -392,7 +837,7 @@ class AircraftConflictGenerator:
|
||||
self.flights.append(
|
||||
FlightData(
|
||||
package=package,
|
||||
aircraft_type=flight.unit_type,
|
||||
country=self.game.faction_for(player=flight.departure.captured).country,
|
||||
flight_type=flight.flight_type,
|
||||
units=group.units,
|
||||
size=len(group.units),
|
||||
@@ -434,23 +879,6 @@ class AircraftConflictGenerator:
|
||||
)
|
||||
)
|
||||
|
||||
if isinstance(flight.flight_plan, RefuelingFlightPlan):
|
||||
callsign = callsign_for_support_unit(group)
|
||||
|
||||
tacan = self.tacan_registy.alloc_for_band(TacanBand.Y)
|
||||
self.air_support.tankers.append(
|
||||
TankerInfo(
|
||||
group_name=str(group.name),
|
||||
callsign=callsign,
|
||||
variant=flight.unit_type.name,
|
||||
freq=channel,
|
||||
tacan=tacan,
|
||||
start_time=flight.flight_plan.patrol_start_time,
|
||||
end_time=flight.flight_plan.patrol_end_time,
|
||||
blue=flight.departure.captured,
|
||||
)
|
||||
)
|
||||
|
||||
def _generate_at_airport(
|
||||
self,
|
||||
name: str,
|
||||
@@ -501,7 +929,7 @@ class AircraftConflictGenerator:
|
||||
group = self.m.flight_group(
|
||||
country=side,
|
||||
name=name,
|
||||
aircraft_type=flight.unit_type.dcs_unit_type,
|
||||
aircraft_type=flight.unit_type,
|
||||
airport=None,
|
||||
position=pos,
|
||||
altitude=alt.meters,
|
||||
@@ -635,7 +1063,7 @@ class AircraftConflictGenerator:
|
||||
control_point: Airfield,
|
||||
country: Country,
|
||||
faction: Faction,
|
||||
aircraft: AircraftType,
|
||||
aircraft: Type[FlyingType],
|
||||
number: int,
|
||||
) -> None:
|
||||
for _ in range(number):
|
||||
@@ -657,7 +1085,7 @@ class AircraftConflictGenerator:
|
||||
group = self._generate_at_airport(
|
||||
name=namegen.next_aircraft_name(country, control_point.id, flight),
|
||||
side=country,
|
||||
unit_type=aircraft.dcs_unit_type,
|
||||
unit_type=aircraft,
|
||||
count=1,
|
||||
start_type="Cold",
|
||||
airport=control_point.airport,
|
||||
@@ -731,7 +1159,7 @@ class AircraftConflictGenerator:
|
||||
group = self._generate_at_group(
|
||||
name=name,
|
||||
side=country,
|
||||
unit_type=flight.unit_type.dcs_unit_type,
|
||||
unit_type=flight.unit_type,
|
||||
count=flight.count,
|
||||
start_type=flight.start_type,
|
||||
at=self.m.find_group(group_name),
|
||||
@@ -744,7 +1172,7 @@ class AircraftConflictGenerator:
|
||||
group = self._generate_at_airport(
|
||||
name=name,
|
||||
side=country,
|
||||
unit_type=flight.unit_type.dcs_unit_type,
|
||||
unit_type=flight.unit_type,
|
||||
count=flight.count,
|
||||
start_type=flight.start_type,
|
||||
airport=cp.airport,
|
||||
@@ -786,7 +1214,7 @@ class AircraftConflictGenerator:
|
||||
if flight.client_count > 0:
|
||||
return True
|
||||
|
||||
return flight.unit_type.always_keeps_gun
|
||||
return flight.unit_type in GUN_RELIANT_AIRFRAMES
|
||||
|
||||
def configure_behavior(
|
||||
self,
|
||||
@@ -825,8 +1253,9 @@ class AircraftConflictGenerator:
|
||||
|
||||
@staticmethod
|
||||
def configure_eplrs(group: FlyingGroup, flight: Flight) -> None:
|
||||
if flight.unit_type.eplrs_capable:
|
||||
group.points[0].tasks.append(EPLRS(group.id))
|
||||
if hasattr(flight.unit_type, "eplrs"):
|
||||
if flight.unit_type.eplrs:
|
||||
group.points[0].tasks.append(EPLRS(group.id))
|
||||
|
||||
def configure_cap(
|
||||
self,
|
||||
@@ -838,7 +1267,7 @@ class AircraftConflictGenerator:
|
||||
group.task = CAP.name
|
||||
self._setup_group(group, package, flight, dynamic_runways)
|
||||
|
||||
if not flight.unit_type.gunfighter:
|
||||
if flight.unit_type not in GUNFIGHTERS:
|
||||
ammo_type = OptRTBOnOutOfAmmo.Values.AAM
|
||||
else:
|
||||
ammo_type = OptRTBOnOutOfAmmo.Values.Cannon
|
||||
@@ -855,7 +1284,7 @@ class AircraftConflictGenerator:
|
||||
group.task = FighterSweep.name
|
||||
self._setup_group(group, package, flight, dynamic_runways)
|
||||
|
||||
if not flight.unit_type.gunfighter:
|
||||
if flight.unit_type not in GUNFIGHTERS:
|
||||
ammo_type = OptRTBOnOutOfAmmo.Values.AAM
|
||||
else:
|
||||
ammo_type = OptRTBOnOutOfAmmo.Values.Cannon
|
||||
@@ -1028,32 +1457,6 @@ class AircraftConflictGenerator:
|
||||
|
||||
group.points[0].tasks.append(AWACSTaskAction())
|
||||
|
||||
def configure_refueling(
|
||||
self,
|
||||
group: FlyingGroup,
|
||||
package: Package,
|
||||
flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData],
|
||||
) -> None:
|
||||
group.task = Refueling.name
|
||||
|
||||
if not isinstance(flight.flight_plan, RefuelingFlightPlan):
|
||||
logging.error(
|
||||
f"Cannot configure racetrack refueling tasks for {flight} because it "
|
||||
"does not have an racetrack refueling flight plan."
|
||||
)
|
||||
return
|
||||
|
||||
self._setup_group(group, package, flight, dynamic_runways)
|
||||
|
||||
self.configure_behavior(
|
||||
flight,
|
||||
group,
|
||||
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||
roe=OptROE.Values.WeaponHold,
|
||||
restrict_jettison=True,
|
||||
)
|
||||
|
||||
def configure_escort(
|
||||
self,
|
||||
group: FlyingGroup,
|
||||
@@ -1132,8 +1535,6 @@ class AircraftConflictGenerator:
|
||||
self.configure_sweep(group, package, flight, dynamic_runways)
|
||||
elif flight_type == FlightType.AEWC:
|
||||
self.configure_awacs(group, package, flight, dynamic_runways)
|
||||
elif flight_type == FlightType.REFUELING:
|
||||
self.configure_refueling(group, package, flight, dynamic_runways)
|
||||
elif flight_type in [FlightType.CAS, FlightType.BAI]:
|
||||
self.configure_cas(group, package, flight, dynamic_runways)
|
||||
elif flight_type == FlightType.DEAD:
|
||||
@@ -1180,7 +1581,7 @@ class AircraftConflictGenerator:
|
||||
# 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.dcs_unit_type is AJS37 and flight.client_count:
|
||||
if flight.unit_type is AJS37 and flight.client_count:
|
||||
viggen_target_points = [
|
||||
(idx, point)
|
||||
for idx, point in enumerate(filtered_points)
|
||||
@@ -1201,7 +1602,7 @@ class AircraftConflictGenerator:
|
||||
|
||||
for idx, point in enumerate(filtered_points):
|
||||
PydcsWaypointBuilder.for_waypoint(
|
||||
point, group, package, flight, self.m, self.air_support
|
||||
point, group, package, flight, self.m
|
||||
).build()
|
||||
|
||||
# Set here rather than when the FlightData is created so they waypoints
|
||||
@@ -1275,14 +1676,12 @@ class PydcsWaypointBuilder:
|
||||
package: Package,
|
||||
flight: Flight,
|
||||
mission: Mission,
|
||||
air_support: AirSupport,
|
||||
) -> None:
|
||||
self.waypoint = waypoint
|
||||
self.group = group
|
||||
self.package = package
|
||||
self.flight = flight
|
||||
self.mission = mission
|
||||
self.air_support = air_support
|
||||
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = self.group.add_waypoint(
|
||||
@@ -1318,7 +1717,6 @@ class PydcsWaypointBuilder:
|
||||
package: Package,
|
||||
flight: Flight,
|
||||
mission: Mission,
|
||||
air_support: AirSupport,
|
||||
) -> PydcsWaypointBuilder:
|
||||
builders = {
|
||||
FlightWaypointType.DROP_OFF: CargoStopBuilder,
|
||||
@@ -1338,16 +1736,15 @@ class PydcsWaypointBuilder:
|
||||
FlightWaypointType.PICKUP: CargoStopBuilder,
|
||||
}
|
||||
builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder)
|
||||
return builder(waypoint, group, package, flight, mission, air_support)
|
||||
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.dcs_unit_type == AJS37
|
||||
) and (self.waypoint.waypoint_type not in 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
|
||||
@@ -1722,8 +2119,6 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
|
||||
# is their first priority and they will not engage any targets because
|
||||
# they're fully focused on orbiting. If the STE task is first, they will
|
||||
# engage targets if available and orbit if they find nothing to shoot.
|
||||
if self.flight.flight_type is FlightType.REFUELING:
|
||||
self.configure_refueling_actions(waypoint)
|
||||
|
||||
# TODO: Move the properties of this task into the flight plan?
|
||||
# CAP is the only current user of this so it's not a big deal, but might
|
||||
@@ -1738,48 +2133,17 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
|
||||
)
|
||||
)
|
||||
|
||||
# TODO: Set orbit speeds for all race tracks and remove this special case.
|
||||
if isinstance(flight_plan, RefuelingFlightPlan):
|
||||
orbit = OrbitAction(
|
||||
altitude=waypoint.alt,
|
||||
pattern=OrbitAction.OrbitPattern.RaceTrack,
|
||||
speed=int(flight_plan.patrol_speed.kph),
|
||||
)
|
||||
else:
|
||||
orbit = OrbitAction(
|
||||
racetrack = ControlledTask(
|
||||
OrbitAction(
|
||||
altitude=waypoint.alt, pattern=OrbitAction.OrbitPattern.RaceTrack
|
||||
)
|
||||
|
||||
racetrack = ControlledTask(orbit)
|
||||
)
|
||||
self.set_waypoint_tot(waypoint, flight_plan.patrol_start_time)
|
||||
racetrack.stop_after_time(int(flight_plan.patrol_end_time.total_seconds()))
|
||||
waypoint.add_task(racetrack)
|
||||
|
||||
return waypoint
|
||||
|
||||
def configure_refueling_actions(self, waypoint: MovingPoint) -> None:
|
||||
waypoint.add_task(Tanker())
|
||||
|
||||
if self.flight.unit_type.dcs_unit_type.tacan:
|
||||
tanker_info = self.air_support.tankers[-1]
|
||||
tacan = tanker_info.tacan
|
||||
tacan_callsign = {
|
||||
"Texaco": "TEX",
|
||||
"Arco": "ARC",
|
||||
"Shell": "SHL",
|
||||
}.get(tanker_info.callsign)
|
||||
|
||||
waypoint.add_task(
|
||||
ActivateBeaconCommand(
|
||||
tacan.number,
|
||||
tacan.band.value,
|
||||
tacan_callsign,
|
||||
bearing=True,
|
||||
unit_id=self.group.units[0].id,
|
||||
aa=True,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class RaceTrackEndBuilder(PydcsWaypointBuilder):
|
||||
def build(self) -> MovingPoint:
|
||||
|
||||
@@ -966,96 +966,7 @@ AIRFIELD_DATA = {
|
||||
runway_length=8871,
|
||||
atc=AtcData(MHz(3, 775), MHz(38, 450), MHz(120, 100), MHz(250, 50)),
|
||||
ils={
|
||||
"28": ("IGNP", MHz(109, 100)),
|
||||
},
|
||||
),
|
||||
"Gecitkale": AirfieldData(
|
||||
theater="Syria",
|
||||
icao="LCGK",
|
||||
elevation=147,
|
||||
runway_length=8156,
|
||||
vor=("GKE", MHz(114, 300)),
|
||||
atc=AtcData(MHz(3, 775), MHz(4, 800), MHz(40, 500), MHz(252, 50)),
|
||||
),
|
||||
"Kingsfield": AirfieldData(
|
||||
theater="Syria",
|
||||
icao="CY-0004",
|
||||
elevation=270,
|
||||
runway_length=3069,
|
||||
atc=AtcData(MHz(4, 650), MHz(40, 200), MHz(121), MHz(251, 750)),
|
||||
),
|
||||
"Larnaca": AirfieldData(
|
||||
theater="Syria",
|
||||
icao="LCRE",
|
||||
elevation=16,
|
||||
runway_length=8009,
|
||||
vor=("LCA", MHz(112, 80)),
|
||||
atc=AtcData(MHz(4, 700), MHz(40, 300), MHz(121, 200), MHz(251, 850)),
|
||||
ils={
|
||||
"22": ("ILC", MHz(110, 300)),
|
||||
},
|
||||
),
|
||||
"Ercan": AirfieldData(
|
||||
theater="Syria",
|
||||
icao="LCEN",
|
||||
elevation=383,
|
||||
runway_length=7559,
|
||||
vor=("ECN", MHz(117)),
|
||||
atc=AtcData(MHz(4, 750), MHz(40, 400), MHz(120, 200), MHz(251, 950)),
|
||||
),
|
||||
"Lakatamia": AirfieldData(
|
||||
theater="Syria",
|
||||
icao="CY-0001",
|
||||
elevation=757,
|
||||
runway_length=1230,
|
||||
atc=AtcData(MHz(4, 725), MHz(40, 350), MHz(120, 200), MHz(251, 900)),
|
||||
),
|
||||
"Nicosia": AirfieldData(
|
||||
theater="Syria",
|
||||
icao="LCNC",
|
||||
elevation=716,
|
||||
runway_length=0,
|
||||
),
|
||||
"Pinarbashi": AirfieldData(
|
||||
theater="Syria",
|
||||
icao="CY-0003",
|
||||
elevation=770,
|
||||
runway_length=3364,
|
||||
atc=AtcData(MHz(4, 825), MHz(40, 550), MHz(121), MHz(252, 100)),
|
||||
),
|
||||
"Akrotiri": AirfieldData(
|
||||
theater="Syria",
|
||||
icao="LCRA",
|
||||
elevation=62,
|
||||
runway_length=8276,
|
||||
tacan=TacanChannel(107, TacanBand.X),
|
||||
tacan_callsign="AKR",
|
||||
vor=("AKR", MHz(116)),
|
||||
atc=AtcData(MHz(4, 625), MHz(40, 150), MHz(128), MHz(251, 700)),
|
||||
ils={
|
||||
"28": ("IAK", MHz(109, 700)),
|
||||
},
|
||||
),
|
||||
"Paphos": AirfieldData(
|
||||
theater="Syria",
|
||||
icao="LCPH",
|
||||
elevation=40,
|
||||
runway_length=8425,
|
||||
vor=("PHA", MHz(117, 900)),
|
||||
atc=AtcData(MHz(4, 675), MHz(40, 250), MHz(119, 900), MHz(251, 800)),
|
||||
ils={
|
||||
"29": ("IPA", MHz(108, 900)),
|
||||
},
|
||||
),
|
||||
"Gazipasa": AirfieldData(
|
||||
theater="Syria",
|
||||
icao="LTFG",
|
||||
elevation=36,
|
||||
runway_length=6885,
|
||||
vor=("GZP", MHz(114, 200)),
|
||||
atc=AtcData(MHz(4, 600), MHz(40, 100), MHz(119, 250), MHz(251, 650)),
|
||||
ils={
|
||||
"8": ("IGZP", MHz(108, 500)),
|
||||
"28": ("IGNP", MHz(109, 10)),
|
||||
},
|
||||
),
|
||||
# NTTR
|
||||
|
||||
@@ -16,7 +16,6 @@ from dcs.task import (
|
||||
)
|
||||
|
||||
from game import db
|
||||
from .flights.ai_flight_planner_db import AEWC_CAPABLE
|
||||
from .naming import namegen
|
||||
from .callsigns import callsign_for_support_unit
|
||||
from .conflictgen import Conflict
|
||||
@@ -54,8 +53,6 @@ class TankerInfo:
|
||||
variant: str
|
||||
freq: RadioFrequency
|
||||
tacan: TacanChannel
|
||||
start_time: Optional[timedelta]
|
||||
end_time: Optional[timedelta]
|
||||
blue: bool
|
||||
|
||||
|
||||
@@ -102,126 +99,120 @@ class AirSupportConflictGenerator:
|
||||
else self.conflict.red_cp
|
||||
)
|
||||
|
||||
if not self.game.settings.disable_legacy_tanker:
|
||||
fallback_tanker_number = 0
|
||||
fallback_tanker_number = 0
|
||||
|
||||
for i, tanker_unit_type in enumerate(
|
||||
self.game.faction_for(player=True).tankers
|
||||
):
|
||||
# TODO: Make loiter altitude a property of the unit type.
|
||||
alt, airspeed = self._get_tanker_params(tanker_unit_type.dcs_unit_type)
|
||||
freq = self.radio_registry.alloc_uhf()
|
||||
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y)
|
||||
tanker_heading = (
|
||||
self.conflict.red_cp.position.heading_between_point(
|
||||
self.conflict.blue_cp.position
|
||||
)
|
||||
+ TANKER_HEADING_OFFSET * i
|
||||
)
|
||||
tanker_position = player_cp.position.point_from_heading(
|
||||
tanker_heading, TANKER_DISTANCE
|
||||
)
|
||||
tanker_group = self.mission.refuel_flight(
|
||||
country=self.mission.country(self.game.player_country),
|
||||
name=namegen.next_tanker_name(
|
||||
self.mission.country(self.game.player_country), tanker_unit_type
|
||||
),
|
||||
airport=None,
|
||||
plane_type=tanker_unit_type,
|
||||
position=tanker_position,
|
||||
altitude=alt,
|
||||
race_distance=58000,
|
||||
frequency=freq.mhz,
|
||||
start_type=StartType.Warm,
|
||||
speed=airspeed,
|
||||
tacanchannel=str(tacan),
|
||||
)
|
||||
tanker_group.set_frequency(freq.mhz)
|
||||
|
||||
callsign = callsign_for_support_unit(tanker_group)
|
||||
tacan_callsign = {
|
||||
"Texaco": "TEX",
|
||||
"Arco": "ARC",
|
||||
"Shell": "SHL",
|
||||
}.get(callsign)
|
||||
if tacan_callsign is None:
|
||||
# The dict above is all the callsigns currently in the game, but
|
||||
# non-Western countries don't use the callsigns and instead just
|
||||
# use numbers. It's possible that none of those nations have
|
||||
# TACAN compatible refueling aircraft, but fallback just in
|
||||
# case.
|
||||
tacan_callsign = f"TK{fallback_tanker_number}"
|
||||
fallback_tanker_number += 1
|
||||
|
||||
if tanker_unit_type != IL_78M:
|
||||
# Override PyDCS tacan channel.
|
||||
tanker_group.points[0].tasks.pop()
|
||||
tanker_group.points[0].tasks.append(
|
||||
ActivateBeaconCommand(
|
||||
tacan.number,
|
||||
tacan.band.value,
|
||||
tacan_callsign,
|
||||
True,
|
||||
tanker_group.units[0].id,
|
||||
True,
|
||||
)
|
||||
)
|
||||
|
||||
tanker_group.points[0].tasks.append(SetInvisibleCommand(True))
|
||||
tanker_group.points[0].tasks.append(SetImmortalCommand(True))
|
||||
|
||||
self.air_support.tankers.append(
|
||||
TankerInfo(
|
||||
str(tanker_group.name),
|
||||
callsign,
|
||||
tanker_unit_type.name,
|
||||
freq,
|
||||
tacan,
|
||||
blue=True,
|
||||
)
|
||||
)
|
||||
|
||||
if not self.game.settings.disable_legacy_aewc:
|
||||
possible_awacs = [
|
||||
a
|
||||
for a in self.game.faction_for(player=True).aircrafts
|
||||
if a in AEWC_CAPABLE
|
||||
]
|
||||
|
||||
if not possible_awacs:
|
||||
logging.warning("No AWACS for faction")
|
||||
return
|
||||
|
||||
awacs_unit = possible_awacs[0]
|
||||
for i, tanker_unit_type in enumerate(
|
||||
db.find_unittype(Refueling, self.conflict.attackers_side)
|
||||
):
|
||||
alt, airspeed = self._get_tanker_params(tanker_unit_type)
|
||||
variant = db.unit_type_name(tanker_unit_type)
|
||||
freq = self.radio_registry.alloc_uhf()
|
||||
|
||||
awacs_flight = self.mission.awacs_flight(
|
||||
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y)
|
||||
tanker_heading = (
|
||||
self.conflict.red_cp.position.heading_between_point(
|
||||
self.conflict.blue_cp.position
|
||||
)
|
||||
+ TANKER_HEADING_OFFSET * i
|
||||
)
|
||||
tanker_position = player_cp.position.point_from_heading(
|
||||
tanker_heading, TANKER_DISTANCE
|
||||
)
|
||||
tanker_group = self.mission.refuel_flight(
|
||||
country=self.mission.country(self.game.player_country),
|
||||
name=namegen.next_awacs_name(
|
||||
self.mission.country(self.game.player_country)
|
||||
name=namegen.next_tanker_name(
|
||||
self.mission.country(self.game.player_country), tanker_unit_type
|
||||
),
|
||||
plane_type=awacs_unit,
|
||||
altitude=AWACS_ALT,
|
||||
airport=None,
|
||||
position=self.conflict.position.random_point_within(
|
||||
AWACS_DISTANCE, AWACS_DISTANCE
|
||||
),
|
||||
plane_type=tanker_unit_type,
|
||||
position=tanker_position,
|
||||
altitude=alt,
|
||||
race_distance=58000,
|
||||
frequency=freq.mhz,
|
||||
start_type=StartType.Warm,
|
||||
speed=airspeed,
|
||||
tacanchannel=str(tacan),
|
||||
)
|
||||
awacs_flight.set_frequency(freq.mhz)
|
||||
tanker_group.set_frequency(freq.mhz)
|
||||
|
||||
awacs_flight.points[0].tasks.append(SetInvisibleCommand(True))
|
||||
awacs_flight.points[0].tasks.append(SetImmortalCommand(True))
|
||||
callsign = callsign_for_support_unit(tanker_group)
|
||||
tacan_callsign = {
|
||||
"Texaco": "TEX",
|
||||
"Arco": "ARC",
|
||||
"Shell": "SHL",
|
||||
}.get(callsign)
|
||||
if tacan_callsign is None:
|
||||
# The dict above is all the callsigns currently in the game, but
|
||||
# non-Western countries don't use the callsigns and instead just
|
||||
# use numbers. It's possible that none of those nations have
|
||||
# TACAN compatible refueling aircraft, but fallback just in
|
||||
# case.
|
||||
tacan_callsign = f"TK{fallback_tanker_number}"
|
||||
fallback_tanker_number += 1
|
||||
|
||||
self.air_support.awacs.append(
|
||||
AwacsInfo(
|
||||
group_name=str(awacs_flight.name),
|
||||
callsign=callsign_for_support_unit(awacs_flight),
|
||||
freq=freq,
|
||||
depature_location=None,
|
||||
start_time=None,
|
||||
end_time=None,
|
||||
if tanker_unit_type != IL_78M:
|
||||
# Override PyDCS tacan channel.
|
||||
tanker_group.points[0].tasks.pop()
|
||||
tanker_group.points[0].tasks.append(
|
||||
ActivateBeaconCommand(
|
||||
tacan.number,
|
||||
tacan.band.value,
|
||||
tacan_callsign,
|
||||
True,
|
||||
tanker_group.units[0].id,
|
||||
True,
|
||||
)
|
||||
)
|
||||
|
||||
tanker_group.points[0].tasks.append(SetInvisibleCommand(True))
|
||||
tanker_group.points[0].tasks.append(SetImmortalCommand(True))
|
||||
|
||||
self.air_support.tankers.append(
|
||||
TankerInfo(
|
||||
str(tanker_group.name),
|
||||
callsign,
|
||||
variant,
|
||||
freq,
|
||||
tacan,
|
||||
blue=True,
|
||||
)
|
||||
)
|
||||
|
||||
if not self.game.settings.disable_legacy_aewc:
|
||||
possible_awacs = db.find_unittype(AWACS, self.conflict.attackers_side)
|
||||
|
||||
if len(possible_awacs) > 0:
|
||||
awacs_unit = possible_awacs[0]
|
||||
freq = self.radio_registry.alloc_uhf()
|
||||
|
||||
awacs_flight = self.mission.awacs_flight(
|
||||
country=self.mission.country(self.game.player_country),
|
||||
name=namegen.next_awacs_name(
|
||||
self.mission.country(self.game.player_country)
|
||||
),
|
||||
plane_type=awacs_unit,
|
||||
altitude=AWACS_ALT,
|
||||
airport=None,
|
||||
position=self.conflict.position.random_point_within(
|
||||
AWACS_DISTANCE, AWACS_DISTANCE
|
||||
),
|
||||
frequency=freq.mhz,
|
||||
start_type=StartType.Warm,
|
||||
)
|
||||
awacs_flight.set_frequency(freq.mhz)
|
||||
|
||||
awacs_flight.points[0].tasks.append(SetInvisibleCommand(True))
|
||||
awacs_flight.points[0].tasks.append(SetImmortalCommand(True))
|
||||
|
||||
self.air_support.awacs.append(
|
||||
AwacsInfo(
|
||||
group_name=str(awacs_flight.name),
|
||||
callsign=callsign_for_support_unit(awacs_flight),
|
||||
freq=freq,
|
||||
depature_location=None,
|
||||
start_time=None,
|
||||
end_time=None,
|
||||
blue=True,
|
||||
)
|
||||
)
|
||||
else:
|
||||
logging.warning("No AWACS for faction")
|
||||
|
||||
87
gen/armor.py
87
gen/armor.py
@@ -10,6 +10,7 @@ from dcs.action import AITaskPush
|
||||
from dcs.condition import GroupLifeLess, Or, TimeAfter, UnitDamaged
|
||||
from dcs.country import Country
|
||||
from dcs.mapping import Point
|
||||
from dcs.planes import MQ_9_Reaper
|
||||
from dcs.point import PointAction
|
||||
from dcs.task import (
|
||||
EPLRS,
|
||||
@@ -25,18 +26,18 @@ from dcs.task import (
|
||||
from dcs.triggers import Event, TriggerOnce
|
||||
from dcs.unit import Vehicle
|
||||
from dcs.unitgroup import VehicleGroup
|
||||
|
||||
from game.data.groundunitclass import GroundUnitClass
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.theater.controlpoint import ControlPoint
|
||||
from dcs.unittype import VehicleType
|
||||
from game import db
|
||||
from game.unitmap import UnitMap
|
||||
from game.utils import heading_sum, opposite_heading
|
||||
from game.theater.controlpoint import ControlPoint
|
||||
|
||||
from gen.ground_forces.ai_ground_planner import (
|
||||
DISTANCE_FROM_FRONTLINE,
|
||||
CombatGroup,
|
||||
CombatGroupRole,
|
||||
)
|
||||
|
||||
from .callsigns import callsign_for_support_unit
|
||||
from .conflictgen import Conflict
|
||||
from .ground_forces.combat_stance import CombatStance
|
||||
@@ -173,14 +174,14 @@ class GroundConflictGenerator:
|
||||
n = "JTAC" + str(self.conflict.blue_cp.id) + str(self.conflict.red_cp.id)
|
||||
code = 1688 - len(self.jtacs)
|
||||
|
||||
utype = self.game.player_faction.jtac_unit
|
||||
if self.game.player_faction.jtac_unit is None:
|
||||
utype = AircraftType.named("MQ-9 Reaper")
|
||||
utype = MQ_9_Reaper
|
||||
if self.game.player_faction.jtac_unit is not None:
|
||||
utype = self.game.player_faction.jtac_unit
|
||||
|
||||
jtac = self.mission.flight_group(
|
||||
country=self.mission.country(self.game.player_country),
|
||||
name=n,
|
||||
aircraft_type=utype.dcs_unit_type,
|
||||
aircraft_type=utype,
|
||||
position=position[0],
|
||||
airport=None,
|
||||
altitude=5000,
|
||||
@@ -224,22 +225,23 @@ class GroundConflictGenerator:
|
||||
else:
|
||||
cp = self.conflict.red_cp
|
||||
|
||||
faction = self.game.faction_for(is_player)
|
||||
if is_player:
|
||||
faction = self.game.player_name
|
||||
else:
|
||||
faction = self.game.enemy_name
|
||||
|
||||
# Disable infantry unit gen if disabled
|
||||
if not self.game.settings.perf_infantry:
|
||||
if self.game.settings.manpads:
|
||||
# 50% of armored units protected by manpad
|
||||
if random.choice([True, False]):
|
||||
manpads = list(faction.infantry_with_class(GroundUnitClass.Manpads))
|
||||
if manpads:
|
||||
u = random.choices(
|
||||
manpads, weights=[m.spawn_weight for m in manpads]
|
||||
)[0]
|
||||
manpads = db.find_manpad(faction)
|
||||
if len(manpads) > 0:
|
||||
u = random.choice(manpads)
|
||||
self.mission.vehicle_group(
|
||||
side,
|
||||
namegen.next_infantry_name(side, cp.id, u),
|
||||
u.dcs_unit_type,
|
||||
u,
|
||||
position=infantry_position,
|
||||
group_size=1,
|
||||
heading=forward_heading,
|
||||
@@ -247,38 +249,30 @@ class GroundConflictGenerator:
|
||||
)
|
||||
return
|
||||
|
||||
possible_infantry_units = set(
|
||||
faction.infantry_with_class(GroundUnitClass.Infantry)
|
||||
possible_infantry_units = db.find_infantry(
|
||||
faction, allow_manpad=self.game.settings.manpads
|
||||
)
|
||||
if self.game.settings.manpads:
|
||||
possible_infantry_units |= set(
|
||||
faction.infantry_with_class(GroundUnitClass.Manpads)
|
||||
)
|
||||
if not possible_infantry_units:
|
||||
if len(possible_infantry_units) == 0:
|
||||
return
|
||||
|
||||
infantry_choices = list(possible_infantry_units)
|
||||
units = random.choices(
|
||||
infantry_choices,
|
||||
weights=[u.spawn_weight for u in infantry_choices],
|
||||
k=INFANTRY_GROUP_SIZE,
|
||||
)
|
||||
u = random.choice(possible_infantry_units)
|
||||
self.mission.vehicle_group(
|
||||
side,
|
||||
namegen.next_infantry_name(side, cp.id, units[0]),
|
||||
units[0].dcs_unit_type,
|
||||
namegen.next_infantry_name(side, cp.id, u),
|
||||
u,
|
||||
position=infantry_position,
|
||||
group_size=1,
|
||||
heading=forward_heading,
|
||||
move_formation=PointAction.OffRoad,
|
||||
)
|
||||
|
||||
for unit in units[1:]:
|
||||
for i in range(INFANTRY_GROUP_SIZE):
|
||||
u = random.choice(possible_infantry_units)
|
||||
position = infantry_position.random_point_within(55, 5)
|
||||
self.mission.vehicle_group(
|
||||
side,
|
||||
namegen.next_infantry_name(side, cp.id, unit),
|
||||
unit.dcs_unit_type,
|
||||
namegen.next_infantry_name(side, cp.id, u),
|
||||
u,
|
||||
position=position,
|
||||
group_size=1,
|
||||
heading=forward_heading,
|
||||
@@ -318,7 +312,7 @@ class GroundConflictGenerator:
|
||||
)
|
||||
artillery_trigger.add_condition(TimeAfter(seconds=random.randint(1, 45) * 60))
|
||||
# TODO: Update to fire at group instead of point
|
||||
fire_task = FireAtPoint(target, gen_group.size * 10, 100)
|
||||
fire_task = FireAtPoint(target, len(gen_group.units) * 10, 100)
|
||||
fire_task.number = 2 if stance != CombatStance.RETREAT else 1
|
||||
dcs_group.add_trigger_action(fire_task)
|
||||
artillery_trigger.add_action(AITaskPush(dcs_group.id, len(dcs_group.tasks)))
|
||||
@@ -508,7 +502,7 @@ class GroundConflictGenerator:
|
||||
return
|
||||
|
||||
for dcs_group, group in ally_groups:
|
||||
if group.unit_type.eplrs_capable:
|
||||
if hasattr(group.units[0], "eplrs") and group.units[0].eplrs:
|
||||
dcs_group.points[0].tasks.append(EPLRS(dcs_group.id))
|
||||
|
||||
if group.role == CombatGroupRole.ARTILLERY:
|
||||
@@ -679,7 +673,7 @@ class GroundConflictGenerator:
|
||||
Search the enemy groups for a potential target suitable to an artillery unit
|
||||
"""
|
||||
# TODO: Update to return a list of groups instead of a single point
|
||||
rng = getattr(group.unit_type.dcs_unit_type, "threat_range", 0)
|
||||
rng = group.units[0].threat_range
|
||||
if not enemy_groups:
|
||||
return None
|
||||
for _ in range(10):
|
||||
@@ -696,7 +690,7 @@ class GroundConflictGenerator:
|
||||
"""
|
||||
For artilery group, decide the distance from frontline with the range of the unit
|
||||
"""
|
||||
rg = getattr(group.unit_type.dcs_unit_type, "threat_range", 0) - 7500
|
||||
rg = group.units[0].threat_range - 7500
|
||||
if rg > DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]:
|
||||
rg = random.randint(
|
||||
DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][0],
|
||||
@@ -729,7 +723,7 @@ class GroundConflictGenerator:
|
||||
|
||||
def _generate_groups(
|
||||
self,
|
||||
groups: list[CombatGroup],
|
||||
groups: List[CombatGroup],
|
||||
frontline_vector: Tuple[Point, int, int],
|
||||
is_player: bool,
|
||||
) -> List[Tuple[VehicleGroup, CombatGroup]]:
|
||||
@@ -760,9 +754,10 @@ class GroundConflictGenerator:
|
||||
if final_position is not None:
|
||||
g = self._generate_group(
|
||||
self.mission.country(country),
|
||||
group.unit_type,
|
||||
group.size,
|
||||
group.units[0],
|
||||
len(group.units),
|
||||
final_position,
|
||||
distance_from_frontline,
|
||||
heading=opposite_heading(spawn_heading),
|
||||
)
|
||||
if is_player:
|
||||
@@ -786,9 +781,10 @@ class GroundConflictGenerator:
|
||||
def _generate_group(
|
||||
self,
|
||||
side: Country,
|
||||
unit_type: GroundUnitType,
|
||||
unit: VehicleType,
|
||||
count: int,
|
||||
at: Point,
|
||||
distance_from_frontline,
|
||||
move_formation: PointAction = PointAction.OffRoad,
|
||||
heading=0,
|
||||
) -> VehicleGroup:
|
||||
@@ -798,17 +794,18 @@ class GroundConflictGenerator:
|
||||
else:
|
||||
cp = self.conflict.red_cp
|
||||
|
||||
logging.info("armorgen: {} for {}".format(unit, side.id))
|
||||
group = self.mission.vehicle_group(
|
||||
side,
|
||||
namegen.next_unit_name(side, cp.id, unit_type),
|
||||
unit_type.dcs_unit_type,
|
||||
namegen.next_unit_name(side, cp.id, unit),
|
||||
unit,
|
||||
position=at,
|
||||
group_size=count,
|
||||
heading=heading,
|
||||
move_formation=move_formation,
|
||||
)
|
||||
|
||||
self.unit_map.add_front_line_units(group, cp, unit_type)
|
||||
self.unit_map.add_front_line_units(group, cp)
|
||||
|
||||
for c in range(count):
|
||||
vehicle: Vehicle = group.units[c]
|
||||
|
||||
10
gen/ato.py
10
gen/ato.py
@@ -168,10 +168,9 @@ class Package:
|
||||
# likely to be the main task than others. For example, a package with
|
||||
# only CAP flights is a CAP package, a flight with CAP and strike is a
|
||||
# strike package, a flight with CAP and DEAD is a DEAD package, and a
|
||||
# flight with strike and SEAD is an OCA/Strike package. This list defines the
|
||||
# priority order for package task names. The package's primary task will be the
|
||||
# first task in this list that matches a flight in the package.
|
||||
tasks_by_priority = [
|
||||
# flight with strike and SEAD is an OCA/Strike package. The type of
|
||||
# package is determined by the highest priority flight in the package.
|
||||
task_priorities = [
|
||||
FlightType.CAS,
|
||||
FlightType.STRIKE,
|
||||
FlightType.ANTISHIP,
|
||||
@@ -184,11 +183,10 @@ class Package:
|
||||
FlightType.TARCAP,
|
||||
FlightType.BARCAP,
|
||||
FlightType.AEWC,
|
||||
FlightType.REFUELING,
|
||||
FlightType.SWEEP,
|
||||
FlightType.ESCORT,
|
||||
]
|
||||
for task in tasks_by_priority:
|
||||
for task in task_priorities:
|
||||
if flight_counts[task]:
|
||||
return task
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import itertools
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dcs import Mission
|
||||
from dcs.ships import HandyWind
|
||||
from dcs.ships import Bulker_Handy_Wind
|
||||
from dcs.unitgroup import ShipGroup
|
||||
|
||||
from game.transfers import CargoShip
|
||||
@@ -35,7 +35,7 @@ class CargoShipGenerator:
|
||||
group = self.mission.ship_group(
|
||||
country,
|
||||
ship.name,
|
||||
HandyWind,
|
||||
Bulker_Handy_Wind,
|
||||
position=waypoints[0],
|
||||
group_size=1,
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@ class SilkwormGenerator(GroupGenerator):
|
||||
positions = self.get_circular_position(5, launcher_distance=120, coverage=180)
|
||||
|
||||
self.add_unit(
|
||||
MissilesSS.Silkworm_SR,
|
||||
MissilesSS.AShM_Silkworm_SR,
|
||||
"SR#0",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
@@ -23,7 +23,7 @@ class SilkwormGenerator(GroupGenerator):
|
||||
# Launchers
|
||||
for i, p in enumerate(positions):
|
||||
self.add_unit(
|
||||
MissilesSS.Silkworm_SR,
|
||||
MissilesSS.AShM_SS_N_2_Silkworm,
|
||||
"Missile#" + str(i),
|
||||
p[0],
|
||||
p[1],
|
||||
@@ -32,7 +32,7 @@ class SilkwormGenerator(GroupGenerator):
|
||||
|
||||
# Commander
|
||||
self.add_unit(
|
||||
Unarmed.KAMAZ_Truck,
|
||||
Unarmed.Truck_KAMAZ_43101,
|
||||
"KAMAZ#0",
|
||||
self.position.x - 35,
|
||||
self.position.y - 20,
|
||||
@@ -41,7 +41,7 @@ class SilkwormGenerator(GroupGenerator):
|
||||
|
||||
# Shorad
|
||||
self.add_unit(
|
||||
AirDefence.ZSU_23_4_Shilka,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
|
||||
"SHILKA#0",
|
||||
self.position.x - 55,
|
||||
self.position.y - 38,
|
||||
@@ -50,7 +50,7 @@ class SilkwormGenerator(GroupGenerator):
|
||||
|
||||
# Shorad 2
|
||||
self.add_unit(
|
||||
AirDefence.Strela_1_9P31,
|
||||
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL,
|
||||
"STRELA#0",
|
||||
self.position.x + 200,
|
||||
self.position.y + 15,
|
||||
|
||||
@@ -153,8 +153,6 @@ class Conflict:
|
||||
if theater.is_on_land(pos):
|
||||
return pos
|
||||
pos = initial.point_from_heading(opposite_heading(heading), distance)
|
||||
if theater.is_on_land(pos):
|
||||
return pos
|
||||
if coerce:
|
||||
pos = theater.nearest_land_pos(initial)
|
||||
return pos
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Dict, TYPE_CHECKING, Type
|
||||
|
||||
from dcs import Mission
|
||||
from dcs.mapping import Point
|
||||
from dcs.point import PointAction
|
||||
from dcs.unit import Vehicle
|
||||
from dcs.unitgroup import VehicleGroup
|
||||
from dcs.unittype import VehicleType
|
||||
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.transfers import Convoy
|
||||
from game.unitmap import UnitMap
|
||||
from game.utils import kph
|
||||
@@ -50,7 +50,7 @@ class ConvoyGenerator:
|
||||
self,
|
||||
name: str,
|
||||
position: Point,
|
||||
units: dict[GroundUnitType, int],
|
||||
units: Dict[Type[VehicleType], int],
|
||||
for_player: bool,
|
||||
) -> VehicleGroup:
|
||||
country = self.mission.country(
|
||||
@@ -63,7 +63,7 @@ class ConvoyGenerator:
|
||||
group = self.mission.vehicle_group(
|
||||
country,
|
||||
name,
|
||||
main_unit_type.dcs_unit_type,
|
||||
main_unit_type,
|
||||
position=position,
|
||||
group_size=main_unit_count,
|
||||
move_formation=PointAction.OnRoad,
|
||||
@@ -76,7 +76,7 @@ class ConvoyGenerator:
|
||||
for unit_type, count in unit_types[1:]:
|
||||
for i in range(count):
|
||||
v = self.mission.vehicle(
|
||||
f"{name} Unit #{next(unit_name_counter)}", unit_type.dcs_unit_type
|
||||
f"{name} Unit #{next(unit_name_counter)}", unit_type
|
||||
)
|
||||
v.position.x = position.x
|
||||
v.position.y = next(y)
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import random
|
||||
|
||||
from dcs.unitgroup import VehicleGroup
|
||||
from dcs.vehicles import Armor
|
||||
|
||||
from game import db, Game
|
||||
from game.data.groundunitclass import GroundUnitClass
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.theater.theatergroundobject import VehicleGroupGroundObject
|
||||
from game import db
|
||||
from gen.defenses.armored_group_generator import (
|
||||
ArmoredGroupGenerator,
|
||||
FixedSizeArmorGroupGenerator,
|
||||
@@ -17,14 +14,8 @@ def generate_armor_group(faction: str, game, ground_object):
|
||||
This generate a group of ground units
|
||||
:return: Generated group
|
||||
"""
|
||||
armor_types = (
|
||||
GroundUnitClass.Apc,
|
||||
GroundUnitClass.Atgm,
|
||||
GroundUnitClass.Ifv,
|
||||
GroundUnitClass.Tank,
|
||||
)
|
||||
possible_unit = [
|
||||
u for u in db.FACTIONS[faction].frontline_units if u.unit_class in armor_types
|
||||
u for u in db.FACTIONS[faction].frontline_units if u in Armor.__dict__.values()
|
||||
]
|
||||
if len(possible_unit) > 0:
|
||||
unit_type = random.choice(possible_unit)
|
||||
@@ -32,9 +23,7 @@ def generate_armor_group(faction: str, game, ground_object):
|
||||
return None
|
||||
|
||||
|
||||
def generate_armor_group_of_type(
|
||||
game: Game, ground_object: VehicleGroupGroundObject, unit_type: GroundUnitType
|
||||
) -> VehicleGroup:
|
||||
def generate_armor_group_of_type(game, ground_object, unit_type):
|
||||
"""
|
||||
This generate a group of ground units of given type
|
||||
:return: Generated group
|
||||
@@ -44,12 +33,7 @@ def generate_armor_group_of_type(
|
||||
return generator.get_generated_group()
|
||||
|
||||
|
||||
def generate_armor_group_of_type_and_size(
|
||||
game: Game,
|
||||
ground_object: VehicleGroupGroundObject,
|
||||
unit_type: GroundUnitType,
|
||||
size: int,
|
||||
) -> VehicleGroup:
|
||||
def generate_armor_group_of_type_and_size(game, ground_object, unit_type, size: int):
|
||||
"""
|
||||
This generate a group of ground units of given type and size
|
||||
:return: Generated group
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
import random
|
||||
|
||||
from game import Game
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.theater.theatergroundobject import VehicleGroupGroundObject
|
||||
from gen.sam.group_generator import GroupGenerator
|
||||
|
||||
|
||||
class ArmoredGroupGenerator(GroupGenerator):
|
||||
def __init__(
|
||||
self,
|
||||
game: Game,
|
||||
ground_object: VehicleGroupGroundObject,
|
||||
unit_type: GroundUnitType,
|
||||
) -> None:
|
||||
super().__init__(game, ground_object)
|
||||
def __init__(self, game, ground_object, unit_type):
|
||||
super(ArmoredGroupGenerator, self).__init__(game, ground_object)
|
||||
self.unit_type = unit_type
|
||||
|
||||
def generate(self) -> None:
|
||||
def generate(self):
|
||||
|
||||
grid_x = random.randint(2, 3)
|
||||
grid_y = random.randint(1, 2)
|
||||
|
||||
@@ -27,7 +20,7 @@ class ArmoredGroupGenerator(GroupGenerator):
|
||||
for j in range(grid_y):
|
||||
index = index + 1
|
||||
self.add_unit(
|
||||
self.unit_type.dcs_unit_type,
|
||||
self.unit_type,
|
||||
"Armor#" + str(index),
|
||||
self.position.x + spacing * i,
|
||||
self.position.y + spacing * j,
|
||||
@@ -36,14 +29,8 @@ class ArmoredGroupGenerator(GroupGenerator):
|
||||
|
||||
|
||||
class FixedSizeArmorGroupGenerator(GroupGenerator):
|
||||
def __init__(
|
||||
self,
|
||||
game: Game,
|
||||
ground_object: VehicleGroupGroundObject,
|
||||
unit_type: GroundUnitType,
|
||||
size: int,
|
||||
) -> None:
|
||||
super().__init__(game, ground_object)
|
||||
def __init__(self, game, ground_object, unit_type, size):
|
||||
super(FixedSizeArmorGroupGenerator, self).__init__(game, ground_object)
|
||||
self.unit_type = unit_type
|
||||
self.size = size
|
||||
|
||||
@@ -54,7 +41,7 @@ class FixedSizeArmorGroupGenerator(GroupGenerator):
|
||||
for i in range(self.size):
|
||||
index = index + 1
|
||||
self.add_unit(
|
||||
self.unit_type.dcs_unit_type,
|
||||
self.unit_type,
|
||||
"Armor#" + str(index),
|
||||
self.position.x + spacing * i,
|
||||
self.position.y,
|
||||
|
||||
@@ -2,7 +2,7 @@ import random
|
||||
|
||||
from gen.sam.group_generator import ShipGroupGenerator
|
||||
|
||||
from dcs.ships import USS_Arleigh_Burke_IIa, TICONDEROG
|
||||
from dcs.ships import DDG_Arleigh_Burke_IIa, CG_Ticonderoga
|
||||
|
||||
|
||||
class CarrierGroupGenerator(ShipGroupGenerator):
|
||||
@@ -22,7 +22,7 @@ class CarrierGroupGenerator(ShipGroupGenerator):
|
||||
|
||||
# Add Arleigh Burke escort
|
||||
self.add_unit(
|
||||
USS_Arleigh_Burke_IIa,
|
||||
DDG_Arleigh_Burke_IIa,
|
||||
"USS Ramage",
|
||||
self.position.x + 6482,
|
||||
self.position.y + 6667,
|
||||
@@ -30,7 +30,7 @@ class CarrierGroupGenerator(ShipGroupGenerator):
|
||||
)
|
||||
|
||||
self.add_unit(
|
||||
USS_Arleigh_Burke_IIa,
|
||||
DDG_Arleigh_Burke_IIa,
|
||||
"USS Mitscher",
|
||||
self.position.x - 7963,
|
||||
self.position.y + 7037,
|
||||
@@ -38,7 +38,7 @@ class CarrierGroupGenerator(ShipGroupGenerator):
|
||||
)
|
||||
|
||||
self.add_unit(
|
||||
USS_Arleigh_Burke_IIa,
|
||||
DDG_Arleigh_Burke_IIa,
|
||||
"USS Forrest Sherman",
|
||||
self.position.x - 7408,
|
||||
self.position.y - 7408,
|
||||
@@ -46,7 +46,7 @@ class CarrierGroupGenerator(ShipGroupGenerator):
|
||||
)
|
||||
|
||||
self.add_unit(
|
||||
USS_Arleigh_Burke_IIa,
|
||||
DDG_Arleigh_Burke_IIa,
|
||||
"USS Lassen",
|
||||
self.position.x + 8704,
|
||||
self.position.y - 6296,
|
||||
@@ -56,7 +56,7 @@ class CarrierGroupGenerator(ShipGroupGenerator):
|
||||
# Add Ticonderoga escort
|
||||
if self.heading >= 180:
|
||||
self.add_unit(
|
||||
TICONDEROG,
|
||||
CG_Ticonderoga,
|
||||
"USS Hué City",
|
||||
self.position.x + 2222,
|
||||
self.position.y - 3333,
|
||||
@@ -64,7 +64,7 @@ class CarrierGroupGenerator(ShipGroupGenerator):
|
||||
)
|
||||
else:
|
||||
self.add_unit(
|
||||
TICONDEROG,
|
||||
CG_Ticonderoga,
|
||||
"USS Hué City",
|
||||
self.position.x - 3333,
|
||||
self.position.y + 2222,
|
||||
|
||||
@@ -5,9 +5,9 @@ from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
from dcs.ships import (
|
||||
Type_052C,
|
||||
Type_052B,
|
||||
Type_054A,
|
||||
Type_052C_Destroyer,
|
||||
Type_052B_Destroyer,
|
||||
Type_054A_Frigate,
|
||||
)
|
||||
|
||||
from game.factions.faction import Faction
|
||||
@@ -30,14 +30,14 @@ class ChineseNavyGroupGenerator(ShipGroupGenerator):
|
||||
|
||||
if include_frigate:
|
||||
self.add_unit(
|
||||
Type_054A,
|
||||
Type_054A_Frigate,
|
||||
"FF1",
|
||||
self.position.x + 1200,
|
||||
self.position.y + 900,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
Type_054A,
|
||||
Type_054A_Frigate,
|
||||
"FF2",
|
||||
self.position.x + 1200,
|
||||
self.position.y - 900,
|
||||
@@ -45,7 +45,7 @@ class ChineseNavyGroupGenerator(ShipGroupGenerator):
|
||||
)
|
||||
|
||||
if include_dd:
|
||||
dd_type = random.choice([Type_052C, Type_052B])
|
||||
dd_type = random.choice([Type_052C_Destroyer, Type_052B_Destroyer])
|
||||
self.add_unit(
|
||||
dd_type,
|
||||
"DD1",
|
||||
@@ -69,5 +69,5 @@ class Type54GroupGenerator(DDGroupGenerator):
|
||||
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
||||
):
|
||||
super(Type54GroupGenerator, self).__init__(
|
||||
game, ground_object, faction, Type_054A
|
||||
game, ground_object, faction, Type_054A_Frigate
|
||||
)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Type
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from game.factions.faction import Faction
|
||||
from game.theater.theatergroundobject import TheaterGroundObject
|
||||
|
||||
from gen.sam.group_generator import ShipGroupGenerator
|
||||
from dcs.unittype import ShipType
|
||||
from dcs.ships import PERRY, USS_Arleigh_Burke_IIa
|
||||
from dcs.ships import FFG_Oliver_Hazzard_Perry, DDG_Arleigh_Burke_IIa
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.game import Game
|
||||
@@ -18,7 +18,7 @@ class DDGroupGenerator(ShipGroupGenerator):
|
||||
game: Game,
|
||||
ground_object: TheaterGroundObject,
|
||||
faction: Faction,
|
||||
ddtype: Type[ShipType],
|
||||
ddtype: ShipType,
|
||||
):
|
||||
super(DDGroupGenerator, self).__init__(game, ground_object, faction)
|
||||
self.ddtype = ddtype
|
||||
@@ -46,7 +46,7 @@ class OliverHazardPerryGroupGenerator(DDGroupGenerator):
|
||||
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
||||
):
|
||||
super(OliverHazardPerryGroupGenerator, self).__init__(
|
||||
game, ground_object, faction, PERRY
|
||||
game, ground_object, faction, FFG_Oliver_Hazzard_Perry
|
||||
)
|
||||
|
||||
|
||||
@@ -55,5 +55,5 @@ class ArleighBurkeGroupGenerator(DDGroupGenerator):
|
||||
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
||||
):
|
||||
super(ArleighBurkeGroupGenerator, self).__init__(
|
||||
game, ground_object, faction, USS_Arleigh_Burke_IIa
|
||||
game, ground_object, faction, DDG_Arleigh_Burke_IIa
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from dcs.ships import La_Combattante_II
|
||||
from dcs.ships import FAC_La_Combattante_IIa
|
||||
|
||||
from game.factions.faction import Faction
|
||||
from game.theater import TheaterGroundObject
|
||||
@@ -8,5 +8,5 @@ from gen.fleet.dd_group import DDGroupGenerator
|
||||
class LaCombattanteIIGroupGenerator(DDGroupGenerator):
|
||||
def __init__(self, game, ground_object: TheaterGroundObject, faction: Faction):
|
||||
super(LaCombattanteIIGroupGenerator, self).__init__(
|
||||
game, ground_object, faction, La_Combattante_II
|
||||
game, ground_object, faction, FAC_La_Combattante_IIa
|
||||
)
|
||||
|
||||
@@ -3,13 +3,13 @@ import random
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dcs.ships import (
|
||||
ALBATROS,
|
||||
MOLNIYA,
|
||||
NEUSTRASH,
|
||||
REZKY,
|
||||
MOSCOW,
|
||||
KILO,
|
||||
SOM,
|
||||
Corvette_1124_4_Grisha,
|
||||
Corvette_1241_1_Molniya,
|
||||
Frigate_11540_Neustrashimy,
|
||||
Frigate_1135M_Rezky,
|
||||
Cruiser_1164_Moskva,
|
||||
SSK_877V_Kilo,
|
||||
SSK_641B_Tango,
|
||||
)
|
||||
|
||||
from gen.fleet.dd_group import DDGroupGenerator
|
||||
@@ -37,7 +37,9 @@ class RussianNavyGroupGenerator(ShipGroupGenerator):
|
||||
include_frigate = True
|
||||
|
||||
if include_frigate:
|
||||
frigate_type = random.choice([ALBATROS, MOLNIYA])
|
||||
frigate_type = random.choice(
|
||||
[Corvette_1124_4_Grisha, Corvette_1241_1_Molniya]
|
||||
)
|
||||
self.add_unit(
|
||||
frigate_type,
|
||||
"FF1",
|
||||
@@ -54,7 +56,7 @@ class RussianNavyGroupGenerator(ShipGroupGenerator):
|
||||
)
|
||||
|
||||
if include_dd:
|
||||
dd_type = random.choice([NEUSTRASH, REZKY])
|
||||
dd_type = random.choice([Frigate_11540_Neustrashimy, Frigate_1135M_Rezky])
|
||||
self.add_unit(
|
||||
dd_type,
|
||||
"DD1",
|
||||
@@ -74,7 +76,7 @@ class RussianNavyGroupGenerator(ShipGroupGenerator):
|
||||
# Only include the Moskva for now, the Pyotry Velikiy is an unkillable monster.
|
||||
# See https://github.com/dcs-liberation/dcs_liberation/issues/567
|
||||
self.add_unit(
|
||||
MOSCOW,
|
||||
Cruiser_1164_Moskva,
|
||||
"CC1",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
@@ -89,7 +91,7 @@ class GrishaGroupGenerator(DDGroupGenerator):
|
||||
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
||||
):
|
||||
super(GrishaGroupGenerator, self).__init__(
|
||||
game, ground_object, faction, ALBATROS
|
||||
game, ground_object, faction, Corvette_1124_4_Grisha
|
||||
)
|
||||
|
||||
|
||||
@@ -98,7 +100,7 @@ class MolniyaGroupGenerator(DDGroupGenerator):
|
||||
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
||||
):
|
||||
super(MolniyaGroupGenerator, self).__init__(
|
||||
game, ground_object, faction, MOLNIYA
|
||||
game, ground_object, faction, Corvette_1241_1_Molniya
|
||||
)
|
||||
|
||||
|
||||
@@ -106,11 +108,15 @@ class KiloSubGroupGenerator(DDGroupGenerator):
|
||||
def __init__(
|
||||
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
||||
):
|
||||
super(KiloSubGroupGenerator, self).__init__(game, ground_object, faction, KILO)
|
||||
super(KiloSubGroupGenerator, self).__init__(
|
||||
game, ground_object, faction, SSK_877V_Kilo
|
||||
)
|
||||
|
||||
|
||||
class TangoSubGroupGenerator(DDGroupGenerator):
|
||||
def __init__(
|
||||
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
||||
):
|
||||
super(TangoSubGroupGenerator, self).__init__(game, ground_object, faction, SOM)
|
||||
super(TangoSubGroupGenerator, self).__init__(
|
||||
game, ground_object, faction, SSK_641B_Tango
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import random
|
||||
|
||||
from dcs.ships import Schnellboot_type_S130
|
||||
from dcs.ships import Boat_Schnellboot_type_S130
|
||||
|
||||
from gen.sam.group_generator import ShipGroupGenerator
|
||||
|
||||
@@ -10,7 +10,7 @@ class SchnellbootGroupGenerator(ShipGroupGenerator):
|
||||
|
||||
for i in range(random.randint(2, 4)):
|
||||
self.add_unit(
|
||||
Schnellboot_type_S130,
|
||||
Boat_Schnellboot_type_S130,
|
||||
"Schnellboot" + str(i),
|
||||
self.position.x + i * random.randint(100, 250),
|
||||
self.position.y + (random.randint(100, 200) - 100),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import random
|
||||
|
||||
from dcs.ships import Uboat_VIIC
|
||||
from dcs.ships import U_boat_VIIC_U_flak
|
||||
|
||||
from gen.sam.group_generator import ShipGroupGenerator
|
||||
|
||||
@@ -10,7 +10,7 @@ class UBoatGroupGenerator(ShipGroupGenerator):
|
||||
|
||||
for i in range(random.randint(1, 4)):
|
||||
self.add_unit(
|
||||
Uboat_VIIC,
|
||||
U_boat_VIIC_U_flak,
|
||||
"Uboat" + str(i),
|
||||
self.position.x + i * random.randint(100, 250),
|
||||
self.position.y + (random.randint(100, 200) - 100),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import random
|
||||
|
||||
from dcs.ships import USS_Samuel_Chase, LST_Mk2
|
||||
from dcs.ships import LS_Samuel_Chase, LST_Mk_II
|
||||
|
||||
from gen.sam.group_generator import ShipGroupGenerator
|
||||
|
||||
@@ -10,7 +10,7 @@ class WW2LSTGroupGenerator(ShipGroupGenerator):
|
||||
|
||||
# Add LS Samuel Chase
|
||||
self.add_unit(
|
||||
USS_Samuel_Chase,
|
||||
LS_Samuel_Chase,
|
||||
"SamuelChase",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
@@ -19,7 +19,7 @@ class WW2LSTGroupGenerator(ShipGroupGenerator):
|
||||
|
||||
for i in range(1, random.randint(3, 4)):
|
||||
self.add_unit(
|
||||
LST_Mk2,
|
||||
LST_Mk_II,
|
||||
"LST" + str(i),
|
||||
self.position.x + i * random.randint(800, 1200),
|
||||
self.position.y,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
import operator
|
||||
import random
|
||||
from collections import defaultdict
|
||||
@@ -17,10 +16,14 @@ from typing import (
|
||||
Set,
|
||||
TYPE_CHECKING,
|
||||
Tuple,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game.factions.faction import Faction
|
||||
from game.infos.information import Information
|
||||
from game.procurement import AircraftProcurementRequest
|
||||
from game.profiling import logged_duration, MultiEventTracer
|
||||
@@ -163,6 +166,8 @@ class AircraftAllocator:
|
||||
flight.max_distance
|
||||
)
|
||||
|
||||
# Prefer using squadrons with pilots first
|
||||
best_understaffed: Optional[Tuple[ControlPoint, Squadron]] = None
|
||||
for airfield in airfields_in_range:
|
||||
if not airfield.is_friendly(self.is_player):
|
||||
continue
|
||||
@@ -174,14 +179,24 @@ class AircraftAllocator:
|
||||
continue
|
||||
# Valid location with enough aircraft available. Find a squadron to fit
|
||||
# the role.
|
||||
squadrons = self.air_wing.auto_assignable_for_task_with_type(
|
||||
aircraft, task
|
||||
)
|
||||
for squadron in squadrons:
|
||||
if squadron.can_provide_pilots(flight.num_aircraft):
|
||||
for squadron in self.air_wing.squadrons_for(aircraft):
|
||||
if task not in squadron.auto_assignable_mission_types:
|
||||
continue
|
||||
if len(squadron.available_pilots) >= flight.num_aircraft:
|
||||
inventory.remove_aircraft(aircraft, flight.num_aircraft)
|
||||
return airfield, squadron
|
||||
return None
|
||||
|
||||
# A compatible squadron that doesn't have enough pilots. Remember it
|
||||
# as a fallback in case we find no better choices.
|
||||
if best_understaffed is None:
|
||||
best_understaffed = airfield, squadron
|
||||
|
||||
if best_understaffed is not None:
|
||||
airfield, squadron = best_understaffed
|
||||
self.global_inventory.for_control_point(airfield).remove_aircraft(
|
||||
squadron.aircraft, flight.num_aircraft
|
||||
)
|
||||
return best_understaffed
|
||||
|
||||
|
||||
class PackageBuilder:
|
||||
@@ -240,7 +255,7 @@ class PackageBuilder:
|
||||
return True
|
||||
|
||||
def find_divert_field(
|
||||
self, aircraft: AircraftType, arrival: ControlPoint
|
||||
self, aircraft: Type[FlyingType], arrival: ControlPoint
|
||||
) -> Optional[ControlPoint]:
|
||||
divert_limit = nautical_miles(150)
|
||||
for airfield in self.closest_airfields.operational_airfields_within(
|
||||
@@ -509,24 +524,6 @@ class ObjectiveFinder:
|
||||
raise RuntimeError("Found no friendly control points. You probably lost.")
|
||||
return farthest
|
||||
|
||||
def closest_friendly_control_point(self) -> ControlPoint:
|
||||
"""Finds the friendly control point that is closest to any threats."""
|
||||
threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||
|
||||
closest = None
|
||||
min_distance = meters(math.inf)
|
||||
for cp in self.friendly_control_points():
|
||||
if isinstance(cp, OffMapSpawn):
|
||||
continue
|
||||
distance = threat_zones.distance_to_threat(cp.position)
|
||||
if distance < min_distance:
|
||||
closest = cp
|
||||
min_distance = distance
|
||||
|
||||
if closest is None:
|
||||
raise RuntimeError("Found no friendly control points. You probably lost.")
|
||||
return closest
|
||||
|
||||
def enemy_control_points(self) -> Iterator[ControlPoint]:
|
||||
"""Iterates over all enemy control points."""
|
||||
return (
|
||||
@@ -584,8 +581,7 @@ class CoalitionMissionPlanner:
|
||||
MAX_OCA_RANGE = nautical_miles(150)
|
||||
MAX_SEAD_RANGE = nautical_miles(150)
|
||||
MAX_STRIKE_RANGE = nautical_miles(150)
|
||||
MAX_AWEC_RANGE = Distance.inf()
|
||||
MAX_TANKER_RANGE = nautical_miles(200)
|
||||
MAX_AWEC_RANGE = nautical_miles(200)
|
||||
|
||||
def __init__(self, game: Game, is_player: bool) -> None:
|
||||
self.game = game
|
||||
@@ -604,7 +600,14 @@ class CoalitionMissionPlanner:
|
||||
also possible for the player to exclude mission types from their squadron
|
||||
designs.
|
||||
"""
|
||||
return self.game.air_wing_for(self.is_player).can_auto_plan(mission_type)
|
||||
all_compatible = aircraft_for_task(mission_type)
|
||||
for squadron in self.game.air_wing_for(self.is_player).iter_squadrons():
|
||||
if (
|
||||
squadron.aircraft in all_compatible
|
||||
and mission_type in squadron.auto_assignable_mission_types
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
def critical_missions(self) -> Iterator[ProposedMission]:
|
||||
"""Identifies the most important missions to plan this turn.
|
||||
@@ -625,11 +628,6 @@ class CoalitionMissionPlanner:
|
||||
asap=True,
|
||||
)
|
||||
|
||||
yield ProposedMission(
|
||||
self.objective_finder.closest_friendly_control_point(),
|
||||
[ProposedFlight(FlightType.REFUELING, 1, self.MAX_TANKER_RANGE)],
|
||||
)
|
||||
|
||||
# Find friendly CPs within 100 nmi from an enemy airfield, plan CAP.
|
||||
for cp in self.objective_finder.vulnerable_control_points():
|
||||
# Plan CAP in such a way, that it is established during the whole desired mission length
|
||||
@@ -844,7 +842,7 @@ class CoalitionMissionPlanner:
|
||||
for cp in self.objective_finder.friendly_control_points():
|
||||
inventory = self.game.aircraft_inventory.for_control_point(cp)
|
||||
for aircraft, available in inventory.all_aircraft:
|
||||
self.message("Unused aircraft", f"{available} {aircraft} from {cp}")
|
||||
self.message("Unused aircraft", f"{available} {aircraft.id} from {cp}")
|
||||
|
||||
def plan_flight(
|
||||
self,
|
||||
|
||||
@@ -8,7 +8,6 @@ from dcs.helicopters import (
|
||||
CH_47D,
|
||||
CH_53E,
|
||||
Ka_50,
|
||||
Mi_24P,
|
||||
Mi_24V,
|
||||
Mi_26,
|
||||
Mi_28N,
|
||||
@@ -52,13 +51,10 @@ from dcs.planes import (
|
||||
F_5E_3,
|
||||
F_86F_Sabre,
|
||||
IL_76MD,
|
||||
IL_78M,
|
||||
I_16,
|
||||
JF_17,
|
||||
J_11A,
|
||||
Ju_88A4,
|
||||
KC130,
|
||||
KC135MPRS,
|
||||
KC_135,
|
||||
KJ_2000,
|
||||
L_39ZA,
|
||||
MQ_9_Reaper,
|
||||
@@ -81,7 +77,6 @@ from dcs.planes import (
|
||||
P_51D_30_NA,
|
||||
RQ_1A_Predator,
|
||||
S_3B,
|
||||
S_3B_Tanker,
|
||||
SpitfireLFMkIX,
|
||||
SpitfireLFMkIXCW,
|
||||
Su_17M4,
|
||||
@@ -105,12 +100,12 @@ from dcs.planes import (
|
||||
)
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from gen.flights.flight import FlightType
|
||||
from pydcs_extensions.a4ec.a4ec import A_4E_C
|
||||
from pydcs_extensions.f22a.f22a import F_22A
|
||||
from pydcs_extensions.hercules.hercules import Hercules
|
||||
from pydcs_extensions.jas39.jas39 import JAS39Gripen, JAS39Gripen_AG
|
||||
from pydcs_extensions.hercules.hercules import Hercules
|
||||
from pydcs_extensions.mb339.mb339 import MB_339PAN
|
||||
from pydcs_extensions.su57.su57 import Su_57
|
||||
|
||||
# All aircraft lists are in priority order. Aircraft higher in the list will be
|
||||
@@ -209,7 +204,6 @@ CAS_CAPABLE = [
|
||||
SA342L,
|
||||
Ka_50,
|
||||
Mi_28N,
|
||||
Mi_24P,
|
||||
Mi_24V,
|
||||
Mi_8MT,
|
||||
UH_1H,
|
||||
@@ -218,6 +212,7 @@ CAS_CAPABLE = [
|
||||
F_5E_3,
|
||||
F_86F_Sabre,
|
||||
C_101CC,
|
||||
MB_339PAN,
|
||||
L_39ZA,
|
||||
A_20G,
|
||||
Ju_88A4,
|
||||
@@ -330,6 +325,7 @@ STRIKE_CAPABLE = [
|
||||
MiG_15bis,
|
||||
F_5E_3,
|
||||
F_86F_Sabre,
|
||||
MB_339PAN,
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
B_17G,
|
||||
@@ -405,17 +401,8 @@ AEWC_CAPABLE = [
|
||||
KJ_2000,
|
||||
]
|
||||
|
||||
# Priority is given to the tankers that can carry the most fuel.
|
||||
REFUELING_CAPABALE = [
|
||||
KC_135,
|
||||
KC135MPRS,
|
||||
IL_78M,
|
||||
KC130,
|
||||
S_3B_Tanker,
|
||||
]
|
||||
|
||||
|
||||
def dcs_types_for_task(task: FlightType) -> list[Type[FlyingType]]:
|
||||
def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
|
||||
cap_missions = (FlightType.BARCAP, FlightType.TARCAP, FlightType.SWEEP)
|
||||
if task in cap_missions:
|
||||
return CAP_CAPABLE
|
||||
@@ -441,8 +428,6 @@ def dcs_types_for_task(task: FlightType) -> list[Type[FlyingType]]:
|
||||
return CAP_CAPABLE
|
||||
elif task == FlightType.AEWC:
|
||||
return AEWC_CAPABLE
|
||||
elif task == FlightType.REFUELING:
|
||||
return REFUELING_CAPABALE
|
||||
elif task == FlightType.TRANSPORT:
|
||||
return TRANSPORT_CAPABLE
|
||||
else:
|
||||
@@ -450,15 +435,7 @@ def dcs_types_for_task(task: FlightType) -> list[Type[FlyingType]]:
|
||||
return []
|
||||
|
||||
|
||||
def aircraft_for_task(task: FlightType) -> list[AircraftType]:
|
||||
dcs_types = dcs_types_for_task(task)
|
||||
types: list[AircraftType] = []
|
||||
for dcs_type in dcs_types:
|
||||
types.extend(AircraftType.for_dcs_type(dcs_type))
|
||||
return types
|
||||
|
||||
|
||||
def tasks_for_aircraft(aircraft: AircraftType) -> list[FlightType]:
|
||||
def tasks_for_aircraft(aircraft: Type[FlyingType]) -> list[FlightType]:
|
||||
tasks = []
|
||||
for task in FlightType:
|
||||
if aircraft in aircraft_for_task(task):
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from enum import Enum
|
||||
from typing import List, Optional, TYPE_CHECKING, Union
|
||||
from typing import List, Optional, TYPE_CHECKING, Type, Union
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.point import MovingPoint, PointAction
|
||||
from dcs.unit import Unit
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game import db
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.squadrons import Pilot, Squadron
|
||||
from game.theater.controlpoint import ControlPoint, MissionTarget
|
||||
from game.utils import Distance, meters
|
||||
@@ -68,7 +69,6 @@ class FlightType(Enum):
|
||||
AEWC = "AEW&C"
|
||||
TRANSPORT = "Transport"
|
||||
SEAD_ESCORT = "SEAD Escort"
|
||||
REFUELING = "Refueling"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
@@ -299,7 +299,7 @@ class Flight:
|
||||
return self.roster.player_count
|
||||
|
||||
@property
|
||||
def unit_type(self) -> AircraftType:
|
||||
def unit_type(self) -> Type[FlyingType]:
|
||||
return self.squadron.aircraft
|
||||
|
||||
@property
|
||||
@@ -324,11 +324,13 @@ class Flight:
|
||||
self.roster.clear()
|
||||
|
||||
def __repr__(self):
|
||||
name = db.unit_type_name(self.unit_type)
|
||||
if self.custom_name:
|
||||
return f"{self.custom_name} {self.count} x {self.unit_type}"
|
||||
return f"[{self.flight_type}] {self.count} x {self.unit_type}"
|
||||
return f"{self.custom_name} {self.count} x {name}"
|
||||
return f"[{self.flight_type}] {self.count} x {name}"
|
||||
|
||||
def __str__(self):
|
||||
name = db.unit_get_expanded_info(self.country, self.unit_type, "name")
|
||||
if self.custom_name:
|
||||
return f"{self.custom_name} {self.count} x {self.unit_type}"
|
||||
return f"[{self.flight_type}] {self.count} x {self.unit_type}"
|
||||
return f"{self.custom_name} {self.count} x {name}"
|
||||
return f"[{self.flight_type}] {self.count} x {name}"
|
||||
|
||||
@@ -16,10 +16,12 @@ from functools import cached_property
|
||||
from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.planes import E_3A, E_2C, A_50, KJ_2000
|
||||
from dcs.unit import Unit
|
||||
from shapely.geometry import Point as ShapelyPoint
|
||||
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.squadrons import Pilot
|
||||
from game.theater import (
|
||||
Airfield,
|
||||
ControlPoint,
|
||||
@@ -27,10 +29,9 @@ from game.theater import (
|
||||
MissionTarget,
|
||||
SamGroundObject,
|
||||
TheaterGroundObject,
|
||||
NavalControlPoint,
|
||||
)
|
||||
from game.theater.theatergroundobject import EwrGroundObject, NavalGroundObject
|
||||
from game.utils import Distance, Speed, feet, meters, nautical_miles, knots
|
||||
from game.theater.theatergroundobject import EwrGroundObject
|
||||
from game.utils import Distance, Speed, feet, meters, nautical_miles
|
||||
from .closestairfields import ObjectiveDistanceCache
|
||||
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
|
||||
from .traveltime import GroundSpeed, TravelTime
|
||||
@@ -768,28 +769,6 @@ class AwacsFlightPlan(LoiterFlightPlan):
|
||||
return self.push_time
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RefuelingFlightPlan(PatrollingFlightPlan):
|
||||
takeoff: FlightWaypoint
|
||||
land: FlightWaypoint
|
||||
divert: Optional[FlightWaypoint]
|
||||
bullseye: FlightWaypoint
|
||||
|
||||
#: Racetrack speed.
|
||||
patrol_speed: Speed
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.takeoff
|
||||
yield from self.nav_to
|
||||
yield self.patrol_start
|
||||
yield self.patrol_end
|
||||
yield from self.nav_from
|
||||
yield self.land
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
yield self.bullseye
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AirliftFlightPlan(FlightPlan):
|
||||
takeoff: FlightWaypoint
|
||||
@@ -940,8 +919,6 @@ class FlightPlanBuilder:
|
||||
return self.generate_aewc(flight)
|
||||
elif task == FlightType.TRANSPORT:
|
||||
return self.generate_transport(flight)
|
||||
elif task == FlightType.REFUELING:
|
||||
return self.generate_refueling_racetrack(flight)
|
||||
raise PlanningError(f"{task} flight plan generation not implemented")
|
||||
|
||||
def regenerate_package_waypoints(self) -> None:
|
||||
@@ -1082,8 +1059,15 @@ class FlightPlanBuilder:
|
||||
|
||||
orbit_location = self.aewc_orbit(location)
|
||||
|
||||
if flight.unit_type.patrol_altitude is not None:
|
||||
patrol_alt = flight.unit_type.patrol_altitude
|
||||
# As high as possible to maximize detection and on-station time.
|
||||
if flight.unit_type == E_2C:
|
||||
patrol_alt = feet(30000)
|
||||
elif flight.unit_type == E_3A:
|
||||
patrol_alt = feet(35000)
|
||||
elif flight.unit_type == A_50:
|
||||
patrol_alt = feet(33000)
|
||||
elif flight.unit_type == KJ_2000:
|
||||
patrol_alt = feet(40000)
|
||||
else:
|
||||
patrol_alt = feet(25000)
|
||||
|
||||
@@ -1147,9 +1131,12 @@ class FlightPlanBuilder:
|
||||
|
||||
from game.transfers import CargoShip
|
||||
|
||||
if isinstance(location, NavalControlPoint):
|
||||
targets = self.anti_ship_targets_for_tgo(location.find_main_tgo())
|
||||
elif isinstance(location, NavalGroundObject):
|
||||
if isinstance(location, ControlPoint):
|
||||
if not location.is_fleet:
|
||||
raise InvalidObjectiveLocation(flight.flight_type, location)
|
||||
# The first group generated will be the carrier group itself.
|
||||
targets = self.anti_ship_targets_for_tgo(location.ground_objects[0])
|
||||
elif isinstance(location, TheaterGroundObject):
|
||||
targets = self.anti_ship_targets_for_tgo(location)
|
||||
elif isinstance(location, CargoShip):
|
||||
targets = [StrikeTarget(location.name, location)]
|
||||
@@ -1625,74 +1612,6 @@ class FlightPlanBuilder:
|
||||
bullseye=builder.bullseye(),
|
||||
)
|
||||
|
||||
def generate_refueling_racetrack(self, flight: Flight) -> RefuelingFlightPlan:
|
||||
location = self.package.target
|
||||
|
||||
closest_boundary = self.threat_zones.closest_boundary(location.position)
|
||||
heading_to_threat_boundary = location.position.heading_between_point(
|
||||
closest_boundary
|
||||
)
|
||||
distance_to_threat = meters(
|
||||
location.position.distance_to_point(closest_boundary)
|
||||
)
|
||||
orbit_heading = heading_to_threat_boundary
|
||||
|
||||
# Station 70nm outside the threat zone.
|
||||
threat_buffer = nautical_miles(70)
|
||||
if self.threat_zones.threatened(location.position):
|
||||
orbit_distance = distance_to_threat + threat_buffer
|
||||
else:
|
||||
orbit_distance = distance_to_threat - threat_buffer
|
||||
|
||||
racetrack_center = location.position.point_from_heading(
|
||||
orbit_heading, orbit_distance.meters
|
||||
)
|
||||
|
||||
racetrack_half_distance = Distance.from_nautical_miles(20).meters
|
||||
|
||||
racetrack_start = racetrack_center.point_from_heading(
|
||||
orbit_heading + 90, racetrack_half_distance
|
||||
)
|
||||
racetrack_end = racetrack_center.point_from_heading(
|
||||
orbit_heading - 90, racetrack_half_distance
|
||||
)
|
||||
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
|
||||
tanker_type = flight.unit_type
|
||||
if tanker_type.patrol_altitude is not None:
|
||||
altitude = tanker_type.patrol_altitude
|
||||
else:
|
||||
altitude = feet(21000)
|
||||
|
||||
if tanker_type.patrol_speed is not None:
|
||||
speed = tanker_type.patrol_speed
|
||||
else:
|
||||
# ~280 knots IAS at 21000.
|
||||
speed = knots(400)
|
||||
|
||||
racetrack = builder.race_track(racetrack_start, racetrack_end, altitude)
|
||||
|
||||
return RefuelingFlightPlan(
|
||||
package=self.package,
|
||||
flight=flight,
|
||||
takeoff=builder.takeoff(flight.departure),
|
||||
nav_to=builder.nav_path(
|
||||
flight.departure.position, racetrack_start, altitude
|
||||
),
|
||||
nav_from=builder.nav_path(racetrack_end, flight.arrival.position, altitude),
|
||||
patrol_start=racetrack[0],
|
||||
patrol_end=racetrack[1],
|
||||
land=builder.land(flight.arrival),
|
||||
divert=builder.divert(flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
patrol_duration=timedelta(hours=1),
|
||||
patrol_speed=speed,
|
||||
# TODO: Factor out a common base of the combat and non-combat race-tracks.
|
||||
# No harm in setting this, but we ought to clean up a bit.
|
||||
engagement_distance=meters(0),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def target_waypoint(
|
||||
flight: Flight, builder: WaypointBuilder, target: StrikeTarget
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from typing import Optional, List, Iterator, TYPE_CHECKING, Mapping
|
||||
from typing import Optional, List, Iterator, Type, TYPE_CHECKING, Mapping
|
||||
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game.data.weapons import Weapon, Pylon
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gen.flights.flight import Flight
|
||||
@@ -26,7 +27,9 @@ class Loadout:
|
||||
def derive_custom(self, name: str) -> Loadout:
|
||||
return Loadout(name, self.pylons, self.date, is_custom=True)
|
||||
|
||||
def degrade_for_date(self, unit_type: AircraftType, date: datetime.date) -> Loadout:
|
||||
def degrade_for_date(
|
||||
self, unit_type: Type[FlyingType], date: datetime.date
|
||||
) -> Loadout:
|
||||
if self.date is not None and self.date <= date:
|
||||
return Loadout(self.name, self.pylons, self.date)
|
||||
|
||||
@@ -58,7 +61,7 @@ class Loadout:
|
||||
# {"CLSID": class ID, "num": pylon number}
|
||||
# "tasks": List (as a dict) of task IDs the payload is used by.
|
||||
# }
|
||||
payloads = flight.unit_type.dcs_unit_type.load_payloads()
|
||||
payloads = flight.unit_type.load_payloads()
|
||||
for payload in payloads.values():
|
||||
name = payload["name"]
|
||||
pylons = payload["pylons"]
|
||||
@@ -123,8 +126,8 @@ class Loadout:
|
||||
for name in cls.default_loadout_names_for(flight):
|
||||
# This operation is cached, but must be called before load_by_name will
|
||||
# work.
|
||||
flight.unit_type.dcs_unit_type.load_payloads()
|
||||
payload = flight.unit_type.dcs_unit_type.loadout_by_name(name)
|
||||
flight.unit_type.load_payloads()
|
||||
payload = flight.unit_type.loadout_by_name(name)
|
||||
if payload is not None:
|
||||
return Loadout(
|
||||
name,
|
||||
|
||||
@@ -25,13 +25,16 @@ if TYPE_CHECKING:
|
||||
class GroundSpeed:
|
||||
@classmethod
|
||||
def for_flight(cls, flight: Flight, altitude: Distance) -> Speed:
|
||||
if not issubclass(flight.unit_type, FlyingType):
|
||||
raise TypeError("Flight has non-flying unit")
|
||||
|
||||
# TODO: Expose both a cruise speed and target speed.
|
||||
# The cruise speed can be used for ascent, hold, join, and RTB to save
|
||||
# on fuel, but mission speed will be fast enough to keep the flight
|
||||
# safer.
|
||||
|
||||
# DCS's max speed is in kph at 0 MSL.
|
||||
max_speed = flight.unit_type.max_speed
|
||||
max_speed = kph(flight.unit_type.max_speed)
|
||||
if max_speed > SPEED_OF_SOUND_AT_SEA_LEVEL:
|
||||
# Aircraft is supersonic. Limit to mach 0.85 to conserve fuel and
|
||||
# account for heavily loaded jets.
|
||||
|
||||
@@ -32,8 +32,6 @@ class ForcedOptionsGenerator:
|
||||
self.mission.forced_options.labels = ForcedOptions.Labels.Abbreviate
|
||||
elif self.game.settings.labels == "Dot Only":
|
||||
self.mission.forced_options.labels = ForcedOptions.Labels.DotOnly
|
||||
elif self.game.settings.labels == "Neutral Dot":
|
||||
self.mission.forced_options.labels = ForcedOptions.Labels.NeutralDot
|
||||
elif self.game.settings.labels == "Off":
|
||||
self.mission.forced_options.labels = ForcedOptions.Labels.None_
|
||||
|
||||
|
||||
@@ -3,9 +3,11 @@ import random
|
||||
from enum import Enum
|
||||
from typing import Dict, List
|
||||
|
||||
from game.data.groundunitclass import GroundUnitClass
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from dcs.unittype import VehicleType
|
||||
|
||||
from game.theater import ControlPoint
|
||||
|
||||
from game.data.groundunitclass import GroundUnitClass
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
|
||||
MAX_COMBAT_GROUP_PER_CP = 10
|
||||
@@ -46,19 +48,17 @@ GROUP_SIZES_BY_COMBAT_STANCE = {
|
||||
|
||||
|
||||
class CombatGroup:
|
||||
def __init__(
|
||||
self, role: CombatGroupRole, unit_type: GroundUnitType, size: int
|
||||
) -> None:
|
||||
self.unit_type = unit_type
|
||||
self.size = size
|
||||
def __init__(self, role: CombatGroupRole):
|
||||
self.units: List[VehicleType] = []
|
||||
self.role = role
|
||||
self.assigned_enemy_cp = None
|
||||
self.start_position = None
|
||||
|
||||
def __str__(self):
|
||||
s = f"ROLE : {self.role}\n"
|
||||
if self.size:
|
||||
s += f"UNITS {self.unit_type} * {self.size}"
|
||||
s = ""
|
||||
s += "ROLE : " + str(self.role) + "\n"
|
||||
if len(self.units) > 0:
|
||||
s += "UNITS " + self.units[0].name + " * " + str(len(self.units))
|
||||
return s
|
||||
|
||||
|
||||
@@ -97,29 +97,28 @@ class GroundPlanner:
|
||||
|
||||
# Create combat groups and assign them randomly to each enemy CP
|
||||
for unit_type in self.cp.base.armor:
|
||||
unit_class = unit_type.unit_class
|
||||
if unit_class is GroundUnitClass.Tank:
|
||||
if unit_type in GroundUnitClass.Tank:
|
||||
collection = self.tank_groups
|
||||
role = CombatGroupRole.TANK
|
||||
elif unit_class is GroundUnitClass.Apc:
|
||||
elif unit_type in GroundUnitClass.Apc:
|
||||
collection = self.apc_group
|
||||
role = CombatGroupRole.APC
|
||||
elif unit_class is GroundUnitClass.Artillery:
|
||||
elif unit_type in GroundUnitClass.Artillery:
|
||||
collection = self.art_group
|
||||
role = CombatGroupRole.ARTILLERY
|
||||
elif unit_class is GroundUnitClass.Ifv:
|
||||
elif unit_type in GroundUnitClass.Ifv:
|
||||
collection = self.ifv_group
|
||||
role = CombatGroupRole.IFV
|
||||
elif unit_class is GroundUnitClass.Logistics:
|
||||
elif unit_type in GroundUnitClass.Logistics:
|
||||
collection = self.logi_groups
|
||||
role = CombatGroupRole.LOGI
|
||||
elif unit_class is GroundUnitClass.Atgm:
|
||||
elif unit_type in GroundUnitClass.Atgm:
|
||||
collection = self.atgm_group
|
||||
role = CombatGroupRole.ATGM
|
||||
elif unit_class is GroundUnitClass.Shorads:
|
||||
elif unit_type in GroundUnitClass.Shorads:
|
||||
collection = self.shorad_groups
|
||||
role = CombatGroupRole.SHORAD
|
||||
elif unit_class is GroundUnitClass.Recon:
|
||||
elif unit_type in GroundUnitClass.Recon:
|
||||
collection = self.recon_groups
|
||||
role = CombatGroupRole.RECON
|
||||
else:
|
||||
@@ -138,17 +137,17 @@ class GroundPlanner:
|
||||
while available > 0:
|
||||
|
||||
if role == CombatGroupRole.SHORAD:
|
||||
count = 1
|
||||
n = 1
|
||||
else:
|
||||
count = random.choice(group_size_choice)
|
||||
if count > available:
|
||||
n = random.choice(group_size_choice)
|
||||
if n > available:
|
||||
if available >= 2:
|
||||
count = 2
|
||||
n = 2
|
||||
else:
|
||||
count = 1
|
||||
available -= count
|
||||
n = 1
|
||||
available -= n
|
||||
|
||||
group = CombatGroup(role, unit_type, count)
|
||||
group = CombatGroup(role)
|
||||
if len(self.connected_enemy_cp) > 0:
|
||||
enemy_cp = random.choice(self.connected_enemy_cp).id
|
||||
self.units_per_cp[enemy_cp].append(group)
|
||||
@@ -156,6 +155,9 @@ class GroundPlanner:
|
||||
else:
|
||||
self.reserve.append(group)
|
||||
group.assigned_enemy_cp = "__reserve__"
|
||||
|
||||
for i in range(n):
|
||||
group.units.append(unit_type)
|
||||
collection.append(group)
|
||||
|
||||
if remaining_available_frontline_units == 0:
|
||||
|
||||
@@ -32,15 +32,15 @@ from typing import Dict, List, Optional, TYPE_CHECKING, Tuple, Iterator
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from dcs.mission import Mission
|
||||
from dcs.unit import Unit
|
||||
from dcs.unittype import FlyingType
|
||||
from tabulate import tabulate
|
||||
|
||||
from game.data.alic import AlicCodes
|
||||
from game.db import unit_type_from_name
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.theater import ConflictTheater, TheaterGroundObject, LatLon
|
||||
from game.theater.bullseye import Bullseye
|
||||
from game.utils import meters
|
||||
from .aircraft import FlightData
|
||||
from .aircraft import AIRCRAFT_DATA, FlightData
|
||||
from .airsupportgen import AwacsInfo, TankerInfo
|
||||
from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator
|
||||
from .flights.flight import FlightWaypoint, FlightWaypointType, FlightType
|
||||
@@ -142,8 +142,7 @@ class KneeboardPage:
|
||||
"""Writes the kneeboard page to the given path."""
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def format_ll(ll: LatLon) -> str:
|
||||
def format_ll(self, ll: LatLon) -> str:
|
||||
ns = "N" if ll.latitude >= 0 else "S"
|
||||
ew = "E" if ll.longitude >= 0 else "W"
|
||||
return f"{ll.latitude:.4}°{ns} {ll.longitude:.4}°{ew}"
|
||||
@@ -356,9 +355,8 @@ class BriefingPage(KneeboardPage):
|
||||
if channel is None:
|
||||
return str(frequency)
|
||||
|
||||
channel_name = self.flight.aircraft_type.channel_name(
|
||||
channel.radio_id, channel.channel
|
||||
)
|
||||
namer = AIRCRAFT_DATA[self.flight.aircraft_type.id].channel_namer
|
||||
channel_name = namer.channel_name(channel.radio_id, channel.channel)
|
||||
return f"{channel_name}\n{frequency}"
|
||||
|
||||
|
||||
@@ -454,10 +452,9 @@ class SupportPage(KneeboardPage):
|
||||
if channel is None:
|
||||
return str(frequency)
|
||||
|
||||
channel_name = self.flight.aircraft_type.channel_name(
|
||||
channel.radio_id, channel.channel
|
||||
)
|
||||
return f"{channel_name}\n{frequency}"
|
||||
namer = AIRCRAFT_DATA[self.flight.aircraft_type.id].channel_namer
|
||||
channel_name = namer.channel_name(channel.radio_id, channel.channel)
|
||||
return f"{channel_name} {frequency}"
|
||||
|
||||
def _format_time(self, time: Optional[datetime.timedelta]) -> str:
|
||||
if time is None:
|
||||
@@ -568,14 +565,14 @@ class KneeboardGenerator(MissionInfoGenerator):
|
||||
temp_dir = Path("kneeboards")
|
||||
temp_dir.mkdir(exist_ok=True)
|
||||
for aircraft, pages in self.pages_by_airframe().items():
|
||||
aircraft_dir = temp_dir / aircraft.dcs_unit_type.id
|
||||
aircraft_dir = temp_dir / aircraft.id
|
||||
aircraft_dir.mkdir(exist_ok=True)
|
||||
for idx, page in enumerate(pages):
|
||||
page_path = aircraft_dir / f"page{idx:02}.png"
|
||||
page.write(page_path)
|
||||
self.mission.add_aircraft_kneeboard(aircraft.dcs_unit_type, page_path)
|
||||
self.mission.add_aircraft_kneeboard(aircraft, page_path)
|
||||
|
||||
def pages_by_airframe(self) -> Dict[AircraftType, List[KneeboardPage]]:
|
||||
def pages_by_airframe(self) -> Dict[FlyingType, List[KneeboardPage]]:
|
||||
"""Returns a list of kneeboard pages per airframe in the mission.
|
||||
|
||||
Only client flights will be included, but because DCS does not support
|
||||
@@ -586,7 +583,7 @@ class KneeboardGenerator(MissionInfoGenerator):
|
||||
A dict mapping aircraft types to the list of kneeboard pages for
|
||||
that aircraft.
|
||||
"""
|
||||
all_flights: Dict[AircraftType, List[KneeboardPage]] = defaultdict(list)
|
||||
all_flights: Dict[FlyingType, List[KneeboardPage]] = defaultdict(list)
|
||||
for flight in self.flights:
|
||||
if not flight.client_units:
|
||||
continue
|
||||
|
||||
@@ -14,21 +14,21 @@ class ScudGenerator(GroupGenerator):
|
||||
|
||||
# Scuds
|
||||
self.add_unit(
|
||||
MissilesSS.Scud_B,
|
||||
MissilesSS.SSM_SS_1C_Scud_B,
|
||||
"V1#0",
|
||||
self.position.x,
|
||||
self.position.y + random.randint(1, 8),
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
MissilesSS.Scud_B,
|
||||
MissilesSS.SSM_SS_1C_Scud_B,
|
||||
"V1#1",
|
||||
self.position.x + 50,
|
||||
self.position.y + random.randint(1, 8),
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
MissilesSS.Scud_B,
|
||||
MissilesSS.SSM_SS_1C_Scud_B,
|
||||
"V1#2",
|
||||
self.position.x + 100,
|
||||
self.position.y + random.randint(1, 8),
|
||||
@@ -37,7 +37,7 @@ class ScudGenerator(GroupGenerator):
|
||||
|
||||
# Commander
|
||||
self.add_unit(
|
||||
Unarmed.UAZ_469,
|
||||
Unarmed.LUV_UAZ_469_Jeep,
|
||||
"Kubel#0",
|
||||
self.position.x - 35,
|
||||
self.position.y - 20,
|
||||
@@ -46,7 +46,7 @@ class ScudGenerator(GroupGenerator):
|
||||
|
||||
# Shorad
|
||||
self.add_unit(
|
||||
AirDefence.ZSU_23_4_Shilka,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
|
||||
"SHILKA#0",
|
||||
self.position.x - 55,
|
||||
self.position.y - 38,
|
||||
@@ -54,7 +54,7 @@ class ScudGenerator(GroupGenerator):
|
||||
)
|
||||
|
||||
self.add_unit(
|
||||
AirDefence.Strela_1_9P31,
|
||||
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL,
|
||||
"STRELA#0",
|
||||
self.position.x + 200,
|
||||
self.position.y + 15,
|
||||
|
||||
@@ -14,21 +14,21 @@ class V1GroupGenerator(GroupGenerator):
|
||||
|
||||
# Ramps
|
||||
self.add_unit(
|
||||
MissilesSS.V1_launcher,
|
||||
MissilesSS.SSM_V_1_Launcher,
|
||||
"V1#0",
|
||||
self.position.x,
|
||||
self.position.y + random.randint(1, 8),
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
MissilesSS.V1_launcher,
|
||||
MissilesSS.SSM_V_1_Launcher,
|
||||
"V1#1",
|
||||
self.position.x + 50,
|
||||
self.position.y + random.randint(1, 8),
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
MissilesSS.V1_launcher,
|
||||
MissilesSS.SSM_V_1_Launcher,
|
||||
"V1#2",
|
||||
self.position.x + 100,
|
||||
self.position.y + random.randint(1, 8),
|
||||
@@ -37,7 +37,7 @@ class V1GroupGenerator(GroupGenerator):
|
||||
|
||||
# Commander
|
||||
self.add_unit(
|
||||
Unarmed.Kubelwagen_82,
|
||||
Unarmed.LUV_Kubelwagen_82,
|
||||
"Kubel#0",
|
||||
self.position.x - 35,
|
||||
self.position.y - 20,
|
||||
@@ -45,7 +45,9 @@ class V1GroupGenerator(GroupGenerator):
|
||||
)
|
||||
|
||||
# Self defense flak
|
||||
flak_unit = random.choice([AirDefence.Flak38, AirDefence.Flak30])
|
||||
flak_unit = random.choice(
|
||||
[AirDefence.AAA_Flak_Vierling_38_Quad_20mm, AirDefence.AAA_Flak_38_20mm]
|
||||
)
|
||||
|
||||
self.add_unit(
|
||||
flak_unit,
|
||||
@@ -56,7 +58,7 @@ class V1GroupGenerator(GroupGenerator):
|
||||
)
|
||||
|
||||
self.add_unit(
|
||||
Unarmed.Blitz_36_6700A,
|
||||
Unarmed.Truck_Opel_Blitz,
|
||||
"Blitz#0",
|
||||
self.position.x + 200,
|
||||
self.position.y + 15,
|
||||
|
||||
@@ -3,9 +3,10 @@ import time
|
||||
from typing import List
|
||||
|
||||
from dcs.country import Country
|
||||
from dcs.unittype import UnitType
|
||||
|
||||
from game import db
|
||||
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.dcs.unittype import UnitType
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
ALPHA_MILITARY = [
|
||||
@@ -289,14 +290,14 @@ class NameGenerator:
|
||||
country.id,
|
||||
cls.aircraft_number,
|
||||
parent_base_id,
|
||||
flight.unit_type.name,
|
||||
db.unit_type_name(flight.unit_type),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def next_unit_name(cls, country: Country, parent_base_id: int, unit_type: UnitType):
|
||||
cls.number += 1
|
||||
return "unit|{}|{}|{}|{}|".format(
|
||||
country.id, cls.number, parent_base_id, unit_type.name
|
||||
country.id, cls.number, parent_base_id, db.unit_type_name(unit_type)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -308,7 +309,7 @@ class NameGenerator:
|
||||
country.id,
|
||||
cls.infantry_number,
|
||||
parent_base_id,
|
||||
unit_type.name,
|
||||
db.unit_type_name(unit_type),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -317,9 +318,11 @@ class NameGenerator:
|
||||
return "awacs|{}|{}|0|".format(country.id, cls.number)
|
||||
|
||||
@classmethod
|
||||
def next_tanker_name(cls, country: Country, unit_type: AircraftType):
|
||||
def next_tanker_name(cls, country: Country, unit_type: UnitType):
|
||||
cls.number += 1
|
||||
return "tanker|{}|{}|0|{}".format(country.id, cls.number, unit_type.name)
|
||||
return "tanker|{}|{}|0|{}".format(
|
||||
country.id, cls.number, db.unit_type_name(unit_type)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def next_carrier_name(cls, country: Country):
|
||||
|
||||
@@ -153,7 +153,7 @@ def get_radio(name: str) -> Radio:
|
||||
for radio in RADIOS:
|
||||
if radio.name == name:
|
||||
return radio
|
||||
raise KeyError(f"Unknown radio: {name}")
|
||||
raise KeyError
|
||||
|
||||
|
||||
class RadioRegistry:
|
||||
|
||||
@@ -27,7 +27,7 @@ class BoforsGenerator(AirDefenseGroupGenerator):
|
||||
for j in range(grid_y):
|
||||
index = index + 1
|
||||
self.add_unit(
|
||||
AirDefence.Bofors40,
|
||||
AirDefence.AAA_Bofors_40mm,
|
||||
"AAA#" + str(index),
|
||||
self.position.x + spacing * i,
|
||||
self.position.y + spacing * j,
|
||||
|
||||
@@ -8,12 +8,12 @@ from gen.sam.airdefensegroupgenerator import (
|
||||
)
|
||||
|
||||
GFLAK = [
|
||||
AirDefence.Flak38,
|
||||
AirDefence.Flak18,
|
||||
AirDefence.Flak36,
|
||||
AirDefence.Flak37,
|
||||
AirDefence.Flak41,
|
||||
AirDefence.Flak30,
|
||||
AirDefence.AAA_Flak_Vierling_38_Quad_20mm,
|
||||
AirDefence.AAA_8_8cm_Flak_18,
|
||||
AirDefence.AAA_8_8cm_Flak_36,
|
||||
AirDefence.AAA_8_8cm_Flak_37,
|
||||
AirDefence.AAA_8_8cm_Flak_41,
|
||||
AirDefence.AAA_Flak_38_20mm,
|
||||
]
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ class FlakGenerator(AirDefenseGroupGenerator):
|
||||
search_pos = self.get_circular_position(random.randint(2, 3), 80)
|
||||
for index, pos in enumerate(search_pos):
|
||||
self.add_unit(
|
||||
AirDefence.Flakscheinwerfer_37,
|
||||
AirDefence.SL_Flakscheinwerfer_37,
|
||||
"SearchLight#" + str(index),
|
||||
pos[0],
|
||||
pos[1],
|
||||
@@ -62,14 +62,14 @@ class FlakGenerator(AirDefenseGroupGenerator):
|
||||
|
||||
# Support
|
||||
self.add_unit(
|
||||
AirDefence.Maschinensatz_33,
|
||||
AirDefence.PU_Maschinensatz_33,
|
||||
"MC33#",
|
||||
self.position.x - 20,
|
||||
self.position.y - 20,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
AirDefence.KDO_Mod40,
|
||||
AirDefence.AAA_SP_Kdo_G_40,
|
||||
"KDO#",
|
||||
self.position.x - 25,
|
||||
self.position.y - 20,
|
||||
@@ -78,7 +78,7 @@ class FlakGenerator(AirDefenseGroupGenerator):
|
||||
|
||||
# Commander
|
||||
self.add_unit(
|
||||
Unarmed.Kubelwagen_82,
|
||||
Unarmed.LUV_Kubelwagen_82,
|
||||
"Kubel#",
|
||||
self.position.x - 35,
|
||||
self.position.y - 20,
|
||||
@@ -89,7 +89,7 @@ class FlakGenerator(AirDefenseGroupGenerator):
|
||||
for i in range(int(max(1, grid_x / 2))):
|
||||
for j in range(int(max(1, grid_x / 2))):
|
||||
self.add_unit(
|
||||
Unarmed.Blitz_36_6700A,
|
||||
Unarmed.Truck_Opel_Blitz,
|
||||
"BLITZ#" + str(index),
|
||||
self.position.x + 125 + 15 * i + random.randint(1, 5),
|
||||
self.position.y + 15 * j + random.randint(1, 5),
|
||||
|
||||
@@ -25,7 +25,7 @@ class Flak18Generator(AirDefenseGroupGenerator):
|
||||
for j in range(2):
|
||||
index = index + 1
|
||||
self.add_unit(
|
||||
AirDefence.Flak18,
|
||||
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),
|
||||
@@ -34,7 +34,7 @@ class Flak18Generator(AirDefenseGroupGenerator):
|
||||
|
||||
# Add a commander truck
|
||||
self.add_unit(
|
||||
Unarmed.Blitz_36_6700A,
|
||||
Unarmed.Truck_Opel_Blitz,
|
||||
"Blitz#",
|
||||
self.position.x - 35,
|
||||
self.position.y - 20,
|
||||
|
||||
@@ -21,7 +21,7 @@ class AllyWW2FlakGenerator(AirDefenseGroupGenerator):
|
||||
positions = self.get_circular_position(4, launcher_distance=30, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(
|
||||
AirDefence.QF_37_AA,
|
||||
AirDefence.AAA_QF_3_7,
|
||||
"AA#" + str(i),
|
||||
position[0],
|
||||
position[1],
|
||||
@@ -31,7 +31,7 @@ class AllyWW2FlakGenerator(AirDefenseGroupGenerator):
|
||||
positions = self.get_circular_position(8, launcher_distance=60, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(
|
||||
AirDefence.M1_37mm,
|
||||
AirDefence.AAA_M1_37mm,
|
||||
"AA#" + str(4 + i),
|
||||
position[0],
|
||||
position[1],
|
||||
@@ -41,7 +41,7 @@ class AllyWW2FlakGenerator(AirDefenseGroupGenerator):
|
||||
positions = self.get_circular_position(8, launcher_distance=90, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(
|
||||
AirDefence.M45_Quadmount,
|
||||
AirDefence.AAA_M45_Quadmount_HB_12_7mm,
|
||||
"AA#" + str(12 + i),
|
||||
position[0],
|
||||
position[1],
|
||||
@@ -50,28 +50,28 @@ class AllyWW2FlakGenerator(AirDefenseGroupGenerator):
|
||||
|
||||
# Add a commander truck
|
||||
self.add_unit(
|
||||
Unarmed.Willys_MB,
|
||||
Unarmed.Car_Willys_Jeep,
|
||||
"CMD#1",
|
||||
self.position.x,
|
||||
self.position.y - 20,
|
||||
random.randint(0, 360),
|
||||
)
|
||||
self.add_unit(
|
||||
Unarmed.M30_CC,
|
||||
Unarmed.Carrier_M30_Cargo,
|
||||
"LOG#1",
|
||||
self.position.x,
|
||||
self.position.y + 20,
|
||||
random.randint(0, 360),
|
||||
)
|
||||
self.add_unit(
|
||||
Unarmed.M4_Tractor,
|
||||
Unarmed.Tractor_M4_Hi_Speed,
|
||||
"LOG#2",
|
||||
self.position.x + 20,
|
||||
self.position.y,
|
||||
random.randint(0, 360),
|
||||
)
|
||||
self.add_unit(
|
||||
Unarmed.Bedford_MWD,
|
||||
Unarmed.Truck_Bedford,
|
||||
"LOG#3",
|
||||
self.position.x - 20,
|
||||
self.position.y,
|
||||
|
||||
@@ -21,7 +21,7 @@ class ZSU57Generator(AirDefenseGroupGenerator):
|
||||
)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(
|
||||
AirDefence.ZSU_57_2,
|
||||
AirDefence.SPAAA_ZSU_57_2,
|
||||
"SPAA#" + str(i),
|
||||
position[0],
|
||||
position[1],
|
||||
|
||||
@@ -27,7 +27,7 @@ class ZU23InsurgentGenerator(AirDefenseGroupGenerator):
|
||||
for j in range(grid_y):
|
||||
index = index + 1
|
||||
self.add_unit(
|
||||
AirDefence.ZU_23_Closed_Insurgent,
|
||||
AirDefence.AAA_ZU_23_Insurgent_Closed_Emplacement,
|
||||
"AAA#" + str(index),
|
||||
self.position.x + spacing * i,
|
||||
self.position.y + spacing * j,
|
||||
|
||||
@@ -29,7 +29,7 @@ class EarlyColdWarFlakGenerator(AirDefenseGroupGenerator):
|
||||
for j in range(2):
|
||||
index = index + 1
|
||||
self.add_unit(
|
||||
AirDefence.Flak18,
|
||||
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),
|
||||
@@ -38,14 +38,14 @@ class EarlyColdWarFlakGenerator(AirDefenseGroupGenerator):
|
||||
|
||||
# Medium range guns
|
||||
self.add_unit(
|
||||
AirDefence.S_60_Type59_Artillery,
|
||||
AirDefence.AAA_S_60_57mm,
|
||||
"SHO#1",
|
||||
self.position.x - 40,
|
||||
self.position.y - 40,
|
||||
self.heading + 180,
|
||||
),
|
||||
self.add_unit(
|
||||
AirDefence.S_60_Type59_Artillery,
|
||||
AirDefence.AAA_S_60_57mm,
|
||||
"SHO#2",
|
||||
self.position.x + spacing * 2 + 40,
|
||||
self.position.y + spacing + 40,
|
||||
@@ -54,14 +54,14 @@ class EarlyColdWarFlakGenerator(AirDefenseGroupGenerator):
|
||||
|
||||
# Short range guns
|
||||
self.add_unit(
|
||||
AirDefence.ZU_23_Emplacement_Closed,
|
||||
AirDefence.AAA_ZU_23_Closed_Emplacement,
|
||||
"SHO#3",
|
||||
self.position.x - 80,
|
||||
self.position.y - 40,
|
||||
self.heading + 180,
|
||||
),
|
||||
self.add_unit(
|
||||
AirDefence.ZU_23_Emplacement_Closed,
|
||||
AirDefence.AAA_ZU_23_Closed_Emplacement,
|
||||
"SHO#4",
|
||||
self.position.x + spacing * 2 + 80,
|
||||
self.position.y + spacing + 40,
|
||||
@@ -70,7 +70,7 @@ class EarlyColdWarFlakGenerator(AirDefenseGroupGenerator):
|
||||
|
||||
# Add a truck
|
||||
self.add_unit(
|
||||
Unarmed.KAMAZ_Truck,
|
||||
Unarmed.Truck_KAMAZ_43101,
|
||||
"Truck#",
|
||||
self.position.x - 60,
|
||||
self.position.y - 20,
|
||||
@@ -102,7 +102,7 @@ class ColdWarFlakGenerator(AirDefenseGroupGenerator):
|
||||
for j in range(2):
|
||||
index = index + 1
|
||||
self.add_unit(
|
||||
AirDefence.Flak18,
|
||||
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),
|
||||
@@ -111,14 +111,14 @@ class ColdWarFlakGenerator(AirDefenseGroupGenerator):
|
||||
|
||||
# Medium range guns
|
||||
self.add_unit(
|
||||
AirDefence.S_60_Type59_Artillery,
|
||||
AirDefence.AAA_S_60_57mm,
|
||||
"SHO#1",
|
||||
self.position.x - 40,
|
||||
self.position.y - 40,
|
||||
self.heading + 180,
|
||||
),
|
||||
self.add_unit(
|
||||
AirDefence.S_60_Type59_Artillery,
|
||||
AirDefence.AAA_S_60_57mm,
|
||||
"SHO#2",
|
||||
self.position.x + spacing * 2 + 40,
|
||||
self.position.y + spacing + 40,
|
||||
@@ -127,14 +127,14 @@ class ColdWarFlakGenerator(AirDefenseGroupGenerator):
|
||||
|
||||
# Short range guns
|
||||
self.add_unit(
|
||||
AirDefence.ZU_23_Emplacement_Closed,
|
||||
AirDefence.AAA_ZU_23_Closed_Emplacement,
|
||||
"SHO#3",
|
||||
self.position.x - 80,
|
||||
self.position.y - 40,
|
||||
self.heading + 180,
|
||||
),
|
||||
self.add_unit(
|
||||
AirDefence.ZU_23_Emplacement_Closed,
|
||||
AirDefence.AAA_ZU_23_Closed_Emplacement,
|
||||
"SHO#4",
|
||||
self.position.x + spacing * 2 + 80,
|
||||
self.position.y + spacing + 40,
|
||||
@@ -143,7 +143,7 @@ class ColdWarFlakGenerator(AirDefenseGroupGenerator):
|
||||
|
||||
# Add a P19 Radar for EWR
|
||||
self.add_unit(
|
||||
AirDefence.P_19_s_125_sr,
|
||||
AirDefence.SAM_P19_Flat_Face_SR__SA_2_3,
|
||||
"SR#0",
|
||||
self.position.x - 60,
|
||||
self.position.y - 20,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from typing import Type
|
||||
|
||||
from dcs.vehicles import AirDefence
|
||||
from dcs.unittype import VehicleType
|
||||
|
||||
@@ -7,7 +5,7 @@ from gen.sam.group_generator import GroupGenerator
|
||||
|
||||
|
||||
class EwrGenerator(GroupGenerator):
|
||||
unit_type: Type[VehicleType]
|
||||
unit_type: VehicleType
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
@@ -27,13 +25,13 @@ class EwrGenerator(GroupGenerator):
|
||||
class BoxSpringGenerator(EwrGenerator):
|
||||
"""1L13 "Box Spring" EWR."""
|
||||
|
||||
unit_type = AirDefence._1L13_EWR
|
||||
unit_type = AirDefence.EWR_1L13
|
||||
|
||||
|
||||
class TallRackGenerator(EwrGenerator):
|
||||
"""55G6 "Tall Rack" EWR."""
|
||||
|
||||
unit_type = AirDefence._55G6_EWR
|
||||
unit_type = AirDefence.EWR_55G6
|
||||
|
||||
|
||||
class DogEarGenerator(EwrGenerator):
|
||||
@@ -42,7 +40,7 @@ class DogEarGenerator(EwrGenerator):
|
||||
This is the SA-8 search radar, but used as an early warning radar.
|
||||
"""
|
||||
|
||||
unit_type = AirDefence.Dog_Ear_radar
|
||||
unit_type = AirDefence.MCC_SR_Sborka_Dog_Ear_SR
|
||||
|
||||
|
||||
class RolandEwrGenerator(EwrGenerator):
|
||||
@@ -51,7 +49,7 @@ class RolandEwrGenerator(EwrGenerator):
|
||||
This is the Roland search radar, but used as an early warning radar.
|
||||
"""
|
||||
|
||||
unit_type = AirDefence.Roland_Radar
|
||||
unit_type = AirDefence.SAM_Roland_EWR
|
||||
|
||||
|
||||
class FlatFaceGenerator(EwrGenerator):
|
||||
@@ -60,7 +58,7 @@ class FlatFaceGenerator(EwrGenerator):
|
||||
This is the SA-3 search radar, but used as an early warning radar.
|
||||
"""
|
||||
|
||||
unit_type = AirDefence.P_19_s_125_sr
|
||||
unit_type = AirDefence.SAM_P19_Flat_Face_SR__SA_2_3
|
||||
|
||||
|
||||
class PatriotEwrGenerator(EwrGenerator):
|
||||
@@ -69,7 +67,7 @@ class PatriotEwrGenerator(EwrGenerator):
|
||||
This is the Patriot search/track radar, but used as an early warning radar.
|
||||
"""
|
||||
|
||||
unit_type = AirDefence.Patriot_str
|
||||
unit_type = AirDefence.SAM_Patriot_STR
|
||||
|
||||
|
||||
class BigBirdGenerator(EwrGenerator):
|
||||
@@ -78,7 +76,7 @@ class BigBirdGenerator(EwrGenerator):
|
||||
This is the SA-10 track radar, but used as an early warning radar.
|
||||
"""
|
||||
|
||||
unit_type = AirDefence.S_300PS_64H6E_sr
|
||||
unit_type = AirDefence.SAM_SA_10_S_300_Grumble_Big_Bird_SR
|
||||
|
||||
|
||||
class SnowDriftGenerator(EwrGenerator):
|
||||
@@ -87,7 +85,7 @@ class SnowDriftGenerator(EwrGenerator):
|
||||
This is the SA-11 search radar, but used as an early warning radar.
|
||||
"""
|
||||
|
||||
unit_type = AirDefence.SA_11_Buk_SR_9S18M1
|
||||
unit_type = AirDefence.SAM_SA_11_Buk_Gadfly_Snow_Drift_SR
|
||||
|
||||
|
||||
class StraightFlushGenerator(EwrGenerator):
|
||||
@@ -96,7 +94,7 @@ class StraightFlushGenerator(EwrGenerator):
|
||||
This is the SA-6 search/track radar, but used as an early warning radar.
|
||||
"""
|
||||
|
||||
unit_type = AirDefence.Kub_1S91_str
|
||||
unit_type = AirDefence.SAM_SA_6_Kub_Straight_Flush_STR
|
||||
|
||||
|
||||
class HawkEwrGenerator(EwrGenerator):
|
||||
@@ -105,4 +103,4 @@ class HawkEwrGenerator(EwrGenerator):
|
||||
This is the Hawk search radar, but used as an early warning radar.
|
||||
"""
|
||||
|
||||
unit_type = AirDefence.Hawk_sr
|
||||
unit_type = AirDefence.SAM_Hawk_SR__AN_MPQ_50
|
||||
|
||||
@@ -18,7 +18,7 @@ class FreyaGenerator(AirDefenseGroupGenerator):
|
||||
|
||||
# TODO : would be better with the Concrete structure that is supposed to protect it
|
||||
self.add_unit(
|
||||
AirDefence.FuMG_401,
|
||||
AirDefence.EWR_FuMG_401_Freya_LZ,
|
||||
"EWR#1",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
@@ -28,7 +28,7 @@ class FreyaGenerator(AirDefenseGroupGenerator):
|
||||
positions = self.get_circular_position(4, launcher_distance=50, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(
|
||||
AirDefence.Flak38,
|
||||
AirDefence.AAA_Flak_Vierling_38_Quad_20mm,
|
||||
"AA#" + str(i),
|
||||
position[0],
|
||||
position[1],
|
||||
@@ -38,7 +38,7 @@ class FreyaGenerator(AirDefenseGroupGenerator):
|
||||
positions = self.get_circular_position(4, launcher_distance=100, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(
|
||||
AirDefence.Flak18,
|
||||
AirDefence.AAA_8_8cm_Flak_18,
|
||||
"AA#" + str(4 + i),
|
||||
position[0],
|
||||
position[1],
|
||||
@@ -47,58 +47,58 @@ class FreyaGenerator(AirDefenseGroupGenerator):
|
||||
|
||||
# Command/Logi
|
||||
self.add_unit(
|
||||
Unarmed.Kubelwagen_82,
|
||||
Unarmed.LUV_Kubelwagen_82,
|
||||
"Kubel#1",
|
||||
self.position.x - 20,
|
||||
self.position.y - 20,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
Unarmed.Sd_Kfz_7,
|
||||
Unarmed.Carrier_Sd_Kfz_7_Tractor,
|
||||
"Sdkfz#1",
|
||||
self.position.x + 20,
|
||||
self.position.y + 22,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
Unarmed.Sd_Kfz_2,
|
||||
Unarmed.LUV_Kettenrad,
|
||||
"Sdkfz#2",
|
||||
self.position.x - 22,
|
||||
self.position.y + 20,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
# Maschinensatz_33 and Kdo.g 40 Telemeter
|
||||
# PU_Maschinensatz_33 and Kdo.g 40 Telemeter
|
||||
self.add_unit(
|
||||
AirDefence.Maschinensatz_33,
|
||||
AirDefence.PU_Maschinensatz_33,
|
||||
"Energy#1",
|
||||
self.position.x + 20,
|
||||
self.position.y - 20,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
AirDefence.KDO_Mod40,
|
||||
AirDefence.AAA_SP_Kdo_G_40,
|
||||
"Telemeter#1",
|
||||
self.position.x + 20,
|
||||
self.position.y - 10,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
Infantry.Soldier_mauser98,
|
||||
Infantry.Infantry_Mauser_98,
|
||||
"Inf#1",
|
||||
self.position.x + 20,
|
||||
self.position.y - 14,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
Infantry.Soldier_mauser98,
|
||||
Infantry.Infantry_Mauser_98,
|
||||
"Inf#2",
|
||||
self.position.x + 20,
|
||||
self.position.y - 22,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
Infantry.Soldier_mauser98,
|
||||
Infantry.Infantry_Mauser_98,
|
||||
"Inf#3",
|
||||
self.position.x + 20,
|
||||
self.position.y - 24,
|
||||
|
||||
@@ -20,7 +20,7 @@ class AvengerGenerator(AirDefenseGroupGenerator):
|
||||
num_launchers = random.randint(2, 3)
|
||||
|
||||
self.add_unit(
|
||||
Unarmed.M_818,
|
||||
Unarmed.Truck_M818_6x6,
|
||||
"TRUCK",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
@@ -31,7 +31,7 @@ class AvengerGenerator(AirDefenseGroupGenerator):
|
||||
)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(
|
||||
AirDefence.M1097_Avenger,
|
||||
AirDefence.SAM_Avenger__Stinger,
|
||||
"SPAA#" + str(i),
|
||||
position[0],
|
||||
position[1],
|
||||
|
||||
@@ -20,7 +20,7 @@ class ChaparralGenerator(AirDefenseGroupGenerator):
|
||||
num_launchers = random.randint(2, 4)
|
||||
|
||||
self.add_unit(
|
||||
Unarmed.M_818,
|
||||
Unarmed.Truck_M818_6x6,
|
||||
"TRUCK",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
@@ -31,7 +31,7 @@ class ChaparralGenerator(AirDefenseGroupGenerator):
|
||||
)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(
|
||||
AirDefence.M48_Chaparral,
|
||||
AirDefence.SAM_Chaparral_M48,
|
||||
"SPAA#" + str(i),
|
||||
position[0],
|
||||
position[1],
|
||||
|
||||
@@ -18,7 +18,7 @@ class GepardGenerator(AirDefenseGroupGenerator):
|
||||
|
||||
def generate(self):
|
||||
self.add_unit(
|
||||
AirDefence.Gepard,
|
||||
AirDefence.SPAAA_Gepard,
|
||||
"SPAAA",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
@@ -26,14 +26,14 @@ class GepardGenerator(AirDefenseGroupGenerator):
|
||||
)
|
||||
if random.randint(0, 1) == 1:
|
||||
self.add_unit(
|
||||
AirDefence.Gepard,
|
||||
AirDefence.SPAAA_Gepard,
|
||||
"SPAAA2",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
Unarmed.M_818,
|
||||
Unarmed.Truck_M818_6x6,
|
||||
"TRUCK",
|
||||
self.position.x + 80,
|
||||
self.position.y,
|
||||
|
||||
@@ -2,6 +2,7 @@ import random
|
||||
from typing import Dict, Iterable, List, Optional, Sequence, Set, Type
|
||||
|
||||
from dcs.unitgroup import VehicleGroup
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
from game import Game
|
||||
from game.factions.faction import Faction
|
||||
@@ -103,6 +104,41 @@ SAM_MAP: Dict[str, Type[AirDefenseGroupGenerator]] = {
|
||||
}
|
||||
|
||||
|
||||
SAM_PRICES = {
|
||||
AirDefence.SAM_Hawk_Platoon_Command_Post__PCP: 35,
|
||||
AirDefence.AAA_ZU_23_Emplacement: 10,
|
||||
AirDefence.AAA_ZU_23_Closed_Emplacement: 10,
|
||||
AirDefence.SPAAA_ZU_23_2_Mounted_Ural_375: 10,
|
||||
AirDefence.SPAAA_ZU_23_2_Insurgent_Mounted_Ural_375: 10,
|
||||
AirDefence.AAA_ZU_23_Insurgent_Closed_Emplacement: 10,
|
||||
AirDefence.AAA_ZU_23_Insurgent_Emplacement: 10,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish: 10,
|
||||
AirDefence.SPAAA_Vulcan_M163: 15,
|
||||
AirDefence.SAM_Linebacker___Bradley_M6: 20,
|
||||
AirDefence.SAM_Rapier_LN: 20,
|
||||
AirDefence.SAM_Avenger__Stinger: 22,
|
||||
AirDefence.SPAAA_Gepard: 24,
|
||||
AirDefence.SAM_Roland_ADS: 40,
|
||||
AirDefence.SAM_Patriot_LN: 85,
|
||||
AirDefence.SAM_Patriot_EPP_III: 85,
|
||||
AirDefence.SAM_Chaparral_M48: 25,
|
||||
AirDefence.AAA_Bofors_40mm: 15,
|
||||
AirDefence.AAA_8_8cm_Flak_36: 15,
|
||||
AirDefence.SAM_SA_2_S_75_Guideline_LN: 30,
|
||||
AirDefence.SAM_SA_3_S_125_Goa_LN: 35,
|
||||
AirDefence.SAM_SA_6_Kub_Gainful_TEL: 45,
|
||||
AirDefence.SAM_SA_8_Osa_Gecko_TEL: 30,
|
||||
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL: 25,
|
||||
AirDefence.SAM_SA_10_S_300_Grumble_TEL_C: 80,
|
||||
AirDefence.SAM_SA_10_S_300_Grumble_C2: 80,
|
||||
AirDefence.SAM_SA_11_Buk_Gadfly_Fire_Dome_TEL: 60,
|
||||
AirDefence.SAM_SA_13_Strela_10M3_Gopher_TEL: 30,
|
||||
AirDefence.SAM_SA_15_Tor_Gauntlet: 40,
|
||||
AirDefence.SAM_SA_19_Tunguska_Grison: 35,
|
||||
AirDefence.HQ_7_Self_Propelled_LN: 35,
|
||||
}
|
||||
|
||||
|
||||
def get_faction_possible_sams_generator(
|
||||
faction: Faction,
|
||||
) -> List[Type[AirDefenseGroupGenerator]]:
|
||||
|
||||
@@ -19,21 +19,21 @@ class HawkGenerator(AirDefenseGroupGenerator):
|
||||
|
||||
def generate(self):
|
||||
self.add_unit(
|
||||
AirDefence.Hawk_sr,
|
||||
AirDefence.SAM_Hawk_SR__AN_MPQ_50,
|
||||
"SR",
|
||||
self.position.x + 20,
|
||||
self.position.y,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
AirDefence.Hawk_pcp,
|
||||
AirDefence.SAM_Hawk_Platoon_Command_Post__PCP,
|
||||
"PCP",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
AirDefence.Hawk_tr,
|
||||
AirDefence.SAM_Hawk_TR__AN_MPQ_46,
|
||||
"TR",
|
||||
self.position.x + 40,
|
||||
self.position.y,
|
||||
@@ -44,7 +44,7 @@ class HawkGenerator(AirDefenseGroupGenerator):
|
||||
aa_group = self.add_auxiliary_group("AA")
|
||||
self.add_unit_to_group(
|
||||
aa_group,
|
||||
AirDefence.Vulcan,
|
||||
AirDefence.SPAAA_Vulcan_M163,
|
||||
"AAA",
|
||||
self.position + Point(20, 30),
|
||||
self.heading,
|
||||
@@ -57,7 +57,7 @@ class HawkGenerator(AirDefenseGroupGenerator):
|
||||
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(
|
||||
AirDefence.Hawk_ln,
|
||||
AirDefence.SAM_Hawk_LN_M192,
|
||||
"LN#" + str(i),
|
||||
position[0],
|
||||
position[1],
|
||||
|
||||
@@ -19,14 +19,14 @@ class HQ7Generator(AirDefenseGroupGenerator):
|
||||
|
||||
def generate(self):
|
||||
self.add_unit(
|
||||
AirDefence.HQ_7_STR_SP,
|
||||
AirDefence.HQ_7_Self_Propelled_STR,
|
||||
"STR",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
AirDefence.HQ_7_LN_SP,
|
||||
AirDefence.HQ_7_Self_Propelled_LN,
|
||||
"LN",
|
||||
self.position.x + 20,
|
||||
self.position.y,
|
||||
@@ -37,14 +37,14 @@ class HQ7Generator(AirDefenseGroupGenerator):
|
||||
aa_group = self.add_auxiliary_group("AA")
|
||||
self.add_unit_to_group(
|
||||
aa_group,
|
||||
AirDefence.Ural_375_ZU_23,
|
||||
AirDefence.SPAAA_ZU_23_2_Mounted_Ural_375,
|
||||
"AAA1",
|
||||
self.position + Point(20, 30),
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit_to_group(
|
||||
aa_group,
|
||||
AirDefence.Ural_375_ZU_23,
|
||||
AirDefence.SPAAA_ZU_23_2_Mounted_Ural_375,
|
||||
"AAA2",
|
||||
self.position - Point(20, 30),
|
||||
self.heading,
|
||||
@@ -57,7 +57,7 @@ class HQ7Generator(AirDefenseGroupGenerator):
|
||||
)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(
|
||||
AirDefence.HQ_7_LN_SP,
|
||||
AirDefence.HQ_7_Self_Propelled_LN,
|
||||
"LN#" + str(i),
|
||||
position[0],
|
||||
position[1],
|
||||
|
||||
@@ -20,7 +20,7 @@ class LinebackerGenerator(AirDefenseGroupGenerator):
|
||||
num_launchers = random.randint(2, 4)
|
||||
|
||||
self.add_unit(
|
||||
Unarmed.M_818,
|
||||
Unarmed.Truck_M818_6x6,
|
||||
"TRUCK",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
@@ -31,7 +31,7 @@ class LinebackerGenerator(AirDefenseGroupGenerator):
|
||||
)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(
|
||||
AirDefence.M6_Linebacker,
|
||||
AirDefence.SAM_Linebacker___Bradley_M6,
|
||||
"M6#" + str(i),
|
||||
position[0],
|
||||
position[1],
|
||||
|
||||
@@ -20,35 +20,35 @@ class PatriotGenerator(AirDefenseGroupGenerator):
|
||||
def generate(self):
|
||||
# Command Post
|
||||
self.add_unit(
|
||||
AirDefence.Patriot_str,
|
||||
AirDefence.SAM_Patriot_STR,
|
||||
"STR",
|
||||
self.position.x + 30,
|
||||
self.position.y + 30,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
AirDefence.Patriot_AMG,
|
||||
AirDefence.SAM_Patriot_CR__AMG_AN_MRC_137,
|
||||
"MRC",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
AirDefence.Patriot_ECS,
|
||||
AirDefence.SAM_Patriot_ECS,
|
||||
"MSQ",
|
||||
self.position.x + 30,
|
||||
self.position.y,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
AirDefence.Patriot_cp,
|
||||
AirDefence.SAM_Patriot_C2_ICC,
|
||||
"ICC",
|
||||
self.position.x + 60,
|
||||
self.position.y,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
AirDefence.Patriot_EPP,
|
||||
AirDefence.SAM_Patriot_EPP_III,
|
||||
"EPP",
|
||||
self.position.x,
|
||||
self.position.y + 30,
|
||||
@@ -61,7 +61,7 @@ class PatriotGenerator(AirDefenseGroupGenerator):
|
||||
)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(
|
||||
AirDefence.Patriot_ln,
|
||||
AirDefence.SAM_Patriot_LN,
|
||||
"LN#" + str(i),
|
||||
position[0],
|
||||
position[1],
|
||||
@@ -77,7 +77,7 @@ class PatriotGenerator(AirDefenseGroupGenerator):
|
||||
for i, (x, y, heading) in enumerate(positions):
|
||||
self.add_unit_to_group(
|
||||
aa_group,
|
||||
AirDefence.Vulcan,
|
||||
AirDefence.SPAAA_Vulcan_M163,
|
||||
f"SPAAA#{i}",
|
||||
Point(x, y),
|
||||
heading,
|
||||
|
||||
@@ -18,14 +18,14 @@ class RapierGenerator(AirDefenseGroupGenerator):
|
||||
|
||||
def generate(self):
|
||||
self.add_unit(
|
||||
AirDefence.Rapier_fsa_blindfire_radar,
|
||||
AirDefence.SAM_Rapier_Blindfire_TR,
|
||||
"BT",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
AirDefence.Rapier_fsa_optical_tracker_unit,
|
||||
AirDefence.SAM_Rapier_Tracker,
|
||||
"OT",
|
||||
self.position.x + 20,
|
||||
self.position.y,
|
||||
@@ -39,7 +39,7 @@ class RapierGenerator(AirDefenseGroupGenerator):
|
||||
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(
|
||||
AirDefence.Rapier_fsa_launcher,
|
||||
AirDefence.SAM_Rapier_LN,
|
||||
"LN#" + str(i),
|
||||
position[0],
|
||||
position[1],
|
||||
|
||||
@@ -16,21 +16,21 @@ class RolandGenerator(AirDefenseGroupGenerator):
|
||||
|
||||
def generate(self):
|
||||
self.add_unit(
|
||||
AirDefence.Roland_Radar,
|
||||
AirDefence.SAM_Roland_EWR,
|
||||
"EWR",
|
||||
self.position.x + 40,
|
||||
self.position.y,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
AirDefence.Roland_ADS,
|
||||
AirDefence.SAM_Roland_ADS,
|
||||
"ADS",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
Unarmed.M_818,
|
||||
Unarmed.Truck_M818_6x6,
|
||||
"TRUCK",
|
||||
self.position.x + 80,
|
||||
self.position.y,
|
||||
|
||||
@@ -22,13 +22,13 @@ class SA10Generator(AirDefenseGroupGenerator):
|
||||
|
||||
def __init__(self, game: Game, ground_object: SamGroundObject):
|
||||
super().__init__(game, ground_object)
|
||||
self.sr1 = AirDefence.S_300PS_40B6MD_sr
|
||||
self.sr2 = AirDefence.S_300PS_64H6E_sr
|
||||
self.cp = AirDefence.S_300PS_54K6_cp
|
||||
self.tr1 = AirDefence.S_300PS_40B6M_tr
|
||||
self.tr2 = AirDefence.S_300PS_40B6M_tr
|
||||
self.ln1 = AirDefence.S_300PS_5P85C_ln
|
||||
self.ln2 = AirDefence.S_300PS_5P85D_ln
|
||||
self.sr1 = AirDefence.SAM_SA_10_S_300_Grumble_Clam_Shell_SR
|
||||
self.sr2 = AirDefence.SAM_SA_10_S_300_Grumble_Big_Bird_SR
|
||||
self.cp = AirDefence.SAM_SA_10_S_300_Grumble_C2
|
||||
self.tr1 = AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR
|
||||
self.tr2 = AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR
|
||||
self.ln1 = AirDefence.SAM_SA_10_S_300_Grumble_TEL_C
|
||||
self.ln2 = AirDefence.SAM_SA_10_S_300_Grumble_TEL_D
|
||||
|
||||
def generate(self):
|
||||
# Search Radar
|
||||
@@ -84,7 +84,7 @@ class SA10Generator(AirDefenseGroupGenerator):
|
||||
for i, (x, y, heading) in enumerate(positions):
|
||||
self.add_unit_to_group(
|
||||
aa_group,
|
||||
AirDefence.ZSU_23_4_Shilka,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
|
||||
f"AA#{i}",
|
||||
Point(x, y),
|
||||
heading,
|
||||
@@ -109,7 +109,7 @@ class Tier2SA10Generator(SA10Generator):
|
||||
for i, (x, y, heading) in enumerate(positions):
|
||||
self.add_unit_to_group(
|
||||
pd_group,
|
||||
AirDefence.Tor_9A331,
|
||||
AirDefence.SAM_SA_15_Tor_Gauntlet,
|
||||
f"PD#{i}",
|
||||
Point(x, y),
|
||||
heading,
|
||||
@@ -131,7 +131,7 @@ class Tier3SA10Generator(SA10Generator):
|
||||
for i, (x, y, heading) in enumerate(positions):
|
||||
self.add_unit_to_group(
|
||||
aa_group,
|
||||
AirDefence._2S6_Tunguska,
|
||||
AirDefence.SAM_SA_19_Tunguska_Grison,
|
||||
f"AA#{i}",
|
||||
Point(x, y),
|
||||
heading,
|
||||
@@ -146,7 +146,7 @@ class Tier3SA10Generator(SA10Generator):
|
||||
for i, (x, y, heading) in enumerate(positions):
|
||||
self.add_unit_to_group(
|
||||
pd_group,
|
||||
AirDefence.Tor_9A331,
|
||||
AirDefence.SAM_SA_15_Tor_Gauntlet,
|
||||
f"PD#{i}",
|
||||
Point(x, y),
|
||||
heading,
|
||||
|
||||
@@ -18,14 +18,14 @@ class SA11Generator(AirDefenseGroupGenerator):
|
||||
|
||||
def generate(self):
|
||||
self.add_unit(
|
||||
AirDefence.SA_11_Buk_SR_9S18M1,
|
||||
AirDefence.SAM_SA_11_Buk_Gadfly_Snow_Drift_SR,
|
||||
"SR",
|
||||
self.position.x + 20,
|
||||
self.position.y,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
AirDefence.SA_11_Buk_CC_9S470M1,
|
||||
AirDefence.SAM_SA_11_Buk_Gadfly_C2,
|
||||
"CC",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
@@ -39,7 +39,7 @@ class SA11Generator(AirDefenseGroupGenerator):
|
||||
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(
|
||||
AirDefence.SA_11_Buk_LN_9A310M1,
|
||||
AirDefence.SAM_SA_11_Buk_Gadfly_Fire_Dome_TEL,
|
||||
"LN#" + str(i),
|
||||
position[0],
|
||||
position[1],
|
||||
|
||||
@@ -18,14 +18,14 @@ class SA13Generator(AirDefenseGroupGenerator):
|
||||
|
||||
def generate(self):
|
||||
self.add_unit(
|
||||
Unarmed.UAZ_469,
|
||||
Unarmed.LUV_UAZ_469_Jeep,
|
||||
"UAZ",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
Unarmed.KAMAZ_Truck,
|
||||
Unarmed.Truck_KAMAZ_43101,
|
||||
"TRUCK",
|
||||
self.position.x + 40,
|
||||
self.position.y,
|
||||
@@ -38,7 +38,7 @@ class SA13Generator(AirDefenseGroupGenerator):
|
||||
)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(
|
||||
AirDefence.Strela_10M3,
|
||||
AirDefence.SAM_SA_13_Strela_10M3_Gopher_TEL,
|
||||
"LN#" + str(i),
|
||||
position[0],
|
||||
position[1],
|
||||
|
||||
@@ -16,21 +16,21 @@ class SA15Generator(AirDefenseGroupGenerator):
|
||||
|
||||
def generate(self):
|
||||
self.add_unit(
|
||||
AirDefence.Tor_9A331,
|
||||
AirDefence.SAM_SA_15_Tor_Gauntlet,
|
||||
"ADS",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
Unarmed.UAZ_469,
|
||||
Unarmed.LUV_UAZ_469_Jeep,
|
||||
"EWR",
|
||||
self.position.x + 40,
|
||||
self.position.y,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
Unarmed.KAMAZ_Truck,
|
||||
Unarmed.Truck_KAMAZ_43101,
|
||||
"TRUCK",
|
||||
self.position.x + 80,
|
||||
self.position.y,
|
||||
|
||||
@@ -17,14 +17,14 @@ class SA17Generator(AirDefenseGroupGenerator):
|
||||
|
||||
def generate(self):
|
||||
self.add_unit(
|
||||
AirDefence.SA_11_Buk_SR_9S18M1,
|
||||
AirDefence.SAM_SA_11_Buk_Gadfly_Snow_Drift_SR,
|
||||
"SR",
|
||||
self.position.x + 20,
|
||||
self.position.y,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
AirDefence.SA_11_Buk_CC_9S470M1,
|
||||
AirDefence.SAM_SA_11_Buk_Gadfly_C2,
|
||||
"CC",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
|
||||
@@ -21,7 +21,7 @@ class SA19Generator(AirDefenseGroupGenerator):
|
||||
|
||||
if num_launchers == 1:
|
||||
self.add_unit(
|
||||
AirDefence._2S6_Tunguska,
|
||||
AirDefence.SAM_SA_19_Tunguska_Grison,
|
||||
"LN#0",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
@@ -33,7 +33,7 @@ class SA19Generator(AirDefenseGroupGenerator):
|
||||
)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(
|
||||
AirDefence._2S6_Tunguska,
|
||||
AirDefence.SAM_SA_19_Tunguska_Grison,
|
||||
"LN#" + str(i),
|
||||
position[0],
|
||||
position[1],
|
||||
|
||||
@@ -18,14 +18,14 @@ class SA2Generator(AirDefenseGroupGenerator):
|
||||
|
||||
def generate(self):
|
||||
self.add_unit(
|
||||
AirDefence.P_19_s_125_sr,
|
||||
AirDefence.SAM_P19_Flat_Face_SR__SA_2_3,
|
||||
"SR",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
AirDefence.SNR_75V,
|
||||
AirDefence.SAM_SA_2_S_75_Fan_Song_TR,
|
||||
"TR",
|
||||
self.position.x + 20,
|
||||
self.position.y,
|
||||
@@ -39,7 +39,7 @@ class SA2Generator(AirDefenseGroupGenerator):
|
||||
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(
|
||||
AirDefence.S_75M_Volhov,
|
||||
AirDefence.SAM_SA_2_S_75_Guideline_LN,
|
||||
"LN#" + str(i),
|
||||
position[0],
|
||||
position[1],
|
||||
|
||||
@@ -18,14 +18,14 @@ class SA3Generator(AirDefenseGroupGenerator):
|
||||
|
||||
def generate(self):
|
||||
self.add_unit(
|
||||
AirDefence.P_19_s_125_sr,
|
||||
AirDefence.SAM_P19_Flat_Face_SR__SA_2_3,
|
||||
"SR",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
AirDefence.Snr_s_125_tr,
|
||||
AirDefence.SAM_SA_3_S_125_Low_Blow_TR,
|
||||
"TR",
|
||||
self.position.x + 20,
|
||||
self.position.y,
|
||||
@@ -39,7 +39,7 @@ class SA3Generator(AirDefenseGroupGenerator):
|
||||
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(
|
||||
AirDefence._5p73_s_125_ln,
|
||||
AirDefence.SAM_SA_3_S_125_Goa_LN,
|
||||
"LN#" + str(i),
|
||||
position[0],
|
||||
position[1],
|
||||
|
||||
@@ -18,7 +18,7 @@ class SA6Generator(AirDefenseGroupGenerator):
|
||||
|
||||
def generate(self):
|
||||
self.add_unit(
|
||||
AirDefence.Kub_1S91_str,
|
||||
AirDefence.SAM_SA_6_Kub_Straight_Flush_STR,
|
||||
"STR",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
@@ -32,7 +32,7 @@ class SA6Generator(AirDefenseGroupGenerator):
|
||||
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(
|
||||
AirDefence.Kub_2P25_ln,
|
||||
AirDefence.SAM_SA_6_Kub_Gainful_TEL,
|
||||
"LN#" + str(i),
|
||||
position[0],
|
||||
position[1],
|
||||
|
||||
@@ -16,14 +16,14 @@ class SA8Generator(AirDefenseGroupGenerator):
|
||||
|
||||
def generate(self):
|
||||
self.add_unit(
|
||||
AirDefence.Osa_9A33_ln,
|
||||
AirDefence.SAM_SA_8_Osa_Gecko_TEL,
|
||||
"OSA",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
AirDefence.SA_8_Osa_LD_9T217,
|
||||
AirDefence.SAM_SA_8_Osa_LD_9T217,
|
||||
"LD",
|
||||
self.position.x + 20,
|
||||
self.position.y,
|
||||
|
||||
@@ -18,14 +18,14 @@ class SA9Generator(AirDefenseGroupGenerator):
|
||||
|
||||
def generate(self):
|
||||
self.add_unit(
|
||||
Unarmed.UAZ_469,
|
||||
Unarmed.LUV_UAZ_469_Jeep,
|
||||
"UAZ",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
Unarmed.KAMAZ_Truck,
|
||||
Unarmed.Truck_KAMAZ_43101,
|
||||
"TRUCK",
|
||||
self.position.x + 40,
|
||||
self.position.y,
|
||||
@@ -38,7 +38,7 @@ class SA9Generator(AirDefenseGroupGenerator):
|
||||
)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(
|
||||
AirDefence.Strela_1_9P31,
|
||||
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL,
|
||||
"LN#" + str(i),
|
||||
position[0],
|
||||
position[1],
|
||||
|
||||
@@ -18,7 +18,7 @@ class VulcanGenerator(AirDefenseGroupGenerator):
|
||||
|
||||
def generate(self):
|
||||
self.add_unit(
|
||||
AirDefence.Vulcan,
|
||||
AirDefence.SPAAA_Vulcan_M163,
|
||||
"SPAAA",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
@@ -26,14 +26,14 @@ class VulcanGenerator(AirDefenseGroupGenerator):
|
||||
)
|
||||
if random.randint(0, 1) == 1:
|
||||
self.add_unit(
|
||||
AirDefence.Vulcan,
|
||||
AirDefence.SPAAA_Vulcan_M163,
|
||||
"SPAAA2",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
Unarmed.M_818,
|
||||
Unarmed.Truck_M818_6x6,
|
||||
"TRUCK",
|
||||
self.position.x + 80,
|
||||
self.position.y,
|
||||
|
||||
@@ -24,7 +24,7 @@ class ZSU23Generator(AirDefenseGroupGenerator):
|
||||
)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(
|
||||
AirDefence.ZSU_23_4_Shilka,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
|
||||
"SPAA#" + str(i),
|
||||
position[0],
|
||||
position[1],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user