Compare commits

..

26 Commits

Author SHA1 Message Date
Marcel
e6866ac245 Fix group name for EWRs.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1246
2021-06-18 16:54:49 -07:00
Dan Albert
aeb043e863 Revert "Update F-16 strike loadout to use GBU-31."
This reverts commit 04fa65107e.
2021-06-18 16:20:14 -07:00
Dan Albert
e33a4ec0c3 Revert "Update pydcs."
This reverts commit f5d4c04b75.
2021-06-18 16:20:02 -07:00
Dan Albert
6b310a10de Revert "Adapt to DCS update."
This reverts commit 3a11068259.
2021-06-18 16:19:38 -07:00
Dan Albert
9433138c20 Revert "Continue pydcs update"
This reverts commit a3d61ea286.
2021-06-18 16:19:34 -07:00
Khopa
a3d61ea286 Continue pydcs update 2021-06-18 01:06:09 +02:00
Dan Albert
3a11068259 Adapt to DCS update. 2021-06-17 22:41:14 +02:00
Dan Albert
f5d4c04b75 Update pydcs. 2021-06-17 22:40:35 +02:00
Dan Albert
04fa65107e Update F-16 strike loadout to use GBU-31. 2021-06-17 22:40:27 +02:00
Mustang-25
abc0c19bd1 Add remaining HAF Viper squadrons.
(cherry picked from commit 4bb8bbbad8)
2021-06-16 21:07:55 -07:00
Dan Albert
0d31de344d Fix Greek F-16 livery ID.
(cherry picked from commit 39adafb1be)
2021-06-16 20:52:55 -07:00
Dan Albert
28c0ae7802 Add a Greek F-16 squadron.
(cherry picked from commit e19bfcdd04)
2021-06-16 20:37:46 -07:00
Dan Albert
c0bef399ad Add the 191. Filo Turkish F-16 squadron.
(cherry picked from commit 6fde92f5ac)
2021-06-16 20:17:42 -07:00
Dan Albert
21681a8240 Import latest beacon data from DCS.
(cherry picked from commit 24884e4a77)
2021-06-16 19:46:58 -07:00
Dan Albert
bc0ac0cbfc Update Turkish faction.
Add a bunch of missing helicopters, some ground units, and remove the
KC-130 which they don't seem to use.

(cherry picked from commit 384be8ceae)
2021-06-16 19:20:53 -07:00
Dan Albert
7f508f0b2c Update the Greek faction.
Greece has C-130s and Patriots. They don't have the E-3 but they do have
AEW&C via the ER-99, which isn't in DCS so just use an E-3 to pretend.
Also remove the Hawk radar as an EWR since we have the P-19 which is
better.

(cherry picked from commit ee9a5e8482)
2021-06-16 19:20:53 -07:00
Dan Albert
ef9e957ef2 Fix incorrect conditional.
(cherry picked from commit 34453fa3be)
2021-06-16 17:24:23 -07:00
Dan Albert
7d08e1ee2c Make non-interactive map elements unobstructive.
This makes most of the lines and polygons on the map non-interactive so
they don't capture mouse events, and also makes the culling exclusion
zones unfilled so they don't obscure real map objects in dense areas.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1217

(cherry picked from commit f727712bfa)
2021-06-16 17:22:19 -07:00
Dan Albert
97ac6a0612 Note procurement fix in the changelog.
(cherry picked from commit 3bb974b9e0)
2021-06-16 17:10:43 -07:00
Dan Albert
83c311c853 Escape the JTAC zone name in the plugin data.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1218

(cherry picked from commit 021445216e)
2021-06-16 17:08:36 -07:00
Dan Albert
df97110546 Bump version to 3.1.0.
We're shipping features for the DCS update after all.
2021-06-16 17:08:36 -07:00
Schneefl0cke
f570cfc12e Fix procurement for factions that lack some units.
Fixes procurement for factions with no aircraft, no ground units, or no
tanks.

(cherry picked from commit c13bf3ccd1)
2021-06-16 16:38:58 -07:00
Dan Albert
70b4f75f25 Make squadron nicknames optional.
(cherry picked from commit 25c289deaa)
2021-06-12 21:43:45 -07:00
Dan Albert
1ac95438ce Include the micro version of the version string.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1209
2021-06-11 16:51:34 -07:00
Dan Albert
73ca6606e1 Update Northern Russia campaign.
https://github.com/dcs-liberation/dcs_liberation/issues/1206
(cherry picked from commit b2705c1a13)
2021-06-11 16:42:36 -07:00
Dan Albert
7a04cf5905 Update the Syria full campaign.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1182

(cherry picked from commit 1796c21f48)
2021-06-11 16:31:16 -07:00
774 changed files with 12072 additions and 13818 deletions

1
.gitignore vendored
View File

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

4
.gitmodules vendored Normal file
View File

@@ -0,0 +1,4 @@
[submodule "pydcs"]
path = pydcs
url = https://github.com/pydcs/dcs
branch = master

View File

@@ -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. 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. It is an external program that generates full and complex DCS missions and manage a persistent combat environment.
![Screenshot](https://user-images.githubusercontent.com/315852/120939254-0b4a9f80-c6cc-11eb-82f5-ce3f8d714bfe.png) ![Logo](https://i.imgur.com/4hq0rLq.png)
## Downloads ## Downloads

View File

@@ -1,94 +1,12 @@
# 4.1.0 # 3.1.0
Saves from 4.0.0 are compatible with 4.1.0.
## Features/Improvements ## Features/Improvements
* **[Campaign]** Air defense sites now generate a fixed number of launchers per type.
* **[Campaign]** Added support for Mariana Islands map.
* **[Campaign AI]** Adjustments to aircraft selection priorities for most mission types.
* **[Engine]** Support for DCS 2.7.4.9632 and newer, including the Marianas map, F-16 JSOWs, NASAMS, and Tin Shield EWR.
* **[Flight Planning]** CAP patrol altitudes are now set per-aircraft. By default the altitude will be set based on the aircraft's maximum speed.
* **[Flight Planning]** CAP patrol speeds are now set per-aircraft to be more suitable/sensible. By default the speed will be set based on the aircraft's maximum speed.
* **[Mission Generation]** Improvements for better support of the Skynet Plugin and long range SAMs are now acting as EWR
* **[Mods]** Support for version v1.5.0-Beta of Gripen mod. In-progress campaigns may need to re-plan Gripen flights to pick up updated loadouts.
* **[Plugins]** Increased time JTAC Autolase messages stay visible on the UI.
* **[Plugins]** Updated SkynetIADS to 2.2.0 (adds NASAMS support).
* **[UI]** Added ability to take notes and have those notes appear as a kneeboard page.
* **[UI]** Hovering over the weather information now dispalys the cloud base (meters and feet).
* **[UI]** Google search link added to unit information when there is no information provided.
* **[UI]** Control point name displayed with ground object group name on map.
* **[UI]** Buy or Replace will now show the correct price for generated ground objects like sams.
* **[UI]** Improved logging for frontline movement to be more descriptive about what happened and why.
* **[UI]** Brought ruler map module into source, which should fix file integrity issues with the module.
## Fixes
* **[Campaign]** Fixed the Silkworm generator to include launchers and not all radars.
* **[Data]** Fixed Introduction dates for targeting pods (ATFLIR and LITENING were both a few years too early).
* **[Data]** Removed SA-10 from Syria 2011 faction.
* **[Economy]** EWRs can now be bought and sold for the correct price and can no longer be used to generate money
* **[Flight Planning]** Helicopters are now correctly identified, and will fly ingress/CAS/BAI/egress and similar at low altitude.
* **[Flight Planning]** Fixed potential issue with angles > 360° or < 0° being generated when summing two angles.
* **[Mission Generation]** The lua data for other plugins is now generated correctly
* **[Mission Generation]** Fixed problem with opfor planning missions against sold ground objects like SAMs
* **[Mission Generation]** The legacy always-available tanker option no longer prevents mission creation.
* **[Mission Generation]** Prevent the creation of a transfer order with 0 units for a rare situtation when a point was captured.
* **[Mission Generation]** Planned transfers which will be impossible after a base capture will no longer prevent the mission result submit.
* **[Mission Generation]** Fix occasional KeyError preventing mission generation when all units of the same type in a convoy were killed.
* **[Mission Generation]** Fix for AAA Flak generator using Opel Blitz preventing the mission from being generated because duplicate unit names were used.
* **[Campaign AI]** Transport aircraft will now be bought only if necessary at control points which can produce ground units and are capable to operate transport aircraft.
* **[UI]** Statistics window tick marks are now always integers.
* **[UI]** Statistics window now shows the correct info for the turn
* **[UI]** Toggling custom loadout for an aircraft with no preset loadouts no longer breaks the flight.
# 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 ## Fixes
* **[Campaign AI]** Fix procurement for factions that lack some unit types. * **[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 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]** 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 # 3.0.0

22
game/data/aaa_db.py Normal file
View 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,
]

View File

@@ -4,37 +4,35 @@ from dcs.vehicles import AirDefence
class AlicCodes: class AlicCodes:
CODES = { CODES = {
AirDefence._1L13_EWR.id: 101, AirDefence.EWR_1L13.id: 101,
AirDefence._55G6_EWR.id: 102, AirDefence.EWR_55G6.id: 102,
AirDefence.S_300PS_40B6MD_sr.id: 103, AirDefence.SAM_SA_10_S_300_Grumble_Clam_Shell_SR.id: 103,
AirDefence.S_300PS_64H6E_sr.id: 104, AirDefence.SAM_SA_10_S_300_Grumble_Big_Bird_SR.id: 104,
AirDefence.SA_11_Buk_SR_9S18M1.id: 107, AirDefence.SAM_SA_11_Buk_Gadfly_Snow_Drift_SR.id: 107,
AirDefence.Kub_1S91_str.id: 108, AirDefence.SAM_SA_6_Kub_Straight_Flush_STR.id: 108,
AirDefence.Dog_Ear_radar.id: 109, AirDefence.MCC_SR_Sborka_Dog_Ear_SR.id: 109,
AirDefence.S_300PS_40B6M_tr.id: 110, AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR.id: 110,
AirDefence.SA_11_Buk_LN_9A310M1.id: 115, AirDefence.SAM_SA_11_Buk_Gadfly_Fire_Dome_TEL.id: 115,
AirDefence.Osa_9A33_ln.id: 117, AirDefence.SAM_SA_8_Osa_Gecko_TEL.id: 117,
AirDefence.Strela_10M3.id: 118, AirDefence.SAM_SA_13_Strela_10M3_Gopher_TEL.id: 118,
AirDefence.Tor_9A331.id: 119, AirDefence.SAM_SA_15_Tor_Gauntlet.id: 119,
AirDefence._2S6_Tunguska.id: 120, AirDefence.SAM_SA_19_Tunguska_Grison.id: 120,
AirDefence.ZSU_23_4_Shilka.id: 121, AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish.id: 121,
AirDefence.P_19_s_125_sr.id: 122, AirDefence.SAM_P19_Flat_Face_SR__SA_2_3.id: 122,
AirDefence.Snr_s_125_tr.id: 123, AirDefence.SAM_SA_3_S_125_Low_Blow_TR.id: 123,
AirDefence.Rapier_fsa_blindfire_radar.id: 124, AirDefence.SAM_Rapier_Blindfire_TR.id: 124,
AirDefence.Rapier_fsa_launcher.id: 125, AirDefence.SAM_Rapier_LN.id: 125,
AirDefence.SNR_75V.id: 126, AirDefence.SAM_SA_2_S_75_Fan_Song_TR.id: 126,
AirDefence.HQ_7_LN_SP.id: 127, AirDefence.HQ_7_Self_Propelled_LN.id: 127,
AirDefence.HQ_7_STR_SP.id: 128, AirDefence.HQ_7_Self_Propelled_STR.id: 128,
AirDefence.RLS_19J6.id: 130, AirDefence.SAM_Roland_ADS.id: 201,
AirDefence.Roland_ADS.id: 201, AirDefence.SAM_Patriot_STR.id: 202,
AirDefence.Patriot_str.id: 202, AirDefence.SAM_Hawk_SR__AN_MPQ_50.id: 203,
AirDefence.Hawk_sr.id: 203, AirDefence.SAM_Hawk_TR__AN_MPQ_46.id: 204,
AirDefence.Hawk_tr.id: 204, AirDefence.SAM_Roland_EWR.id: 205,
AirDefence.Roland_Radar.id: 205, AirDefence.SAM_Hawk_CWAR_AN_MPQ_55.id: 206,
AirDefence.Hawk_cwar.id: 206, AirDefence.SPAAA_Gepard.id: 207,
AirDefence.Gepard.id: 207, AirDefence.SPAAA_Vulcan_M163.id: 208,
AirDefence.Vulcan.id: 208,
AirDefence.NASAMS_Radar_MPQ64F1.id: 209,
} }
@classmethod @classmethod

View 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,
]

View File

@@ -1,17 +1,239 @@
from __future__ import annotations
from enum import unique, Enum 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 @unique
class GroundUnitClass(Enum): class GroundUnitClass(Enum):
Tank = "Tank" Tank = (
Atgm = "ATGM" "Tank",
Ifv = "IFV" (
Apc = "APC" Armor.MBT_T_55,
Artillery = "Artillery" Armor.MBT_T_72B,
Logistics = "Logistics" Armor.MBT_T_72B3,
Recon = "Recon" Armor.MBT_T_80U,
Infantry = "Infantry" Armor.MBT_T_90,
Shorads = "SHORADS" Armor.MBT_Leopard_2A4,
Manpads = "MANPADS" Armor.MBT_Leopard_2A4_Trs,
Armor.MBT_Leopard_2A5,
Armor.MBT_Leopard_2A6M,
Armor.MBT_Leopard_1A3,
Armor.MBT_Leclerc,
Armor.MBT_Challenger_II,
Armor.MBT_Chieftain_Mk_3,
Armor.MBT_M1A2_Abrams,
Armor.MBT_M60A3_Patton,
Armor.MBT_Merkava_IV,
Armor.ZTZ_96B,
# WW2
# Axis
Armor.Tk_PzIV_H,
Armor.SPG_Sturmpanzer_IV_Brummbar,
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
Armor.HT_Pz_Kpfw_VI_Tiger_I,
Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II,
# Allies
Armor.Tk_M4_Sherman,
Armor.CT_Centaur_IV,
Armor.CT_Cromwell_IV,
Armor.HIT_Churchill_VII,
# Mods
frenchpack.DIM__TOYOTA_BLUE,
frenchpack.DIM__TOYOTA_GREEN,
frenchpack.DIM__TOYOTA_DESERT,
frenchpack.DIM__KAMIKAZE,
frenchpack.AMX_30B2,
frenchpack.Leclerc_Serie_XXI,
),
)
Atgm = (
"ATGM",
(
Armor.ATGM_HMMWV,
Armor.ATGM_VAB_Mephisto,
Armor.ATGM_Stryker,
Armor.IFV_BMP_2,
# WW2 (Tank Destroyers)
# Axxis
Armor.SPG_StuG_III_Ausf__G,
Armor.SPG_StuG_IV,
Armor.SPG_Jagdpanzer_IV,
Armor.SPG_Jagdpanther_G1,
Armor.SPG_Sd_Kfz_184_Elefant,
# Allies
Armor.SPG_M10_GMC,
Armor.MT_M4A4_Sherman_Firefly,
# Mods
frenchpack.VBAE_CRAB_MMP,
frenchpack.VAB_MEPHISTO,
frenchpack.TRM_2000_PAMELA,
),
)
Ifv = (
"IFV",
(
Armor.IFV_BMP_3,
Armor.IFV_BMP_2,
Armor.IFV_BMP_1,
Armor.IFV_Marder,
Armor.IFV_Warrior,
Armor.SPG_Stryker_MGS,
Armor.IFV_M2A2_Bradley,
Armor.IFV_BMD_1,
Armor.ZBD_04A,
# Mods
frenchpack.VBAE_CRAB,
frenchpack.VAB_T20_13,
),
)
Apc = (
"APC",
(
Armor.IFV_M1126_Stryker_ICV,
Armor.APC_M113,
Armor.APC_BTR_80,
Armor.IFV_BTR_82A,
Armor.APC_MTLB,
Armor.APC_AAV_7_Amphibious,
Armor.APC_TPz_Fuchs,
Armor.APC_BTR_RD,
# WW2
Armor.APC_M2A1_Halftrack,
Armor.APC_Sd_Kfz_251_Halftrack,
# Mods
frenchpack.VAB__50,
frenchpack.VBL__50,
frenchpack.VBL_AANF1,
),
)
Artillery = (
"Artillery",
(
Artillery.Grad_MRL_FDDM__FC,
Artillery.MLRS_9A52_Smerch_HE_300mm,
Artillery.SPH_2S1_Gvozdika_122mm,
Artillery.SPH_2S3_Akatsia_152mm,
Artillery.MLRS_BM_21_Grad_122mm,
Artillery.MLRS_9K57_Uragan_BM_27_220mm,
Artillery.SPH_M109_Paladin_155mm,
Artillery.MLRS_M270_227mm,
Artillery.SPM_2S9_Nona_120mm_M,
Artillery.SPH_Dana_vz77_152mm,
Artillery.SPH_T155_Firtina_155mm,
Artillery.PLZ_05,
Artillery.SPH_2S19_Msta_152mm,
Artillery.MLRS_9A52_Smerch_CM_300mm,
# WW2
Artillery.SPG_M12_GMC_155mm,
),
)
Logistics = (
"Logistics",
(
Unarmed.Carrier_M30_Cargo,
Unarmed.Truck_M818_6x6,
Unarmed.Truck_KAMAZ_43101,
Unarmed.Truck_Ural_375,
Unarmed.Truck_GAZ_66,
Unarmed.Truck_GAZ_3307,
Unarmed.Truck_GAZ_3308,
Unarmed.Truck_Ural_4320_31_Arm_d,
Unarmed.Truck_Ural_4320T,
Unarmed.Truck_Opel_Blitz,
Unarmed.LUV_Kubelwagen_82,
Unarmed.Carrier_Sd_Kfz_7_Tractor,
Unarmed.LUV_Kettenrad,
Unarmed.Car_Willys_Jeep,
Unarmed.LUV_Land_Rover_109,
Unarmed.Truck_Land_Rover_101_FC,
# Mods
frenchpack.VBL,
frenchpack.VAB,
),
)
Recon = (
"Recon",
(
Armor.Scout_HMMWV,
Armor.Scout_Cobra,
Armor.LT_PT_76,
Armor.IFV_LAV_25,
Armor.Scout_BRDM_2,
# WW2
Armor.LT_Mk_VII_Tetrarch,
Armor.IFV_Sd_Kfz_234_2_Puma,
Armor.Car_M8_Greyhound_Armored,
Armor.Car_Daimler_Armored,
# Mods
frenchpack.ERC_90,
frenchpack.AMX_10RCR,
frenchpack.AMX_10RCR_SEPAR,
),
)
Infantry = (
"Infantry",
(
Infantry.Insurgent_AK_74,
Infantry.Infantry_AK_74,
Infantry.Infantry_M1_Garand,
Infantry.Infantry_Mauser_98,
Infantry.Infantry_SMLE_No_4_Mk_1,
Infantry.Infantry_M4_Georgia,
Infantry.Infantry_AK_74_Rus,
Infantry.Paratrooper_AKS,
Infantry.Paratrooper_RPG_16,
Infantry.Infantry_M249,
Infantry.Infantry_M4,
Infantry.Infantry_RPG,
),
)
Shorads = (
"SHORADS",
(
AirDefence.SPAAA_ZU_23_2_Mounted_Ural_375,
AirDefence.SPAAA_ZU_23_2_Insurgent_Mounted_Ural_375,
AirDefence.SPAAA_ZSU_57_2,
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
AirDefence.SAM_SA_8_Osa_Gecko_TEL,
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL,
AirDefence.SAM_SA_13_Strela_10M3_Gopher_TEL,
AirDefence.SAM_SA_15_Tor_Gauntlet,
AirDefence.SAM_SA_19_Tunguska_Grison,
AirDefence.SPAAA_Gepard,
AirDefence.SPAAA_Vulcan_M163,
AirDefence.SAM_Linebacker___Bradley_M6,
AirDefence.SAM_Chaparral_M48,
AirDefence.SAM_Avenger__Stinger,
AirDefence.SAM_Roland_ADS,
AirDefence.HQ_7_Self_Propelled_LN,
AirDefence.AAA_8_8cm_Flak_18,
AirDefence.AAA_8_8cm_Flak_36,
AirDefence.AAA_8_8cm_Flak_37,
AirDefence.AAA_8_8cm_Flak_41,
AirDefence.AAA_Bofors_40mm,
AirDefence.AAA_S_60_57mm,
AirDefence.AAA_M1_37mm,
AirDefence.AAA_QF_3_7,
),
)
def __init__(
self, class_name: str, unit_list: tuple[Type[VehicleType], ...]
) -> None:
self.class_name = class_name
self.unit_list = unit_list
def __contains__(self, unit_type: Type[VehicleType]) -> bool:
return unit_type in self.unit_list

View File

@@ -1,108 +1,108 @@
from dcs.ships import ( from dcs.ships import (
PIOTR, Battlecruiser_1144_2_Pyotr_Velikiy,
MOSCOW, Cruiser_1164_Moskva,
VINSON, CVN_70_Carl_Vinson,
CVN_71, CVN_71_Theodore_Roosevelt,
CVN_72, CVN_72_Abraham_Lincoln,
CVN_73, CVN_73_George_Washington,
Stennis, CVN_74_John_C__Stennis,
KUZNECOW, CV_1143_5_Admiral_Kuznetsov,
CV_1143_5, CV_1143_5_Admiral_Kuznetsov_2017,
NEUSTRASH, Frigate_11540_Neustrashimy,
ALBATROS, Corvette_1124_4_Grisha,
REZKY, Frigate_1135M_Rezky,
MOLNIYA, Corvette_1241_1_Molniya,
LHA_Tarawa, LHA_1_Tarawa,
PERRY, FFG_Oliver_Hazzard_Perry,
TICONDEROG, CG_Ticonderoga,
Type_052B, Type_052B_Destroyer,
Type_052C, Type_052C_Destroyer,
Type_054A, Type_054A_Frigate,
USS_Arleigh_Burke_IIa, DDG_Arleigh_Burke_IIa,
) )
from dcs.vehicles import AirDefence from dcs.vehicles import AirDefence
TELARS = { TELARS = {
AirDefence._2S6_Tunguska, AirDefence.SAM_SA_19_Tunguska_Grison,
AirDefence.SA_11_Buk_SR_9S18M1, AirDefence.SAM_SA_11_Buk_Gadfly_Fire_Dome_TEL,
AirDefence.Osa_9A33_ln, AirDefence.SAM_SA_8_Osa_Gecko_TEL,
AirDefence.Tor_9A331, AirDefence.SAM_SA_15_Tor_Gauntlet,
AirDefence.Roland_ADS, AirDefence.SAM_Roland_ADS,
} }
TRACK_RADARS = { TRACK_RADARS = {
AirDefence.Kub_1S91_str, AirDefence.SAM_SA_6_Kub_Straight_Flush_STR,
AirDefence.Snr_s_125_tr, AirDefence.SAM_SA_3_S_125_Low_Blow_TR,
AirDefence.S_300PS_40B6M_tr, AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR,
AirDefence.Hawk_tr, AirDefence.SAM_Hawk_TR__AN_MPQ_46,
AirDefence.Patriot_str, AirDefence.SAM_Patriot_STR,
AirDefence.SNR_75V, AirDefence.SAM_SA_2_S_75_Fan_Song_TR,
AirDefence.Rapier_fsa_blindfire_radar, AirDefence.SAM_Rapier_Blindfire_TR,
AirDefence.HQ_7_STR_SP, AirDefence.HQ_7_Self_Propelled_STR,
} }
LAUNCHER_TRACKER_PAIRS = { LAUNCHER_TRACKER_PAIRS = {
AirDefence.Kub_2P25_ln: AirDefence.Kub_1S91_str, AirDefence.SAM_SA_6_Kub_Gainful_TEL: AirDefence.SAM_SA_6_Kub_Straight_Flush_STR,
AirDefence._5p73_s_125_ln: AirDefence.Snr_s_125_tr, AirDefence.SAM_SA_3_S_125_Goa_LN: AirDefence.SAM_SA_3_S_125_Low_Blow_TR,
AirDefence.S_300PS_5P85C_ln: AirDefence.S_300PS_40B6M_tr, AirDefence.SAM_SA_10_S_300_Grumble_TEL_D: AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR,
AirDefence.S_300PS_5P85D_ln: AirDefence.S_300PS_40B6M_tr, AirDefence.SAM_SA_10_S_300_Grumble_TEL_C: AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR,
AirDefence.Hawk_ln: AirDefence.Hawk_tr, AirDefence.SAM_Hawk_LN_M192: AirDefence.SAM_Hawk_TR__AN_MPQ_46,
AirDefence.Patriot_ln: AirDefence.Patriot_str, AirDefence.SAM_Patriot_LN: AirDefence.SAM_Patriot_STR,
AirDefence.S_75M_Volhov: AirDefence.SNR_75V, AirDefence.SAM_SA_2_S_75_Guideline_LN: AirDefence.SAM_SA_2_S_75_Fan_Song_TR,
AirDefence.Rapier_fsa_launcher: AirDefence.Rapier_fsa_blindfire_radar, AirDefence.SAM_Rapier_LN: AirDefence.SAM_Rapier_Blindfire_TR,
AirDefence.HQ_7_LN_SP: AirDefence.HQ_7_STR_SP, AirDefence.HQ_7_Self_Propelled_LN: AirDefence.HQ_7_Self_Propelled_STR,
} }
UNITS_WITH_RADAR = { UNITS_WITH_RADAR = {
# Radars # Radars
AirDefence._2S6_Tunguska, AirDefence.SAM_SA_19_Tunguska_Grison,
AirDefence.SA_11_Buk_LN_9A310M1, AirDefence.SAM_SA_11_Buk_Gadfly_Fire_Dome_TEL,
AirDefence.Osa_9A33_ln, AirDefence.SAM_SA_8_Osa_Gecko_TEL,
AirDefence.Tor_9A331, AirDefence.SAM_SA_15_Tor_Gauntlet,
AirDefence.Gepard, AirDefence.SPAAA_Gepard,
AirDefence.Vulcan, AirDefence.SPAAA_Vulcan_M163,
AirDefence.Roland_ADS, AirDefence.SAM_Roland_ADS,
AirDefence.ZSU_23_4_Shilka, AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
AirDefence._1L13_EWR, AirDefence.EWR_1L13,
AirDefence.Kub_1S91_str, AirDefence.SAM_SA_6_Kub_Straight_Flush_STR,
AirDefence.S_300PS_40B6M_tr, AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR,
AirDefence.S_300PS_40B6MD_sr, AirDefence.SAM_SA_10_S_300_Grumble_Clam_Shell_SR,
AirDefence._55G6_EWR, AirDefence.EWR_55G6,
AirDefence.S_300PS_64H6E_sr, AirDefence.SAM_SA_10_S_300_Grumble_Big_Bird_SR,
AirDefence.SA_11_Buk_SR_9S18M1, AirDefence.SAM_SA_11_Buk_Gadfly_Snow_Drift_SR,
AirDefence.Dog_Ear_radar, AirDefence.MCC_SR_Sborka_Dog_Ear_SR,
AirDefence.Hawk_tr, AirDefence.SAM_Hawk_TR__AN_MPQ_46,
AirDefence.Hawk_sr, AirDefence.SAM_Hawk_SR__AN_MPQ_50,
AirDefence.Patriot_str, AirDefence.SAM_Patriot_STR,
AirDefence.Hawk_cwar, AirDefence.SAM_Hawk_CWAR_AN_MPQ_55,
AirDefence.P_19_s_125_sr, AirDefence.SAM_P19_Flat_Face_SR__SA_2_3,
AirDefence.Roland_Radar, AirDefence.SAM_Roland_EWR,
AirDefence.Snr_s_125_tr, AirDefence.SAM_SA_3_S_125_Low_Blow_TR,
AirDefence.SNR_75V, AirDefence.SAM_SA_2_S_75_Fan_Song_TR,
AirDefence.Rapier_fsa_blindfire_radar, AirDefence.SAM_Rapier_Blindfire_TR,
AirDefence.HQ_7_LN_SP, AirDefence.HQ_7_Self_Propelled_LN,
AirDefence.HQ_7_STR_SP, AirDefence.HQ_7_Self_Propelled_STR,
AirDefence.FuMG_401, AirDefence.EWR_FuMG_401_Freya_LZ,
AirDefence.FuSe_65, AirDefence.EWR_FuSe_65_Würzburg_Riese,
# Ships # Ships
VINSON, CVN_70_Carl_Vinson,
PERRY, FFG_Oliver_Hazzard_Perry,
TICONDEROG, CG_Ticonderoga,
ALBATROS, Corvette_1124_4_Grisha,
KUZNECOW, CV_1143_5_Admiral_Kuznetsov,
MOLNIYA, Corvette_1241_1_Molniya,
MOSCOW, Cruiser_1164_Moskva,
NEUSTRASH, Frigate_11540_Neustrashimy,
PIOTR, Battlecruiser_1144_2_Pyotr_Velikiy,
REZKY, Frigate_1135M_Rezky,
CV_1143_5, CV_1143_5_Admiral_Kuznetsov_2017,
Stennis, CVN_74_John_C__Stennis,
CVN_71, CVN_71_Theodore_Roosevelt,
CVN_72, CVN_72_Abraham_Lincoln,
CVN_73, CVN_73_George_Washington,
USS_Arleigh_Burke_IIa, DDG_Arleigh_Burke_IIa,
LHA_Tarawa, LHA_1_Tarawa,
Type_052B, Type_052B_Destroyer,
Type_054A, Type_054A_Frigate,
Type_052C, Type_052C_Destroyer,
} }

View File

@@ -4,15 +4,15 @@ import datetime
import inspect import inspect
import logging import logging
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass, field from dataclasses import dataclass
from typing import Dict, Iterator, Optional, Set, Tuple, cast, Any from typing import Dict, Iterator, Optional, Set, Tuple, Type, Union, cast
from dcs.unitgroup import FlyingGroup from dcs.unitgroup import FlyingGroup
from dcs.unittype import FlyingType
from dcs.weapons_data import Weapons, weapon_ids from dcs.weapons_data import Weapons, weapon_ids
from game.dcs.aircrafttype import AircraftType
PydcsWeapon = Any PydcsWeapon = Dict[str, Union[int, str]]
PydcsWeaponAssignment = Tuple[int, PydcsWeapon] PydcsWeaponAssignment = Tuple[int, PydcsWeapon]
@@ -21,8 +21,8 @@ class Weapon:
"""Wraps a pydcs weapon dict in a hashable type.""" """Wraps a pydcs weapon dict in a hashable type."""
cls_id: str cls_id: str
name: str = field(compare=False) name: str
weight: int = field(compare=False) weight: int
def available_on(self, date: datetime.date) -> bool: def available_on(self, date: datetime.date) -> bool:
introduction_year = WEAPON_INTRODUCTION_YEARS.get(self) introduction_year = WEAPON_INTRODUCTION_YEARS.get(self)
@@ -83,7 +83,7 @@ class Pylon:
# configuration. # configuration.
return weapon in self.allowed or weapon.cls_id == "<CLEAN>" return weapon in self.allowed or weapon.cls_id == "<CLEAN>"
def equip(self, group: FlyingGroup[Any], weapon: Weapon) -> None: def equip(self, group: FlyingGroup, weapon: Weapon) -> None:
if not self.can_equip(weapon): if not self.can_equip(weapon):
logging.error(f"Pylon {self.number} cannot equip {weapon.name}") logging.error(f"Pylon {self.number} cannot equip {weapon.name}")
group.load_pylon(self.make_pydcs_assignment(weapon), self.number) group.load_pylon(self.make_pydcs_assignment(weapon), self.number)
@@ -97,12 +97,12 @@ class Pylon:
yield weapon yield weapon
@classmethod @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. # In pydcs these are all arbitrary inner classes of the aircraft type.
# The only way to identify them is by their name. # The only way to identify them is by their name.
pylons = [ pylons = [
v 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") if inspect.isclass(v) and v.__name__.startswith("Pylon")
] ]
@@ -121,8 +121,8 @@ class Pylon:
return cls(number, allowed) return cls(number, allowed)
@classmethod @classmethod
def iter_pylons(cls, aircraft: AircraftType) -> Iterator[Pylon]: def iter_pylons(cls, aircraft: Type[FlyingType]) -> Iterator[Pylon]:
for pylon in sorted(list(aircraft.dcs_unit_type.pylons)): for pylon in sorted(list(aircraft.pylons)):
yield cls.for_aircraft(aircraft, pylon) yield cls.for_aircraft(aircraft, pylon)
@@ -888,16 +888,16 @@ WEAPON_INTRODUCTION_YEARS = {
Weapon.from_pydcs(Weapons.ALQ_184): 1989, Weapon.from_pydcs(Weapons.ALQ_184): 1989,
Weapon.from_pydcs(Weapons.AN_ALQ_164_DECM_Pod): 1984, Weapon.from_pydcs(Weapons.AN_ALQ_164_DECM_Pod): 1984,
# TGP Pods # TGP Pods
Weapon.from_pydcs(Weapons.AN_AAQ_28_LITENING___Targeting_Pod): 1999, Weapon.from_pydcs(Weapons.AN_AAQ_28_LITENING___Targeting_Pod): 1995,
Weapon.from_pydcs(Weapons.AN_AAQ_28_LITENING___Targeting_Pod_): 1999, Weapon.from_pydcs(Weapons.AN_AAQ_28_LITENING___Targeting_Pod_): 1995,
Weapon.from_pydcs(Weapons.AN_ASQ_228_ATFLIR___Targeting_Pod): 2003, Weapon.from_pydcs(Weapons.AN_ASQ_228_ATFLIR___Targeting_Pod): 1993,
Weapon.from_pydcs( Weapon.from_pydcs(
Weapons.AN_ASQ_173_Laser_Spot_Tracker_Strike_CAMera__LST_SCAM_ Weapons.AN_ASQ_173_Laser_Spot_Tracker_Strike_CAMera__LST_SCAM_
): 1993, ): 1993,
Weapon.from_pydcs(Weapons.AWW_13_DATALINK_POD): 1967, Weapon.from_pydcs(Weapons.AWW_13_DATALINK_POD): 1967,
Weapon.from_pydcs(Weapons.LANTIRN_Targeting_Pod): 1990, Weapon.from_pydcs(Weapons.LANTIRN_Targeting_Pod): 1985,
Weapon.from_pydcs(Weapons.Lantirn_F_16): 1990, Weapon.from_pydcs(Weapons.Lantirn_F_16): 1985,
Weapon.from_pydcs(Weapons.Lantirn_Target_Pod): 1990, Weapon.from_pydcs(Weapons.Lantirn_Target_Pod): 1985,
Weapon.from_pydcs(Weapons.Pavetack_F_111): 1982, Weapon.from_pydcs(Weapons.Pavetack_F_111): 1982,
# BLU-107 # BLU-107
Weapon.from_pydcs(Weapons.BLU_107___440lb_Anti_Runway_Penetrator_Bomb): 1983, Weapon.from_pydcs(Weapons.BLU_107___440lb_Anti_Runway_Penetrator_Bomb): 1983,

1356
game/db.py

File diff suppressed because it is too large Load Diff

View File

@@ -1,332 +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_OF_SOUND_AT_SEA_LEVEL,
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("speed", None)
return PatrolConfig(
feet(altitude) if altitude is not None else None,
knots(speed) if speed is not None else None,
)
# TODO: Split into PlaneType and HelicopterType?
@dataclass(frozen=True)
class AircraftType(UnitType[Type[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)
@property
def preferred_patrol_altitude(self) -> Distance:
if self.patrol_altitude is not None:
return self.patrol_altitude
else:
# Estimate based on max speed.
# Aircaft with max speed 600 kph will prefer patrol at 10 000 ft
# Aircraft with max speed 2800 kph will prefer pratrol at 33 000 ft
altitude_for_lowest_speed = feet(10 * 1000)
altitude_for_highest_speed = feet(33 * 1000)
lowest_speed = kph(600)
highest_speed = kph(2800)
factor = (self.max_speed - lowest_speed).kph / (
highest_speed - lowest_speed
).kph
altitude = (
altitude_for_lowest_speed
+ (altitude_for_highest_speed - altitude_for_lowest_speed) * factor
)
logging.debug(
f"Preferred patrol altitude for {self.dcs_unit_type.id}: {altitude.feet}"
)
rounded_altitude = feet(round(1000 * round(altitude.feet / 1000)))
return max(
altitude_for_lowest_speed,
min(altitude_for_highest_speed, rounded_altitude),
)
def preferred_patrol_speed(self, altitude: Distance) -> Speed:
"""Preferred true airspeed when patrolling"""
if self.patrol_speed is not None:
return self.patrol_speed
else:
# Estimate based on max speed.
max_speed = self.max_speed
if max_speed > SPEED_OF_SOUND_AT_SEA_LEVEL * 1.6:
# Fast airplanes, should manage pretty high patrol speed
return (
Speed.from_mach(0.85, altitude)
if altitude.feet > 20000
else Speed.from_mach(0.7, altitude)
)
elif max_speed > SPEED_OF_SOUND_AT_SEA_LEVEL * 1.2:
# Medium-fast like F/A-18C
return (
Speed.from_mach(0.8, altitude)
if altitude.feet > 20000
else Speed.from_mach(0.65, altitude)
)
elif max_speed > SPEED_OF_SOUND_AT_SEA_LEVEL * 0.7:
# Semi-fast like airliners or similar
return (
Speed.from_mach(0.5, altitude)
if altitude.feet > 20000
else Speed.from_mach(0.4, altitude)
)
else:
# Slow like warbirds or helicopters
# Use whichever is slowest - mach 0.35 or 70% of max speed
logging.debug(f"{self.name} max_speed * 0.7 is {max_speed * 0.7}")
return min(Speed.from_mach(0.35, altitude), max_speed * 0.7)
def alloc_flight_radio(self, radio_registry: RadioRegistry) -> RadioFrequency:
from gen.radios import ChannelInUseError, kHz
if self.intra_flight_radio is not None:
return radio_registry.alloc_for_radio(self.intra_flight_radio)
# The default radio frequency is set in megahertz. For some aircraft, it is a
# floating point value. For all current aircraft, adjusting to kilohertz will be
# sufficient to convert to an integer.
in_khz = float(self.dcs_unit_type.radio_frequency) * 1000
if not in_khz.is_integer():
logging.warning(
f"Found unexpected sub-kHz default radio for {self}: {in_khz} kHz. "
"Truncating to integer. The truncated frequency may not be valid for "
"the aircraft."
)
freq = kHz(int(in_khz))
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",
f"No data. <a href=\"https://google.com/search?q=DCS+{variant.replace(' ', '+')}\"><span style=\"color:#FFFFFF\">Google {variant}</span></a>",
),
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,
)

View File

@@ -1,100 +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[Type[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",
f"No data. <a href=\"https://google.com/search?q=DCS+{variant.replace(' ', '+')}\"><span style=\"color:#FFFFFF\">Google {variant}</span></a>",
),
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),
)

View File

@@ -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=Type[DcsUnitType])
@dataclass(frozen=True)
class UnitType(Generic[DcsUnitTypeT]):
dcs_unit_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)

View File

@@ -14,13 +14,13 @@ from typing import (
Dict, Dict,
Iterator, Iterator,
List, List,
Type,
TYPE_CHECKING, TYPE_CHECKING,
Union,
) )
from dcs.unittype import FlyingType, UnitType
from game import db from game import db
from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
from game.theater import Airfield, ControlPoint from game.theater import Airfield, ControlPoint
from game.transfers import CargoShip from game.transfers import CargoShip
from game.unitmap import ( from game.unitmap import (
@@ -49,8 +49,8 @@ class AirLosses:
def losses(self) -> Iterator[FlyingUnit]: def losses(self) -> Iterator[FlyingUnit]:
return itertools.chain(self.player, self.enemy) return itertools.chain(self.player, self.enemy)
def by_type(self, player: bool) -> Dict[AircraftType, int]: def by_type(self, player: bool) -> Dict[Type[FlyingType], int]:
losses_by_type: Dict[AircraftType, int] = defaultdict(int) losses_by_type: Dict[Type[FlyingType], int] = defaultdict(int)
losses = self.player if player else self.enemy losses = self.player if player else self.enemy
for loss in losses: for loss in losses:
losses_by_type[loss.flight.unit_type] += 1 losses_by_type[loss.flight.unit_type] += 1
@@ -78,8 +78,8 @@ class GroundLosses:
player_airlifts: List[AirliftUnits] = field(default_factory=list) player_airlifts: List[AirliftUnits] = field(default_factory=list)
enemy_airlifts: List[AirliftUnits] = field(default_factory=list) enemy_airlifts: List[AirliftUnits] = field(default_factory=list)
player_ground_objects: List[GroundObjectUnit[Any]] = field(default_factory=list) player_ground_objects: List[GroundObjectUnit] = field(default_factory=list)
enemy_ground_objects: List[GroundObjectUnit[Any]] = field(default_factory=list) enemy_ground_objects: List[GroundObjectUnit] = field(default_factory=list)
player_buildings: List[Building] = field(default_factory=list) player_buildings: List[Building] = field(default_factory=list)
enemy_buildings: List[Building] = field(default_factory=list) enemy_buildings: List[Building] = field(default_factory=list)
@@ -105,9 +105,8 @@ class StateData:
#: Names of vehicle (and ship) units that were killed during the mission. #: Names of vehicle (and ship) units that were killed during the mission.
killed_ground_units: List[str] killed_ground_units: List[str]
#: List of descriptions of destroyed statics. Format of each element is a mapping of #: Names of static units that were destroyed during the mission.
#: the coordinate type ("x", "y", "z", "type", "orientation") to the value. destroyed_statics: List[str]
destroyed_statics: List[dict[str, Union[float, str]]]
#: Mangled names of bases that were captured during the mission. #: Mangled names of bases that were captured during the mission.
base_capture_events: List[str] base_capture_events: List[str]
@@ -166,7 +165,7 @@ class Debriefing:
yield from self.ground_losses.enemy_airlifts yield from self.ground_losses.enemy_airlifts
@property @property
def ground_object_losses(self) -> Iterator[GroundObjectUnit[Any]]: def ground_object_losses(self) -> Iterator[GroundObjectUnit]:
yield from self.ground_losses.player_ground_objects yield from self.ground_losses.player_ground_objects
yield from self.ground_losses.enemy_ground_objects yield from self.ground_losses.enemy_ground_objects
@@ -183,8 +182,8 @@ class Debriefing:
def casualty_count(self, control_point: ControlPoint) -> int: def casualty_count(self, control_point: ControlPoint) -> int:
return len([x for x in self.front_line_losses if x.origin == control_point]) return len([x for x in self.front_line_losses if x.origin == control_point])
def front_line_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]: def front_line_losses_by_type(self, player: bool) -> Dict[Type[UnitType], int]:
losses_by_type: dict[GroundUnitType, int] = defaultdict(int) losses_by_type: Dict[Type[UnitType], int] = defaultdict(int)
if player: if player:
losses = self.ground_losses.player_front_line losses = self.ground_losses.player_front_line
else: else:
@@ -193,8 +192,8 @@ class Debriefing:
losses_by_type[loss.unit_type] += 1 losses_by_type[loss.unit_type] += 1
return losses_by_type return losses_by_type
def convoy_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]: def convoy_losses_by_type(self, player: bool) -> Dict[Type[UnitType], int]:
losses_by_type: dict[GroundUnitType, int] = defaultdict(int) losses_by_type: Dict[Type[UnitType], int] = defaultdict(int)
if player: if player:
losses = self.ground_losses.player_convoy losses = self.ground_losses.player_convoy
else: else:
@@ -203,8 +202,8 @@ class Debriefing:
losses_by_type[loss.unit_type] += 1 losses_by_type[loss.unit_type] += 1
return losses_by_type return losses_by_type
def cargo_ship_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]: def cargo_ship_losses_by_type(self, player: bool) -> Dict[Type[UnitType], int]:
losses_by_type: dict[GroundUnitType, int] = defaultdict(int) losses_by_type: Dict[Type[UnitType], int] = defaultdict(int)
if player: if player:
ships = self.ground_losses.player_cargo_ships ships = self.ground_losses.player_cargo_ships
else: else:
@@ -214,8 +213,8 @@ class Debriefing:
losses_by_type[unit_type] += count losses_by_type[unit_type] += count
return losses_by_type return losses_by_type
def airlift_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]: def airlift_losses_by_type(self, player: bool) -> Dict[Type[UnitType], int]:
losses_by_type: dict[GroundUnitType, int] = defaultdict(int) losses_by_type: Dict[Type[UnitType], int] = defaultdict(int)
if player: if player:
losses = self.ground_losses.player_airlifts losses = self.ground_losses.player_airlifts
else: else:
@@ -372,38 +371,32 @@ class PollDebriefingFileThread(threading.Thread):
self.game = game self.game = game
self.unit_map = unit_map self.unit_map = unit_map
def stop(self) -> None: def stop(self):
self._stop_event.set() self._stop_event.set()
def stopped(self) -> bool: def stopped(self):
return self._stop_event.is_set() return self._stop_event.is_set()
def run(self) -> None: def run(self):
if os.path.isfile("state.json"): if os.path.isfile("state.json"):
last_modified = os.path.getmtime("state.json") last_modified = os.path.getmtime("state.json")
else: else:
last_modified = 0 last_modified = 0
while not self.stopped(): while not self.stopped():
try: if (
if ( os.path.isfile("state.json")
os.path.isfile("state.json") and os.path.getmtime("state.json") > last_modified
and os.path.getmtime("state.json") > last_modified ):
): with open("state.json", "r") as json_file:
with open("state.json", "r") as json_file: json_data = json.load(json_file)
json_data = json.load(json_file) debriefing = Debriefing(json_data, self.game, self.unit_map)
debriefing = Debriefing(json_data, self.game, self.unit_map) self.callback(debriefing)
self.callback(debriefing) break
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."
)
time.sleep(5) time.sleep(5)
def wait_for_debriefing( def wait_for_debriefing(
callback: Callable[[Debriefing], None], game: Game, unit_map: UnitMap callback: Callable[[Debriefing], None], game: Game, unit_map
) -> PollDebriefingFileThread: ) -> PollDebriefingFileThread:
thread = PollDebriefingFileThread(callback, game, unit_map) thread = PollDebriefingFileThread(callback, game, unit_map)
thread.start() thread.start()

View File

@@ -1,10 +1,14 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING
from .event import Event from .event import Event
if TYPE_CHECKING:
from game.theater import ConflictTheater
class AirWarEvent(Event): class AirWarEvent(Event):
"""Event handler for the air battle""" """Event handler for the air battle"""
def __str__(self) -> str: def __str__(self):
return "AirWar" return "AirWar"

View File

@@ -5,6 +5,7 @@ from typing import List, TYPE_CHECKING, Type
from dcs.mapping import Point from dcs.mapping import Point
from dcs.task import Task from dcs.task import Task
from dcs.unittype import VehicleType
from game import persistency from game import persistency
from game.debriefing import AirLosses, Debriefing from game.debriefing import AirLosses, Debriefing
@@ -13,7 +14,6 @@ from game.operation.operation import Operation
from game.theater import ControlPoint from game.theater import ControlPoint
from gen import AirTaskingOrder from gen import AirTaskingOrder
from gen.ground_forces.combat_stance import CombatStance from gen.ground_forces.combat_stance import CombatStance
from ..dcs.groundunittype import GroundUnitType
from ..unitmap import UnitMap from ..unitmap import UnitMap
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -37,13 +37,13 @@ class Event:
def __init__( def __init__(
self, self,
game: Game, game,
from_cp: ControlPoint, from_cp: ControlPoint,
target_cp: ControlPoint, target_cp: ControlPoint,
location: Point, location: Point,
attacker_name: str, attacker_name: str,
defender_name: str, defender_name: str,
) -> None: ):
self.game = game self.game = game
self.from_cp = from_cp self.from_cp = from_cp
self.to_cp = target_cp self.to_cp = target_cp
@@ -53,7 +53,7 @@ class Event:
@property @property
def is_player_attacking(self) -> bool: def is_player_attacking(self) -> bool:
return self.attacker_name == self.game.player_faction.name return self.attacker_name == self.game.player_name
@property @property
def tasks(self) -> List[Type[Task]]: def tasks(self) -> List[Type[Task]]:
@@ -122,7 +122,7 @@ class Event:
def commit_air_losses(self, debriefing: Debriefing) -> None: def commit_air_losses(self, debriefing: Debriefing) -> None:
for loss in debriefing.air_losses.losses: for loss in debriefing.air_losses.losses:
if loss.pilot is not None and ( if (
not loss.pilot.player not loss.pilot.player
or not self.game.settings.invulnerable_player_pilots or not self.game.settings.invulnerable_player_pilots
): ):
@@ -219,10 +219,10 @@ class Event:
for loss in debriefing.ground_object_losses: for loss in debriefing.ground_object_losses:
# TODO: This should be stored in the TGO, not in the pydcs Group. # TODO: This should be stored in the TGO, not in the pydcs Group.
if not hasattr(loss.group, "units_losts"): if not hasattr(loss.group, "units_losts"):
loss.group.units_losts = [] # type: ignore loss.group.units_losts = []
loss.group.units.remove(loss.unit) loss.group.units.remove(loss.unit)
loss.group.units_losts.append(loss.unit) # type: ignore loss.group.units_losts.append(loss.unit)
def commit_building_losses(self, debriefing: Debriefing) -> None: def commit_building_losses(self, debriefing: Debriefing) -> None:
for loss in debriefing.building_losses: for loss in debriefing.building_losses:
@@ -264,7 +264,7 @@ class Event:
except Exception: except Exception:
logging.exception(f"Could not process base capture {captured}") logging.exception(f"Could not process base capture {captured}")
def commit(self, debriefing: Debriefing) -> None: def commit(self, debriefing: Debriefing):
logging.info("Committing mission results") logging.info("Committing mission results")
self.commit_air_losses(debriefing) self.commit_air_losses(debriefing)
@@ -297,16 +297,15 @@ class Event:
delta = 0.0 delta = 0.0
player_won = True player_won = True
status_msg: str = ""
ally_casualties = debriefing.casualty_count(cp) ally_casualties = debriefing.casualty_count(cp)
enemy_casualties = debriefing.casualty_count(enemy_cp) enemy_casualties = debriefing.casualty_count(enemy_cp)
ally_units_alive = cp.base.total_armor ally_units_alive = cp.base.total_armor
enemy_units_alive = enemy_cp.base.total_armor enemy_units_alive = enemy_cp.base.total_armor
print(f"Remaining allied units: {ally_units_alive}") print(ally_units_alive)
print(f"Remaining enemy units: {enemy_units_alive}") print(enemy_units_alive)
print(f"Allied casualties {ally_casualties}") print(ally_casualties)
print(f"Enemy casualties {enemy_casualties}") print(enemy_casualties)
ratio = (1.0 + enemy_casualties) / (1.0 + ally_casualties) ratio = (1.0 + enemy_casualties) / (1.0 + ally_casualties)
@@ -319,31 +318,24 @@ class Event:
if ally_units_alive == 0: if ally_units_alive == 0:
player_won = False player_won = False
delta = STRONG_DEFEAT_INFLUENCE delta = STRONG_DEFEAT_INFLUENCE
status_msg = f"No allied units alive at {cp.name}-{enemy_cp.name} frontline. Allied ground forces suffer a strong defeat."
elif enemy_units_alive == 0: elif enemy_units_alive == 0:
player_won = True player_won = True
delta = STRONG_DEFEAT_INFLUENCE delta = STRONG_DEFEAT_INFLUENCE
status_msg = f"No enemy units alive at {cp.name}-{enemy_cp.name} frontline. Allied ground forces win a strong victory."
elif cp.stances[enemy_cp.id] == CombatStance.RETREAT: elif cp.stances[enemy_cp.id] == CombatStance.RETREAT:
player_won = False player_won = False
delta = STRONG_DEFEAT_INFLUENCE delta = STRONG_DEFEAT_INFLUENCE
status_msg = f"Allied forces are retreating along the {cp.name}-{enemy_cp.name} frontline, suffering a strong defeat."
else: else:
if enemy_casualties > ally_casualties: if enemy_casualties > ally_casualties:
player_won = True player_won = True
if cp.stances[enemy_cp.id] == CombatStance.BREAKTHROUGH: if cp.stances[enemy_cp.id] == CombatStance.BREAKTHROUGH:
delta = STRONG_DEFEAT_INFLUENCE delta = STRONG_DEFEAT_INFLUENCE
status_msg = f"Allied forces break through the {cp.name}-{enemy_cp.name} frontline, winning a strong victory"
else: else:
if ratio > 3: if ratio > 3:
delta = STRONG_DEFEAT_INFLUENCE delta = STRONG_DEFEAT_INFLUENCE
status_msg = f"Enemy casualties massively outnumber allied casualties along the {cp.name}-{enemy_cp.name} frontline. Allied forces win a strong victory."
elif ratio < 1.5: elif ratio < 1.5:
delta = MINOR_DEFEAT_INFLUENCE delta = MINOR_DEFEAT_INFLUENCE
status_msg = f"Enemy casualties minorly outnumber allied casualties along the {cp.name}-{enemy_cp.name} frontline. Allied forces win a minor victory."
else: else:
delta = DEFEAT_INFLUENCE delta = DEFEAT_INFLUENCE
status_msg = f"Enemy casualties outnumber allied casualties along the {cp.name}-{enemy_cp.name} frontline. Allied forces claim a victory."
elif ally_casualties > enemy_casualties: elif ally_casualties > enemy_casualties:
if ( if (
@@ -353,66 +345,54 @@ class Event:
# Even with casualties if the enemy is overwhelmed, they are going to lose ground # Even with casualties if the enemy is overwhelmed, they are going to lose ground
player_won = True player_won = True
delta = MINOR_DEFEAT_INFLUENCE delta = MINOR_DEFEAT_INFLUENCE
status_msg = f"Despite suffering losses, allied forces still outnumber enemy forces along the {cp.name}-{enemy_cp.name} frontline. Due to allied force's aggressive posture, allied forces claim a minor victory."
elif ( elif (
ally_units_alive > 3 * enemy_units_alive ally_units_alive > 3 * enemy_units_alive
and player_aggresive and player_aggresive
): ):
player_won = True player_won = True
delta = STRONG_DEFEAT_INFLUENCE delta = STRONG_DEFEAT_INFLUENCE
status_msg = f"Despite suffering losses, allied forces still heavily outnumber enemy forces along the {cp.name}-{enemy_cp.name} frontline. Due to allied force's aggressive posture, allied forces claim a major victory."
else: else:
# But if the enemy is not outnumbered, we lose # But is the enemy is not outnumbered, we lose
player_won = False player_won = False
if cp.stances[enemy_cp.id] == CombatStance.BREAKTHROUGH: if cp.stances[enemy_cp.id] == CombatStance.BREAKTHROUGH:
delta = STRONG_DEFEAT_INFLUENCE delta = STRONG_DEFEAT_INFLUENCE
status_msg = f"Allied casualties outnumber enemy casualties along the {cp.name}-{enemy_cp.name} frontline. Allied forces have overextended themselves, suffering a major defeat."
else: else:
delta = DEFEAT_INFLUENCE delta = STRONG_DEFEAT_INFLUENCE
status_msg = f"Allied casualties outnumber enemy casualties along the {cp.name}-{enemy_cp.name} frontline. Allied forces suffer a defeat."
# No progress with defensive strategies # No progress with defensive strategies
if player_won and cp.stances[enemy_cp.id] in [ if player_won and cp.stances[enemy_cp.id] in [
CombatStance.DEFENSIVE, CombatStance.DEFENSIVE,
CombatStance.AMBUSH, CombatStance.AMBUSH,
]: ]:
print( print("Defensive stance, progress is limited")
f"Allied forces have adopted a defensive stance along the {cp.name}-{enemy_cp.name} "
f"frontline, making only limited progress."
)
delta = MINOR_DEFEAT_INFLUENCE delta = MINOR_DEFEAT_INFLUENCE
# Handle the case where there are no casualties at all on either side but both sides still have units if player_won:
if delta == 0.0: print(cp.name + " won ! factor > " + str(delta))
print(status_msg) cp.base.affect_strength(delta)
enemy_cp.base.affect_strength(-delta)
info = Information( info = Information(
"Frontline Report", "Frontline Report",
f"Our ground forces from {cp.name} reached a stalemate with enemy forces from {enemy_cp.name}.", "Our ground forces from "
+ cp.name
+ " are making progress toward "
+ enemy_cp.name,
self.game.turn, self.game.turn,
) )
self.game.informations.append(info) self.game.informations.append(info)
else: else:
if player_won: print(cp.name + " lost ! factor > " + str(delta))
print(status_msg) enemy_cp.base.affect_strength(delta)
cp.base.affect_strength(delta) cp.base.affect_strength(-delta)
enemy_cp.base.affect_strength(-delta) info = Information(
info = Information( "Frontline Report",
"Frontline Report", "Our ground forces from "
f"Our ground forces from {cp.name} are making progress toward {enemy_cp.name}. {status_msg}", + cp.name
self.game.turn, + " are losing ground against the enemy forces from "
) + enemy_cp.name,
self.game.informations.append(info) self.game.turn,
else: )
print(status_msg) self.game.informations.append(info)
enemy_cp.base.affect_strength(delta)
cp.base.affect_strength(-delta)
info = Information(
"Frontline Report",
f"Our ground forces from {cp.name} are losing ground against the enemy forces from "
f"{enemy_cp.name}. {status_msg}",
self.game.turn,
)
self.game.informations.append(info)
def redeploy_units(self, cp: ControlPoint) -> None: def redeploy_units(self, cp: ControlPoint) -> None:
""" " """ "
@@ -454,12 +434,12 @@ class Event:
moved_units[frontline_unit] = int(count * move_factor) moved_units[frontline_unit] = int(count * move_factor)
total_units_redeployed = total_units_redeployed + int(count * move_factor) total_units_redeployed = total_units_redeployed + int(count * move_factor)
destination.base.commission_units(moved_units) destination.base.commision_units(moved_units)
source.base.commit_losses(moved_units) source.base.commit_losses(moved_units)
# Also transfer pending deliveries. # Also transfer pending deliveries.
for unit_type, count in source.pending_unit_deliveries.units.items(): for unit_type, count in source.pending_unit_deliveries.units.items():
if not isinstance(unit_type, GroundUnitType): if not issubclass(unit_type, VehicleType):
continue continue
if count <= 0: if count <= 0:
# Don't transfer *sales*... # Don't transfer *sales*...

View File

@@ -8,5 +8,5 @@ class FrontlineAttackEvent(Event):
future unique Event handling future unique Event handling
""" """
def __str__(self) -> str: def __str__(self):
return "Frontline attack" return "Frontline attack"

View File

@@ -1,13 +1,15 @@
from __future__ import annotations from __future__ import annotations
from game.data.groundunitclass import GroundUnitClass
import itertools
import logging import logging
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Optional, Dict, Type, List, Any, Iterator, TYPE_CHECKING from typing import Optional, Dict, Type, List, Any, cast
import dcs import dcs
from dcs.countries import country_dict 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 ( from game.data.building_data import (
WW2_ALLIES_BUILDINGS, WW2_ALLIES_BUILDINGS,
@@ -21,12 +23,7 @@ from game.data.doctrine import (
COLDWAR_DOCTRINE, COLDWAR_DOCTRINE,
WWII_DOCTRINE, WWII_DOCTRINE,
) )
from game.data.groundunitclass import GroundUnitClass from pydcs_extensions.mod_units import MODDED_VEHICLES, MODDED_AIRPLANES
from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
if TYPE_CHECKING:
from game.theater.start_generator import ModSettings
@dataclass @dataclass
@@ -48,25 +45,25 @@ class Faction:
description: str = field(default="") description: str = field(default="")
# Available aircraft # Available aircraft
aircrafts: List[AircraftType] = field(default_factory=list) aircrafts: List[Type[FlyingType]] = field(default_factory=list)
# Available awacs aircraft # Available awacs aircraft
awacs: List[AircraftType] = field(default_factory=list) awacs: List[Type[FlyingType]] = field(default_factory=list)
# Available tanker aircraft # Available tanker aircraft
tankers: List[AircraftType] = field(default_factory=list) tankers: List[Type[FlyingType]] = field(default_factory=list)
# Available frontline units # Available frontline units
frontline_units: List[GroundUnitType] = field(default_factory=list) frontline_units: List[Type[VehicleType]] = field(default_factory=list)
# Available artillery units # 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 used
infantry_units: List[GroundUnitType] = field(default_factory=list) infantry_units: List[Type[VehicleType]] = field(default_factory=list)
# Logistics units used # 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 # Possible SAMS site generators for this faction
air_defenses: List[str] = field(default_factory=list) air_defenses: List[str] = field(default_factory=list)
@@ -84,10 +81,10 @@ class Faction:
requirements: Dict[str, str] = field(default_factory=dict) requirements: Dict[str, str] = field(default_factory=dict)
# possible aircraft carrier units # possible aircraft carrier units
aircraft_carrier: List[Type[ShipType]] = field(default_factory=list) aircraft_carrier: List[Type[UnitType]] = field(default_factory=list)
# possible helicopter carrier units # possible helicopter carrier units
helicopter_carrier: List[Type[ShipType]] = field(default_factory=list) helicopter_carrier: List[Type[UnitType]] = field(default_factory=list)
# Possible carrier names # Possible carrier names
carrier_names: List[str] = field(default_factory=list) carrier_names: List[str] = field(default_factory=list)
@@ -117,7 +114,7 @@ class Faction:
has_jtac: bool = field(default=False) has_jtac: bool = field(default=False)
# Unit to use as JTAC for this faction # 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: Doctrine = field(default=MODERN_DOCTRINE) doctrine: Doctrine = field(default=MODERN_DOCTRINE)
@@ -126,7 +123,7 @@ class Faction:
building_set: List[str] = field(default_factory=list) building_set: List[str] = field(default_factory=list)
# List of default livery overrides # List of default livery overrides
liveries_overrides: Dict[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 #: Set to True if the faction should force the "Unrestricted satnav" option
#: for the mission. This option enables GPS for capable aircraft regardless #: for the mission. This option enables GPS for capable aircraft regardless
@@ -137,11 +134,15 @@ class Faction:
#: both will use it. #: both will use it.
unrestricted_satnav: bool = False unrestricted_satnav: bool = False
def has_access_to_unittype(self, unit_class: GroundUnitClass) -> bool: def has_access_to_unittype(self, unitclass: GroundUnitClass) -> bool:
for vehicle in itertools.chain(self.frontline_units, self.artillery_units): has_access = False
if vehicle.unit_class is unit_class: for vehicle in unitclass.unit_list:
if vehicle in self.frontline_units:
return True return True
return False if vehicle in self.artillery_units:
return True
return has_access
@classmethod @classmethod
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction: def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
@@ -162,26 +163,16 @@ class Faction:
faction.authors = json.get("authors", "") faction.authors = json.get("authors", "")
faction.description = json.get("description", "") faction.description = json.get("description", "")
faction.aircrafts = [AircraftType.named(n) for n in json.get("aircrafts", [])] faction.aircrafts = load_all_aircraft(json.get("aircrafts", []))
faction.awacs = [AircraftType.named(n) for n in json.get("awacs", [])] faction.awacs = load_all_aircraft(json.get("awacs", []))
faction.tankers = [AircraftType.named(n) for n in json.get("tankers", [])] faction.tankers = load_all_aircraft(json.get("tankers", []))
faction.aircrafts = list( faction.aircrafts = list(set(faction.aircrafts + faction.awacs))
set(faction.aircrafts + faction.awacs + faction.tankers)
)
faction.frontline_units = [ faction.frontline_units = load_all_vehicles(json.get("frontline_units", []))
GroundUnitType.named(n) for n in 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.artillery_units = [ faction.logistics_units = load_all_vehicles(json.get("logistics_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.ewrs = json.get("ewrs", []) faction.ewrs = json.get("ewrs", [])
@@ -205,7 +196,7 @@ class Faction:
faction.has_jtac = json.get("has_jtac", False) faction.has_jtac = json.get("has_jtac", False)
jtac_name = json.get("jtac_unit", None) jtac_name = json.get("jtac_unit", None)
if jtac_name is not None: if jtac_name is not None:
faction.jtac_unit = AircraftType.named(jtac_name) faction.jtac_unit = load_aircraft(jtac_name)
else: else:
faction.jtac_unit = None faction.jtac_unit = None
faction.navy_group_count = int(json.get("navy_group_count", 1)) faction.navy_group_count = int(json.get("navy_group_count", 1))
@@ -239,113 +230,90 @@ class Faction:
# Load liveries override # Load liveries override
faction.liveries_overrides = {} faction.liveries_overrides = {}
liveries_overrides = json.get("liveries_overrides", {}) liveries_overrides = json.get("liveries_overrides", {})
for name, livery in liveries_overrides.items(): for k, v in liveries_overrides.items():
aircraft = AircraftType.named(name) k = load_aircraft(k)
faction.liveries_overrides[aircraft] = [s.lower() for s in livery] if k is not None:
faction.liveries_overrides[k] = [s.lower() for s in v]
faction.unrestricted_satnav = json.get("unrestricted_satnav", False) faction.unrestricted_satnav = json.get("unrestricted_satnav", False)
return faction return faction
@property @property
def ground_units(self) -> Iterator[GroundUnitType]: def units(self) -> List[Type[UnitType]]:
yield from self.artillery_units return (
yield from self.frontline_units self.infantry_units
yield from self.logistics_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: ModSettings) -> Faction: def unit_loader(unit: str, class_repository: List[Any]) -> Optional[Type[UnitType]]:
# aircraft """
if not mod_settings.a4_skyhawk: Find unit by name
self.remove_aircraft("A-4E-C") :param unit: Unit name as string
if not mod_settings.hercules: :param class_repository: Repository of classes (Either a module, a class, or a list of classes)
self.remove_aircraft("Hercules") :return: The unit as a PyDCS type
if not mod_settings.f22_raptor: """
self.remove_aircraft("F-22A") if unit is None:
if not mod_settings.jas39_gripen: return None
self.remove_aircraft("JAS39Gripen") elif unit in plane_map.keys():
self.remove_aircraft("JAS39Gripen_AG") return plane_map[unit]
if not mod_settings.su57_felon: else:
self.remove_aircraft("Su-57") for mother_class in class_repository:
# frenchpack if getattr(mother_class, unit, None) is not None:
if not mod_settings.frenchpack: return getattr(mother_class, unit)
self.remove_vehicle("AMX10RCR") if type(mother_class) is list:
self.remove_vehicle("SEPAR") for m in mother_class:
self.remove_vehicle("ERC") if m.__name__ == unit:
self.remove_vehicle("M120") return m
self.remove_vehicle("AA20") logging.error(f"FACTION ERROR : Unable to find {unit} in pydcs")
self.remove_vehicle("TRM2000") return None
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 remove_aircraft(self, name: str) -> None:
for i in self.aircrafts:
if i.dcs_unit_type.id == name:
self.aircrafts.remove(i)
def remove_air_defenses(self, name: str) -> None: def load_aircraft(name: str) -> Optional[Type[FlyingType]]:
for i in self.air_defenses: return cast(
if i == name: Optional[FlyingType],
self.air_defenses.remove(i) unit_loader(name, [dcs.planes, dcs.helicopters, MODDED_AIRPLANES]),
)
def remove_vehicle(self, name: str) -> None:
for i in self.frontline_units: def load_all_aircraft(data) -> List[Type[FlyingType]]:
if i.dcs_unit_type.id == name: items = []
self.frontline_units.remove(i) 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]]: def load_ship(name: str) -> Optional[Type[ShipType]]:
if (ship := getattr(dcs.ships, name, None)) is not None: return cast(Optional[FlyingType], unit_loader(name, [dcs.ships]))
return ship
logging.error(f"FACTION ERROR : Unable to find {name} in dcs.ships")
return None
def load_all_ships(data: list[str]) -> List[Type[ShipType]]: def load_all_ships(data) -> List[Type[ShipType]]:
items = [] items = []
for name in data: for name in data:
item = load_ship(name) item = load_ship(name)

View File

@@ -1,11 +1,10 @@
import itertools import itertools
import logging import logging
import math
import random import random
import sys import sys
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from enum import Enum from enum import Enum
from typing import Any, List, Type, Union, cast from typing import Any, Dict, List, Iterator
from dcs.action import Coalition from dcs.action import Coalition
from dcs.mapping import Point from dcs.mapping import Point
@@ -13,10 +12,11 @@ from dcs.task import CAP, CAS, PinpointStrike
from dcs.vehicles import AirDefence from dcs.vehicles import AirDefence
from faker import Faker from faker import Faker
from game import db
from game.inventory import GlobalAircraftInventory from game.inventory import GlobalAircraftInventory
from game.models.game_stats import GameStats from game.models.game_stats import GameStats
from game.plugins import LuaPluginManager from game.plugins import LuaPluginManager
from gen import naming from game.theater.theatergroundobject import MissileSiteGroundObject
from gen.ato import AirTaskingOrder from gen.ato import AirTaskingOrder
from gen.conflictgen import Conflict from gen.conflictgen import Conflict
from gen.flights.ai_flight_planner import CoalitionMissionPlanner from gen.flights.ai_flight_planner import CoalitionMissionPlanner
@@ -34,8 +34,8 @@ from .navmesh import NavMesh
from .procurement import AircraftProcurementRequest, ProcurementAi from .procurement import AircraftProcurementRequest, ProcurementAi
from .profiling import logged_duration from .profiling import logged_duration
from .settings import Settings, AutoAtoBehavior from .settings import Settings, AutoAtoBehavior
from .squadrons import AirWing from .squadrons import Pilot, AirWing
from .theater import ConflictTheater, ControlPoint from .theater import ConflictTheater
from .theater.bullseye import Bullseye from .theater.bullseye import Bullseye
from .theater.transitnetwork import TransitNetwork, TransitNetworkBuilder from .theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
from .threatzones import ThreatZones from .threatzones import ThreatZones
@@ -86,8 +86,8 @@ class TurnState(Enum):
class Game: class Game:
def __init__( def __init__(
self, self,
player_faction: Faction, player_name: str,
enemy_faction: Faction, enemy_name: str,
theater: ConflictTheater, theater: ConflictTheater,
start_date: datetime, start_date: datetime,
settings: Settings, settings: Settings,
@@ -97,29 +97,28 @@ class Game:
self.settings = settings self.settings = settings
self.events: List[Event] = [] self.events: List[Event] = []
self.theater = theater self.theater = theater
self.player_faction = player_faction self.player_name = player_name
self.player_country = player_faction.country self.player_country = db.FACTIONS[player_name].country
self.enemy_faction = enemy_faction self.enemy_name = enemy_name
self.enemy_country = enemy_faction.country self.enemy_country = db.FACTIONS[enemy_name].country
# pass_turn() will be called when initialization is complete which will # pass_turn() will be called when initialization is complete which will
# increment this to turn 0 before it reaches the player. # increment this to turn 0 before it reaches the player.
self.turn = -1 self.turn = -1
# NB: This is the *start* date. It is never updated. # NB: This is the *start* date. It is never updated.
self.date = date(start_date.year, start_date.month, start_date.day) self.date = date(start_date.year, start_date.month, start_date.day)
self.game_stats = GameStats() self.game_stats = GameStats()
self.notes = "" self.game_stats.update(self)
self.ground_planners: dict[int, GroundPlanner] = {} self.ground_planners: Dict[int, GroundPlanner] = {}
self.informations = [] self.informations = []
self.informations.append(Information("Game Start", "-" * 40, 0)) 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. # Culling Zones are for areas around points of interest that contain things we may not wish to cull.
self.__culling_zones: List[Point] = [] self.__culling_zones: List[Point] = []
self.__destroyed_units: list[dict[str, Union[float, str]]] = [] self.__destroyed_units: List[str] = []
self.savepath = "" self.savepath = ""
self.budget = player_budget self.budget = player_budget
self.enemy_budget = enemy_budget self.enemy_budget = enemy_budget
self.current_unit_id = 0 self.current_unit_id = 0
self.current_group_id = 0 self.current_group_id = 0
self.name_generator = naming.namegen
self.conditions = self.generate_conditions() self.conditions = self.generate_conditions()
@@ -149,7 +148,7 @@ class Game:
self.on_load(game_still_initializing=True) self.on_load(game_still_initializing=True)
def __getstate__(self) -> dict[str, Any]: def __getstate__(self) -> Dict[str, Any]:
state = self.__dict__.copy() state = self.__dict__.copy()
# Avoid persisting any volatile types that can be deterministically # Avoid persisting any volatile types that can be deterministically
# recomputed on load for the sake of save compatibility. # recomputed on load for the sake of save compatibility.
@@ -161,7 +160,7 @@ class Game:
del state["red_faker"] del state["red_faker"]
return state return state
def __setstate__(self, state: dict[str, Any]) -> None: def __setstate__(self, state: Dict[str, Any]) -> None:
self.__dict__.update(state) self.__dict__.update(state)
# Regenerate any state that was not persisted. # Regenerate any state that was not persisted.
self.on_load() self.on_load()
@@ -188,7 +187,7 @@ class Game:
self.theater, self.current_day, self.current_turn_time_of_day, self.settings self.theater, self.current_day, self.current_turn_time_of_day, self.settings
) )
def sanitize_sides(self) -> None: def sanitize_sides(self):
""" """
Make sure the opposing factions are using different countries Make sure the opposing factions are using different countries
:return: :return:
@@ -201,6 +200,14 @@ class Game:
else: else:
self.enemy_country = "Russia" 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: def faction_for(self, player: bool) -> Faction:
if player: if player:
return self.player_faction return self.player_faction
@@ -226,21 +233,26 @@ class Game:
return self.blue_bullseye return self.blue_bullseye
return self.red_bullseye return self.red_bullseye
def _generate_player_event( def _roll(self, prob, mult):
self, event_class: Type[Event], player_cp: ControlPoint, enemy_cp: ControlPoint if self.settings.version == "dev":
) -> None: # always generate all events for dev
return 100
else:
return random.randint(1, 100) <= prob * mult
def _generate_player_event(self, event_class, player_cp, enemy_cp):
self.events.append( self.events.append(
event_class( event_class(
self, self,
player_cp, player_cp,
enemy_cp, enemy_cp,
enemy_cp.position, enemy_cp.position,
self.player_faction.name, self.player_name,
self.enemy_faction.name, self.enemy_name,
) )
) )
def _generate_events(self) -> None: def _generate_events(self):
for front_line in self.theater.conflicts(): for front_line in self.theater.conflicts():
self._generate_player_event( self._generate_player_event(
FrontlineAttackEvent, FrontlineAttackEvent,
@@ -254,22 +266,21 @@ class Game:
else: else:
self.enemy_budget += amount self.enemy_budget += amount
def process_player_income(self) -> None: def process_player_income(self):
self.budget += Income(self, player=True).total self.budget += Income(self, player=True).total
def process_enemy_income(self) -> None: def process_enemy_income(self):
# TODO: Clean up save compat. # TODO: Clean up save compat.
if not hasattr(self, "enemy_budget"): if not hasattr(self, "enemy_budget"):
self.enemy_budget = 0 self.enemy_budget = 0
self.enemy_budget += Income(self, player=False).total self.enemy_budget += Income(self, player=False).total
@staticmethod def initiate_event(self, event: Event) -> UnitMap:
def initiate_event(event: Event) -> UnitMap:
# assert event in self.events # assert event in self.events
logging.info("Generating {} (regular)".format(event)) logging.info("Generating {} (regular)".format(event))
return event.generate() return event.generate()
def finish_event(self, event: Event, debriefing: Debriefing) -> None: def finish_event(self, event: Event, debriefing: Debriefing):
logging.info("Finishing event {}".format(event)) logging.info("Finishing event {}".format(event))
event.commit(debriefing) event.commit(debriefing)
@@ -278,15 +289,17 @@ class Game:
else: else:
logging.info("finish_event: event not in the events!") logging.info("finish_event: event not in the events!")
def is_player_attack(self, event):
if isinstance(event, Event):
return (
event
and event.attacker_name
and event.attacker_name == self.player_name
)
else:
raise RuntimeError(f"{event} was passed when an Event type was expected")
def on_load(self, game_still_initializing: bool = False) -> None: 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) LuaPluginManager.load_settings(self.settings)
ObjectiveDistanceCache.set_theater(self.theater) ObjectiveDistanceCache.set_theater(self.theater)
self.compute_conflicts_position() self.compute_conflicts_position()
@@ -300,33 +313,6 @@ class Game:
self.red_ato.clear() self.red_ato.clear()
def finish_turn(self, skipped: bool = False) -> None: def finish_turn(self, skipped: bool = False) -> None:
"""Finalizes the current turn and advances to the next turn.
This handles the turn-end portion of passing a turn. Initialization of the next
turn is handled by `initialize_turn`. These are separate processes because while
turns may be initialized more than once under some circumstances (see the
documentation for `initialize_turn`), `finish_turn` performs the work that
should be guaranteed to happen only once per turn:
* Turn counter increment.
* Delivering units ordered the previous turn.
* Transfer progress.
* Squadron replenishment.
* Income distribution.
* Base strength (front line position) adjustment.
* Weather/time-of-day generation.
Some actions (like transit network assembly) will happen both here and in
`initialize_turn`. We need the network to be up to date so we can account for
base captures when processing the transfers that occurred last turn, but we also
need it to be up to date in the case of a re-initialization in `initialize_turn`
(such as to account for a cheat base capture) so that orders are only placed
where a supply route exists to the destination. This is a relatively cheap
operation so duplicating the effort is not a problem.
Args:
skipped: True if the turn was skipped.
"""
self.informations.append( self.informations.append(
Information("End of turn #" + str(self.turn), "-" * 40, 0) Information("End of turn #" + str(self.turn), "-" * 40, 0)
) )
@@ -341,18 +327,15 @@ class Game:
# one hop ahead. ControlPoint.process_turn handles unit deliveries. # one hop ahead. ControlPoint.process_turn handles unit deliveries.
self.transfers.perform_transfers() 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() self.reset_ato()
for control_point in self.theater.controlpoints: for control_point in self.theater.controlpoints:
control_point.process_turn(self) control_point.process_turn(self)
self.blue_air_wing.replenish() if not skipped and self.turn > 1:
self.red_air_wing.replenish()
if not skipped:
for cp in self.theater.player_points(): for cp in self.theater.player_points():
cp.base.affect_strength(+PLAYER_BASE_STRENGTH_RECOVERY) cp.base.affect_strength(+PLAYER_BASE_STRENGTH_RECOVERY)
elif self.turn > 1: else:
for cp in self.theater.player_points(): for cp in self.theater.player_points():
if not cp.is_carrier and not cp.is_lha: if not cp.is_carrier and not cp.is_lha:
cp.base.affect_strength(-PLAYER_BASE_STRENGTH_RECOVERY) cp.base.affect_strength(-PLAYER_BASE_STRENGTH_RECOVERY)
@@ -363,18 +346,10 @@ class Game:
self.process_player_income() self.process_player_income()
def begin_turn_0(self) -> None: def begin_turn_0(self) -> None:
"""Initialization for the first turn of the game."""
self.turn = 0 self.turn = 0
self.initialize_turn() self.initialize_turn()
def pass_turn(self, no_action: bool = False) -> None: def pass_turn(self, no_action: bool = False) -> None:
"""Ends the current turn and initializes the new turn.
Called both when skipping a turn or by ending the turn as the result of combat.
Args:
no_action: True if the turn was skipped.
"""
logging.info("Pass turn") logging.info("Pass turn")
with logged_duration("Turn finalization"): with logged_duration("Turn finalization"):
self.finish_turn(no_action) self.finish_turn(no_action)
@@ -384,7 +359,7 @@ class Game:
# Autosave progress # Autosave progress
persistency.autosave(self) persistency.autosave(self)
def check_win_loss(self) -> TurnState: def check_win_loss(self):
player_airbases = { player_airbases = {
cp for cp in self.theater.player_points() if cp.runway_is_operational() cp for cp in self.theater.player_points() if cp.runway_is_operational()
} }
@@ -404,90 +379,26 @@ class Game:
self.blue_bullseye = Bullseye(enemy_cp.position) self.blue_bullseye = Bullseye(enemy_cp.position)
self.red_bullseye = Bullseye(player_cp.position) self.red_bullseye = Bullseye(player_cp.position)
def initialize_turn(self, for_red: bool = True, for_blue: bool = True) -> None: def initialize_turn(self) -> None:
"""Performs turn initialization for the specified players.
Turn initialization performs all of the beginning-of-turn actions. *End-of-turn*
processing happens in `pass_turn` (despite the name, it's called both for
skipping the turn and ending the turn after combat).
Special care needs to be taken here because initialization can occur more than
once per turn. A number of events can require re-initializing a turn:
* Cheat capture. Bases changing hands invalidates many missions in both ATOs,
purchase orders, threat zones, transit networks, etc. Practically speaking,
after a base capture the turn needs to be treated as fully new. The game might
even be over after a capture.
* Cheat front line position. CAS missions are no longer in the correct location,
and the ground planner may also need changes.
* Selling/buying units at TGOs. Selling a TGO might leave missions in the ATO
with invalid targets. Buying a new SAM (or even replacing some units in a SAM)
potentially changes the threat zone and may alter mission priorities and
flight planning.
Most of the work is delegated to initialize_turn_for, which handles the
coalition-specific turn initialization. In some cases only one coalition will be
(re-) initialized. This is the case when buying or selling TGO units, since we
don't want to force the player to redo all their planning just because they
repaired a SAM, but should replan opfor when that happens. On the other hand,
base captures are significant enough (and likely enough to be the first thing
the player does in a turn) that we replan blue as well. Front lines are less
impactful but also likely to be early, so they also cause a blue replan.
Args:
for_red: True if opfor should be re-initialized.
for_blue: True if the player coalition should be re-initialized.
"""
self.events = [] self.events = []
self._generate_events() self._generate_events()
self.set_bullseye() self.set_bullseye()
# Update statistics # Update statistics
self.game_stats.update(self) self.game_stats.update(self)
self.blue_air_wing.reset()
self.red_air_wing.reset()
self.aircraft_inventory.reset()
for cp in self.theater.controlpoints:
self.aircraft_inventory.set_from_control_point(cp)
# Check for win or loss condition # Check for win or loss condition
turn_state = self.check_win_loss() turn_state = self.check_win_loss()
if turn_state in (TurnState.LOSS, TurnState.WIN): if turn_state in (TurnState.LOSS, TurnState.WIN):
return self.process_win_loss(turn_state) return self.process_win_loss(turn_state)
# Plan Coalition specific turn
if for_red:
self.initialize_turn_for(player=False)
if for_blue:
self.initialize_turn_for(player=True)
# Plan GroundWar
for cp in self.theater.controlpoints:
if cp.has_frontline:
gplanner = GroundPlanner(cp, self)
gplanner.plan_groundwar()
self.ground_planners[cp.id] = gplanner
def initialize_turn_for(self, player: bool) -> None:
"""Processes coalition-specific turn initialization.
For more information on turn initialization in general, see the documentation
for `Game.initialize_turn`.
Args:
player: True if the player coalition is being initialized. False for opfor
initialization.
"""
self.ato_for(player).clear()
self.air_wing_for(player).reset()
self.aircraft_inventory.reset()
for cp in self.theater.controlpoints:
self.aircraft_inventory.set_from_control_point(cp)
# Refund all pending deliveries for opfor and if player
# has automate_aircraft_reinforcements
if (not player and not cp.captured) or (
player
and cp.captured
and self.settings.automate_aircraft_reinforcements
):
cp.pending_unit_deliveries.refund_all(self)
# Plan flights & combat for next turn # Plan flights & combat for next turn
with logged_duration("Computing conflict positions"): with logged_duration("Computing conflict positions"):
self.compute_conflicts_position() self.compute_conflicts_position()
@@ -497,48 +408,55 @@ class Game:
self.compute_transit_networks() self.compute_transit_networks()
self.ground_planners = {} self.ground_planners = {}
self.procurement_requests_for(player).clear() self.blue_procurement_requests.clear()
self.red_procurement_requests.clear()
with logged_duration("Procurement of airlift assets"): with logged_duration("Procurement of airlift assets"):
self.transfers.order_airlift_assets() self.transfers.order_airlift_assets()
with logged_duration("Transport planning"): with logged_duration("Transport planning"):
self.transfers.plan_transports() self.transfers.plan_transports()
if not player or ( with logged_duration("Blue mission planning"):
player and self.settings.auto_ato_behavior is not AutoAtoBehavior.Disabled if self.settings.auto_ato_behavior is not AutoAtoBehavior.Disabled:
): blue_planner = CoalitionMissionPlanner(self, is_player=True)
color = "Blue" if player else "Red" blue_planner.plan_missions()
with logged_duration(f"{color} mission planning"):
mission_planner = CoalitionMissionPlanner(self, player)
mission_planner.plan_missions()
self.plan_procurement_for(player) with logged_duration("Red mission planning"):
red_planner = CoalitionMissionPlanner(self, is_player=False)
red_planner.plan_missions()
def plan_procurement_for(self, for_player: bool) -> None: for cp in self.theater.controlpoints:
if cp.has_frontline:
gplanner = GroundPlanner(cp, self)
gplanner.plan_groundwar()
self.ground_planners[cp.id] = gplanner
self.plan_procurement()
def plan_procurement(self) -> None:
# The first turn needs to buy a *lot* of aircraft to fill CAPs, so it # The first turn needs to buy a *lot* of aircraft to fill CAPs, so it
# gets much more of the budget that turn. Otherwise budget (after # gets much more of the budget that turn. Otherwise budget (after
# repairs) is split evenly between air and ground. For the default # repairs) is split evenly between air and ground. For the default
# starting budget of 2000 this gives 600 to ground forces and 1400 to # starting budget of 2000 this gives 600 to ground forces and 1400 to
# aircraft. After that the budget will be spend proportionally based on how much is already invested # aircraft. After that the budget will be spend proportionally based on how much is already invested
if for_player: self.budget = ProcurementAi(
self.budget = ProcurementAi( self,
self, for_player=True,
for_player=True, faction=self.player_faction,
faction=self.player_faction, manage_runways=self.settings.automate_runway_repair,
manage_runways=self.settings.automate_runway_repair, manage_front_line=self.settings.automate_front_line_reinforcements,
manage_front_line=self.settings.automate_front_line_reinforcements, manage_aircraft=self.settings.automate_aircraft_reinforcements,
manage_aircraft=self.settings.automate_aircraft_reinforcements, ).spend_budget(self.budget)
).spend_budget(self.budget)
else: self.enemy_budget = ProcurementAi(
self.enemy_budget = ProcurementAi( self,
self, for_player=False,
for_player=False, faction=self.enemy_faction,
faction=self.enemy_faction, manage_runways=True,
manage_runways=True, manage_front_line=True,
manage_front_line=True, manage_aircraft=True,
manage_aircraft=True, ).spend_budget(self.enemy_budget)
).spend_budget(self.enemy_budget)
def message(self, text: str) -> None: def message(self, text: str) -> None:
self.informations.append(Information(text, turn=self.turn)) self.informations.append(Information(text, turn=self.turn))
@@ -551,14 +469,14 @@ class Game:
def current_day(self) -> date: def current_day(self) -> date:
return self.date + timedelta(days=self.turn // 4) return self.date + timedelta(days=self.turn // 4)
def next_unit_id(self) -> int: def next_unit_id(self):
""" """
Next unit id for pre-generated units Next unit id for pre-generated units
""" """
self.current_unit_id += 1 self.current_unit_id += 1
return self.current_unit_id return self.current_unit_id
def next_group_id(self) -> int: def next_group_id(self):
""" """
Next unit id for pre-generated units Next unit id for pre-generated units
""" """
@@ -592,7 +510,7 @@ class Game:
return self.blue_navmesh return self.blue_navmesh
return self.red_navmesh return self.red_navmesh
def compute_conflicts_position(self) -> None: def compute_conflicts_position(self):
""" """
Compute the current conflict center position(s), mainly used for culling calculation Compute the current conflict center position(s), mainly used for culling calculation
:return: List of points of interests :return: List of points of interests
@@ -615,7 +533,7 @@ class Game:
# If there is no conflict take the center point between the two nearest opposing bases # If there is no conflict take the center point between the two nearest opposing bases
if len(zones) == 0: if len(zones) == 0:
cpoint = None cpoint = None
min_distance = math.inf min_distance = sys.maxsize
for cp in self.theater.player_points(): for cp in self.theater.player_points():
for cp2 in self.theater.enemy_points(): for cp2 in self.theater.enemy_points():
d = cp.position.distance_to_point(cp2.position) d = cp.position.distance_to_point(cp2.position)
@@ -651,15 +569,15 @@ class Game:
self.__culling_zones = zones self.__culling_zones = zones
def add_destroyed_units(self, data: dict[str, Union[float, str]]) -> None: def add_destroyed_units(self, data):
pos = Point(cast(float, data["x"]), cast(float, data["z"])) pos = Point(data["x"], data["z"])
if self.theater.is_on_land(pos): if self.theater.is_on_land(pos):
self.__destroyed_units.append(data) self.__destroyed_units.append(data)
def get_destroyed_units(self) -> list[dict[str, Union[float, str]]]: def get_destroyed_units(self):
return self.__destroyed_units return self.__destroyed_units
def position_culled(self, pos: Point) -> bool: def position_culled(self, pos):
""" """
Check if unit can be generated at given position depending on culling performance settings Check if unit can be generated at given position depending on culling performance settings
:param pos: Position you are tryng to spawn stuff at :param pos: Position you are tryng to spawn stuff at
@@ -672,7 +590,7 @@ class Game:
return False return False
return True return True
def get_culling_zones(self) -> list[Point]: def get_culling_zones(self):
""" """
Check culling points Check culling points
:return: List of culling zones :return: List of culling zones
@@ -680,28 +598,30 @@ class Game:
return self.__culling_zones return self.__culling_zones
# 1 = red, 2 = blue # 1 = red, 2 = blue
def get_player_coalition_id(self) -> int: def get_player_coalition_id(self):
return 2 return 2
def get_enemy_coalition_id(self) -> int: def get_enemy_coalition_id(self):
return 1 return 1
def get_player_coalition(self) -> Coalition: def get_player_coalition(self):
return Coalition.Blue return Coalition.Blue
def get_enemy_coalition(self) -> Coalition: def get_enemy_coalition(self):
return Coalition.Red return Coalition.Red
def get_player_color(self) -> str: def get_player_color(self):
return "blue" return "blue"
def get_enemy_color(self) -> str: def get_enemy_color(self):
return "red" return "red"
def process_win_loss(self, turn_state: TurnState) -> None: def process_win_loss(self, turn_state: TurnState):
if turn_state is TurnState.WIN: if turn_state is TurnState.WIN:
self.message( return self.message(
"Congratulations, you are victorious! Start a new campaign to continue." "Congratulations, you are victorious! Start a new campaign to continue."
) )
elif turn_state is TurnState.LOSS: elif turn_state is TurnState.LOSS:
self.message("Game Over, you lose. Start a new campaign to continue.") return self.message(
"Game Over, you lose. Start a new campaign to continue."
)

View File

@@ -2,13 +2,13 @@ import datetime
class Information: class Information:
def __init__(self, title: str = "", text: str = "", turn: int = 0) -> None: def __init__(self, title="", text="", turn=0):
self.title = title self.title = title
self.text = text self.text = text
self.turn = turn self.turn = turn
self.timestamp = datetime.datetime.now() self.timestamp = datetime.datetime.now()
def __str__(self) -> str: def __str__(self):
return "[{}][{}] {} {}".format( return "[{}][{}] {} {}".format(
self.timestamp.strftime("%Y-%m-%d %H:%M:%S") self.timestamp.strftime("%Y-%m-%d %H:%M:%S")
if self.timestamp is not None if self.timestamp is not None

View File

@@ -6,7 +6,6 @@ from typing import Dict, Iterable, Iterator, Set, Tuple, TYPE_CHECKING, Type
from dcs.unittype import FlyingType from dcs.unittype import FlyingType
from game.dcs.aircrafttype import AircraftType
from gen.flights.flight import Flight from gen.flights.flight import Flight
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -18,9 +17,9 @@ class ControlPointAircraftInventory:
def __init__(self, control_point: ControlPoint) -> None: def __init__(self, control_point: ControlPoint) -> None:
self.control_point = control_point 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. """Adds aircraft to the inventory.
Args: Args:
@@ -29,7 +28,7 @@ class ControlPointAircraftInventory:
""" """
self.inventory[aircraft] += count 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. """Removes aircraft from the inventory.
Args: Args:
@@ -43,12 +42,12 @@ class ControlPointAircraftInventory:
available = self.inventory[aircraft] available = self.inventory[aircraft]
if available < count: if available < count:
raise ValueError( raise ValueError(
f"Cannot remove {count} {aircraft} from " f"Cannot remove {count} {aircraft.id} from "
f"{self.control_point.name}. Only have {available}." f"{self.control_point.name}. Only have {available}."
) )
self.inventory[aircraft] -= count 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. """Returns the number of available aircraft of the given type.
Args: Args:
@@ -60,14 +59,14 @@ class ControlPointAircraftInventory:
return 0 return 0
@property @property
def types_available(self) -> Iterator[AircraftType]: def types_available(self) -> Iterator[Type[FlyingType]]:
"""Iterates over all available aircraft types.""" """Iterates over all available aircraft types."""
for aircraft, count in self.inventory.items(): for aircraft, count in self.inventory.items():
if count > 0: if count > 0:
yield aircraft yield aircraft
@property @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.""" """Iterates over all available aircraft types, including amounts."""
for aircraft, count in self.inventory.items(): for aircraft, count in self.inventory.items():
if count > 0: if count > 0:
@@ -108,9 +107,9 @@ class GlobalAircraftInventory:
return self.inventories[control_point] return self.inventories[control_point]
@property @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.""" """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(): for control_point, inventory in self.inventories.items():
if control_point.captured: if control_point.captured:
for aircraft in inventory.types_available: for aircraft in inventory.types_available:

View File

@@ -0,0 +1,13 @@
class DestroyedUnit:
"""
Store info about a destroyed unit
"""
x: int
y: int
name: str
def __init__(self, x, y, name):
self.x = x
self.y = y
self.name = name

View File

@@ -1,9 +1,4 @@
from __future__ import annotations from typing import List
from typing import List, TYPE_CHECKING
if TYPE_CHECKING:
from game import Game
class FactionTurnMetadata: class FactionTurnMetadata:
@@ -15,7 +10,7 @@ class FactionTurnMetadata:
vehicles_count: int = 0 vehicles_count: int = 0
sam_count: int = 0 sam_count: int = 0
def __init__(self) -> None: def __init__(self):
self.aircraft_count = 0 self.aircraft_count = 0
self.vehicles_count = 0 self.vehicles_count = 0
self.sam_count = 0 self.sam_count = 0
@@ -29,7 +24,7 @@ class GameTurnMetadata:
allied_units: FactionTurnMetadata allied_units: FactionTurnMetadata
enemy_units: FactionTurnMetadata enemy_units: FactionTurnMetadata
def __init__(self) -> None: def __init__(self):
self.allied_units = FactionTurnMetadata() self.allied_units = FactionTurnMetadata()
self.enemy_units = FactionTurnMetadata() self.enemy_units = FactionTurnMetadata()
@@ -39,19 +34,15 @@ class GameStats:
Store statistics for the current game Store statistics for the current game
""" """
def __init__(self) -> None: def __init__(self):
self.data_per_turn: List[GameTurnMetadata] = [] self.data_per_turn: List[GameTurnMetadata] = []
def update(self, game: Game) -> None: def update(self, game):
""" """
Save data for current turn Save data for current turn
:param game: Game we want to save the data about :param game: Game we want to save the data about
""" """
# Remove the current turn if its just an update for this turn
if 0 < game.turn < len(self.data_per_turn):
del self.data_per_turn[-1]
turn_data = GameTurnMetadata() turn_data = GameTurnMetadata()
for cp in game.theater.controlpoints: for cp in game.theater.controlpoints:

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import logging import logging
import os import os
from pathlib import Path from pathlib import Path
from typing import Iterable, List, Set, TYPE_CHECKING, cast from typing import Iterable, List, Set, TYPE_CHECKING
from dcs import Mission from dcs import Mission
from dcs.action import DoScript, DoScriptFile from dcs.action import DoScript, DoScriptFile
@@ -17,7 +17,7 @@ from dcs.triggers import TriggerStart
from game.plugins import LuaPluginManager from game.plugins import LuaPluginManager
from game.theater.theatergroundobject import TheaterGroundObject from game.theater.theatergroundobject import TheaterGroundObject
from gen import Conflict, FlightType, VisualGenerator 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.airfields import AIRFIELD_DATA
from gen.airsupportgen import AirSupport, AirSupportConflictGenerator from gen.airsupportgen import AirSupport, AirSupportConflictGenerator
from gen.armor import GroundConflictGenerator, JtacInfo from gen.armor import GroundConflictGenerator, JtacInfo
@@ -62,7 +62,7 @@ class Operation:
plugin_scripts: List[str] = [] plugin_scripts: List[str] = []
@classmethod @classmethod
def prepare(cls, game: Game) -> None: def prepare(cls, game: Game):
with open("resources/default_options.lua", "r") as f: with open("resources/default_options.lua", "r") as f:
options_dict = loads(f.read())["options"] options_dict = loads(f.read())["options"]
cls._set_mission(Mission(game.theater.terrain)) cls._set_mission(Mission(game.theater.terrain))
@@ -70,6 +70,20 @@ class Operation:
cls._setup_mission_coalitions() cls._setup_mission_coalitions()
cls.current_mission.options.load_from_dict(options_dict) cls.current_mission.options.load_from_dict(options_dict)
@classmethod
def conflicts(cls) -> Iterable[Conflict]:
assert cls.game
for frontline in cls.game.theater.conflicts():
yield Conflict(
cls.game.theater,
frontline,
cls.game.player_name,
cls.game.enemy_name,
cls.game.player_country,
cls.game.enemy_country,
frontline.position,
)
@classmethod @classmethod
def air_conflict(cls) -> Conflict: def air_conflict(cls) -> Conflict:
assert cls.game assert cls.game
@@ -81,10 +95,10 @@ class Operation:
return Conflict( return Conflict(
cls.game.theater, cls.game.theater,
FrontLine(player_cp, enemy_cp), FrontLine(player_cp, enemy_cp),
cls.game.player_faction.name, cls.game.player_name,
cls.game.enemy_faction.name, cls.game.enemy_name,
cls.current_mission.country(cls.game.player_country), cls.game.player_country,
cls.current_mission.country(cls.game.enemy_country), cls.game.enemy_country,
mid_point, mid_point,
) )
@@ -93,7 +107,7 @@ class Operation:
cls.current_mission = mission cls.current_mission = mission
@classmethod @classmethod
def _setup_mission_coalitions(cls) -> None: def _setup_mission_coalitions(cls):
cls.current_mission.coalition["blue"] = Coalition( cls.current_mission.coalition["blue"] = Coalition(
"blue", bullseye=cls.game.blue_bullseye.to_pydcs() "blue", bullseye=cls.game.blue_bullseye.to_pydcs()
) )
@@ -149,7 +163,7 @@ class Operation:
airsupportgen: AirSupportConflictGenerator, airsupportgen: AirSupportConflictGenerator,
jtacs: List[JtacInfo], jtacs: List[JtacInfo],
airgen: AircraftConflictGenerator, airgen: AircraftConflictGenerator,
) -> None: ):
"""Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings)""" """Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings)"""
gens: List[MissionInfoGenerator] = [ gens: List[MissionInfoGenerator] = [
@@ -201,7 +215,23 @@ class Operation:
for flight in flights: for flight in flights:
if not flight.client_units: if not flight.client_units:
continue 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 @classmethod
def _create_tacan_registry( def _create_tacan_registry(
@@ -237,7 +267,7 @@ class Operation:
# beacon list. # beacon list.
@classmethod @classmethod
def _generate_ground_units(cls) -> None: def _generate_ground_units(cls):
cls.groundobjectgen = GroundObjectsGenerator( cls.groundobjectgen = GroundObjectsGenerator(
cls.current_mission, cls.current_mission,
cls.game, cls.game,
@@ -252,16 +282,11 @@ class Operation:
"""Add destroyed units to the Mission""" """Add destroyed units to the Mission"""
for d in cls.game.get_destroyed_units(): for d in cls.game.get_destroyed_units():
try: try:
type_name = d["type"] utype = db.unit_type_from_name(d["type"])
if not isinstance(type_name, str):
raise TypeError(
"Expected the type of the destroyed static to be a string"
)
utype = db.unit_type_from_name(type_name)
except KeyError: except KeyError:
continue continue
pos = Point(cast(float, d["x"]), cast(float, d["z"])) pos = Point(d["x"], d["z"])
if ( if (
utype is not None utype is not None
and not cls.game.position_culled(pos) and not cls.game.position_culled(pos)
@@ -350,7 +375,6 @@ class Operation:
cls.game.settings, cls.game.settings,
cls.game, cls.game,
cls.radio_registry, cls.radio_registry,
cls.tacan_registry,
cls.unit_map, cls.unit_map,
air_support=cls.airsupportgen.air_support, air_support=cls.airsupportgen.air_support,
) )
@@ -380,8 +404,8 @@ class Operation:
player_cp = front_line.blue_cp player_cp = front_line.blue_cp
enemy_cp = front_line.red_cp enemy_cp = front_line.red_cp
conflict = Conflict.frontline_cas_conflict( conflict = Conflict.frontline_cas_conflict(
cls.game.player_faction.name, cls.game.player_name,
cls.game.enemy_faction.name, cls.game.enemy_name,
cls.current_mission.country(cls.game.player_country), cls.current_mission.country(cls.game.player_country),
cls.current_mission.country(cls.game.enemy_country), cls.current_mission.country(cls.game.enemy_country),
front_line, front_line,
@@ -409,7 +433,7 @@ class Operation:
CargoShipGenerator(cls.current_mission, cls.game, cls.unit_map).generate() CargoShipGenerator(cls.current_mission, cls.game, cls.unit_map).generate()
@classmethod @classmethod
def reset_naming_ids(cls) -> None: def reset_naming_ids(cls):
namegen.reset_numbers() namegen.reset_numbers()
@classmethod @classmethod
@@ -430,8 +454,8 @@ class Operation:
"BlueAA": {}, "BlueAA": {},
} # type: ignore } # type: ignore
for i, tanker in enumerate(airsupportgen.air_support.tankers): for tanker in airsupportgen.air_support.tankers:
luaData["Tankers"][i] = { luaData["Tankers"][tanker.callsign] = {
"dcsGroupName": tanker.group_name, "dcsGroupName": tanker.group_name,
"callsign": tanker.callsign, "callsign": tanker.callsign,
"variant": tanker.variant, "variant": tanker.variant,
@@ -439,22 +463,23 @@ class Operation:
"tacan": str(tanker.tacan.number) + tanker.tacan.band.name, "tacan": str(tanker.tacan.number) + tanker.tacan.band.name,
} }
for i, awacs in enumerate(airsupportgen.air_support.awacs): if airsupportgen.air_support.awacs:
luaData["AWACs"][i] = { for awacs in airsupportgen.air_support.awacs:
"dcsGroupName": awacs.group_name, luaData["AWACs"][awacs.callsign] = {
"callsign": awacs.callsign, "dcsGroupName": awacs.group_name,
"radio": awacs.freq.mhz, "callsign": awacs.callsign,
} "radio": awacs.freq.mhz,
}
for i, jtac in enumerate(jtacs): for jtac in jtacs:
luaData["JTACs"][i] = { luaData["JTACs"][jtac.callsign] = {
"dcsGroupName": jtac.group_name, "dcsGroupName": jtac.group_name,
"callsign": jtac.callsign, "callsign": jtac.callsign,
"zone": jtac.region, "zone": jtac.region,
"dcsUnit": jtac.unit_name, "dcsUnit": jtac.unit_name,
"laserCode": jtac.code, "laserCode": jtac.code,
} }
flight_count = 0
for flight in airgen.flights: for flight in airgen.flights:
if flight.friendly and flight.flight_type in [ if flight.friendly and flight.flight_type in [
FlightType.ANTISHIP, FlightType.ANTISHIP,
@@ -475,7 +500,7 @@ class Operation:
elif hasattr(flightTarget, "name"): elif hasattr(flightTarget, "name"):
flightTargetName = flightTarget.name flightTargetName = flightTarget.name
flightTargetType = flightType + " TGT (Airbase)" flightTargetType = flightType + " TGT (Airbase)"
luaData["TargetPoints"][flight_count] = { luaData["TargetPoints"][flightTargetName] = {
"name": flightTargetName, "name": flightTargetName,
"type": flightTargetType, "type": flightTargetType,
"position": { "position": {
@@ -483,7 +508,6 @@ class Operation:
"y": flightTarget.position.y, "y": flightTarget.position.y,
}, },
} }
flight_count += 1
for cp in cls.game.theater.controlpoints: for cp in cls.game.theater.controlpoints:
for ground_object in cp.ground_objects: for ground_object in cp.ground_objects:

View File

@@ -1,23 +1,17 @@
from __future__ import annotations
import logging import logging
import os import os
import pickle import pickle
import shutil import shutil
from pathlib import Path from typing import Optional
from typing import Optional, TYPE_CHECKING
if TYPE_CHECKING:
from game import Game
_dcs_saved_game_folder: Optional[str] = None _dcs_saved_game_folder: Optional[str] = None
_file_abs_path = None
def setup(user_folder: str) -> None: def setup(user_folder: str):
global _dcs_saved_game_folder global _dcs_saved_game_folder
_dcs_saved_game_folder = user_folder _dcs_saved_game_folder = user_folder
if not save_dir().exists(): _file_abs_path = os.path.join(base_path(), "default.liberation")
save_dir().mkdir(parents=True)
def base_path() -> str: def base_path() -> str:
@@ -26,23 +20,19 @@ def base_path() -> str:
return _dcs_saved_game_folder return _dcs_saved_game_folder
def save_dir() -> Path:
return Path(base_path()) / "Liberation" / "Saves"
def _temporary_save_file() -> str: def _temporary_save_file() -> str:
return str(save_dir() / "tmpsave.liberation") return os.path.join(base_path(), "tmpsave.liberation")
def _autosave_path() -> str: 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: 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: str) -> Optional[Game]: def load_game(path):
with open(path, "rb") as f: with open(path, "rb") as f:
try: try:
save = pickle.load(f) save = pickle.load(f)
@@ -53,7 +43,7 @@ def load_game(path: str) -> Optional[Game]:
return None return None
def save_game(game: Game) -> bool: def save_game(game) -> bool:
try: try:
with open(_temporary_save_file(), "wb") as f: with open(_temporary_save_file(), "wb") as f:
pickle.dump(game, f) pickle.dump(game, f)
@@ -64,7 +54,7 @@ def save_game(game: Game) -> bool:
return False return False
def autosave(game: Game) -> bool: def autosave(game) -> bool:
""" """
Autosave to the autosave location Autosave to the autosave location
:param game: Game to save :param game: Game to save

View File

@@ -38,7 +38,7 @@ class PluginSettings:
self.settings = Settings() self.settings = Settings()
self.initialize_settings() self.initialize_settings()
def set_settings(self, settings: Settings) -> None: def set_settings(self, settings: Settings):
self.settings = settings self.settings = settings
self.initialize_settings() self.initialize_settings()
@@ -146,7 +146,7 @@ class LuaPlugin(PluginSettings):
return cls(definition) return cls(definition)
def set_settings(self, settings: Settings) -> None: def set_settings(self, settings: Settings):
super().set_settings(settings) super().set_settings(settings)
for option in self.definition.options: for option in self.definition.options:
option.set_settings(self.settings) option.set_settings(self.settings)

View File

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

View File

@@ -1,9 +0,0 @@
from typing import Protocol
from dcs import Point
class Positioned(Protocol):
@property
def position(self) -> Point:
raise NotImplementedError

View File

@@ -3,12 +3,12 @@ from __future__ import annotations
import math import math
import random import random
from dataclasses import dataclass 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 import db
from game.data.groundunitclass import GroundUnitClass 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.factions.faction import Faction
from game.theater import ControlPoint, MissionTarget from game.theater import ControlPoint, MissionTarget
from game.utils import Distance from game.utils import Distance
@@ -125,7 +125,7 @@ class ProcurementAi:
if available % 2 == 0: if available % 2 == 0:
continue continue
inventory.remove_aircraft(aircraft, 1) inventory.remove_aircraft(aircraft, 1)
total += aircraft.price total += db.PRICES[aircraft]
return total return total
def repair_runways(self, budget: float) -> float: def repair_runways(self, budget: float) -> float:
@@ -147,17 +147,17 @@ class ProcurementAi:
def affordable_ground_unit_of_class( def affordable_ground_unit_of_class(
self, budget: float, unit_class: GroundUnitClass self, budget: float, unit_class: GroundUnitClass
) -> Optional[GroundUnitType]: ) -> Optional[Type[VehicleType]]:
faction_units = set(self.faction.frontline_units) | set( faction_units = set(self.faction.frontline_units) | set(
self.faction.artillery_units 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 # faction has no access to needed unit type, take a random unit
if not of_class: if not of_class:
of_class = faction_units 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: if not affordable_units:
return None return None
return random.choice(affordable_units) return random.choice(affordable_units)
@@ -179,7 +179,7 @@ class ProcurementAi:
# Can't afford any more units. # Can't afford any more units.
break break
budget -= unit.price budget -= db.PRICES[unit]
cp.pending_unit_deliveries.order({unit: 1}) cp.pending_unit_deliveries.order({unit: 1})
return budget return budget
@@ -215,12 +215,12 @@ class ProcurementAi:
airbase: ControlPoint, airbase: ControlPoint,
number: int, number: int,
max_price: float, max_price: float,
) -> Optional[AircraftType]: ) -> Optional[Type[FlyingType]]:
best_choice: Optional[AircraftType] = None best_choice: Optional[Type[FlyingType]] = None
for unit in aircraft_for_task(task): for unit in aircraft_for_task(task):
if unit not in self.faction.aircrafts: if unit not in self.faction.aircrafts:
continue continue
if unit.price * number > max_price: if db.PRICES[unit] * number > max_price:
continue continue
if not airbase.can_operate(unit): if not airbase.can_operate(unit):
continue continue
@@ -241,7 +241,7 @@ class ProcurementAi:
def affordable_aircraft_for( def affordable_aircraft_for(
self, request: AircraftProcurementRequest, airbase: ControlPoint, budget: float self, request: AircraftProcurementRequest, airbase: ControlPoint, budget: float
) -> Optional[AircraftType]: ) -> Optional[Type[FlyingType]]:
return self._affordable_aircraft_for_task( return self._affordable_aircraft_for_task(
request.task_capability, airbase, request.number, budget request.task_capability, airbase, request.number, budget
) )
@@ -259,7 +259,7 @@ class ProcurementAi:
# able to operate expensive aircraft. # able to operate expensive aircraft.
continue continue
budget -= unit.price * request.number budget -= db.PRICES[unit] * request.number
airbase.pending_unit_deliveries.order({unit: request.number}) airbase.pending_unit_deliveries.order({unit: request.number})
return budget, True return budget, True
return budget, False return budget, False
@@ -360,9 +360,9 @@ class ProcurementAi:
class_cost = 0 class_cost = 0
total_cost = 0 total_cost = 0
for unit_type, count in allocations.all.items(): for unit_type, count in allocations.all.items():
cost = unit_type.price * count cost = db.PRICES[unit_type] * count
total_cost += cost total_cost += cost
if unit_type.unit_class is unit_class: if unit_type in unit_class:
class_cost += cost class_cost += cost
if not total_cost: if not total_cost:
return 0 return 0

View File

@@ -5,8 +5,7 @@ import timeit
from collections import defaultdict from collections import defaultdict
from contextlib import contextmanager from contextlib import contextmanager
from datetime import timedelta from datetime import timedelta
from types import TracebackType from typing import Iterator
from typing import Iterator, Optional, Type
@contextmanager @contextmanager
@@ -24,12 +23,7 @@ class MultiEventTracer:
def __enter__(self) -> MultiEventTracer: def __enter__(self) -> MultiEventTracer:
return self return self
def __exit__( def __exit__(self, exc_type, exc_val, exc_tb) -> None:
self,
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
) -> None:
for event, duration in self.events.items(): for event, duration in self.events.items():
logging.debug("%s took %s", event, duration) logging.debug("%s took %s", event, duration)

View File

@@ -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"

View File

@@ -1,48 +0,0 @@
"""Tools for aiding in save compat removal after compatibility breaks."""
from collections import Callable
from typing import TypeVar
from game.version import MAJOR_VERSION
ReturnT = TypeVar("ReturnT")
class DeprecatedSaveCompatError(RuntimeError):
def __init__(self, function_name: str) -> None:
super().__init__(
f"{function_name} has save compat code for a different major version."
)
def has_save_compat_for(
major: int,
) -> Callable[[Callable[..., ReturnT]], Callable[..., ReturnT]]:
"""Declares a function or method as having save compat code for a given version.
If the function has save compatibility for the current major version, there is no
change in behavior.
If the function has save compatibility for a *different* (future or past) major
version, DeprecatedSaveCompatError will be raised during startup. Since a break in
save compatibility is the definition of a major version break, there's no need to
keep around old save compat code; it only serves to mask initialization bugs.
Args:
major: The major version for which the decorated function has save
compatibility.
Returns:
The decorated function or method.
Raises:
DeprecatedSaveCompatError: The decorated function has save compat code for
another version of liberation, and that code (and the decorator declaring it)
should be removed from this branch.
"""
def decorator(func: Callable[..., ReturnT]) -> Callable[..., ReturnT]:
if major != MAJOR_VERSION:
raise DeprecatedSaveCompatError(func.__name__)
return func
return decorator

View File

@@ -1,7 +1,7 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import timedelta from datetime import timedelta
from enum import Enum, unique from enum import Enum, unique
from typing import Dict, Optional, Any from typing import Dict, Optional
from dcs.forcedoptions import ForcedOptions from dcs.forcedoptions import ForcedOptions
@@ -20,7 +20,6 @@ class Settings:
# Difficulty settings # Difficulty settings
player_skill: str = "Good" player_skill: str = "Good"
enemy_skill: str = "Average" enemy_skill: str = "Average"
ai_pilot_levelling: bool = True
enemy_vehicle_skill: str = "Average" enemy_vehicle_skill: str = "Average"
map_coalition_visibility: ForcedOptions.Views = ForcedOptions.Views.All map_coalition_visibility: ForcedOptions.Views = ForcedOptions.Views.All
labels: str = "Full" labels: str = "Full"
@@ -34,18 +33,6 @@ class Settings:
player_income_multiplier: float = 1.0 player_income_multiplier: float = 1.0
enemy_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" default_start_type: str = "Cold"
# Mission specific # Mission specific
@@ -57,7 +44,6 @@ class Settings:
automate_aircraft_reinforcements: bool = False automate_aircraft_reinforcements: bool = False
restrict_weapons_by_date: bool = False restrict_weapons_by_date: bool = False
disable_legacy_aewc: bool = True disable_legacy_aewc: bool = True
disable_legacy_tanker: bool = True
generate_dark_kneeboard: bool = False generate_dark_kneeboard: bool = False
invulnerable_player_pilots: bool = True invulnerable_player_pilots: bool = True
auto_ato_behavior: AutoAtoBehavior = AutoAtoBehavior.Default auto_ato_behavior: AutoAtoBehavior = AutoAtoBehavior.Default
@@ -104,7 +90,7 @@ class Settings:
def set_plugin_option(self, identifier: str, enabled: bool) -> None: def set_plugin_option(self, identifier: str, enabled: bool) -> None:
self.plugins[self.plugin_settings_key(identifier)] = enabled self.plugins[self.plugin_settings_key(identifier)] = enabled
def __setstate__(self, state: dict[str, Any]) -> None: def __setstate__(self, state) -> None:
# __setstate__ is called with the dict of the object being unpickled. We # __setstate__ is called with the dict of the object being unpickled. We
# can provide save compatibility for new settings options (which # can provide save compatibility for new settings options (which
# normally would not be present in the unpickled object) by creating a # normally would not be present in the unpickled object) by creating a

View File

@@ -8,18 +8,19 @@ from dataclasses import dataclass, field
from enum import unique, Enum from enum import unique, Enum
from pathlib import Path from pathlib import Path
from typing import ( from typing import (
Type,
Tuple, Tuple,
TYPE_CHECKING, TYPE_CHECKING,
Optional, Optional,
Iterator, Iterator,
Sequence, Sequence,
Any,
) )
import yaml import yaml
from dcs.unittype import FlyingType
from faker import Faker from faker import Faker
from game.dcs.aircrafttype import AircraftType from game.db import flying_type_from_name
from game.settings import AutoAtoBehavior from game.settings import AutoAtoBehavior
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -78,20 +79,11 @@ class Squadron:
nickname: Optional[str] nickname: Optional[str]
country: str country: str
role: str role: str
aircraft: AircraftType aircraft: Type[FlyingType]
livery: Optional[str] livery: Optional[str]
mission_types: tuple[FlightType, ...] mission_types: tuple[FlightType, ...]
pilots: list[Pilot]
#: The pool of pilots that have not yet been assigned to the squadron. This only available_pilots: list[Pilot] = field(init=False, hash=False, compare=False)
#: 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
)
auto_assignable_mission_types: set[FlightType] = field( auto_assignable_mission_types: set[FlightType] = field(
init=False, hash=False, compare=False init=False, hash=False, compare=False
) )
@@ -103,9 +95,7 @@ class Squadron:
player: bool player: bool
def __post_init__(self) -> None: def __post_init__(self) -> None:
if any(p.status is not PilotStatus.Active for p in self.pilot_pool): self.available_pilots = list(self.active_pilots)
raise ValueError("Squadrons can only be created with active pilots.")
self._recruit_pilots(self.game.settings.squadron_pilot_limit)
self.auto_assignable_mission_types = set(self.mission_types) self.auto_assignable_mission_types = set(self.mission_types)
def __str__(self) -> str: def __str__(self) -> str:
@@ -113,19 +103,12 @@ class Squadron:
return self.name return self.name
return f'{self.name} "{self.nickname}"' 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]: 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: 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. # For opfor, so player/AI option is irrelevant.
if not self.player: if not self.player:
@@ -146,12 +129,11 @@ class Squadron:
# No pilot was found that matched the user's preference. # No pilot was found that matched the user's preference.
# #
# If they chose to *never* assign players and only players remain in the pool, # 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 # If they prefer players and we're out of players, just return an AI pilot.
# pilot.
if not prefer_players: if not prefer_players:
return self.claim_new_pilot_if_allowed() self.enlist_new_pilots(1)
return self.available_pilots.pop() return self.available_pilots.pop()
def claim_pilot(self, pilot: Pilot) -> None: def claim_pilot(self, pilot: Pilot) -> None:
@@ -171,48 +153,23 @@ class Squadron:
# repopulating the same size flight from the same squadron. # repopulating the same size flight from the same squadron.
self.available_pilots.extend(reversed(pilots)) self.available_pilots.extend(reversed(pilots))
def _recruit_pilots(self, count: int) -> None: def enlist_new_pilots(self, count: int) -> None:
new_pilots = self.pilot_pool[:count] new_pilots = [Pilot(self.faker.name()) for _ in range(count)]
self.pilot_pool = self.pilot_pool[count:] self.pilots.extend(new_pilots)
count -= len(new_pilots)
new_pilots.extend([Pilot(self.faker.name()) for _ in range(count)])
self.current_roster.extend(new_pilots)
self.available_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: def return_all_pilots(self) -> None:
self.available_pilots = list(self.active_pilots) 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) -> None:
if not self.has_unfilled_pilot_slots:
raise RuntimeError(
f"Cannot return {pilot} from leave because {self} is full"
)
pilot.return_from_leave()
@property @property
def faker(self) -> Faker: def faker(self) -> Faker:
return self.game.faker_for(self.player) return self.game.faker_for(self.player)
def _pilots_with_status(self, status: PilotStatus) -> list[Pilot]: 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]: 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 @property
def active_pilots(self) -> list[Pilot]: def active_pilots(self) -> list[Pilot]:
@@ -223,47 +180,27 @@ class Squadron:
return self._pilots_with_status(PilotStatus.OnLeave) return self._pilots_with_status(PilotStatus.OnLeave)
@property @property
def number_of_pilots_including_inactive(self) -> int: def number_of_pilots_including_dead(self) -> int:
return len(self.current_roster) return len(self.pilots)
@property @property
def _number_of_unfilled_pilot_slots(self) -> int: def number_of_living_pilots(self) -> int:
return self.game.settings.squadron_pilot_limit - len(self.active_pilots) return len(self._pilots_without_status(PilotStatus.Dead))
@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 pilot_at_index(self, index: int) -> Pilot: def pilot_at_index(self, index: int) -> Pilot:
return self.current_roster[index] return self.pilots[index]
@classmethod @classmethod
def from_yaml(cls, path: Path, game: Game, player: bool) -> Squadron: 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.ai_flight_planner_db import tasks_for_aircraft
from gen.flights.flight import FlightType 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) data = yaml.safe_load(squadron_file)
name = data["aircraft"] unit_type = flying_type_from_name(data["aircraft"])
try: if unit_type is None:
unit_type = AircraftType.named(name) raise KeyError(f"Could not find any aircraft with the ID {unit_type}")
except KeyError as ex:
raise KeyError(f"Could not find any aircraft named {name}") from ex
pilots = [Pilot(n, player=False) for n in data.get("pilots", [])] pilots = [Pilot(n, player=False) for n in data.get("pilots", [])]
pilots.extend([Pilot(n, player=True) for n in data.get("players", [])]) pilots.extend([Pilot(n, player=True) for n in data.get("players", [])])
@@ -286,12 +223,12 @@ class Squadron:
aircraft=unit_type, aircraft=unit_type,
livery=data.get("livery"), livery=data.get("livery"),
mission_types=tuple(mission_types), mission_types=tuple(mission_types),
pilot_pool=pilots, pilots=pilots,
game=game, game=game,
player=player, player=player,
) )
def __setstate__(self, state: dict[str, Any]) -> None: def __setstate__(self, state) -> None:
# TODO: Remove save compat. # TODO: Remove save compat.
if "auto_assignable_mission_types" not in state: if "auto_assignable_mission_types" not in state:
state["auto_assignable_mission_types"] = set(state["mission_types"]) state["auto_assignable_mission_types"] = set(state["mission_types"])
@@ -310,8 +247,8 @@ class SquadronLoader:
yield Path(persistency.base_path()) / "Liberation/Squadrons" yield Path(persistency.base_path()) / "Liberation/Squadrons"
yield Path("resources/squadrons") yield Path("resources/squadrons")
def load(self) -> dict[AircraftType, list[Squadron]]: def load(self) -> dict[Type[FlyingType], list[Squadron]]:
squadrons: dict[AircraftType, list[Squadron]] = defaultdict(list) squadrons: dict[Type[FlyingType], list[Squadron]] = defaultdict(list)
country = self.game.country_for(self.player) country = self.game.country_for(self.player)
faction = self.game.faction_for(self.player) faction = self.game.faction_for(self.player)
any_country = country.startswith("Combined Joint Task Forces ") any_country = country.startswith("Combined Joint Task Forces ")
@@ -376,35 +313,21 @@ class AirWing:
aircraft=aircraft, aircraft=aircraft,
livery=None, livery=None,
mission_types=tuple(tasks_for_aircraft(aircraft)), mission_types=tuple(tasks_for_aircraft(aircraft)),
pilot_pool=[], pilots=[],
game=game, game=game,
player=player, player=player,
) )
] ]
def squadrons_for(self, aircraft: AircraftType) -> Sequence[Squadron]: def squadrons_for(self, aircraft: Type[FlyingType]) -> Sequence[Squadron]:
return self.squadrons[aircraft] return self.squadrons[aircraft]
def can_auto_plan(self, task: FlightType) -> bool: def squadrons_for_task(self, task: FlightType) -> Iterator[Squadron]:
try:
next(self.auto_assignable_for_task(task))
return True
except StopIteration:
return False
def auto_assignable_for_task(self, task: FlightType) -> Iterator[Squadron]:
for squadron in self.iter_squadrons(): for squadron in self.iter_squadrons():
if squadron.can_auto_assign(task): if task in squadron.mission_types:
yield squadron yield squadron
def auto_assignable_for_task_with_type( def squadron_for(self, aircraft: Type[FlyingType]) -> Squadron:
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:
return self.squadrons_for(aircraft)[0] return self.squadrons_for(aircraft)[0]
def iter_squadrons(self) -> Iterator[Squadron]: def iter_squadrons(self) -> Iterator[Squadron]:
@@ -413,10 +336,6 @@ class AirWing:
def squadron_at_index(self, index: int) -> Squadron: def squadron_at_index(self, index: int) -> Squadron:
return list(self.iter_squadrons())[index] return list(self.iter_squadrons())[index]
def replenish(self) -> None:
for squadron in self.iter_squadrons():
squadron.replenish_lost_pilots()
def reset(self) -> None: def reset(self) -> None:
for squadron in self.iter_squadrons(): for squadron in self.iter_squadrons():
squadron.return_all_pilots() squadron.return_all_pilots()

View File

@@ -1,20 +1,33 @@
import itertools import itertools
import logging import logging
from typing import Any import math
import typing
from typing import Dict, Type
from game.dcs.aircrafttype import AircraftType from dcs.task import AWACS, CAP, CAS, Embarking, PinpointStrike, Task, Transport
from game.dcs.groundunittype import GroundUnitType from dcs.unittype import FlyingType, UnitType, VehicleType
from game.dcs.unittype import UnitType from dcs.vehicles import AirDefence, Armor
BASE_MAX_STRENGTH = 1.0 from game import db
BASE_MIN_STRENGTH = 0.0 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
class Base: class Base:
def __init__(self) -> None: def __init__(self):
self.aircraft: dict[AircraftType, int] = {} self.aircraft: Dict[Type[FlyingType], int] = {}
self.armor: dict[GroundUnitType, int] = {} self.armor: Dict[Type[VehicleType], int] = {}
self.strength = 1.0 # TODO: Appears unused.
self.aa: Dict[AirDefence, int] = {}
self.commision_points: Dict[Type, float] = {}
self.strength = 1
@property @property
def total_aircraft(self) -> int: def total_aircraft(self) -> int:
@@ -28,54 +41,147 @@ class Base:
def total_armor_value(self) -> int: def total_armor_value(self) -> int:
total = 0 total = 0
for unit_type, count in self.armor.items(): 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 return total
def total_units_of_type(self, unit_type: UnitType[Any]) -> int: @property
def total_aa(self) -> int:
return sum(self.aa.values())
def total_units(self, task: Task) -> int:
return sum( return sum(
[ [
c 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 if t == unit_type
] ]
) )
def commission_units(self, units: dict[Any, int]) -> None: @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(): for unit_type, unit_count in units.items():
if unit_count <= 0: if unit_count <= 0:
continue continue
target_dict: dict[Any, int] for_task = db.unit_task(unit_type)
if isinstance(unit_type, AircraftType):
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 target_dict = self.aircraft
elif isinstance(unit_type, GroundUnitType): elif for_task == PinpointStrike:
target_dict = self.armor 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: else:
logging.error(f"Unexpected unit type of {unit_type}") logging.error("Unable to determine target dict for " + str(unit_type))
return
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]) -> None:
for unit_type, count in units_lost.items(): for unit_type, count in units_lost.items():
target_dict: dict[Any, int]
if unit_type in self.aircraft: if unit_type in self.aircraft:
target_dict = self.aircraft target_array = self.aircraft
elif unit_type in self.armor: elif unit_type in self.armor:
target_dict = self.armor target_array = self.armor
else: else:
print("Base didn't find event type {}".format(unit_type)) print("Base didn't find event type {}".format(unit_type))
continue 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)) print("Base didn't find event type {}".format(unit_type))
continue continue
target_dict[unit_type] = max(target_dict[unit_type] - count, 0) target_array[unit_type] = max(target_array[unit_type] - count, 0)
if target_dict[unit_type] == 0: if target_array[unit_type] == 0:
del target_dict[unit_type] del target_array[unit_type]
def affect_strength(self, amount: float) -> None: def affect_strength(self, amount):
self.strength += amount self.strength += amount
if self.strength > BASE_MAX_STRENGTH: if self.strength > BASE_MAX_STRENGTH:
self.strength = BASE_MAX_STRENGTH self.strength = BASE_MAX_STRENGTH
@@ -84,3 +190,55 @@ class Base:
def set_strength_to_minimum(self) -> None: def set_strength_to_minimum(self) -> None:
self.strength = BASE_MIN_STRENGTH 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(),
)

View File

@@ -5,7 +5,7 @@ import math
from dataclasses import dataclass from dataclasses import dataclass
from functools import cached_property from functools import cached_property
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Iterator, List, Optional, Tuple, TYPE_CHECKING from typing import Any, Dict, Iterator, List, Optional, Tuple
from dcs import Mission from dcs import Mission
from dcs.countries import ( from dcs.countries import (
@@ -16,10 +16,10 @@ from dcs.country import Country
from dcs.mapping import Point from dcs.mapping import Point
from dcs.planes import F_15C from dcs.planes import F_15C
from dcs.ships import ( from dcs.ships import (
HandyWind, Bulker_Handy_Wind,
Stennis, CVN_74_John_C__Stennis,
USS_Arleigh_Burke_IIa, DDG_Arleigh_Burke_IIa,
LHA_Tarawa, LHA_1_Tarawa,
) )
from dcs.statics import Fortification, Warehouse from dcs.statics import Fortification, Warehouse
from dcs.terrain import ( from dcs.terrain import (
@@ -29,14 +29,14 @@ from dcs.terrain import (
persiangulf, persiangulf,
syria, syria,
thechannel, thechannel,
marianaislands,
) )
from dcs.terrain.terrain import Airport, Terrain from dcs.terrain.terrain import Airport, Terrain
from dcs.unitgroup import ( from dcs.unitgroup import (
FlyingGroup,
Group,
ShipGroup, ShipGroup,
StaticGroup, StaticGroup,
VehicleGroup, VehicleGroup,
PlaneGroup,
) )
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
from pyproj import CRS, Transformer from pyproj import CRS, Transformer
@@ -56,14 +56,10 @@ from .landmap import Landmap, load_landmap, poly_contains
from .latlon import LatLon from .latlon import LatLon
from .projections import TransverseMercator from .projections import TransverseMercator
from ..point_with_heading import PointWithHeading from ..point_with_heading import PointWithHeading
from ..positioned import Positioned
from ..profiling import logged_duration from ..profiling import logged_duration
from ..scenery_group import SceneryGroup from ..scenery_group import SceneryGroup
from ..utils import Distance, meters from ..utils import Distance, meters
if TYPE_CHECKING:
from . import TheaterGroundObject
SIZE_TINY = 150 SIZE_TINY = 150
SIZE_SMALL = 600 SIZE_SMALL = 600
SIZE_REGULAR = 1000 SIZE_REGULAR = 1000
@@ -81,53 +77,53 @@ class MizCampaignLoader:
OFF_MAP_UNIT_TYPE = F_15C.id OFF_MAP_UNIT_TYPE = F_15C.id
CV_UNIT_TYPE = Stennis.id CV_UNIT_TYPE = CVN_74_John_C__Stennis.id
LHA_UNIT_TYPE = LHA_Tarawa.id LHA_UNIT_TYPE = LHA_1_Tarawa.id
FRONT_LINE_UNIT_TYPE = Armor.M_113.id FRONT_LINE_UNIT_TYPE = Armor.APC_M113.id
SHIPPING_LANE_UNIT_TYPE = HandyWind.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" FARP_HELIPAD = "SINGLE_HELIPAD"
OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id
SHIP_UNIT_TYPE = USS_Arleigh_Burke_IIa.id SHIP_UNIT_TYPE = DDG_Arleigh_Burke_IIa.id
MISSILE_SITE_UNIT_TYPE = MissilesSS.Scud_B.id MISSILE_SITE_UNIT_TYPE = MissilesSS.SSM_SS_1C_Scud_B.id
COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.Hy_launcher.id COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.AShM_SS_N_2_Silkworm.id
# Multiple options for air defenses so campaign designers can more accurately see # Multiple options for air defenses so campaign designers can more accurately see
# the coverage of their IADS for the expected type. # the coverage of their IADS for the expected type.
LONG_RANGE_SAM_UNIT_TYPES = { LONG_RANGE_SAM_UNIT_TYPES = {
AirDefence.Patriot_ln.id, AirDefence.SAM_Patriot_LN.id,
AirDefence.S_300PS_5P85C_ln.id, AirDefence.SAM_SA_10_S_300_Grumble_TEL_C.id,
AirDefence.S_300PS_5P85D_ln.id, AirDefence.SAM_SA_10_S_300_Grumble_TEL_D.id,
} }
MEDIUM_RANGE_SAM_UNIT_TYPES = { MEDIUM_RANGE_SAM_UNIT_TYPES = {
AirDefence.Hawk_ln.id, AirDefence.SAM_Hawk_LN_M192.id,
AirDefence.S_75M_Volhov.id, AirDefence.SAM_SA_2_S_75_Guideline_LN.id,
AirDefence._5p73_s_125_ln.id, AirDefence.SAM_SA_3_S_125_Goa_LN.id,
} }
SHORT_RANGE_SAM_UNIT_TYPES = { SHORT_RANGE_SAM_UNIT_TYPES = {
AirDefence.M1097_Avenger.id, AirDefence.SAM_Avenger__Stinger.id,
AirDefence.Rapier_fsa_launcher.id, AirDefence.SAM_Rapier_LN.id,
AirDefence._2S6_Tunguska.id, AirDefence.SAM_SA_19_Tunguska_Grison.id,
AirDefence.Strela_1_9P31.id, AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL.id,
} }
AAA_UNIT_TYPES = { AAA_UNIT_TYPES = {
AirDefence.Flak18.id, AirDefence.AAA_8_8cm_Flak_18.id,
AirDefence.Vulcan.id, AirDefence.SPAAA_Vulcan_M163.id,
AirDefence.ZSU_23_4_Shilka.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 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 STRIKE_TARGET_UNIT_TYPE = Fortification.Tech_combine.id
@@ -185,7 +181,7 @@ class MizCampaignLoader:
def red(self) -> Country: def red(self) -> Country:
return self.country(blue=False) return self.country(blue=False)
def off_map_spawns(self, blue: bool) -> Iterator[PlaneGroup]: def off_map_spawns(self, blue: bool) -> Iterator[FlyingGroup]:
for group in self.country(blue).plane_group: for group in self.country(blue).plane_group:
if group.units[0].type == self.OFF_MAP_UNIT_TYPE: if group.units[0].type == self.OFF_MAP_UNIT_TYPE:
yield group yield group
@@ -309,26 +305,26 @@ class MizCampaignLoader:
control_point.captured = blue control_point.captured = blue
control_point.captured_invert = group.late_activation control_point.captured_invert = group.late_activation
control_points[control_point.id] = control_point control_points[control_point.id] = control_point
for ship in self.carriers(blue): for group in self.carriers(blue):
# TODO: Name the carrier. # TODO: Name the carrier.
control_point = Carrier( control_point = Carrier(
"carrier", ship.position, next(self.control_point_id) "carrier", group.position, next(self.control_point_id)
) )
control_point.captured = blue control_point.captured = blue
control_point.captured_invert = ship.late_activation control_point.captured_invert = group.late_activation
control_points[control_point.id] = control_point control_points[control_point.id] = control_point
for ship in self.lhas(blue): for group in self.lhas(blue):
# TODO: Name the LHA.db # TODO: Name the LHA.db
control_point = Lha("lha", ship.position, next(self.control_point_id)) control_point = Lha("lha", group.position, next(self.control_point_id))
control_point.captured = blue control_point.captured = blue
control_point.captured_invert = ship.late_activation control_point.captured_invert = group.late_activation
control_points[control_point.id] = control_point control_points[control_point.id] = control_point
for fob in self.fobs(blue): for group in self.fobs(blue):
control_point = Fob( control_point = Fob(
str(fob.name), fob.position, next(self.control_point_id) str(group.name), group.position, next(self.control_point_id)
) )
control_point.captured = blue control_point.captured = blue
control_point.captured_invert = fob.late_activation control_point.captured_invert = group.late_activation
control_points[control_point.id] = control_point control_points[control_point.id] = control_point
return control_points return control_points
@@ -389,22 +385,22 @@ class MizCampaignLoader:
origin, list(reversed(waypoints)) origin, list(reversed(waypoints))
) )
def objective_info(self, near: Positioned) -> Tuple[ControlPoint, Distance]: def objective_info(self, group: Group) -> Tuple[ControlPoint, Distance]:
closest = self.theater.closest_control_point(near.position) closest = self.theater.closest_control_point(group.position)
distance = meters(closest.position.distance_to_point(near.position)) distance = meters(closest.position.distance_to_point(group.position))
return closest, distance return closest, distance
def add_preset_locations(self) -> None: def add_preset_locations(self) -> None:
for static in self.offshore_strike_targets: for group in self.offshore_strike_targets:
closest, distance = self.objective_info(static) closest, distance = self.objective_info(group)
closest.preset_locations.offshore_strike_locations.append( closest.preset_locations.offshore_strike_locations.append(
PointWithHeading.from_point(static.position, static.units[0].heading) PointWithHeading.from_point(group.position, group.units[0].heading)
) )
for ship in self.ships: for group in self.ships:
closest, distance = self.objective_info(ship) closest, distance = self.objective_info(group)
closest.preset_locations.ships.append( closest.preset_locations.ships.append(
PointWithHeading.from_point(ship.position, ship.units[0].heading) PointWithHeading.from_point(group.position, group.units[0].heading)
) )
for group in self.missile_sites: for group in self.missile_sites:
@@ -455,33 +451,33 @@ class MizCampaignLoader:
PointWithHeading.from_point(group.position, group.units[0].heading) PointWithHeading.from_point(group.position, group.units[0].heading)
) )
for static in self.helipads: for group in self.helipads:
closest, distance = self.objective_info(static) closest, distance = self.objective_info(group)
closest.helipads.append( closest.helipads.append(
PointWithHeading.from_point(static.position, static.units[0].heading) PointWithHeading.from_point(group.position, group.units[0].heading)
) )
for static in self.factories: for group in self.factories:
closest, distance = self.objective_info(static) closest, distance = self.objective_info(group)
closest.preset_locations.factories.append( closest.preset_locations.factories.append(
PointWithHeading.from_point(static.position, static.units[0].heading) PointWithHeading.from_point(group.position, group.units[0].heading)
) )
for static in self.ammunition_depots: for group in self.ammunition_depots:
closest, distance = self.objective_info(static) closest, distance = self.objective_info(group)
closest.preset_locations.ammunition_depots.append( closest.preset_locations.ammunition_depots.append(
PointWithHeading.from_point(static.position, static.units[0].heading) PointWithHeading.from_point(group.position, group.units[0].heading)
) )
for static in self.strike_targets: for group in self.strike_targets:
closest, distance = self.objective_info(static) closest, distance = self.objective_info(group)
closest.preset_locations.strike_locations.append( closest.preset_locations.strike_locations.append(
PointWithHeading.from_point(static.position, static.units[0].heading) PointWithHeading.from_point(group.position, group.units[0].heading)
) )
for scenery_group in self.scenery: for group in self.scenery:
closest, distance = self.objective_info(scenery_group) closest, distance = self.objective_info(group)
closest.preset_locations.scenery.append(scenery_group) closest.preset_locations.scenery.append(group)
def populate_theater(self) -> None: def populate_theater(self) -> None:
for control_point in self.control_points.values(): for control_point in self.control_points.values():
@@ -508,7 +504,7 @@ class ConflictTheater:
""" """
daytime_map: Dict[str, Tuple[int, int]] daytime_map: Dict[str, Tuple[int, int]]
def __init__(self) -> None: def __init__(self):
self.controlpoints: List[ControlPoint] = [] self.controlpoints: List[ControlPoint] = []
self.point_to_ll_transformer = Transformer.from_crs( self.point_to_ll_transformer = Transformer.from_crs(
self.projection_parameters.to_crs(), CRS("WGS84") self.projection_parameters.to_crs(), CRS("WGS84")
@@ -540,12 +536,10 @@ class ConflictTheater:
CRS("WGS84"), self.projection_parameters.to_crs() CRS("WGS84"), self.projection_parameters.to_crs()
) )
def add_controlpoint(self, point: ControlPoint) -> None: def add_controlpoint(self, point: ControlPoint):
self.controlpoints.append(point) self.controlpoints.append(point)
def find_ground_objects_by_obj_name( def find_ground_objects_by_obj_name(self, obj_name):
self, obj_name: str
) -> list[TheaterGroundObject[Any]]:
found = [] found = []
for cp in self.controlpoints: for cp in self.controlpoints:
for g in cp.ground_objects: for g in cp.ground_objects:
@@ -587,12 +581,12 @@ class ConflictTheater:
return True return True
def nearest_land_pos(self, near: Point, extend_dist: int = 50) -> Point: def nearest_land_pos(self, point: Point, extend_dist: int = 50) -> Point:
"""Returns the nearest point inside a land exclusion zone from point """Returns the nearest point inside a land exclusion zone from point
`extend_dist` determines how far inside the zone the point should be placed""" `extend_dist` determines how far inside the zone the point should be placed"""
if self.is_on_land(near): if self.is_on_land(point):
return near return point
point = geometry.Point(near.x, near.y) point = geometry.Point(point.x, point.y)
nearest_points = [] nearest_points = []
if not self.landmap: if not self.landmap:
raise RuntimeError("Landmap not initialized") raise RuntimeError("Landmap not initialized")
@@ -862,22 +856,3 @@ class SyriaTheater(ConflictTheater):
from .syria import PARAMETERS from .syria import PARAMETERS
return PARAMETERS return PARAMETERS
class MarianaIslandsTheater(ConflictTheater):
terrain = marianaislands.MarianaIslands()
overview_image = "marianaislands.gif"
landmap = load_landmap("resources\\marianaislandslandmap.p")
daytime_map = {
"dawn": (6, 8),
"day": (8, 16),
"dusk": (16, 18),
"night": (0, 5),
}
@property
def projection_parameters(self) -> TransverseMercator:
from .marianaislands import PARAMETERS
return PARAMETERS

View File

@@ -16,6 +16,7 @@ from typing import (
Optional, Optional,
Set, Set,
TYPE_CHECKING, TYPE_CHECKING,
Type,
Union, Union,
Sequence, Sequence,
Iterable, Iterable,
@@ -24,13 +25,14 @@ from typing import (
from dcs.mapping import Point from dcs.mapping import Point
from dcs.ships import ( from dcs.ships import (
Stennis, CVN_74_John_C__Stennis,
KUZNECOW, CV_1143_5_Admiral_Kuznetsov,
LHA_Tarawa, LHA_1_Tarawa,
Type_071, Type_071_Amphibious_Transport_Dock,
) )
from dcs.terrain.terrain import Airport, ParkingSlot from dcs.terrain.terrain import Airport, ParkingSlot
from dcs.unit import Unit from dcs.unit import Unit
from dcs.unittype import FlyingType, VehicleType
from game import db from game import db
from game.point_with_heading import PointWithHeading from game.point_with_heading import PointWithHeading
@@ -43,10 +45,8 @@ from .missiontarget import MissionTarget
from .theatergroundobject import ( from .theatergroundobject import (
GenericCarrierGroundObject, GenericCarrierGroundObject,
TheaterGroundObject, TheaterGroundObject,
NavalGroundObject,
) )
from ..dcs.aircrafttype import AircraftType from ..db import PRICES
from ..dcs.groundunittype import GroundUnitType
from ..utils import nautical_miles from ..utils import nautical_miles
from ..weather import Conditions from ..weather import Conditions
@@ -125,19 +125,19 @@ class PresetLocations:
@dataclass(frozen=True) @dataclass(frozen=True)
class AircraftAllocations: class AircraftAllocations:
present: dict[AircraftType, int] present: dict[Type[FlyingType], int]
ordered: dict[AircraftType, int] ordered: dict[Type[FlyingType], int]
transferring: dict[AircraftType, int] transferring: dict[Type[FlyingType], int]
@property @property
def total_value(self) -> int: def total_value(self) -> int:
total: int = 0 total: int = 0
for unit_type, count in self.present.items(): 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(): 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(): for unit_type, count in self.transferring.items():
total += unit_type.price * count total += PRICES[unit_type] * count
return total return total
@@ -160,13 +160,13 @@ class AircraftAllocations:
@dataclass(frozen=True) @dataclass(frozen=True)
class GroundUnitAllocations: class GroundUnitAllocations:
present: dict[GroundUnitType, int] present: dict[Type[VehicleType], int]
ordered: dict[GroundUnitType, int] ordered: dict[Type[VehicleType], int]
transferring: dict[GroundUnitType, int] transferring: dict[Type[VehicleType], int]
@property @property
def all(self) -> dict[GroundUnitType, int]: def all(self) -> dict[Type[VehicleType], int]:
combined: dict[GroundUnitType, int] = defaultdict(int) combined: dict[Type[VehicleType], int] = defaultdict(int)
for unit_type, count in itertools.chain( for unit_type, count in itertools.chain(
self.present.items(), self.ordered.items(), self.transferring.items() self.present.items(), self.ordered.items(), self.transferring.items()
): ):
@@ -177,11 +177,11 @@ class GroundUnitAllocations:
def total_value(self) -> int: def total_value(self) -> int:
total: int = 0 total: int = 0
for unit_type, count in self.present.items(): 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(): 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(): for unit_type, count in self.transferring.items():
total += unit_type.price * count total += PRICES[unit_type] * count
return total return total
@@ -291,15 +291,15 @@ class ControlPoint(MissionTarget, ABC):
at: db.StartingPosition, at: db.StartingPosition,
size: int, size: int,
importance: float, importance: float,
has_frontline: bool = True, has_frontline=True,
cptype: ControlPointType = ControlPointType.AIRBASE, cptype=ControlPointType.AIRBASE,
) -> None: ):
super().__init__(name, position) super().__init__(name, position)
# TODO: Should be Airbase specific. # TODO: Should be Airbase specific.
self.id = cp_id self.id = cp_id
self.full_name = name self.full_name = name
self.at = at self.at = at
self.connected_objectives: List[TheaterGroundObject[Any]] = [] self.connected_objectives: List[TheaterGroundObject] = []
self.preset_locations = PresetLocations() self.preset_locations = PresetLocations()
self.helipads: List[PointWithHeading] = [] self.helipads: List[PointWithHeading] = []
@@ -323,11 +323,11 @@ class ControlPoint(MissionTarget, ABC):
self.target_position: Optional[Point] = None self.target_position: Optional[Point] = None
def __repr__(self) -> str: def __repr__(self):
return f"<{self.__class__}: {self.name}>" return f"<{__class__}: {self.name}>"
@property @property
def ground_objects(self) -> List[TheaterGroundObject[Any]]: def ground_objects(self) -> List[TheaterGroundObject]:
return list(self.connected_objectives) return list(self.connected_objectives)
@property @property
@@ -335,11 +335,11 @@ class ControlPoint(MissionTarget, ABC):
def heading(self) -> int: def heading(self) -> int:
... ...
def __str__(self) -> str: def __str__(self):
return self.name return self.name
@property @property
def is_global(self) -> bool: def is_global(self):
return not self.connected_points return not self.connected_points
def transitive_connected_friendly_points( def transitive_connected_friendly_points(
@@ -406,21 +406,21 @@ class ControlPoint(MissionTarget, ABC):
return False return False
@property @property
def is_carrier(self) -> bool: def is_carrier(self):
""" """
:return: Whether this control point is an aircraft carrier :return: Whether this control point is an aircraft carrier
""" """
return False return False
@property @property
def is_fleet(self) -> bool: def is_fleet(self):
""" """
:return: Whether this control point is a boat (mobile) :return: Whether this control point is a boat (mobile)
""" """
return False return False
@property @property
def is_lha(self) -> bool: def is_lha(self):
""" """
:return: Whether this control point is an LHA :return: Whether this control point is an LHA
""" """
@@ -440,7 +440,7 @@ class ControlPoint(MissionTarget, ABC):
@property @property
@abstractmethod @abstractmethod
def total_aircraft_parking(self) -> int: def total_aircraft_parking(self):
""" """
:return: The maximum number of aircraft that can be stored in this :return: The maximum number of aircraft that can be stored in this
control point control point
@@ -472,7 +472,7 @@ class ControlPoint(MissionTarget, ABC):
... ...
# TODO: Should be naval specific. # TODO: Should be naval specific.
def get_carrier_group_name(self) -> Optional[str]: def get_carrier_group_name(self):
""" """
Get the carrier group name if the airbase is a carrier Get the carrier group name if the airbase is a carrier
:return: Carrier group name :return: Carrier group name
@@ -486,24 +486,22 @@ class ControlPoint(MissionTarget, ABC):
for group in g.groups: for group in g.groups:
for u in group.units: for u in group.units:
if db.unit_type_from_name(u.type) in [ if db.unit_type_from_name(u.type) in [
Stennis, CVN_74_John_C__Stennis,
KUZNECOW, CV_1143_5_Admiral_Kuznetsov,
]: ]:
return group.name return group.name
elif g.dcs_identifier == "LHA": elif g.dcs_identifier == "LHA":
for group in g.groups: for group in g.groups:
for u in group.units: 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 group.name
return None return None
# TODO: Should be Airbase specific. # TODO: Should be Airbase specific.
def is_connected(self, to: ControlPoint) -> bool: def is_connected(self, to) -> bool:
return to in self.connected_points return to in self.connected_points
def find_ground_objects_by_obj_name( def find_ground_objects_by_obj_name(self, obj_name):
self, obj_name: str
) -> list[TheaterGroundObject[Any]]:
found = [] found = []
for g in self.ground_objects: for g in self.ground_objects:
if g.obj_name == obj_name: if g.obj_name == obj_name:
@@ -525,7 +523,7 @@ class ControlPoint(MissionTarget, ABC):
f"vehicles have been captured and sold for ${total}M." f"vehicles have been captured and sold for ${total}M."
) )
def retreat_ground_units(self, game: Game) -> None: def retreat_ground_units(self, game: Game):
# When there are multiple valid destinations, deliver units to whichever # When there are multiple valid destinations, deliver units to whichever
# base is least defended first. The closest approximation of unit # base is least defended first. The closest approximation of unit
# strength we have is price # strength we have is price
@@ -543,19 +541,27 @@ class ControlPoint(MissionTarget, ABC):
while self.base.armor: while self.base.armor:
unit_type, count = self.base.armor.popitem() unit_type, count = self.base.armor.popitem()
for _ in range(count): 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) destination = heapq.heappushpop(destinations, destination)
def capture_aircraft(self, game: Game, airframe: AircraftType, count: int) -> None: def capture_aircraft(
value = airframe.price * count 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.adjust_budget(value, player=not self.captured)
game.message( game.message(
f"No valid retreat destination in range of {self.name} for {airframe}" f"No valid retreat destination in range of {self.name} for "
f"{count} aircraft have been captured and sold for ${value}M." f"{airframe.id}. {count} aircraft have been captured and sold for "
f"${value}M."
) )
def aircraft_retreat_destination( def aircraft_retreat_destination(
self, game: Game, airframe: AircraftType self, game: Game, airframe: Type[FlyingType]
) -> Optional[ControlPoint]: ) -> Optional[ControlPoint]:
closest = ObjectiveDistanceCache.get_closest_airfields(self) closest = ObjectiveDistanceCache.get_closest_airfields(self)
# TODO: Should be airframe dependent. # TODO: Should be airframe dependent.
@@ -573,17 +579,17 @@ class ControlPoint(MissionTarget, ABC):
return None return None
def _retreat_air_units( def _retreat_air_units(
self, game: Game, airframe: AircraftType, count: int self, game: Game, airframe: Type[FlyingType], count: int
) -> None: ) -> None:
while count: 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) destination = self.aircraft_retreat_destination(game, airframe)
if destination is None: if destination is None:
self.capture_aircraft(game, airframe, count) self.capture_aircraft(game, airframe, count)
return return
parking = destination.unclaimed_parking(game) parking = destination.unclaimed_parking(game)
transfer_amount = min([parking, count]) transfer_amount = min([parking, count])
destination.base.commission_units({airframe: transfer_amount}) destination.base.commision_units({airframe: transfer_amount})
count -= transfer_amount count -= transfer_amount
def retreat_air_units(self, game: Game) -> None: def retreat_air_units(self, game: Game) -> None:
@@ -612,16 +618,16 @@ class ControlPoint(MissionTarget, ABC):
self.base.set_strength_to_minimum() self.base.set_strength_to_minimum()
@abstractmethod @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: if self.captured:
ato = game.blue_ato ato = game.blue_ato
else: else:
ato = game.red_ato ato = game.red_ato
transferring: defaultdict[AircraftType, int] = defaultdict(int) transferring: defaultdict[Type[FlyingType], int] = defaultdict(int)
for package in ato.packages: for package in ato.packages:
for flight in package.flights: for flight in package.flights:
if flight.departure == flight.arrival: if flight.departure == flight.arrival:
@@ -686,7 +692,7 @@ class ControlPoint(MissionTarget, ABC):
def allocated_aircraft(self, game: Game) -> AircraftAllocations: def allocated_aircraft(self, game: Game) -> AircraftAllocations:
on_order = {} on_order = {}
for unit_bought, count in self.pending_unit_deliveries.units.items(): 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 on_order[unit_bought] = count
return AircraftAllocations( return AircraftAllocations(
@@ -698,10 +704,10 @@ class ControlPoint(MissionTarget, ABC):
) -> GroundUnitAllocations: ) -> GroundUnitAllocations:
on_order = {} on_order = {}
for unit_bought, count in self.pending_unit_deliveries.units.items(): 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 on_order[unit_bought] = count
transferring: dict[GroundUnitType, int] = defaultdict(int) transferring: dict[Type[VehicleType], int] = defaultdict(int)
for transfer in transfers: for transfer in transfers:
if transfer.destination == self: if transfer.destination == self:
for unit_type, count in transfer.units.items(): for unit_type, count in transfer.units.items():
@@ -751,7 +757,7 @@ class ControlPoint(MissionTarget, ABC):
return len([obj for obj in self.connected_objectives if obj.category == "ammo"]) return len([obj for obj in self.connected_objectives if obj.category == "ammo"])
@property @property
def strike_targets(self) -> Sequence[Union[MissionTarget, Unit]]: def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
return [] return []
@property @property
@@ -767,8 +773,8 @@ class ControlPoint(MissionTarget, ABC):
class Airfield(ControlPoint): class Airfield(ControlPoint):
def __init__( def __init__(
self, airport: Airport, size: int, importance: float, has_frontline: bool = True self, airport: Airport, size: int, importance: float, has_frontline=True
) -> None: ):
super().__init__( super().__init__(
airport.id, airport.id,
airport.name, airport.name,
@@ -782,7 +788,7 @@ class Airfield(ControlPoint):
self.airport = airport self.airport = airport
self._runway_status = RunwayStatus() self._runway_status = RunwayStatus()
def can_operate(self, aircraft: AircraftType) -> bool: def can_operate(self, aircraft: FlyingType) -> bool:
# TODO: Allow helicopters. # TODO: Allow helicopters.
# Need to implement ground spawns so the helos don't use the runway. # Need to implement ground spawns so the helos don't use the runway.
# TODO: Allow harrier. # TODO: Allow harrier.
@@ -804,7 +810,6 @@ class Airfield(ControlPoint):
if self.is_friendly(for_player): if self.is_friendly(for_player):
yield from [ yield from [
FlightType.AEWC, FlightType.AEWC,
FlightType.REFUELING,
# TODO: FlightType.INTERCEPTION # TODO: FlightType.INTERCEPTION
# TODO: FlightType.LOGISTICS # TODO: FlightType.LOGISTICS
] ]
@@ -882,12 +887,9 @@ class NavalControlPoint(ControlPoint, ABC):
def heading(self) -> int: def heading(self) -> int:
return 0 # TODO compute heading return 0 # TODO compute heading
def find_main_tgo(self) -> GenericCarrierGroundObject: def find_main_tgo(self) -> TheaterGroundObject:
for g in self.ground_objects: for g in self.ground_objects:
if isinstance(g, GenericCarrierGroundObject) and g.dcs_identifier in [ if g.dcs_identifier in ["CARRIER", "LHA"]:
"CARRIER",
"LHA",
]:
return g return g
raise RuntimeError(f"Found no carrier/LHA group for {self.name}") raise RuntimeError(f"Found no carrier/LHA group for {self.name}")
@@ -897,10 +899,10 @@ class NavalControlPoint(ControlPoint, ABC):
for group in self.find_main_tgo().groups: for group in self.find_main_tgo().groups:
for u in group.units: for u in group.units:
if db.unit_type_from_name(u.type) in [ if db.unit_type_from_name(u.type) in [
Stennis, CVN_74_John_C__Stennis,
LHA_Tarawa, LHA_1_Tarawa,
KUZNECOW, CV_1143_5_Admiral_Kuznetsov,
Type_071, Type_071_Amphibious_Transport_Dock,
]: ]:
return True return True
return False return False
@@ -957,20 +959,17 @@ class Carrier(NavalControlPoint):
yield from super().mission_types(for_player) yield from super().mission_types(for_player)
if self.is_friendly(for_player): if self.is_friendly(for_player):
yield from [ yield FlightType.AEWC
FlightType.AEWC,
FlightType.REFUELING,
]
def capture(self, game: Game, for_player: bool) -> None: def capture(self, game: Game, for_player: bool) -> None:
raise RuntimeError("Carriers cannot be captured") raise RuntimeError("Carriers cannot be captured")
@property @property
def is_carrier(self) -> bool: def is_carrier(self):
return True return True
def can_operate(self, aircraft: AircraftType) -> bool: def can_operate(self, aircraft: FlyingType) -> bool:
return aircraft.carrier_capable return aircraft in db.CARRIER_CAPABLE
@property @property
def total_aircraft_parking(self) -> int: def total_aircraft_parking(self) -> int:
@@ -1003,8 +1002,8 @@ class Lha(NavalControlPoint):
def is_lha(self) -> bool: def is_lha(self) -> bool:
return True return True
def can_operate(self, aircraft: AircraftType) -> bool: def can_operate(self, aircraft: FlyingType) -> bool:
return aircraft.lha_capable return aircraft in db.LHA_CAPABLE
@property @property
def total_aircraft_parking(self) -> int: def total_aircraft_parking(self) -> int:
@@ -1043,7 +1042,7 @@ class OffMapSpawn(ControlPoint):
def total_aircraft_parking(self) -> int: def total_aircraft_parking(self) -> int:
return 1000 return 1000
def can_operate(self, aircraft: AircraftType) -> bool: def can_operate(self, aircraft: FlyingType) -> bool:
return True return True
@property @property
@@ -1114,7 +1113,7 @@ class Fob(ControlPoint):
def total_aircraft_parking(self) -> int: def total_aircraft_parking(self) -> int:
return 0 return 0
def can_operate(self, aircraft: AircraftType) -> bool: def can_operate(self, aircraft: FlyingType) -> bool:
return False return False
@property @property

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import logging import logging
from dataclasses import dataclass from dataclasses import dataclass
from typing import Iterator, List, Tuple, Any from typing import Iterator, List, Tuple
from dcs.mapping import Point from dcs.mapping import Point
@@ -66,15 +66,7 @@ class FrontLine(MissionTarget):
self.segments: List[FrontLineSegment] = [ self.segments: List[FrontLineSegment] = [
FrontLineSegment(a, b) for a, b in pairwise(route) FrontLineSegment(a, b) for a, b in pairwise(route)
] ]
super().__init__( self.name = f"Front line {blue_point}/{red_point}"
f"Front line {blue_point}/{red_point}",
self.point_from_a(self._position_distance),
)
def __setstate__(self, state: dict[str, Any]) -> None:
self.__dict__.update(state)
if not hasattr(self, "position"):
self.position = self.point_from_a(self._position_distance)
def control_point_hostile_to(self, player: bool) -> ControlPoint: def control_point_hostile_to(self, player: bool) -> ControlPoint:
if player: if player:
@@ -89,12 +81,19 @@ class FrontLine(MissionTarget):
yield from [ yield from [
FlightType.CAS, FlightType.CAS,
FlightType.AEWC, FlightType.AEWC,
FlightType.REFUELING
# TODO: FlightType.TROOP_TRANSPORT # TODO: FlightType.TROOP_TRANSPORT
# TODO: FlightType.EVAC # TODO: FlightType.EVAC
] ]
yield from super().mission_types(for_player) yield from super().mission_types(for_player)
@property
def position(self):
"""
The position where the conflict should occur
according to the current strength of each control point.
"""
return self.point_from_a(self._position_distance)
@property @property
def points(self) -> Iterator[Point]: def points(self) -> Iterator[Point]:
yield self.segments[0].point_a yield self.segments[0].point_a
@@ -107,12 +106,12 @@ class FrontLine(MissionTarget):
return self.blue_cp, self.red_cp return self.blue_cp, self.red_cp
@property @property
def attack_distance(self) -> float: def attack_distance(self):
"""The total distance of all segments""" """The total distance of all segments"""
return sum(i.attack_distance for i in self.segments) return sum(i.attack_distance for i in self.segments)
@property @property
def attack_heading(self) -> float: def attack_heading(self):
"""The heading of the active attack segment from player to enemy control point""" """The heading of the active attack segment from player to enemy control point"""
return self.active_segment.attack_heading return self.active_segment.attack_heading
@@ -149,9 +148,6 @@ class FrontLine(MissionTarget):
) )
else: else:
remaining_dist -= segment.attack_distance remaining_dist -= segment.attack_distance
raise RuntimeError(
f"Could not find front line point {distance} from {self.blue_cp}"
)
@property @property
def _position_distance(self) -> float: def _position_distance(self) -> float:

View File

@@ -14,7 +14,7 @@ class Landmap:
exclusion_zones: MultiPolygon exclusion_zones: MultiPolygon
sea_zones: MultiPolygon sea_zones: MultiPolygon
def __post_init__(self) -> None: def __post_init__(self):
if not self.inclusion_zones.is_valid: if not self.inclusion_zones.is_valid:
raise RuntimeError("Inclusion zones not valid") raise RuntimeError("Inclusion zones not valid")
if not self.exclusion_zones.is_valid: if not self.exclusion_zones.is_valid:
@@ -36,5 +36,13 @@ def load_landmap(filename: str) -> Optional[Landmap]:
return None return None
def poly_contains(x: float, y: float, poly: Union[MultiPolygon, Polygon]) -> bool: def poly_contains(x, y, poly: Union[MultiPolygon, Polygon]):
return poly.contains(geometry.Point(x, y)) return poly.contains(geometry.Point(x, y))
def poly_centroid(poly) -> Tuple[float, float]:
x_list = [vertex[0] for vertex in poly]
y_list = [vertex[1] for vertex in poly]
x = sum(x_list) / len(poly)
y = sum(y_list) / len(poly)
return (x, y)

View File

@@ -1,8 +0,0 @@
from game.theater.projections import TransverseMercator
PARAMETERS = TransverseMercator(
central_meridian=147,
false_easting=238417.99999989968,
false_northing=-1491840.000000048,
scale_factor=0.9996,
)

View File

@@ -1,6 +1,5 @@
from __future__ import annotations from __future__ import annotations
from collections import Sequence
from typing import Iterator, TYPE_CHECKING, List, Union from typing import Iterator, TYPE_CHECKING, List, Union
from dcs.mapping import Point from dcs.mapping import Point
@@ -21,7 +20,7 @@ class MissionTarget:
self.name = name self.name = name
self.position = position self.position = position
def distance_to(self, other: MissionTarget) -> float: def distance_to(self, other: MissionTarget) -> int:
"""Computes the distance to the given mission target.""" """Computes the distance to the given mission target."""
return self.position.distance_to_point(other.position) return self.position.distance_to_point(other.position)
@@ -46,5 +45,5 @@ class MissionTarget:
] ]
@property @property
def strike_targets(self) -> Sequence[Union[MissionTarget, Unit]]: def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
return [] return []

View File

@@ -78,33 +78,20 @@ class GeneratorSettings:
no_enemy_navy: bool 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: class GameGenerator:
def __init__( def __init__(
self, self,
player: Faction, player: str,
enemy: Faction, enemy: str,
theater: ConflictTheater, theater: ConflictTheater,
settings: Settings, settings: Settings,
generator_settings: GeneratorSettings, generator_settings: GeneratorSettings,
mod_settings: ModSettings,
) -> None: ) -> None:
self.player = player self.player = player
self.enemy = enemy self.enemy = enemy
self.theater = theater self.theater = theater
self.settings = settings self.settings = settings
self.generator_settings = generator_settings self.generator_settings = generator_settings
self.mod_settings = mod_settings
def generate(self) -> Game: def generate(self) -> Game:
with logged_duration("TGO population"): with logged_duration("TGO population"):
@@ -112,8 +99,8 @@ class GameGenerator:
namegen.reset() namegen.reset()
self.prepare_theater() self.prepare_theater()
game = Game( game = Game(
player_faction=self.player.apply_mod_settings(self.mod_settings), player_name=self.player,
enemy_faction=self.enemy.apply_mod_settings(self.mod_settings), enemy_name=self.enemy,
theater=self.theater, theater=self.theater,
start_date=self.generator_settings.start_date, start_date=self.generator_settings.start_date,
settings=self.settings, settings=self.settings,
@@ -172,9 +159,9 @@ class ControlPointGroundObjectGenerator:
@property @property
def faction_name(self) -> str: def faction_name(self) -> str:
if self.control_point.captured: if self.control_point.captured:
return self.game.player_faction.name return self.game.player_name
else: else:
return self.game.enemy_faction.name return self.game.enemy_name
@property @property
def faction(self) -> Faction: def faction(self) -> Faction:

View File

@@ -2,13 +2,13 @@ from __future__ import annotations
import itertools import itertools
import logging import logging
from collections import Sequence from typing import Iterator, List, TYPE_CHECKING, Union
from typing import Iterator, List, TYPE_CHECKING, Union, Generic, TypeVar, Any
from dcs.mapping import Point from dcs.mapping import Point
from dcs.triggers import TriggerZone from dcs.triggers import TriggerZone
from dcs.unit import Unit from dcs.unit import Unit
from dcs.unitgroup import ShipGroup, VehicleGroup from dcs.unitgroup import Group
from dcs.unittype import VehicleType
from .. import db from .. import db
from ..data.radar_db import ( from ..data.radar_db import (
@@ -47,10 +47,7 @@ NAME_BY_CATEGORY = {
} }
GroupT = TypeVar("GroupT", ShipGroup, VehicleGroup) class TheaterGroundObject(MissionTarget):
class TheaterGroundObject(MissionTarget, Generic[GroupT]):
def __init__( def __init__(
self, self,
name: str, name: str,
@@ -69,7 +66,7 @@ class TheaterGroundObject(MissionTarget, Generic[GroupT]):
self.control_point = control_point self.control_point = control_point
self.dcs_identifier = dcs_identifier self.dcs_identifier = dcs_identifier
self.sea_object = sea_object self.sea_object = sea_object
self.groups: List[GroupT] = [] self.groups: List[Group] = []
@property @property
def is_dead(self) -> bool: def is_dead(self) -> bool:
@@ -150,7 +147,7 @@ class TheaterGroundObject(MissionTarget, Generic[GroupT]):
return True return True
return False return False
def _max_range_of_type(self, group: GroupT, range_type: str) -> Distance: def _max_range_of_type(self, group: Group, range_type: str) -> Distance:
if not self.might_have_aa: if not self.might_have_aa:
return meters(0) return meters(0)
@@ -171,13 +168,13 @@ class TheaterGroundObject(MissionTarget, Generic[GroupT]):
def max_detection_range(self) -> Distance: def max_detection_range(self) -> Distance:
return max(self.detection_range(g) for g in self.groups) return max(self.detection_range(g) for g in self.groups)
def detection_range(self, group: GroupT) -> Distance: def detection_range(self, group: Group) -> Distance:
return self._max_range_of_type(group, "detection_range") return self._max_range_of_type(group, "detection_range")
def max_threat_range(self) -> Distance: def max_threat_range(self) -> Distance:
return max(self.threat_range(g) for g in self.groups) return max(self.threat_range(g) for g in self.groups)
def threat_range(self, group: GroupT, radar_only: bool = False) -> Distance: def threat_range(self, group: Group, radar_only: bool = False) -> Distance:
return self._max_range_of_type(group, "threat_range") return self._max_range_of_type(group, "threat_range")
@property @property
@@ -190,7 +187,7 @@ class TheaterGroundObject(MissionTarget, Generic[GroupT]):
return False return False
@property @property
def strike_targets(self) -> Sequence[Union[MissionTarget, Unit]]: def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
return self.units return self.units
@property @property
@@ -209,7 +206,7 @@ class TheaterGroundObject(MissionTarget, Generic[GroupT]):
raise NotImplementedError raise NotImplementedError
class BuildingGroundObject(TheaterGroundObject[VehicleGroup]): class BuildingGroundObject(TheaterGroundObject):
def __init__( def __init__(
self, self,
name: str, name: str,
@@ -220,7 +217,7 @@ class BuildingGroundObject(TheaterGroundObject[VehicleGroup]):
heading: int, heading: int,
control_point: ControlPoint, control_point: ControlPoint,
dcs_identifier: str, dcs_identifier: str,
is_fob_structure: bool = False, is_fob_structure=False,
) -> None: ) -> None:
super().__init__( super().__init__(
name=name, name=name,
@@ -256,7 +253,7 @@ class BuildingGroundObject(TheaterGroundObject[VehicleGroup]):
def kill(self) -> None: def kill(self) -> None:
self._dead = True self._dead = True
def iter_building_group(self) -> Iterator[TheaterGroundObject[Any]]: def iter_building_group(self) -> Iterator[TheaterGroundObject]:
for tgo in self.control_point.ground_objects: for tgo in self.control_point.ground_objects:
if tgo.obj_name == self.obj_name and not tgo.is_dead: if tgo.obj_name == self.obj_name and not tgo.is_dead:
yield tgo yield tgo
@@ -341,7 +338,7 @@ class FactoryGroundObject(BuildingGroundObject):
) )
class NavalGroundObject(TheaterGroundObject[ShipGroup]): class NavalGroundObject(TheaterGroundObject):
def mission_types(self, for_player: bool) -> Iterator[FlightType]: def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
@@ -410,7 +407,7 @@ class LhaGroundObject(GenericCarrierGroundObject):
return f"{self.faction_color}|EWR|{super().group_name}" return f"{self.faction_color}|EWR|{super().group_name}"
class MissileSiteGroundObject(TheaterGroundObject[VehicleGroup]): class MissileSiteGroundObject(TheaterGroundObject):
def __init__( def __init__(
self, name: str, group_id: int, position: Point, control_point: ControlPoint self, name: str, group_id: int, position: Point, control_point: ControlPoint
) -> None: ) -> None:
@@ -434,14 +431,14 @@ class MissileSiteGroundObject(TheaterGroundObject[VehicleGroup]):
return False return False
class CoastalSiteGroundObject(TheaterGroundObject[VehicleGroup]): class CoastalSiteGroundObject(TheaterGroundObject):
def __init__( def __init__(
self, self,
name: str, name: str,
group_id: int, group_id: int,
position: Point, position: Point,
control_point: ControlPoint, control_point: ControlPoint,
heading: int, heading,
) -> None: ) -> None:
super().__init__( super().__init__(
name=name, name=name,
@@ -463,10 +460,10 @@ class CoastalSiteGroundObject(TheaterGroundObject[VehicleGroup]):
return False return False
# The SamGroundObject represents all type of AA # TODO: Differentiate types.
# The TGO can have multiple types of units (AAA,SAM,Support...) # This type gets used both for AA sites (SAM, AAA, or SHORAD). These should each
# Differentiation can be made during generation with the airdefensegroupgenerator # be split into their own types.
class SamGroundObject(TheaterGroundObject[VehicleGroup]): class SamGroundObject(TheaterGroundObject):
def __init__( def __init__(
self, self,
name: str, name: str,
@@ -484,6 +481,18 @@ class SamGroundObject(TheaterGroundObject[VehicleGroup]):
dcs_identifier="AA", dcs_identifier="AA",
sea_object=False, sea_object=False,
) )
# Set by the SAM unit generator if the generated group is compatible
# with Skynet.
self.skynet_capable = False
@property
def group_name(self) -> str:
if self.skynet_capable:
# Prefix the group names of SAM sites with the side color so Skynet
# can find them.
return f"{self.faction_color}|SAM|{self.group_id}"
else:
return super().group_name
def mission_types(self, for_player: bool) -> Iterator[FlightType]: def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
@@ -497,25 +506,33 @@ class SamGroundObject(TheaterGroundObject[VehicleGroup]):
def might_have_aa(self) -> bool: def might_have_aa(self) -> bool:
return True return True
def threat_range(self, group: VehicleGroup, radar_only: bool = False) -> Distance: def threat_range(self, group: Group, radar_only: bool = False) -> Distance:
max_non_radar = meters(0) max_non_radar = meters(0)
live_trs = set() live_trs = set()
max_telar_range = meters(0) max_telar_range = meters(0)
launchers = set() launchers = set()
for unit in group.units: for unit in group.units:
unit_type = db.vehicle_type_from_name(unit.type) unit_type = db.unit_type_from_name(unit.type)
if unit_type is None or not issubclass(unit_type, VehicleType):
continue
if unit_type in TRACK_RADARS: if unit_type in TRACK_RADARS:
live_trs.add(unit_type) live_trs.add(unit_type)
elif unit_type in TELARS: elif unit_type in TELARS:
max_telar_range = max(max_telar_range, meters(unit_type.threat_range)) max_telar_range = max(
max_telar_range, meters(getattr(unit_type, "threat_range", 0))
)
elif unit_type in LAUNCHER_TRACKER_PAIRS: elif unit_type in LAUNCHER_TRACKER_PAIRS:
launchers.add(unit_type) launchers.add(unit_type)
else: else:
max_non_radar = max(max_non_radar, meters(unit_type.threat_range)) max_non_radar = max(
max_non_radar, meters(getattr(unit_type, "threat_range", 0))
)
max_tel_range = meters(0) max_tel_range = meters(0)
for launcher in launchers: for launcher in launchers:
if LAUNCHER_TRACKER_PAIRS[launcher] in live_trs: if LAUNCHER_TRACKER_PAIRS[launcher] in live_trs:
max_tel_range = max(max_tel_range, meters(unit_type.threat_range)) max_tel_range = max(
max_tel_range, meters(getattr(launcher, "threat_range"))
)
if radar_only: if radar_only:
return max(max_tel_range, max_telar_range) return max(max_tel_range, max_telar_range)
else: else:
@@ -530,7 +547,7 @@ class SamGroundObject(TheaterGroundObject[VehicleGroup]):
return True return True
class VehicleGroupGroundObject(TheaterGroundObject[VehicleGroup]): class VehicleGroupGroundObject(TheaterGroundObject):
def __init__( def __init__(
self, self,
name: str, name: str,
@@ -558,7 +575,7 @@ class VehicleGroupGroundObject(TheaterGroundObject[VehicleGroup]):
return True return True
class EwrGroundObject(TheaterGroundObject[VehicleGroup]): class EwrGroundObject(TheaterGroundObject):
def __init__( def __init__(
self, self,
name: str, name: str,

View File

@@ -27,10 +27,7 @@ ThreatPoly = Union[MultiPolygon, Polygon]
class ThreatZones: class ThreatZones:
def __init__( def __init__(
self, self, airbases: ThreatPoly, air_defenses: ThreatPoly, radar_sam_threats
airbases: ThreatPoly,
air_defenses: ThreatPoly,
radar_sam_threats: ThreatPoly,
) -> None: ) -> None:
self.airbases = airbases self.airbases = airbases
self.air_defenses = air_defenses self.air_defenses = air_defenses
@@ -47,10 +44,8 @@ class ThreatZones:
boundary = self.closest_boundary(point) boundary = self.closest_boundary(point)
return meters(boundary.distance_to_point(point)) return meters(boundary.distance_to_point(point))
# Type checking ignored because singledispatchmethod doesn't work with required type
# definitions. The implementation methods are all typed, so should be fine.
@singledispatchmethod @singledispatchmethod
def threatened(self, position) -> bool: # type: ignore def threatened(self, position) -> bool:
raise NotImplementedError raise NotImplementedError
@threatened.register @threatened.register
@@ -66,10 +61,8 @@ class ThreatZones:
LineString([self.dcs_to_shapely_point(a), self.dcs_to_shapely_point(b)]) LineString([self.dcs_to_shapely_point(a), self.dcs_to_shapely_point(b)])
) )
# Type checking ignored because singledispatchmethod doesn't work with required type
# definitions. The implementation methods are all typed, so should be fine.
@singledispatchmethod @singledispatchmethod
def threatened_by_aircraft(self, target) -> bool: # type: ignore def threatened_by_aircraft(self, target) -> bool:
raise NotImplementedError raise NotImplementedError
@threatened_by_aircraft.register @threatened_by_aircraft.register
@@ -89,10 +82,8 @@ class ThreatZones:
LineString((self.dcs_to_shapely_point(p.position) for p in waypoints)) LineString((self.dcs_to_shapely_point(p.position) for p in waypoints))
) )
# Type checking ignored because singledispatchmethod doesn't work with required type
# definitions. The implementation methods are all typed, so should be fine.
@singledispatchmethod @singledispatchmethod
def threatened_by_air_defense(self, target) -> bool: # type: ignore def threatened_by_air_defense(self, target) -> bool:
raise NotImplementedError raise NotImplementedError
@threatened_by_air_defense.register @threatened_by_air_defense.register
@@ -111,10 +102,8 @@ class ThreatZones:
self.dcs_to_shapely_point(target.position) self.dcs_to_shapely_point(target.position)
) )
# Type checking ignored because singledispatchmethod doesn't work with required type
# definitions. The implementation methods are all typed, so should be fine.
@singledispatchmethod @singledispatchmethod
def threatened_by_radar_sam(self, target) -> bool: # type: ignore def threatened_by_radar_sam(self, target) -> bool:
raise NotImplementedError raise NotImplementedError
@threatened_by_radar_sam.register @threatened_by_radar_sam.register

View File

@@ -6,19 +6,20 @@ from collections import defaultdict
from dataclasses import dataclass, field from dataclasses import dataclass, field
from functools import singledispatchmethod from functools import singledispatchmethod
from typing import ( from typing import (
Dict,
Generic, Generic,
Iterator, Iterator,
List, List,
Optional, Optional,
TYPE_CHECKING, TYPE_CHECKING,
Type,
TypeVar, TypeVar,
Sequence, Sequence,
) )
from dcs.mapping import Point 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.procurement import AircraftProcurementRequest
from game.squadrons import Squadron from game.squadrons import Squadron
from game.theater import ControlPoint, MissionTarget from game.theater import ControlPoint, MissionTarget
@@ -28,7 +29,7 @@ from game.theater.transitnetwork import (
) )
from game.utils import meters, nautical_miles from game.utils import meters, nautical_miles
from gen.ato import Package from gen.ato import Package
from gen.flights.ai_flight_planner_db import TRANSPORT_CAPABLE, aircraft_for_task from gen.flights.ai_flight_planner_db import TRANSPORT_CAPABLE
from gen.flights.closestairfields import ObjectiveDistanceCache from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.flights.flight import Flight, FlightType from gen.flights.flight import Flight, FlightType
from gen.flights.flightplan import FlightPlanBuilder from gen.flights.flightplan import FlightPlanBuilder
@@ -71,18 +72,10 @@ class TransferOrder:
player: bool = field(init=False) player: bool = field(init=False)
#: The units being transferred. #: The units being transferred.
units: dict[GroundUnitType, int] units: Dict[Type[VehicleType], int]
transport: Optional[Transport] = field(default=None) 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: def __post_init__(self) -> None:
self.position = self.origin self.position = self.origin
self.player = self.origin.is_friendly(to_player=True) self.player = self.origin.is_friendly(to_player=True)
@@ -96,30 +89,27 @@ class TransferOrder:
def kill_all(self) -> None: def kill_all(self) -> None:
self.units.clear() 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]: 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")
if self.units[unit_type] == 1: self.units[unit_type] -= 1
del self.units[unit_type]
else:
self.units[unit_type] -= 1
@property @property
def size(self) -> int: 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 unit_type, count in self.units.items():
for _ in range(count): for _ in range(count):
yield unit_type yield unit_type
@property @property
def completed(self) -> bool: 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: def disband_at(self, location: ControlPoint) -> None:
logging.info(f"Units halting at {location}.") logging.info(f"Units halting at {location}.")
location.base.commission_units(self.units) location.base.commision_units(self.units)
self.units.clear() self.units.clear()
@property @property
@@ -130,66 +120,24 @@ class TransferOrder:
) )
return self.transport.destination 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 disband(self) -> None:
"""
Disbands the specific transfer at the current position if friendly, at a
possible escape route or kills all units if none is possible
"""
if self.position.is_friendly(self.player):
self.disband_at(self.position)
elif (escape_route := self.find_escape_route()) is not None:
self.disband_at(escape_route)
else:
logging.info(
f"No escape route available. Units were surrounded and destroyed "
"during transfer."
)
self.kill_all()
def is_completable(self, network: TransitNetwork) -> bool:
"""
Checks if the transfer can be completed with the current theater state / transit
network to ensure that there is possible route between the current position and
the planned destination. This also ensures that the points are friendly.
"""
if self.transport is None:
# Check if unplanned transfers could be completed
if not self.position.is_friendly(self.player):
logging.info(
f"Current position ({self.position}) "
f"of the halting transfer was captured."
)
return False
if not network.has_path_between(self.position, self.destination):
logging.info(
f"Destination of transfer ({self.destination}) "
f"can not be reached anymore."
)
return False
if self.transport is not None and not self.next_stop.is_friendly(self.player):
# check if already proceeding transfers can reach the next stop
logging.info(
f"The next stop of the transfer ({self.next_stop}) "
f"was captured while transfer was on route."
)
return False
return True
def proceed(self) -> None: def proceed(self) -> None:
"""
Let the transfer proceed to the next stop and disbands it if the next stop
is the destination
"""
if self.transport is None: if self.transport is None:
return return
if not self.destination.is_friendly(self.player):
logging.info(f"Transfer destination {self.destination} was captured.")
if self.position.is_friendly(self.player):
self.disband_at(self.position)
elif (escape_route := self.transport.find_escape_route()) is not None:
self.disband_at(escape_route)
else:
logging.info(
f"No escape route available. Units were surrounded and destroyed "
"during transfer."
)
self.kill_all()
return
self.position = self.next_stop self.position = self.next_stop
self.transport = None self.transport = None
@@ -208,7 +156,7 @@ class Airlift(Transport):
self.flight = flight self.flight = flight
@property @property
def units(self) -> dict[GroundUnitType, int]: def units(self) -> Dict[Type[VehicleType], int]:
return self.transfer.units return self.transfer.units
@property @property
@@ -243,9 +191,9 @@ class AirliftPlanner:
self.package = Package(target=next_stop, auto_asap=True) self.package = Package(target=next_stop, auto_asap=True)
def compatible_with_mission( def compatible_with_mission(
self, unit_type: AircraftType, airfield: ControlPoint self, unit_type: Type[FlyingType], airfield: ControlPoint
) -> bool: ) -> bool:
if unit_type not in aircraft_for_task(FlightType.TRANSPORT): if not unit_type in TRANSPORT_CAPABLE:
return False return False
if not self.transfer.origin.can_operate(unit_type): if not self.transfer.origin.can_operate(unit_type):
return False return False
@@ -253,7 +201,7 @@ class AirliftPlanner:
return False return False
# Cargo planes have no maximum range. # Cargo planes have no maximum range.
if not unit_type.dcs_unit_type.helicopter: if not unit_type.helicopter:
return True return True
# A helicopter that is transport capable and able to operate at both bases. Need # A helicopter that is transport capable and able to operate at both bases. Need
@@ -279,46 +227,36 @@ class AirliftPlanner:
distance_cache = ObjectiveDistanceCache.get_closest_airfields( distance_cache = ObjectiveDistanceCache.get_closest_airfields(
self.transfer.position self.transfer.position
) )
air_wing = self.game.air_wing_for(self.for_player)
for cp in distance_cache.closest_airfields: for cp in distance_cache.closest_airfields:
if cp.captured != self.for_player: if cp.captured != self.for_player:
continue continue
inventory = self.game.aircraft_inventory.for_control_point(cp) inventory = self.game.aircraft_inventory.for_control_point(cp)
for unit_type, available in inventory.all_aircraft: for unit_type, available in inventory.all_aircraft:
squadrons = air_wing.auto_assignable_for_task_with_type( squadrons = [
unit_type, FlightType.TRANSPORT s
) for s in self.game.air_wing_for(self.for_player).squadrons_for(
for squadron in squadrons: unit_type
if self.compatible_with_mission(unit_type, cp): )
while ( if FlightType.TRANSPORT in s.auto_assignable_mission_types
available ]
and squadron.has_available_pilots if not squadrons:
and self.transfer.transport is None continue
): squadron = squadrons[0]
flight_size = self.create_airlift_flight( if self.compatible_with_mission(unit_type, cp):
squadron, inventory while available and self.transfer.transport is None:
) flight_size = self.create_airlift_flight(squadron, inventory)
available -= flight_size available -= flight_size
if self.package.flights: if self.package.flights:
self.game.ato_for(self.for_player).add_package(self.package) self.game.ato_for(self.for_player).add_package(self.package)
def create_airlift_flight( def create_airlift_flight(
self, squadron: Squadron, inventory: ControlPointAircraftInventory self, squadron: Squadron, inventory: ControlPointAircraftInventory
) -> int: ) -> int:
available_aircraft = inventory.available(squadron.aircraft) available = inventory.available(squadron.aircraft)
capacity_each = 1 if squadron.aircraft.dcs_unit_type.helicopter else 2 capacity_each = 1 if squadron.aircraft.helicopter else 2
required = math.ceil(self.transfer.size / capacity_each) required = math.ceil(self.transfer.size / capacity_each)
flight_size = min( flight_size = min(required, available, squadron.aircraft.group_size_max)
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
capacity = flight_size * capacity_each capacity = flight_size * capacity_each
if capacity < self.transfer.size: if capacity < self.transfer.size:
@@ -370,7 +308,7 @@ class MultiGroupTransport(MissionTarget, Transport):
transfer.transport = None transfer.transport = None
self.transfers.remove(transfer) 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: for transfer in self.transfers:
try: try:
transfer.kill_unit(unit_type) transfer.kill_unit(unit_type)
@@ -390,21 +328,16 @@ class MultiGroupTransport(MissionTarget, Transport):
@property @property
def size(self) -> int: 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 @property
def units(self) -> dict[GroundUnitType, int]: def units(self) -> Dict[Type[VehicleType], int]:
units: dict[GroundUnitType, int] = defaultdict(int) units: Dict[Type[VehicleType], int] = defaultdict(int)
for transfer in self.transfers: for transfer in self.transfers:
for unit_type, count in transfer.units.items(): for unit_type, count in transfer.units.items():
units[unit_type] += count units[unit_type] += count
return units return units
def iter_units(self) -> Iterator[GroundUnitType]:
for unit_type, count in self.units.items():
for _ in range(count):
yield unit_type
@property @property
def player_owned(self) -> bool: def player_owned(self) -> bool:
return self.origin.captured return self.origin.captured
@@ -470,8 +403,8 @@ TransportType = TypeVar("TransportType", bound=MultiGroupTransport)
class TransportMap(Generic[TransportType]): class TransportMap(Generic[TransportType]):
def __init__(self) -> None: def __init__(self) -> None:
# Dict of origin -> destination -> transport. # Dict of origin -> destination -> transport.
self.transports: dict[ self.transports: Dict[
ControlPoint, dict[ControlPoint, TransportType] ControlPoint, Dict[ControlPoint, TransportType]
] = defaultdict(dict) ] = defaultdict(dict)
def create_transport( def create_transport(
@@ -525,14 +458,14 @@ class TransportMap(Generic[TransportType]):
yield from destination_dict.values() yield from destination_dict.values()
class ConvoyMap(TransportMap[Convoy]): class ConvoyMap(TransportMap):
def create_transport( def create_transport(
self, origin: ControlPoint, destination: ControlPoint self, origin: ControlPoint, destination: ControlPoint
) -> Convoy: ) -> Convoy:
return Convoy(origin, destination) return Convoy(origin, destination)
class CargoShipMap(TransportMap[CargoShip]): class CargoShipMap(TransportMap):
def create_transport( def create_transport(
self, origin: ControlPoint, destination: ControlPoint self, origin: ControlPoint, destination: ControlPoint
) -> CargoShip: ) -> CargoShip:
@@ -598,14 +531,8 @@ class PendingTransfers:
self.pending_transfers.append(new_transfer) self.pending_transfers.append(new_transfer)
return new_transfer return new_transfer
# Type checking ignored because singledispatchmethod doesn't work with required type
# definitions. The implementation methods are all typed, so should be fine.
@singledispatchmethod @singledispatchmethod
def cancel_transport( # type: ignore def cancel_transport(self, transport, transfer: TransferOrder) -> None:
self,
transport,
transfer: TransferOrder,
) -> None:
pass pass
@cancel_transport.register @cancel_transport.register
@@ -635,15 +562,9 @@ class PendingTransfers:
if transfer.transport is not None: if transfer.transport is not None:
self.cancel_transport(transfer.transport, transfer) self.cancel_transport(transfer.transport, transfer)
self.pending_transfers.remove(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: def perform_transfers(self) -> None:
"""
Performs completable transfers from the list of pending transfers and adds
uncompleted transfers which are en route back to the list of pending transfers.
Disbands all convoys and cargo ships
"""
self.disband_uncompletable_transfers()
incomplete = [] incomplete = []
for transfer in self.pending_transfers: for transfer in self.pending_transfers:
transfer.proceed() transfer.proceed()
@@ -654,78 +575,24 @@ class PendingTransfers:
self.cargo_ships.disband_all() self.cargo_ships.disband_all()
def plan_transports(self) -> None: def plan_transports(self) -> None:
"""
Plan transports for all pending and completable transfers which don't have a
transport assigned already. This calculates the shortest path between current
position and destination on every execution to ensure the route is adopted to
recent changes in the theater state / transit network.
"""
self.disband_uncompletable_transfers()
for transfer in self.pending_transfers: for transfer in self.pending_transfers:
if transfer.transport is None: if transfer.transport is None:
self.arrange_transport(transfer) self.arrange_transport(transfer)
def disband_uncompletable_transfers(self) -> None:
"""
Disbands all transfers from the list of pending_transfers which can not be
completed anymore because the theater state changed or the transit network does
not allow a route to the destination anymore
"""
completable_transfers = []
for transfer in self.pending_transfers:
if not transfer.is_completable(self.network_for(transfer.position)):
transfer.disband()
else:
completable_transfers.append(transfer)
self.pending_transfers = completable_transfers
def order_airlift_assets(self) -> None: def order_airlift_assets(self) -> None:
for control_point in self.game.theater.controlpoints: for control_point in self.game.theater.controlpoints:
if self.game.air_wing_for(control_point.captured).can_auto_plan( self.order_airlift_assets_at(control_point)
FlightType.TRANSPORT
):
self.order_airlift_assets_at(control_point)
def desired_airlift_capacity(self, control_point: ControlPoint) -> int: @staticmethod
def desired_airlift_capacity(control_point: ControlPoint) -> int:
if control_point.has_factory: return 4 if control_point.has_factory else 0
is_major_hub = control_point.total_aircraft_parking > 0
# Check if there is a CP which is only reachable via Airlift
transit_network = self.network_for(control_point)
for cp in self.game.theater.control_points_for(control_point.captured):
# check if the CP has no factory, is reachable from the current
# position and can only be reached with airlift connections
if (
cp.can_deploy_ground_units
and not cp.has_factory
and transit_network.has_link(control_point, cp)
and not any(
link_type
for link, link_type in transit_network.nodes[cp].items()
if not link_type == TransitConnection.Airlift
)
):
return 4
if (
is_major_hub
and cp.has_factory
and cp.total_aircraft_parking > control_point.total_aircraft_parking
):
is_major_hub = False
if is_major_hub:
# If the current CP is a major hub keep always 2 planes on reserve
return 2
return 0
def current_airlift_capacity(self, control_point: ControlPoint) -> int: def current_airlift_capacity(self, control_point: ControlPoint) -> int:
inventory = self.game.aircraft_inventory.for_control_point(control_point) inventory = self.game.aircraft_inventory.for_control_point(control_point)
squadrons = self.game.air_wing_for( squadrons = self.game.air_wing_for(control_point.captured).squadrons_for_task(
control_point.captured FlightType.TRANSPORT
).auto_assignable_for_task(FlightType.TRANSPORT) )
unit_types = {s.aircraft for s in squadrons} unit_types = {s.aircraft for s in squadrons}.intersection(TRANSPORT_CAPABLE)
return sum( return sum(
count count
for unit_type, count in inventory.all_aircraft for unit_type, count in inventory.all_aircraft
@@ -733,16 +600,9 @@ class PendingTransfers:
) )
def order_airlift_assets_at(self, control_point: ControlPoint) -> None: def order_airlift_assets_at(self, control_point: ControlPoint) -> None:
unclaimed_parking = control_point.unclaimed_parking(self.game) gap = self.desired_airlift_capacity(
# Buy a maximum of unclaimed_parking only to prevent that aircraft procurement control_point
# take place at another base ) - self.current_airlift_capacity(control_point)
gap = min(
[
self.desired_airlift_capacity(control_point)
- self.current_airlift_capacity(control_point),
unclaimed_parking,
]
)
if gap <= 0: if gap <= 0:
return return
@@ -752,10 +612,6 @@ class PendingTransfers:
# aesthetic. # aesthetic.
gap += 1 gap += 1
if gap > unclaimed_parking:
# Prevent to buy more aircraft than possible
return
self.game.procurement_requests_for(player=control_point.captured).append( self.game.procurement_requests_for(player=control_point.captured).append(
AircraftProcurementRequest( AircraftProcurementRequest(
control_point, nautical_miles(200), FlightType.TRANSPORT, gap control_point, nautical_miles(200), FlightType.TRANSPORT, gap

View File

@@ -3,11 +3,12 @@ from __future__ import annotations
import logging import logging
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass 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 game.theater import ControlPoint
from .dcs.groundunittype import GroundUnitType from .db import PRICES
from .dcs.unittype import UnitType
from .theater.transitnetwork import ( from .theater.transitnetwork import (
NoPathError, NoPathError,
TransitNetwork, TransitNetwork,
@@ -28,48 +29,41 @@ class PendingUnitDeliveries:
self.destination = destination self.destination = destination
# Maps unit type to order quantity. # Maps unit type to order quantity.
self.units: dict[UnitType[Any], int] = defaultdict(int) self.units: Dict[Type[UnitType], int] = defaultdict(int)
def __str__(self) -> str: def __str__(self) -> str:
return f"Pending delivery to {self.destination}" return f"Pending delivery to {self.destination}"
def order(self, units: dict[UnitType[Any], int]) -> None: def order(self, units: Dict[Type[UnitType], int]) -> None:
for k, v in units.items(): for k, v in units.items():
self.units[k] += v self.units[k] += v
def sell(self, units: dict[UnitType[Any], int]) -> None: def sell(self, units: Dict[Type[UnitType], int]) -> None:
for k, v in units.items(): for k, v in units.items():
if self.units[k] > v: self.units[k] -= v
self.units[k] -= v
else:
del self.units[k]
def refund_all(self, game: Game) -> None: def refund_all(self, game: Game) -> None:
self.refund(game, self.units) self.refund(game, self.units)
self.units = defaultdict(int) self.units = defaultdict(int)
def refund_ground_units(self, game: Game) -> None: def refund(self, game: Game, units: Dict[Type[UnitType], int]) -> 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[Any], int]) -> None:
for unit_type, count in units.items(): for unit_type, count in units.items():
logging.info(f"Refunding {count} {unit_type} at {self.destination.name}") try:
game.adjust_budget( price = PRICES[unit_type]
unit_type.price * count, player=self.destination.captured except KeyError:
) logging.error(f"Could not refund {unit_type.id}, price unknown")
continue
def pending_orders(self, unit_type: UnitType[Any]) -> 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) pending_units = self.units.get(unit_type)
if pending_units is None: if pending_units is None:
pending_units = 0 pending_units = 0
return pending_units return pending_units
def available_next_turn(self, unit_type: UnitType[Any]) -> int: def available_next_turn(self, unit_type: Type[UnitType]) -> int:
current_units = self.destination.base.total_units_of_type(unit_type) current_units = self.destination.base.total_units_of_type(unit_type)
return self.pending_orders(unit_type) + current_units return self.pending_orders(unit_type) + current_units
@@ -80,16 +74,18 @@ class PendingUnitDeliveries:
f"{self.destination.name} lost its source for ground unit " f"{self.destination.name} lost its source for ground unit "
"reinforcements. Refunding purchase price." "reinforcements. Refunding purchase price."
) )
self.refund_ground_units(game) self.refund_all(game)
return
bought_units: dict[UnitType[Any], int] = {} bought_units: Dict[Type[UnitType], int] = {}
units_needing_transfer: dict[GroundUnitType, int] = {} units_needing_transfer: Dict[Type[VehicleType], int] = {}
sold_units: dict[UnitType[Any], int] = {} sold_units: Dict[Type[UnitType], int] = {}
for unit_type, count in self.units.items(): for unit_type, count in self.units.items():
coalition = "Ally" if self.destination.captured else "Enemy" coalition = "Ally" if self.destination.captured else "Enemy"
d: dict[Any, int] name = unit_type.id
if ( if (
isinstance(unit_type, GroundUnitType) issubclass(unit_type, VehicleType)
and self.destination != ground_unit_source and self.destination != ground_unit_source
): ):
source = ground_unit_source source = ground_unit_source
@@ -101,27 +97,22 @@ class PendingUnitDeliveries:
if count >= 0: if count >= 0:
d[unit_type] = count d[unit_type] = count
game.message( game.message(
f"{coalition} reinforcements: {unit_type} x {count} at {source}" f"{coalition} reinforcements: {name} x {count} at {source}"
) )
else: else:
sold_units[unit_type] = -count 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.units = defaultdict(int)
self.destination.base.commission_units(bought_units) self.destination.base.commision_units(bought_units)
self.destination.base.commit_losses(sold_units) self.destination.base.commit_losses(sold_units)
if units_needing_transfer: if units_needing_transfer:
if ground_unit_source is None: ground_unit_source.base.commision_units(units_needing_transfer)
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)
self.create_transfer(game, ground_unit_source, units_needing_transfer) self.create_transfer(game, ground_unit_source, units_needing_transfer)
def create_transfer( def create_transfer(
self, game: Game, source: ControlPoint, units: dict[GroundUnitType, int] self, game: Game, source: ControlPoint, units: Dict[Type[VehicleType], int]
) -> None: ) -> None:
game.transfers.new_transfer(TransferOrder(source, self.destination, units)) game.transfers.new_transfer(TransferOrder(source, self.destination, units))

View File

@@ -2,12 +2,13 @@
import itertools import itertools
import math import math
from dataclasses import dataclass from dataclasses import dataclass
from typing import Dict, Optional, Any, Union, TypeVar, Generic from typing import Dict, Optional, Type
from dcs.unit import Vehicle, Ship from dcs.unit import Unit
from dcs.unitgroup import FlyingGroup, VehicleGroup, StaticGroup, ShipGroup, MovingGroup 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.squadrons import Pilot
from game.theater import Airfield, ControlPoint, TheaterGroundObject from game.theater import Airfield, ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import BuildingGroundObject, SceneryGroundObject from game.theater.theatergroundobject import BuildingGroundObject, SceneryGroundObject
@@ -18,34 +19,31 @@ from gen.flights.flight import Flight
@dataclass(frozen=True) @dataclass(frozen=True)
class FlyingUnit: class FlyingUnit:
flight: Flight flight: Flight
pilot: Optional[Pilot] pilot: Pilot
@dataclass(frozen=True) @dataclass(frozen=True)
class FrontLineUnit: class FrontLineUnit:
unit_type: GroundUnitType unit_type: Type[VehicleType]
origin: ControlPoint origin: ControlPoint
UnitT = TypeVar("UnitT", Ship, Vehicle)
@dataclass(frozen=True) @dataclass(frozen=True)
class GroundObjectUnit(Generic[UnitT]): class GroundObjectUnit:
ground_object: TheaterGroundObject[Any] ground_object: TheaterGroundObject
group: MovingGroup[UnitT] group: Group
unit: UnitT unit: Unit
@dataclass(frozen=True) @dataclass(frozen=True)
class ConvoyUnit: class ConvoyUnit:
unit_type: GroundUnitType unit_type: Type[VehicleType]
convoy: Convoy convoy: Convoy
@dataclass(frozen=True) @dataclass(frozen=True)
class AirliftUnits: class AirliftUnits:
cargo: tuple[GroundUnitType, ...] cargo: tuple[Type[VehicleType], ...]
transfer: TransferOrder transfer: TransferOrder
@@ -59,19 +57,21 @@ class UnitMap:
self.aircraft: Dict[str, FlyingUnit] = {} self.aircraft: Dict[str, FlyingUnit] = {}
self.airfields: Dict[str, Airfield] = {} self.airfields: Dict[str, Airfield] = {}
self.front_line_units: Dict[str, FrontLineUnit] = {} self.front_line_units: Dict[str, FrontLineUnit] = {}
self.ground_object_units: Dict[str, GroundObjectUnit[Any]] = {} self.ground_object_units: Dict[str, GroundObjectUnit] = {}
self.buildings: Dict[str, Building] = {} self.buildings: Dict[str, Building] = {}
self.convoys: Dict[str, ConvoyUnit] = {} self.convoys: Dict[str, ConvoyUnit] = {}
self.cargo_ships: Dict[str, CargoShip] = {} self.cargo_ships: Dict[str, CargoShip] = {}
self.airlifts: Dict[str, AirliftUnits] = {} self.airlifts: Dict[str, AirliftUnits] = {}
def add_aircraft(self, group: FlyingGroup[Any], flight: Flight) -> None: def add_aircraft(self, group: FlyingGroup, flight: Flight) -> None:
for pilot, unit in zip(flight.roster.pilots, group.units): for pilot, unit in zip(flight.roster.pilots, group.units):
# The actual name is a String (the pydcs translatable string), which # The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__. # doesn't define __eq__.
name = str(unit.name) name = str(unit.name)
if name in self.aircraft: if name in self.aircraft:
raise RuntimeError(f"Duplicate unit name: {name}") 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) self.aircraft[name] = FlyingUnit(flight, pilot)
if flight.cargo is not None: if flight.cargo is not None:
self.add_airlift_units(group, flight.cargo) self.add_airlift_units(group, flight.cargo)
@@ -87,15 +87,20 @@ class UnitMap:
def airfield(self, name: str) -> Optional[Airfield]: def airfield(self, name: str) -> Optional[Airfield]:
return self.airfields.get(name, None) return self.airfields.get(name, None)
def add_front_line_units( def add_front_line_units(self, group: Group, origin: ControlPoint) -> None:
self, group: VehicleGroup, origin: ControlPoint, unit_type: GroundUnitType
) -> None:
for unit in group.units: for unit in group.units:
# The actual name is a String (the pydcs translatable string), which # The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__. # doesn't define __eq__.
name = str(unit.name) name = str(unit.name)
if name in self.front_line_units: if name in self.front_line_units:
raise RuntimeError(f"Duplicate front line unit: {name}") 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) self.front_line_units[name] = FrontLineUnit(unit_type, origin)
def front_line_unit(self, name: str) -> Optional[FrontLineUnit]: def front_line_unit(self, name: str) -> Optional[FrontLineUnit]:
@@ -103,9 +108,9 @@ class UnitMap:
def add_ground_object_units( def add_ground_object_units(
self, self,
ground_object: TheaterGroundObject[Any], ground_object: TheaterGroundObject,
persistence_group: Union[ShipGroup, VehicleGroup], persistence_group: Group,
miz_group: Union[ShipGroup, VehicleGroup], miz_group: Group,
) -> None: ) -> None:
"""Adds a group associated with a TGO to the unit map. """Adds a group associated with a TGO to the unit map.
@@ -134,22 +139,29 @@ class UnitMap:
ground_object, persistence_group, persistent_unit ground_object, persistence_group, persistent_unit
) )
def ground_object_unit(self, name: str) -> Optional[GroundObjectUnit[Any]]: def ground_object_unit(self, name: str) -> Optional[GroundObjectUnit]:
return self.ground_object_units.get(name, None) return self.ground_object_units.get(name, None)
def add_convoy_units(self, group: VehicleGroup, convoy: Convoy) -> 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 # The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__. # doesn't define __eq__.
name = str(unit.name) name = str(unit.name)
if name in self.convoys: if name in self.convoys:
raise RuntimeError(f"Duplicate convoy unit: {name}") 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) self.convoys[name] = ConvoyUnit(unit_type, convoy)
def convoy_unit(self, name: str) -> Optional[ConvoyUnit]: def convoy_unit(self, name: str) -> Optional[ConvoyUnit]:
return self.convoys.get(name, None) return self.convoys.get(name, None)
def add_cargo_ship(self, group: ShipGroup, ship: CargoShip) -> None: def add_cargo_ship(self, group: Group, ship: CargoShip) -> None:
if len(group.units) > 1: if len(group.units) > 1:
# Cargo ship "groups" are single units. Killing the one ship kills the whole # Cargo ship "groups" are single units. Killing the one ship kills the whole
# transfer. If we ever want to add escorts or create multiple cargo ships in # transfer. If we ever want to add escorts or create multiple cargo ships in
@@ -166,9 +178,7 @@ class UnitMap:
def cargo_ship(self, name: str) -> Optional[CargoShip]: def cargo_ship(self, name: str) -> Optional[CargoShip]:
return self.cargo_ships.get(name, None) return self.cargo_ships.get(name, None)
def add_airlift_units( def add_airlift_units(self, group: FlyingGroup, transfer: TransferOrder) -> None:
self, group: FlyingGroup[Any], transfer: TransferOrder
) -> None:
capacity_each = math.ceil(transfer.size / len(group.units)) capacity_each = math.ceil(transfer.size / len(group.units))
for idx, transport in enumerate(group.units): for idx, transport in enumerate(group.units):
# Slice the units in groups based on the capacity of each unit. Cargo is # Slice the units in groups based on the capacity of each unit. Cargo is
@@ -191,9 +201,7 @@ class UnitMap:
def airlift_unit(self, name: str) -> Optional[AirliftUnits]: def airlift_unit(self, name: str) -> Optional[AirliftUnits]:
return self.airlifts.get(name, None) return self.airlifts.get(name, None)
def add_building( def add_building(self, ground_object: BuildingGroundObject, group: Group) -> None:
self, ground_object: BuildingGroundObject, group: StaticGroup
) -> None:
# The actual name is a String (the pydcs translatable string), which # The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__. # doesn't define __eq__.
# The name of the initiator in the DCS dead event will have " object" # The name of the initiator in the DCS dead event will have " object"

View File

@@ -2,9 +2,8 @@ from __future__ import annotations
import itertools import itertools
import math import math
from collections import Iterable
from dataclasses import dataclass from dataclasses import dataclass
from typing import Union, Any from typing import Union
METERS_TO_FEET = 3.28084 METERS_TO_FEET = 3.28084
FEET_TO_METERS = 1 / METERS_TO_FEET FEET_TO_METERS = 1 / METERS_TO_FEET
@@ -17,12 +16,17 @@ MS_TO_KPH = 3.6
KPH_TO_MS = 1 / MS_TO_KPH KPH_TO_MS = 1 / MS_TO_KPH
def heading_sum(h: int, a: int) -> int: def heading_sum(h, a) -> int:
h += a h += a
return h % 360 if h > 360:
return h - 360
elif h < 0:
return 360 + h
else:
return h
def opposite_heading(h: int) -> int: def opposite_heading(h):
return heading_sum(h, 180) return heading_sum(h, 180)
@@ -54,10 +58,6 @@ class Distance:
def from_nautical_miles(cls, value: float) -> Distance: def from_nautical_miles(cls, value: float) -> Distance:
return cls(value * NM_TO_METERS) return cls(value * NM_TO_METERS)
@classmethod
def inf(cls) -> Distance:
return cls.from_meters(math.inf)
def __add__(self, other: Distance) -> Distance: def __add__(self, other: Distance) -> Distance:
return meters(self.meters + other.meters) return meters(self.meters + other.meters)
@@ -181,7 +181,7 @@ def mach(value: float, altitude: Distance) -> Speed:
SPEED_OF_SOUND_AT_SEA_LEVEL = knots(661.5) SPEED_OF_SOUND_AT_SEA_LEVEL = knots(661.5)
def pairwise(iterable: Iterable[Any]) -> Iterable[tuple[Any, Any]]: def pairwise(iterable):
""" """
itertools recipe itertools recipe
s -> (s0,s1), (s1,s2), (s2, s3), ... s -> (s0,s1), (s1,s2), (s2, s3), ...

View File

@@ -1,15 +1,8 @@
from pathlib import Path from pathlib import Path
MAJOR_VERSION = 4
MINOR_VERSION = 1
MICRO_VERSION = 0
def _build_version_string() -> str: def _build_version_string() -> str:
components = [ components = ["3.1.0"]
".".join(str(v) for v in (MAJOR_VERSION, MINOR_VERSION, MICRO_VERSION))
]
build_number_path = Path("resources/buildnumber") build_number_path = Path("resources/buildnumber")
if build_number_path.exists(): if build_number_path.exists():
with build_number_path.open("r") as build_number_file: with build_number_path.open("r") as build_number_file:
@@ -77,9 +70,9 @@ VERSION = _build_version_string()
#: Version 4.2 #: Version 4.2
#: * Adds support for AAA objectives. Place with any of the following units (either red #: * Adds support for AAA objectives. Place with any of the following units (either red
#: or blue): #: or blue):
#: * Flak18, #: * AAA_8_8cm_Flak_18,
#: * Vulcan, #: * SPAAA_Vulcan_M163,
#: * ZSU_23_4_Shilka, #: * SPAAA_ZSU_23_4_Shilka_Gun_Dish,
#: #:
#: Version 5.0 #: Version 5.0
#: * Ammunition Depots objective locations are now predetermined using the "Ammunition #: * Ammunition Depots objective locations are now predetermined using the "Ammunition
@@ -94,20 +87,4 @@ VERSION = _build_version_string()
#: Version 6.0 #: Version 6.0
#: * Random objective generation no is longer supported. Fixed objective locations were #: * Random objective generation no is longer supported. Fixed objective locations were
#: added in 4.1. #: added in 4.1.
#: CAMPAIGN_FORMAT_VERSION = (6, 0)
#: 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.
#:
#: Version 7.1
#: * Support for Mariana Islands terrain
#:
#: Version 8.0
#: * DCS 2.7.4.9632 changed scenery target IDs. Any mission using map buildings as
#: strike targets must check and potentially recreate all those objectives.
CAMPAIGN_FORMAT_VERSION = (8, 0)

View File

@@ -83,7 +83,7 @@ class Weather:
raise NotImplementedError raise NotImplementedError
@staticmethod @staticmethod
def random_wind(minimum: int, maximum: int) -> WindConditions: def random_wind(minimum: int, maximum) -> WindConditions:
wind_direction = random.randint(0, 360) wind_direction = random.randint(0, 360)
at_0m_factor = 1 at_0m_factor = 1
at_2000m_factor = 2 at_2000m_factor = 2

File diff suppressed because it is too large Load Diff

View File

@@ -966,96 +966,7 @@ AIRFIELD_DATA = {
runway_length=8871, runway_length=8871,
atc=AtcData(MHz(3, 775), MHz(38, 450), MHz(120, 100), MHz(250, 50)), atc=AtcData(MHz(3, 775), MHz(38, 450), MHz(120, 100), MHz(250, 50)),
ils={ ils={
"28": ("IGNP", MHz(109, 100)), "28": ("IGNP", MHz(109, 10)),
},
),
"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)),
}, },
), ),
# NTTR # NTTR
@@ -1521,47 +1432,4 @@ AIRFIELD_DATA = {
runway_length=3953, runway_length=3953,
atc=AtcData(MHz(3, 850), MHz(118, 200), MHz(38, 600), MHz(250, 200)), atc=AtcData(MHz(3, 850), MHz(118, 200), MHz(38, 600), MHz(250, 200)),
), ),
"Antonio B. Won Pat Intl": AirfieldData(
theater="MarianaIslands",
icao="PGUM",
elevation=255,
runway_length=9359,
atc=AtcData(MHz(3, 825), MHz(118, 100), MHz(38, 550), MHz(340, 200)),
ils={
"06": ("IGUM", MHz(110, 30)),
},
),
"Andersen AFB": AirfieldData(
theater="MarianaIslands",
icao="PGUA",
elevation=545,
runway_length=10490,
tacan=TacanChannel(54, TacanBand.X),
tacan_callsign="UAM",
atc=AtcData(MHz(3, 850), MHz(126, 200), MHz(38, 600), MHz(250, 100)),
),
"Rota Intl": AirfieldData(
theater="MarianaIslands",
icao="PGRO",
elevation=568,
runway_length=6105,
atc=AtcData(MHz(3, 750), MHz(123, 600), MHz(38, 400), MHz(250, 0)),
),
"Tinian Intl": AirfieldData(
theater="MarianaIslands",
icao="PGWT",
elevation=240,
runway_length=7777,
atc=AtcData(MHz(3, 800), MHz(123, 650), MHz(38, 500), MHz(250, 50)),
),
"Saipan Intl": AirfieldData(
theater="MarianaIslands",
icao="PGSN",
elevation=213,
runway_length=7790,
atc=AtcData(MHz(3, 775), MHz(125, 700), MHz(38, 450), MHz(256, 900)),
ils={
"07": ("IGSN", MHz(109, 90)),
},
),
} }

View File

@@ -1,12 +1,11 @@
from __future__ import annotations
import logging import logging
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import timedelta from datetime import timedelta
from typing import List, Type, Tuple, Optional, TYPE_CHECKING from typing import List, Type, Tuple, Optional
from dcs.mission import Mission, StartType from dcs.mission import Mission, StartType
from dcs.planes import IL_78M, KC130, KC135MPRS, KC_135, PlaneType from dcs.planes import IL_78M, KC130, KC135MPRS, KC_135
from dcs.unittype import UnitType
from dcs.task import ( from dcs.task import (
AWACS, AWACS,
ActivateBeaconCommand, ActivateBeaconCommand,
@@ -15,17 +14,14 @@ from dcs.task import (
SetImmortalCommand, SetImmortalCommand,
SetInvisibleCommand, SetInvisibleCommand,
) )
from dcs.unittype import UnitType
from game import db
from .naming import namegen
from .callsigns import callsign_for_support_unit from .callsigns import callsign_for_support_unit
from .conflictgen import Conflict from .conflictgen import Conflict
from .flights.ai_flight_planner_db import AEWC_CAPABLE
from .naming import namegen
from .radios import RadioFrequency, RadioRegistry from .radios import RadioFrequency, RadioRegistry
from .tacan import TacanBand, TacanChannel, TacanRegistry from .tacan import TacanBand, TacanChannel, TacanRegistry
if TYPE_CHECKING:
from game import Game
TANKER_DISTANCE = 15000 TANKER_DISTANCE = 15000
TANKER_ALT = 4572 TANKER_ALT = 4572
@@ -57,8 +53,6 @@ class TankerInfo:
variant: str variant: str
freq: RadioFrequency freq: RadioFrequency
tacan: TacanChannel tacan: TacanChannel
start_time: Optional[timedelta]
end_time: Optional[timedelta]
blue: bool blue: bool
@@ -73,7 +67,7 @@ class AirSupportConflictGenerator:
self, self,
mission: Mission, mission: Mission,
conflict: Conflict, conflict: Conflict,
game: Game, game,
radio_registry: RadioRegistry, radio_registry: RadioRegistry,
tacan_registry: TacanRegistry, tacan_registry: TacanRegistry,
) -> None: ) -> None:
@@ -98,145 +92,127 @@ class AirSupportConflictGenerator:
return (TANKER_ALT + 500, 596) return (TANKER_ALT + 500, 596)
return (TANKER_ALT, 574) return (TANKER_ALT, 574)
def generate(self) -> None: def generate(self):
player_cp = ( player_cp = (
self.conflict.blue_cp self.conflict.blue_cp
if self.conflict.blue_cp.captured if self.conflict.blue_cp.captured
else self.conflict.red_cp 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( for i, tanker_unit_type in enumerate(
self.game.faction_for(player=True).tankers db.find_unittype(Refueling, self.conflict.attackers_side)
): ):
unit_type = tanker_unit_type.dcs_unit_type alt, airspeed = self._get_tanker_params(tanker_unit_type)
if not issubclass(unit_type, PlaneType): variant = db.unit_type_name(tanker_unit_type)
logging.warning(f"Refueling aircraft {unit_type} must be a plane") freq = self.radio_registry.alloc_uhf()
continue 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)
# TODO: Make loiter altitude a property of the unit type. callsign = callsign_for_support_unit(tanker_group)
alt, airspeed = self._get_tanker_params(tanker_unit_type.dcs_unit_type) tacan_callsign = {
freq = self.radio_registry.alloc_uhf() "Texaco": "TEX",
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y) "Arco": "ARC",
tanker_heading = ( "Shell": "SHL",
self.conflict.red_cp.position.heading_between_point( }.get(callsign)
self.conflict.blue_cp.position 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_HEADING_OFFSET * i
) )
tanker_position = player_cp.position.point_from_heading(
tanker_heading, TANKER_DISTANCE 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,
) )
tanker_group = self.mission.refuel_flight( )
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), country=self.mission.country(self.game.player_country),
name=namegen.next_tanker_name( name=namegen.next_awacs_name(
self.mission.country(self.game.player_country), tanker_unit_type self.mission.country(self.game.player_country)
), ),
plane_type=awacs_unit,
altitude=AWACS_ALT,
airport=None, airport=None,
plane_type=unit_type, position=self.conflict.position.random_point_within(
position=tanker_position, AWACS_DISTANCE, AWACS_DISTANCE
altitude=alt, ),
race_distance=58000,
frequency=freq.mhz, frequency=freq.mhz,
start_type=StartType.Warm, start_type=StartType.Warm,
speed=airspeed,
tacanchannel=str(tacan),
) )
tanker_group.set_frequency(freq.mhz) awacs_flight.set_frequency(freq.mhz)
callsign = callsign_for_support_unit(tanker_group) awacs_flight.points[0].tasks.append(SetInvisibleCommand(True))
tacan_callsign = { awacs_flight.points[0].tasks.append(SetImmortalCommand(True))
"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: self.air_support.awacs.append(
# Override PyDCS tacan channel. AwacsInfo(
tanker_group.points[0].tasks.pop() group_name=str(awacs_flight.name),
tanker_group.points[0].tasks.append( callsign=callsign_for_support_unit(awacs_flight),
ActivateBeaconCommand( freq=freq,
tacan.number, depature_location=None,
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,
start_time=None, start_time=None,
end_time=None, end_time=None,
blue=True, blue=True,
) )
) )
else:
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") logging.warning("No AWACS for faction")
return
awacs_unit = possible_awacs[0]
freq = self.radio_registry.alloc_uhf()
unit_type = awacs_unit.dcs_unit_type
if not issubclass(unit_type, PlaneType):
logging.warning(f"AWACS aircraft {unit_type} must be a plane")
return
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=unit_type,
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,
)
)

View File

@@ -1,7 +1,6 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
import math
import random import random
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING, List, Optional, Tuple from typing import TYPE_CHECKING, List, Optional, Tuple
@@ -11,6 +10,7 @@ from dcs.action import AITaskPush
from dcs.condition import GroupLifeLess, Or, TimeAfter, UnitDamaged from dcs.condition import GroupLifeLess, Or, TimeAfter, UnitDamaged
from dcs.country import Country from dcs.country import Country
from dcs.mapping import Point from dcs.mapping import Point
from dcs.planes import MQ_9_Reaper
from dcs.point import PointAction from dcs.point import PointAction
from dcs.task import ( from dcs.task import (
EPLRS, EPLRS,
@@ -24,20 +24,20 @@ from dcs.task import (
SetInvisibleCommand, SetInvisibleCommand,
) )
from dcs.triggers import Event, TriggerOnce from dcs.triggers import Event, TriggerOnce
from dcs.unit import Vehicle, Skill from dcs.unit import Vehicle
from dcs.unitgroup import VehicleGroup from dcs.unitgroup import VehicleGroup
from dcs.unittype import VehicleType
from game.data.groundunitclass import GroundUnitClass from game import db
from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
from game.theater.controlpoint import ControlPoint
from game.unitmap import UnitMap from game.unitmap import UnitMap
from game.utils import heading_sum, opposite_heading from game.utils import heading_sum, opposite_heading
from game.theater.controlpoint import ControlPoint
from gen.ground_forces.ai_ground_planner import ( from gen.ground_forces.ai_ground_planner import (
DISTANCE_FROM_FRONTLINE, DISTANCE_FROM_FRONTLINE,
CombatGroup, CombatGroup,
CombatGroupRole, CombatGroupRole,
) )
from .callsigns import callsign_for_support_unit from .callsigns import callsign_for_support_unit
from .conflictgen import Conflict from .conflictgen import Conflict
from .ground_forces.combat_stance import CombatStance from .ground_forces.combat_stance import CombatStance
@@ -98,7 +98,7 @@ class GroundConflictGenerator:
self.unit_map = unit_map self.unit_map = unit_map
self.jtacs: List[JtacInfo] = [] self.jtacs: List[JtacInfo] = []
def _enemy_stance(self) -> CombatStance: def _enemy_stance(self):
"""Picks the enemy stance according to the number of planned groups on the frontline for each side""" """Picks the enemy stance according to the number of planned groups on the frontline for each side"""
if len(self.enemy_planned_combat_groups) > len( if len(self.enemy_planned_combat_groups) > len(
self.player_planned_combat_groups self.player_planned_combat_groups
@@ -123,11 +123,20 @@ class GroundConflictGenerator:
] ]
) )
def generate(self) -> None: @staticmethod
def _group_point(point: Point, base_distance) -> Point:
distance = random.randint(
int(base_distance * SPREAD_DISTANCE_FACTOR[0]),
int(base_distance * SPREAD_DISTANCE_FACTOR[1]),
)
return point.random_point_within(
distance, base_distance * SPREAD_DISTANCE_SIZE_FACTOR
)
def generate(self):
position = Conflict.frontline_position( position = Conflict.frontline_position(
self.conflict.front_line, self.game.theater self.conflict.front_line, self.game.theater
) )
frontline_vector = Conflict.frontline_vector( frontline_vector = Conflict.frontline_vector(
self.conflict.front_line, self.game.theater self.conflict.front_line, self.game.theater
) )
@@ -142,13 +151,6 @@ class GroundConflictGenerator:
self.enemy_planned_combat_groups, frontline_vector, False self.enemy_planned_combat_groups, frontline_vector, False
) )
# TODO: Differentiate AirConflict and GroundConflict classes.
if self.conflict.heading is None:
raise RuntimeError(
"Cannot generate ground units for non-ground conflict. Ground unit "
"conflicts cannot have the heading `None`."
)
# Plan combat actions for groups # Plan combat actions for groups
self.plan_action_for_groups( self.plan_action_for_groups(
self.player_stance, self.player_stance,
@@ -172,14 +174,14 @@ class GroundConflictGenerator:
n = "JTAC" + str(self.conflict.blue_cp.id) + str(self.conflict.red_cp.id) n = "JTAC" + str(self.conflict.blue_cp.id) + str(self.conflict.red_cp.id)
code = 1688 - len(self.jtacs) code = 1688 - len(self.jtacs)
utype = self.game.player_faction.jtac_unit utype = MQ_9_Reaper
if utype is None: if self.game.player_faction.jtac_unit is not None:
utype = AircraftType.named("MQ-9 Reaper") utype = self.game.player_faction.jtac_unit
jtac = self.mission.flight_group( jtac = self.mission.flight_group(
country=self.mission.country(self.game.player_country), country=self.mission.country(self.game.player_country),
name=n, name=n,
aircraft_type=utype.dcs_unit_type, aircraft_type=utype,
position=position[0], position=position[0],
airport=None, airport=None,
altitude=5000, altitude=5000,
@@ -223,22 +225,23 @@ class GroundConflictGenerator:
else: else:
cp = self.conflict.red_cp 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 # Disable infantry unit gen if disabled
if not self.game.settings.perf_infantry: if not self.game.settings.perf_infantry:
if self.game.settings.manpads: if self.game.settings.manpads:
# 50% of armored units protected by manpad # 50% of armored units protected by manpad
if random.choice([True, False]): if random.choice([True, False]):
manpads = list(faction.infantry_with_class(GroundUnitClass.Manpads)) manpads = db.find_manpad(faction)
if manpads: if len(manpads) > 0:
u = random.choices( u = random.choice(manpads)
manpads, weights=[m.spawn_weight for m in manpads]
)[0]
self.mission.vehicle_group( self.mission.vehicle_group(
side, side,
namegen.next_infantry_name(side, cp.id, u), namegen.next_infantry_name(side, cp.id, u),
u.dcs_unit_type, u,
position=infantry_position, position=infantry_position,
group_size=1, group_size=1,
heading=forward_heading, heading=forward_heading,
@@ -246,38 +249,30 @@ class GroundConflictGenerator:
) )
return return
possible_infantry_units = set( possible_infantry_units = db.find_infantry(
faction.infantry_with_class(GroundUnitClass.Infantry) faction, allow_manpad=self.game.settings.manpads
) )
if self.game.settings.manpads: if len(possible_infantry_units) == 0:
possible_infantry_units |= set(
faction.infantry_with_class(GroundUnitClass.Manpads)
)
if not possible_infantry_units:
return return
infantry_choices = list(possible_infantry_units) u = random.choice(possible_infantry_units)
units = random.choices(
infantry_choices,
weights=[u.spawn_weight for u in infantry_choices],
k=INFANTRY_GROUP_SIZE,
)
self.mission.vehicle_group( self.mission.vehicle_group(
side, side,
namegen.next_infantry_name(side, cp.id, units[0]), namegen.next_infantry_name(side, cp.id, u),
units[0].dcs_unit_type, u,
position=infantry_position, position=infantry_position,
group_size=1, group_size=1,
heading=forward_heading, heading=forward_heading,
move_formation=PointAction.OffRoad, move_formation=PointAction.OffRoad,
) )
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) position = infantry_position.random_point_within(55, 5)
self.mission.vehicle_group( self.mission.vehicle_group(
side, side,
namegen.next_infantry_name(side, cp.id, unit), namegen.next_infantry_name(side, cp.id, u),
unit.dcs_unit_type, u,
position=position, position=position,
group_size=1, group_size=1,
heading=forward_heading, heading=forward_heading,
@@ -317,7 +312,7 @@ class GroundConflictGenerator:
) )
artillery_trigger.add_condition(TimeAfter(seconds=random.randint(1, 45) * 60)) artillery_trigger.add_condition(TimeAfter(seconds=random.randint(1, 45) * 60))
# TODO: Update to fire at group instead of point # TODO: Update to fire at group instead of point
fire_task = FireAtPoint(target, 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 fire_task.number = 2 if stance != CombatStance.RETREAT else 1
dcs_group.add_trigger_action(fire_task) dcs_group.add_trigger_action(fire_task)
artillery_trigger.add_action(AITaskPush(dcs_group.id, len(dcs_group.tasks))) artillery_trigger.add_action(AITaskPush(dcs_group.id, len(dcs_group.tasks)))
@@ -360,6 +355,7 @@ class GroundConflictGenerator:
self.mission.triggerrules.triggers.append(artillery_fallback) self.mission.triggerrules.triggers.append(artillery_fallback)
for u in dcs_group.units: for u in dcs_group.units:
u.initial = True
u.heading = forward_heading + random.randint(-5, 5) u.heading = forward_heading + random.randint(-5, 5)
return True return True
return False return False
@@ -506,7 +502,7 @@ class GroundConflictGenerator:
return return
for dcs_group, group in ally_groups: 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)) dcs_group.points[0].tasks.append(EPLRS(dcs_group.id))
if group.role == CombatGroupRole.ARTILLERY: if group.role == CombatGroupRole.ARTILLERY:
@@ -568,10 +564,10 @@ class GroundConflictGenerator:
) )
# Fallback task # Fallback task
task = ControlledTask(GoToWaypoint(to_index=len(dcs_group.points))) fallback = ControlledTask(GoToWaypoint(to_index=len(dcs_group.points)))
task.enabled = False fallback.enabled = False
dcs_group.add_trigger_action(Hold()) dcs_group.add_trigger_action(Hold())
dcs_group.add_trigger_action(task) dcs_group.add_trigger_action(fallback)
# Create trigger # Create trigger
fallback = TriggerOnce(Event.NoEvent, "Morale manager #" + str(dcs_group.id)) fallback = TriggerOnce(Event.NoEvent, "Morale manager #" + str(dcs_group.id))
@@ -632,7 +628,7 @@ class GroundConflictGenerator:
@param enemy_groups Potential enemy groups @param enemy_groups Potential enemy groups
@param n number of nearby groups to take @param n number of nearby groups to take
""" """
targets = [] # type: List[VehicleGroup] targets = [] # type: List[Optional[VehicleGroup]]
sorted_list = sorted( sorted_list = sorted(
enemy_groups, enemy_groups,
key=lambda group: player_group.points[0].position.distance_to_point( key=lambda group: player_group.points[0].position.distance_to_point(
@@ -656,7 +652,7 @@ class GroundConflictGenerator:
@param group Group for which we should find the nearest ennemy @param group Group for which we should find the nearest ennemy
@param enemy_groups Potential enemy groups @param enemy_groups Potential enemy groups
""" """
min_distance = math.inf min_distance = 99999999
target = None target = None
for dcs_group, _ in enemy_groups: for dcs_group, _ in enemy_groups:
dist = player_group.points[0].position.distance_to_point( dist = player_group.points[0].position.distance_to_point(
@@ -677,7 +673,7 @@ class GroundConflictGenerator:
Search the enemy groups for a potential target suitable to an artillery unit Search the enemy groups for a potential target suitable to an artillery unit
""" """
# TODO: Update to return a list of groups instead of a single point # 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: if not enemy_groups:
return None return None
for _ in range(10): for _ in range(10):
@@ -694,7 +690,7 @@ class GroundConflictGenerator:
""" """
For artilery group, decide the distance from frontline with the range of the unit For artilery group, decide the distance from frontline with the range of the unit
""" """
rg = group.unit_type.dcs_unit_type.threat_range - 7500 rg = group.units[0].threat_range - 7500
if rg > DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]: if rg > DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]:
rg = random.randint( rg = random.randint(
DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][0], DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][0],
@@ -714,7 +710,7 @@ class GroundConflictGenerator:
distance_from_frontline: int, distance_from_frontline: int,
heading: int, heading: int,
spawn_heading: int, spawn_heading: int,
) -> Optional[Point]: ):
shifted = conflict_position.point_from_heading( shifted = conflict_position.point_from_heading(
heading, random.randint(0, combat_width) heading, random.randint(0, combat_width)
) )
@@ -727,7 +723,7 @@ class GroundConflictGenerator:
def _generate_groups( def _generate_groups(
self, self,
groups: list[CombatGroup], groups: List[CombatGroup],
frontline_vector: Tuple[Point, int, int], frontline_vector: Tuple[Point, int, int],
is_player: bool, is_player: bool,
) -> List[Tuple[VehicleGroup, CombatGroup]]: ) -> List[Tuple[VehicleGroup, CombatGroup]]:
@@ -758,15 +754,16 @@ class GroundConflictGenerator:
if final_position is not None: if final_position is not None:
g = self._generate_group( g = self._generate_group(
self.mission.country(country), self.mission.country(country),
group.unit_type, group.units[0],
group.size, len(group.units),
final_position, final_position,
distance_from_frontline,
heading=opposite_heading(spawn_heading), heading=opposite_heading(spawn_heading),
) )
if is_player: if is_player:
g.set_skill(Skill(self.game.settings.player_skill)) g.set_skill(self.game.settings.player_skill)
else: else:
g.set_skill(Skill(self.game.settings.enemy_vehicle_skill)) g.set_skill(self.game.settings.enemy_vehicle_skill)
positioned_groups.append((g, group)) positioned_groups.append((g, group))
if group.role in [CombatGroupRole.APC, CombatGroupRole.IFV]: if group.role in [CombatGroupRole.APC, CombatGroupRole.IFV]:
@@ -784,11 +781,12 @@ class GroundConflictGenerator:
def _generate_group( def _generate_group(
self, self,
side: Country, side: Country,
unit_type: GroundUnitType, unit: VehicleType,
count: int, count: int,
at: Point, at: Point,
distance_from_frontline,
move_formation: PointAction = PointAction.OffRoad, move_formation: PointAction = PointAction.OffRoad,
heading: int = 0, heading=0,
) -> VehicleGroup: ) -> VehicleGroup:
if side == self.conflict.attackers_country: if side == self.conflict.attackers_country:
@@ -796,17 +794,18 @@ class GroundConflictGenerator:
else: else:
cp = self.conflict.red_cp cp = self.conflict.red_cp
logging.info("armorgen: {} for {}".format(unit, side.id))
group = self.mission.vehicle_group( group = self.mission.vehicle_group(
side, side,
namegen.next_unit_name(side, cp.id, unit_type), namegen.next_unit_name(side, cp.id, unit),
unit_type.dcs_unit_type, unit,
position=at, position=at,
group_size=count, group_size=count,
heading=heading, heading=heading,
move_formation=move_formation, 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): for c in range(count):
vehicle: Vehicle = group.units[c] vehicle: Vehicle = group.units[c]

View File

@@ -168,10 +168,9 @@ class Package:
# likely to be the main task than others. For example, a package with # 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 # 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 # 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 # flight with strike and SEAD is an OCA/Strike package. The type of
# priority order for package task names. The package's primary task will be the # package is determined by the highest priority flight in the package.
# first task in this list that matches a flight in the package. task_priorities = [
tasks_by_priority = [
FlightType.CAS, FlightType.CAS,
FlightType.STRIKE, FlightType.STRIKE,
FlightType.ANTISHIP, FlightType.ANTISHIP,
@@ -184,11 +183,10 @@ class Package:
FlightType.TARCAP, FlightType.TARCAP,
FlightType.BARCAP, FlightType.BARCAP,
FlightType.AEWC, FlightType.AEWC,
FlightType.REFUELING,
FlightType.SWEEP, FlightType.SWEEP,
FlightType.ESCORT, FlightType.ESCORT,
] ]
for task in tasks_by_priority: for task in task_priorities:
if flight_counts[task]: if flight_counts[task]:
return task return task

View File

@@ -1,13 +1,12 @@
"""Support for working with DCS group callsigns.""" """Support for working with DCS group callsigns."""
import logging import logging
import re import re
from typing import Any
from dcs.unitgroup import FlyingGroup from dcs.unitgroup import FlyingGroup
from dcs.flyingunit import FlyingUnit from dcs.flyingunit import FlyingUnit
def callsign_for_support_unit(group: FlyingGroup[Any]) -> str: def callsign_for_support_unit(group: FlyingGroup) -> str:
# Either something like Overlord11 for Western AWACS, or else just a number. # Either something like Overlord11 for Western AWACS, or else just a number.
# Convert to either "Overlord" or "Flight 123". # Convert to either "Overlord" or "Flight 123".
lead = group.units[0] lead = group.units[0]

View File

@@ -4,7 +4,7 @@ import itertools
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from dcs import Mission from dcs import Mission
from dcs.ships import HandyWind from dcs.ships import Bulker_Handy_Wind
from dcs.unitgroup import ShipGroup from dcs.unitgroup import ShipGroup
from game.transfers import CargoShip from game.transfers import CargoShip
@@ -35,7 +35,7 @@ class CargoShipGenerator:
group = self.mission.ship_group( group = self.mission.ship_group(
country, country,
ship.name, ship.name,
HandyWind, Bulker_Handy_Wind,
position=waypoints[0], position=waypoints[0],
group_size=1, group_size=1,
) )

View File

@@ -1,11 +1,6 @@
import logging import logging
import random import random
from typing import Optional from game import db
from dcs.unitgroup import VehicleGroup
from game import db, Game
from game.theater.theatergroundobject import CoastalSiteGroundObject
from gen.coastal.silkworm import SilkwormGenerator from gen.coastal.silkworm import SilkwormGenerator
COASTAL_MAP = { COASTAL_MAP = {
@@ -13,13 +8,10 @@ COASTAL_MAP = {
} }
def generate_coastal_group( def generate_coastal_group(game, ground_object, faction_name: str):
game: Game, ground_object: CoastalSiteGroundObject, faction_name: str
) -> Optional[VehicleGroup]:
""" """
This generate a coastal defenses group This generate a coastal defenses group
:return: The generated group, or None if this faction does not support coastal :return: Nothing, but put the group reference inside the ground object
defenses.
""" """
faction = db.FACTIONS[faction_name] faction = db.FACTIONS[faction_name]
if len(faction.coastal_defenses) > 0: if len(faction.coastal_defenses) > 0:

View File

@@ -1,24 +1,19 @@
from dcs.vehicles import MissilesSS, Unarmed, AirDefence from dcs.vehicles import MissilesSS, Unarmed, AirDefence
from game import Game from gen.sam.group_generator import GroupGenerator
from game.factions.faction import Faction
from game.theater.theatergroundobject import CoastalSiteGroundObject
from gen.sam.group_generator import VehicleGroupGenerator
class SilkwormGenerator(VehicleGroupGenerator[CoastalSiteGroundObject]): class SilkwormGenerator(GroupGenerator):
def __init__( def __init__(self, game, ground_object, faction):
self, game: Game, ground_object: CoastalSiteGroundObject, faction: Faction
) -> None:
super(SilkwormGenerator, self).__init__(game, ground_object) super(SilkwormGenerator, self).__init__(game, ground_object)
self.faction = faction self.faction = faction
def generate(self) -> None: def generate(self):
positions = self.get_circular_position(5, launcher_distance=120, coverage=180) positions = self.get_circular_position(5, launcher_distance=120, coverage=180)
self.add_unit( self.add_unit(
MissilesSS.Silkworm_SR, MissilesSS.AShM_Silkworm_SR,
"SR#0", "SR#0",
self.position.x, self.position.x,
self.position.y, self.position.y,
@@ -28,7 +23,7 @@ class SilkwormGenerator(VehicleGroupGenerator[CoastalSiteGroundObject]):
# Launchers # Launchers
for i, p in enumerate(positions): for i, p in enumerate(positions):
self.add_unit( self.add_unit(
MissilesSS.Hy_launcher, MissilesSS.AShM_SS_N_2_Silkworm,
"Missile#" + str(i), "Missile#" + str(i),
p[0], p[0],
p[1], p[1],
@@ -37,7 +32,7 @@ class SilkwormGenerator(VehicleGroupGenerator[CoastalSiteGroundObject]):
# Commander # Commander
self.add_unit( self.add_unit(
Unarmed.KAMAZ_Truck, Unarmed.Truck_KAMAZ_43101,
"KAMAZ#0", "KAMAZ#0",
self.position.x - 35, self.position.x - 35,
self.position.y - 20, self.position.y - 20,
@@ -46,7 +41,7 @@ class SilkwormGenerator(VehicleGroupGenerator[CoastalSiteGroundObject]):
# Shorad # Shorad
self.add_unit( self.add_unit(
AirDefence.ZSU_23_4_Shilka, AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
"SHILKA#0", "SHILKA#0",
self.position.x - 55, self.position.x - 55,
self.position.y - 38, self.position.y - 38,
@@ -55,7 +50,7 @@ class SilkwormGenerator(VehicleGroupGenerator[CoastalSiteGroundObject]):
# Shorad 2 # Shorad 2
self.add_unit( self.add_unit(
AirDefence.Strela_1_9P31, AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL,
"STRELA#0", "STRELA#0",
self.position.x + 200, self.position.x + 200,
self.position.y + 15, self.position.y + 15,

View File

@@ -1,5 +1,3 @@
from __future__ import annotations
import logging import logging
from typing import Tuple, Optional from typing import Tuple, Optional
@@ -56,15 +54,13 @@ class Conflict:
def frontline_position( def frontline_position(
cls, frontline: FrontLine, theater: ConflictTheater cls, frontline: FrontLine, theater: ConflictTheater
) -> Tuple[Point, int]: ) -> Tuple[Point, int]:
attack_heading = int(frontline.attack_heading) attack_heading = frontline.attack_heading
position = cls.find_ground_position( position = cls.find_ground_position(
frontline.position, frontline.position,
FRONTLINE_LENGTH, FRONTLINE_LENGTH,
heading_sum(attack_heading, 90), heading_sum(attack_heading, 90),
theater, theater,
) )
if position is None:
raise RuntimeError("Could not find front line position")
return position, opposite_heading(attack_heading) return position, opposite_heading(attack_heading)
@classmethod @classmethod
@@ -95,7 +91,7 @@ class Conflict:
defender: Country, defender: Country,
front_line: FrontLine, front_line: FrontLine,
theater: ConflictTheater, theater: ConflictTheater,
) -> Conflict: ):
assert cls.has_frontline_between(front_line.blue_cp, front_line.red_cp) assert cls.has_frontline_between(front_line.blue_cp, front_line.red_cp)
position, heading, distance = cls.frontline_vector(front_line, theater) position, heading, distance = cls.frontline_vector(front_line, theater)
conflict = cls( conflict = cls(
@@ -142,7 +138,7 @@ class Conflict:
max_distance: int, max_distance: int,
heading: int, heading: int,
theater: ConflictTheater, theater: ConflictTheater,
coerce: bool = True, coerce=True,
) -> Optional[Point]: ) -> Optional[Point]:
""" """
Finds the nearest valid ground position along a provided heading and it's inverse up to max_distance. Finds the nearest valid ground position along a provided heading and it's inverse up to max_distance.
@@ -157,8 +153,6 @@ class Conflict:
if theater.is_on_land(pos): if theater.is_on_land(pos):
return pos return pos
pos = initial.point_from_heading(opposite_heading(heading), distance) pos = initial.point_from_heading(opposite_heading(heading), distance)
if theater.is_on_land(pos):
return pos
if coerce: if coerce:
pos = theater.nearest_land_pos(initial) pos = theater.nearest_land_pos(initial)
return pos return pos

View File

@@ -1,15 +1,15 @@
from __future__ import annotations from __future__ import annotations
import itertools import itertools
from typing import TYPE_CHECKING from typing import Dict, TYPE_CHECKING, Type
from dcs import Mission from dcs import Mission
from dcs.mapping import Point from dcs.mapping import Point
from dcs.point import PointAction from dcs.point import PointAction
from dcs.unit import Vehicle from dcs.unit import Vehicle
from dcs.unitgroup import VehicleGroup from dcs.unitgroup import VehicleGroup
from dcs.unittype import VehicleType
from game.dcs.groundunittype import GroundUnitType
from game.transfers import Convoy from game.transfers import Convoy
from game.unitmap import UnitMap from game.unitmap import UnitMap
from game.utils import kph from game.utils import kph
@@ -50,7 +50,7 @@ class ConvoyGenerator:
self, self,
name: str, name: str,
position: Point, position: Point,
units: dict[GroundUnitType, int], units: Dict[Type[VehicleType], int],
for_player: bool, for_player: bool,
) -> VehicleGroup: ) -> VehicleGroup:
country = self.mission.country( country = self.mission.country(
@@ -63,7 +63,7 @@ class ConvoyGenerator:
group = self.mission.vehicle_group( group = self.mission.vehicle_group(
country, country,
name, name,
main_unit_type.dcs_unit_type, main_unit_type,
position=position, position=position,
group_size=main_unit_count, group_size=main_unit_count,
move_formation=PointAction.OnRoad, move_formation=PointAction.OnRoad,
@@ -76,7 +76,7 @@ class ConvoyGenerator:
for unit_type, count in unit_types[1:]: for unit_type, count in unit_types[1:]:
for i in range(count): for i in range(count):
v = self.mission.vehicle( 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.x = position.x
v.position.y = next(y) v.position.y = next(y)

View File

@@ -1,33 +1,21 @@
import random import random
from typing import Optional
from dcs.unitgroup import VehicleGroup from dcs.vehicles import Armor
from game import db, Game from game import db
from game.data.groundunitclass import GroundUnitClass
from game.dcs.groundunittype import GroundUnitType
from game.theater.theatergroundobject import VehicleGroupGroundObject
from gen.defenses.armored_group_generator import ( from gen.defenses.armored_group_generator import (
ArmoredGroupGenerator, ArmoredGroupGenerator,
FixedSizeArmorGroupGenerator, FixedSizeArmorGroupGenerator,
) )
def generate_armor_group( def generate_armor_group(faction: str, game, ground_object):
faction: str, game: Game, ground_object: VehicleGroupGroundObject
) -> Optional[VehicleGroup]:
""" """
This generate a group of ground units This generate a group of ground units
:return: Generated group :return: Generated group
""" """
armor_types = (
GroundUnitClass.Apc,
GroundUnitClass.Atgm,
GroundUnitClass.Ifv,
GroundUnitClass.Tank,
)
possible_unit = [ 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: if len(possible_unit) > 0:
unit_type = random.choice(possible_unit) unit_type = random.choice(possible_unit)
@@ -35,9 +23,7 @@ def generate_armor_group(
return None return None
def generate_armor_group_of_type( def generate_armor_group_of_type(game, ground_object, unit_type):
game: Game, ground_object: VehicleGroupGroundObject, unit_type: GroundUnitType
) -> VehicleGroup:
""" """
This generate a group of ground units of given type This generate a group of ground units of given type
:return: Generated group :return: Generated group
@@ -47,12 +33,7 @@ def generate_armor_group_of_type(
return generator.get_generated_group() return generator.get_generated_group()
def generate_armor_group_of_type_and_size( def generate_armor_group_of_type_and_size(game, ground_object, unit_type, size: int):
game: Game,
ground_object: VehicleGroupGroundObject,
unit_type: GroundUnitType,
size: int,
) -> VehicleGroup:
""" """
This generate a group of ground units of given type and size This generate a group of ground units of given type and size
:return: Generated group :return: Generated group

View File

@@ -1,22 +1,15 @@
import random import random
from game import Game from gen.sam.group_generator import GroupGenerator
from game.dcs.groundunittype import GroundUnitType
from game.theater.theatergroundobject import VehicleGroupGroundObject
from gen.sam.group_generator import VehicleGroupGenerator
class ArmoredGroupGenerator(VehicleGroupGenerator[VehicleGroupGroundObject]): class ArmoredGroupGenerator(GroupGenerator):
def __init__( def __init__(self, game, ground_object, unit_type):
self, super(ArmoredGroupGenerator, self).__init__(game, ground_object)
game: Game,
ground_object: VehicleGroupGroundObject,
unit_type: GroundUnitType,
) -> None:
super().__init__(game, ground_object)
self.unit_type = unit_type self.unit_type = unit_type
def generate(self) -> None: def generate(self):
grid_x = random.randint(2, 3) grid_x = random.randint(2, 3)
grid_y = random.randint(1, 2) grid_y = random.randint(1, 2)
@@ -27,7 +20,7 @@ class ArmoredGroupGenerator(VehicleGroupGenerator[VehicleGroupGroundObject]):
for j in range(grid_y): for j in range(grid_y):
index = index + 1 index = index + 1
self.add_unit( self.add_unit(
self.unit_type.dcs_unit_type, self.unit_type,
"Armor#" + str(index), "Armor#" + str(index),
self.position.x + spacing * i, self.position.x + spacing * i,
self.position.y + spacing * j, self.position.y + spacing * j,
@@ -35,26 +28,20 @@ class ArmoredGroupGenerator(VehicleGroupGenerator[VehicleGroupGroundObject]):
) )
class FixedSizeArmorGroupGenerator(VehicleGroupGenerator[VehicleGroupGroundObject]): class FixedSizeArmorGroupGenerator(GroupGenerator):
def __init__( def __init__(self, game, ground_object, unit_type, size):
self, super(FixedSizeArmorGroupGenerator, self).__init__(game, ground_object)
game: Game,
ground_object: VehicleGroupGroundObject,
unit_type: GroundUnitType,
size: int,
) -> None:
super().__init__(game, ground_object)
self.unit_type = unit_type self.unit_type = unit_type
self.size = size self.size = size
def generate(self) -> None: def generate(self):
spacing = random.randint(20, 70) spacing = random.randint(20, 70)
index = 0 index = 0
for i in range(self.size): for i in range(self.size):
index = index + 1 index = index + 1
self.add_unit( self.add_unit(
self.unit_type.dcs_unit_type, self.unit_type,
"Armor#" + str(index), "Armor#" + str(index),
self.position.x + spacing * i, self.position.x + spacing * i,
self.position.y, self.position.y,

View File

@@ -22,7 +22,7 @@ class EnvironmentGenerator:
def set_fog(self, fog: Optional[Fog]) -> None: def set_fog(self, fog: Optional[Fog]) -> None:
if fog is None: if fog is None:
return return
self.mission.weather.fog_visibility = int(fog.visibility.meters) self.mission.weather.fog_visibility = fog.visibility.meters
self.mission.weather.fog_thickness = fog.thickness self.mission.weather.fog_thickness = fog.thickness
def set_wind(self, wind: WindConditions) -> None: def set_wind(self, wind: WindConditions) -> None:
@@ -30,7 +30,7 @@ class EnvironmentGenerator:
self.mission.weather.wind_at_2000 = wind.at_2000m self.mission.weather.wind_at_2000 = wind.at_2000m
self.mission.weather.wind_at_8000 = wind.at_8000m self.mission.weather.wind_at_8000 = wind.at_8000m
def generate(self) -> None: def generate(self):
self.mission.start_time = self.conditions.start_time self.mission.start_time = self.conditions.start_time
self.set_clouds(self.conditions.weather.clouds) self.set_clouds(self.conditions.weather.clouds)
self.set_fog(self.conditions.weather.fog) self.set_fog(self.conditions.weather.fog)

View File

@@ -2,11 +2,11 @@ import random
from gen.sam.group_generator import ShipGroupGenerator 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): class CarrierGroupGenerator(ShipGroupGenerator):
def generate(self) -> None: def generate(self):
# Carrier Strike Group 8 # Carrier Strike Group 8
if self.faction.carrier_names[0] == "Carrier Strike Group 8": if self.faction.carrier_names[0] == "Carrier Strike Group 8":
@@ -22,7 +22,7 @@ class CarrierGroupGenerator(ShipGroupGenerator):
# Add Arleigh Burke escort # Add Arleigh Burke escort
self.add_unit( self.add_unit(
USS_Arleigh_Burke_IIa, DDG_Arleigh_Burke_IIa,
"USS Ramage", "USS Ramage",
self.position.x + 6482, self.position.x + 6482,
self.position.y + 6667, self.position.y + 6667,
@@ -30,7 +30,7 @@ class CarrierGroupGenerator(ShipGroupGenerator):
) )
self.add_unit( self.add_unit(
USS_Arleigh_Burke_IIa, DDG_Arleigh_Burke_IIa,
"USS Mitscher", "USS Mitscher",
self.position.x - 7963, self.position.x - 7963,
self.position.y + 7037, self.position.y + 7037,
@@ -38,7 +38,7 @@ class CarrierGroupGenerator(ShipGroupGenerator):
) )
self.add_unit( self.add_unit(
USS_Arleigh_Burke_IIa, DDG_Arleigh_Burke_IIa,
"USS Forrest Sherman", "USS Forrest Sherman",
self.position.x - 7408, self.position.x - 7408,
self.position.y - 7408, self.position.y - 7408,
@@ -46,7 +46,7 @@ class CarrierGroupGenerator(ShipGroupGenerator):
) )
self.add_unit( self.add_unit(
USS_Arleigh_Burke_IIa, DDG_Arleigh_Burke_IIa,
"USS Lassen", "USS Lassen",
self.position.x + 8704, self.position.x + 8704,
self.position.y - 6296, self.position.y - 6296,
@@ -56,7 +56,7 @@ class CarrierGroupGenerator(ShipGroupGenerator):
# Add Ticonderoga escort # Add Ticonderoga escort
if self.heading >= 180: if self.heading >= 180:
self.add_unit( self.add_unit(
TICONDEROG, CG_Ticonderoga,
"USS Hué City", "USS Hué City",
self.position.x + 2222, self.position.x + 2222,
self.position.y - 3333, self.position.y - 3333,
@@ -64,7 +64,7 @@ class CarrierGroupGenerator(ShipGroupGenerator):
) )
else: else:
self.add_unit( self.add_unit(
TICONDEROG, CG_Ticonderoga,
"USS Hué City", "USS Hué City",
self.position.x - 3333, self.position.x - 3333,
self.position.y + 2222, self.position.y + 2222,

View File

@@ -3,23 +3,24 @@ from __future__ import annotations
import random import random
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from dcs.ships import ( from dcs.ships import (
Type_052C, Type_052C_Destroyer,
Type_052B, Type_052B_Destroyer,
Type_054A, Type_054A_Frigate,
) )
from game.factions.faction import Faction from game.factions.faction import Faction
from game.theater.theatergroundobject import ShipGroundObject
from gen.fleet.dd_group import DDGroupGenerator from gen.fleet.dd_group import DDGroupGenerator
from gen.sam.group_generator import ShipGroupGenerator from gen.sam.group_generator import ShipGroupGenerator
from game.theater.theatergroundobject import TheaterGroundObject
if TYPE_CHECKING: if TYPE_CHECKING:
from game.game import Game from game.game import Game
class ChineseNavyGroupGenerator(ShipGroupGenerator): class ChineseNavyGroupGenerator(ShipGroupGenerator):
def generate(self) -> None: def generate(self):
include_frigate = random.choice([True, True, False]) include_frigate = random.choice([True, True, False])
include_dd = random.choice([True, False]) include_dd = random.choice([True, False])
@@ -29,14 +30,14 @@ class ChineseNavyGroupGenerator(ShipGroupGenerator):
if include_frigate: if include_frigate:
self.add_unit( self.add_unit(
Type_054A, Type_054A_Frigate,
"FF1", "FF1",
self.position.x + 1200, self.position.x + 1200,
self.position.y + 900, self.position.y + 900,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
Type_054A, Type_054A_Frigate,
"FF2", "FF2",
self.position.x + 1200, self.position.x + 1200,
self.position.y - 900, self.position.y - 900,
@@ -44,7 +45,7 @@ class ChineseNavyGroupGenerator(ShipGroupGenerator):
) )
if include_dd: if include_dd:
dd_type = random.choice([Type_052C, Type_052B]) dd_type = random.choice([Type_052C_Destroyer, Type_052B_Destroyer])
self.add_unit( self.add_unit(
dd_type, dd_type,
"DD1", "DD1",
@@ -64,7 +65,9 @@ class ChineseNavyGroupGenerator(ShipGroupGenerator):
class Type54GroupGenerator(DDGroupGenerator): class Type54GroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction): def __init__(
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
super(Type54GroupGenerator, self).__init__( super(Type54GroupGenerator, self).__init__(
game, ground_object, faction, Type_054A game, ground_object, faction, Type_054A_Frigate
) )

View File

@@ -1,13 +1,12 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Type
from dcs.ships import PERRY, USS_Arleigh_Burke_IIa
from dcs.unittype import ShipType
from game.factions.faction import Faction from game.factions.faction import Faction
from game.theater.theatergroundobject import ShipGroundObject from game.theater.theatergroundobject import TheaterGroundObject
from gen.sam.group_generator import ShipGroupGenerator from gen.sam.group_generator import ShipGroupGenerator
from dcs.unittype import ShipType
from dcs.ships import FFG_Oliver_Hazzard_Perry, DDG_Arleigh_Burke_IIa
if TYPE_CHECKING: if TYPE_CHECKING:
from game.game import Game from game.game import Game
@@ -17,14 +16,14 @@ class DDGroupGenerator(ShipGroupGenerator):
def __init__( def __init__(
self, self,
game: Game, game: Game,
ground_object: ShipGroundObject, ground_object: TheaterGroundObject,
faction: Faction, faction: Faction,
ddtype: Type[ShipType], ddtype: ShipType,
): ):
super(DDGroupGenerator, self).__init__(game, ground_object, faction) super(DDGroupGenerator, self).__init__(game, ground_object, faction)
self.ddtype = ddtype self.ddtype = ddtype
def generate(self) -> None: def generate(self):
self.add_unit( self.add_unit(
self.ddtype, self.ddtype,
"DD1", "DD1",
@@ -43,14 +42,18 @@ class DDGroupGenerator(ShipGroupGenerator):
class OliverHazardPerryGroupGenerator(DDGroupGenerator): class OliverHazardPerryGroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction): def __init__(
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
super(OliverHazardPerryGroupGenerator, self).__init__( super(OliverHazardPerryGroupGenerator, self).__init__(
game, ground_object, faction, PERRY game, ground_object, faction, FFG_Oliver_Hazzard_Perry
) )
class ArleighBurkeGroupGenerator(DDGroupGenerator): class ArleighBurkeGroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction): def __init__(
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
super(ArleighBurkeGroupGenerator, self).__init__( super(ArleighBurkeGroupGenerator, self).__init__(
game, ground_object, faction, USS_Arleigh_Burke_IIa game, ground_object, faction, DDG_Arleigh_Burke_IIa
) )

View File

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

View File

@@ -4,7 +4,7 @@ from gen.sam.group_generator import ShipGroupGenerator
class LHAGroupGenerator(ShipGroupGenerator): class LHAGroupGenerator(ShipGroupGenerator):
def generate(self) -> None: def generate(self):
# Add carrier # Add carrier
if len(self.faction.helicopter_carrier) > 0: if len(self.faction.helicopter_carrier) > 0:

View File

@@ -1,29 +1,29 @@
from __future__ import annotations from __future__ import annotations
import random import random
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from dcs.ships import ( from dcs.ships import (
ALBATROS, Corvette_1124_4_Grisha,
MOLNIYA, Corvette_1241_1_Molniya,
NEUSTRASH, Frigate_11540_Neustrashimy,
REZKY, Frigate_1135M_Rezky,
MOSCOW, Cruiser_1164_Moskva,
KILO, SSK_877V_Kilo,
SOM, SSK_641B_Tango,
) )
from game.factions.faction import Faction
from game.theater.theatergroundobject import ShipGroundObject
from gen.fleet.dd_group import DDGroupGenerator from gen.fleet.dd_group import DDGroupGenerator
from gen.sam.group_generator import ShipGroupGenerator from gen.sam.group_generator import ShipGroupGenerator
from game.factions.faction import Faction
from game.theater.theatergroundobject import TheaterGroundObject
if TYPE_CHECKING: if TYPE_CHECKING:
from game.game import Game from game.game import Game
class RussianNavyGroupGenerator(ShipGroupGenerator): class RussianNavyGroupGenerator(ShipGroupGenerator):
def generate(self) -> None: def generate(self):
include_frigate = random.choice([True, True, False]) include_frigate = random.choice([True, True, False])
include_dd = random.choice([True, False]) include_dd = random.choice([True, False])
@@ -37,7 +37,9 @@ class RussianNavyGroupGenerator(ShipGroupGenerator):
include_frigate = True include_frigate = True
if include_frigate: if include_frigate:
frigate_type = random.choice([ALBATROS, MOLNIYA]) frigate_type = random.choice(
[Corvette_1124_4_Grisha, Corvette_1241_1_Molniya]
)
self.add_unit( self.add_unit(
frigate_type, frigate_type,
"FF1", "FF1",
@@ -54,7 +56,7 @@ class RussianNavyGroupGenerator(ShipGroupGenerator):
) )
if include_dd: if include_dd:
dd_type = random.choice([NEUSTRASH, REZKY]) dd_type = random.choice([Frigate_11540_Neustrashimy, Frigate_1135M_Rezky])
self.add_unit( self.add_unit(
dd_type, dd_type,
"DD1", "DD1",
@@ -74,7 +76,7 @@ class RussianNavyGroupGenerator(ShipGroupGenerator):
# Only include the Moskva for now, the Pyotry Velikiy is an unkillable monster. # Only include the Moskva for now, the Pyotry Velikiy is an unkillable monster.
# See https://github.com/dcs-liberation/dcs_liberation/issues/567 # See https://github.com/dcs-liberation/dcs_liberation/issues/567
self.add_unit( self.add_unit(
MOSCOW, Cruiser_1164_Moskva,
"CC1", "CC1",
self.position.x, self.position.x,
self.position.y, self.position.y,
@@ -85,24 +87,36 @@ class RussianNavyGroupGenerator(ShipGroupGenerator):
class GrishaGroupGenerator(DDGroupGenerator): class GrishaGroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction): def __init__(
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
super(GrishaGroupGenerator, self).__init__( super(GrishaGroupGenerator, self).__init__(
game, ground_object, faction, ALBATROS game, ground_object, faction, Corvette_1124_4_Grisha
) )
class MolniyaGroupGenerator(DDGroupGenerator): class MolniyaGroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction): def __init__(
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
super(MolniyaGroupGenerator, self).__init__( super(MolniyaGroupGenerator, self).__init__(
game, ground_object, faction, MOLNIYA game, ground_object, faction, Corvette_1241_1_Molniya
) )
class KiloSubGroupGenerator(DDGroupGenerator): class KiloSubGroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction): def __init__(
super(KiloSubGroupGenerator, self).__init__(game, ground_object, faction, KILO) self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
super(KiloSubGroupGenerator, self).__init__(
game, ground_object, faction, SSK_877V_Kilo
)
class TangoSubGroupGenerator(DDGroupGenerator): class TangoSubGroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction): def __init__(
super(TangoSubGroupGenerator, self).__init__(game, ground_object, faction, SOM) self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
super(TangoSubGroupGenerator, self).__init__(
game, ground_object, faction, SSK_641B_Tango
)

View File

@@ -1,16 +1,16 @@
import random import random
from dcs.ships import Schnellboot_type_S130 from dcs.ships import Boat_Schnellboot_type_S130
from gen.sam.group_generator import ShipGroupGenerator from gen.sam.group_generator import ShipGroupGenerator
class SchnellbootGroupGenerator(ShipGroupGenerator): class SchnellbootGroupGenerator(ShipGroupGenerator):
def generate(self) -> None: def generate(self):
for i in range(random.randint(2, 4)): for i in range(random.randint(2, 4)):
self.add_unit( self.add_unit(
Schnellboot_type_S130, Boat_Schnellboot_type_S130,
"Schnellboot" + str(i), "Schnellboot" + str(i),
self.position.x + i * random.randint(100, 250), self.position.x + i * random.randint(100, 250),
self.position.y + (random.randint(100, 200) - 100), self.position.y + (random.randint(100, 200) - 100),

View File

@@ -1,17 +1,7 @@
from __future__ import annotations
import logging import logging
import random import random
from typing import TYPE_CHECKING, Optional
from dcs.unitgroup import ShipGroup
from game import db from game import db
from game.theater.theatergroundobject import (
LhaGroundObject,
CarrierGroundObject,
ShipGroundObject,
)
from gen.fleet.carrier_group import CarrierGroupGenerator from gen.fleet.carrier_group import CarrierGroupGenerator
from gen.fleet.cn_dd_group import ChineseNavyGroupGenerator, Type54GroupGenerator from gen.fleet.cn_dd_group import ChineseNavyGroupGenerator, Type54GroupGenerator
from gen.fleet.dd_group import ( from gen.fleet.dd_group import (
@@ -31,9 +21,6 @@ from gen.fleet.schnellboot import SchnellbootGroupGenerator
from gen.fleet.uboat import UBoatGroupGenerator from gen.fleet.uboat import UBoatGroupGenerator
from gen.fleet.ww2lst import WW2LSTGroupGenerator from gen.fleet.ww2lst import WW2LSTGroupGenerator
if TYPE_CHECKING:
from game import Game
SHIP_MAP = { SHIP_MAP = {
"SchnellbootGroupGenerator": SchnellbootGroupGenerator, "SchnellbootGroupGenerator": SchnellbootGroupGenerator,
@@ -52,12 +39,10 @@ SHIP_MAP = {
} }
def generate_ship_group( def generate_ship_group(game, ground_object, faction_name: str):
game: Game, ground_object: ShipGroundObject, faction_name: str
) -> Optional[ShipGroup]:
""" """
This generate a ship group This generate a ship group
:return: The generated group, or None if this faction does not support ships. :return: Nothing, but put the group reference inside the ground object
""" """
faction = db.FACTIONS[faction_name] faction = db.FACTIONS[faction_name]
if len(faction.navy_generators) > 0: if len(faction.navy_generators) > 0:
@@ -76,30 +61,26 @@ def generate_ship_group(
return None return None
def generate_carrier_group( def generate_carrier_group(faction: str, game, ground_object):
faction: str, game: Game, ground_object: CarrierGroundObject """
) -> ShipGroup: This generate a carrier group
"""Generates a carrier group. :param parentCp: The parent control point
:param faction: The faction the TGO belongs to.
:param game: The Game the group is being generated for.
:param ground_object: The ground object which will own the ship group :param ground_object: The ground object which will own the ship group
:return: The generated group. :param country: Owner country
:return: Nothing, but put the group reference inside the ground object
""" """
generator = CarrierGroupGenerator(game, ground_object, db.FACTIONS[faction]) generator = CarrierGroupGenerator(game, ground_object, db.FACTIONS[faction])
generator.generate() generator.generate()
return generator.get_generated_group() return generator.get_generated_group()
def generate_lha_group( def generate_lha_group(faction: str, game, ground_object):
faction: str, game: Game, ground_object: LhaGroundObject """
) -> ShipGroup: This generate a lha carrier group
"""Generate an LHA group. :param parentCp: The parent control point
:param faction: The faction the TGO belongs to.
:param game: The Game the group is being generated for.
:param ground_object: The ground object which will own the ship group :param ground_object: The ground object which will own the ship group
:return: The generated group. :param country: Owner country
:return: Nothing, but put the group reference inside the ground object
""" """
generator = LHAGroupGenerator(game, ground_object, db.FACTIONS[faction]) generator = LHAGroupGenerator(game, ground_object, db.FACTIONS[faction])
generator.generate() generator.generate()

View File

@@ -1,16 +1,16 @@
import random import random
from dcs.ships import Uboat_VIIC from dcs.ships import U_boat_VIIC_U_flak
from gen.sam.group_generator import ShipGroupGenerator from gen.sam.group_generator import ShipGroupGenerator
class UBoatGroupGenerator(ShipGroupGenerator): class UBoatGroupGenerator(ShipGroupGenerator):
def generate(self) -> None: def generate(self):
for i in range(random.randint(1, 4)): for i in range(random.randint(1, 4)):
self.add_unit( self.add_unit(
Uboat_VIIC, U_boat_VIIC_U_flak,
"Uboat" + str(i), "Uboat" + str(i),
self.position.x + i * random.randint(100, 250), self.position.x + i * random.randint(100, 250),
self.position.y + (random.randint(100, 200) - 100), self.position.y + (random.randint(100, 200) - 100),

View File

@@ -1,16 +1,16 @@
import random 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 from gen.sam.group_generator import ShipGroupGenerator
class WW2LSTGroupGenerator(ShipGroupGenerator): class WW2LSTGroupGenerator(ShipGroupGenerator):
def generate(self) -> None: def generate(self):
# Add LS Samuel Chase # Add LS Samuel Chase
self.add_unit( self.add_unit(
USS_Samuel_Chase, LS_Samuel_Chase,
"SamuelChase", "SamuelChase",
self.position.x, self.position.x,
self.position.y, self.position.y,
@@ -19,7 +19,7 @@ class WW2LSTGroupGenerator(ShipGroupGenerator):
for i in range(1, random.randint(3, 4)): for i in range(1, random.randint(3, 4)):
self.add_unit( self.add_unit(
LST_Mk2, LST_Mk_II,
"LST" + str(i), "LST" + str(i),
self.position.x + i * random.randint(800, 1200), self.position.x + i * random.randint(800, 1200),
self.position.y, self.position.y,

View File

@@ -1,7 +1,6 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
import math
import operator import operator
import random import random
from collections import defaultdict from collections import defaultdict
@@ -17,11 +16,14 @@ from typing import (
Set, Set,
TYPE_CHECKING, TYPE_CHECKING,
Tuple, Tuple,
Type,
TypeVar, TypeVar,
Any, 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.infos.information import Information
from game.procurement import AircraftProcurementRequest from game.procurement import AircraftProcurementRequest
from game.profiling import logged_duration, MultiEventTracer from game.profiling import logged_duration, MultiEventTracer
@@ -164,6 +166,8 @@ class AircraftAllocator:
flight.max_distance flight.max_distance
) )
# Prefer using squadrons with pilots first
best_understaffed: Optional[Tuple[ControlPoint, Squadron]] = None
for airfield in airfields_in_range: for airfield in airfields_in_range:
if not airfield.is_friendly(self.is_player): if not airfield.is_friendly(self.is_player):
continue continue
@@ -175,14 +179,24 @@ class AircraftAllocator:
continue continue
# Valid location with enough aircraft available. Find a squadron to fit # Valid location with enough aircraft available. Find a squadron to fit
# the role. # the role.
squadrons = self.air_wing.auto_assignable_for_task_with_type( for squadron in self.air_wing.squadrons_for(aircraft):
aircraft, task if task not in squadron.auto_assignable_mission_types:
) continue
for squadron in squadrons: if len(squadron.available_pilots) >= flight.num_aircraft:
if squadron.can_provide_pilots(flight.num_aircraft):
inventory.remove_aircraft(aircraft, flight.num_aircraft) inventory.remove_aircraft(aircraft, flight.num_aircraft)
return airfield, squadron 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: class PackageBuilder:
@@ -241,7 +255,7 @@ class PackageBuilder:
return True return True
def find_divert_field( def find_divert_field(
self, aircraft: AircraftType, arrival: ControlPoint self, aircraft: Type[FlyingType], arrival: ControlPoint
) -> Optional[ControlPoint]: ) -> Optional[ControlPoint]:
divert_limit = nautical_miles(150) divert_limit = nautical_miles(150)
for airfield in self.closest_airfields.operational_airfields_within( for airfield in self.closest_airfields.operational_airfields_within(
@@ -285,7 +299,7 @@ class ObjectiveFinder:
self.game = game self.game = game
self.is_player = is_player self.is_player = is_player
def enemy_air_defenses(self) -> Iterator[tuple[TheaterGroundObject[Any], Distance]]: def enemy_air_defenses(self) -> Iterator[tuple[TheaterGroundObject, Distance]]:
"""Iterates over all enemy SAM sites.""" """Iterates over all enemy SAM sites."""
doctrine = self.game.faction_for(self.is_player).doctrine doctrine = self.game.faction_for(self.is_player).doctrine
threat_zones = self.game.threat_zone_for(not self.is_player) threat_zones = self.game.threat_zone_for(not self.is_player)
@@ -315,14 +329,14 @@ class ObjectiveFinder:
yield ground_object, target_range yield ground_object, target_range
def threatening_air_defenses(self) -> Iterator[TheaterGroundObject[Any]]: def threatening_air_defenses(self) -> Iterator[TheaterGroundObject]:
"""Iterates over enemy SAMs in threat range of friendly control points. """Iterates over enemy SAMs in threat range of friendly control points.
SAM sites are sorted by their closest proximity to any friendly control SAM sites are sorted by their closest proximity to any friendly control
point (airfield or fleet). point (airfield or fleet).
""" """
target_ranges: list[tuple[TheaterGroundObject[Any], Distance]] = [] target_ranges: list[tuple[TheaterGroundObject, Distance]] = []
for target, threat_range in self.enemy_air_defenses(): for target, threat_range in self.enemy_air_defenses():
ranges: list[Distance] = [] ranges: list[Distance] = []
for cp in self.friendly_control_points(): for cp in self.friendly_control_points():
@@ -375,9 +389,9 @@ class ObjectiveFinder:
def _targets_by_range( def _targets_by_range(
self, targets: Iterable[MissionTargetType] self, targets: Iterable[MissionTargetType]
) -> Iterator[MissionTargetType]: ) -> Iterator[MissionTargetType]:
target_ranges: list[tuple[MissionTargetType, float]] = [] target_ranges: List[Tuple[MissionTargetType, int]] = []
for target in targets: for target in targets:
ranges: list[float] = [] ranges: List[int] = []
for cp in self.friendly_control_points(): for cp in self.friendly_control_points():
ranges.append(target.distance_to(cp)) ranges.append(target.distance_to(cp))
target_ranges.append((target, min(ranges))) target_ranges.append((target, min(ranges)))
@@ -386,13 +400,13 @@ class ObjectiveFinder:
for target, _range in target_ranges: for target, _range in target_ranges:
yield target yield target
def strike_targets(self) -> Iterator[TheaterGroundObject[Any]]: def strike_targets(self) -> Iterator[TheaterGroundObject]:
"""Iterates over enemy strike targets. """Iterates over enemy strike targets.
Targets are sorted by their closest proximity to any friendly control Targets are sorted by their closest proximity to any friendly control
point (airfield or fleet). point (airfield or fleet).
""" """
targets: list[tuple[TheaterGroundObject[Any], float]] = [] targets: List[Tuple[TheaterGroundObject, int]] = []
# Building objectives are made of several individual TGOs (one per # Building objectives are made of several individual TGOs (one per
# building). # building).
found_targets: Set[str] = set() found_targets: Set[str] = set()
@@ -431,7 +445,7 @@ class ObjectiveFinder:
continue continue
if ground_object.name in found_targets: if ground_object.name in found_targets:
continue continue
ranges: list[float] = [] ranges: List[int] = []
for friendly_cp in self.friendly_control_points(): for friendly_cp in self.friendly_control_points():
ranges.append(ground_object.distance_to(friendly_cp)) ranges.append(ground_object.distance_to(friendly_cp))
targets.append((ground_object, min(ranges))) targets.append((ground_object, min(ranges)))
@@ -510,24 +524,6 @@ class ObjectiveFinder:
raise RuntimeError("Found no friendly control points. You probably lost.") raise RuntimeError("Found no friendly control points. You probably lost.")
return farthest 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]: def enemy_control_points(self) -> Iterator[ControlPoint]:
"""Iterates over all enemy control points.""" """Iterates over all enemy control points."""
return ( return (
@@ -585,8 +581,7 @@ class CoalitionMissionPlanner:
MAX_OCA_RANGE = nautical_miles(150) MAX_OCA_RANGE = nautical_miles(150)
MAX_SEAD_RANGE = nautical_miles(150) MAX_SEAD_RANGE = nautical_miles(150)
MAX_STRIKE_RANGE = nautical_miles(150) MAX_STRIKE_RANGE = nautical_miles(150)
MAX_AWEC_RANGE = Distance.inf() MAX_AWEC_RANGE = nautical_miles(200)
MAX_TANKER_RANGE = nautical_miles(200)
def __init__(self, game: Game, is_player: bool) -> None: def __init__(self, game: Game, is_player: bool) -> None:
self.game = game self.game = game
@@ -605,7 +600,14 @@ class CoalitionMissionPlanner:
also possible for the player to exclude mission types from their squadron also possible for the player to exclude mission types from their squadron
designs. 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]: def critical_missions(self) -> Iterator[ProposedMission]:
"""Identifies the most important missions to plan this turn. """Identifies the most important missions to plan this turn.
@@ -626,11 +628,6 @@ class CoalitionMissionPlanner:
asap=True, 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. # Find friendly CPs within 100 nmi from an enemy airfield, plan CAP.
for cp in self.objective_finder.vulnerable_control_points(): for cp in self.objective_finder.vulnerable_control_points():
# Plan CAP in such a way, that it is established during the whole desired mission length # Plan CAP in such a way, that it is established during the whole desired mission length
@@ -845,7 +842,7 @@ class CoalitionMissionPlanner:
for cp in self.objective_finder.friendly_control_points(): for cp in self.objective_finder.friendly_control_points():
inventory = self.game.aircraft_inventory.for_control_point(cp) inventory = self.game.aircraft_inventory.for_control_point(cp)
for aircraft, available in inventory.all_aircraft: for aircraft, available in inventory.all_aircraft:
self.message("Unused aircraft", f"{available} {aircraft} from {cp}") self.message("Unused aircraft", f"{available} {aircraft.id} from {cp}")
def plan_flight( def plan_flight(
self, self,
@@ -1058,7 +1055,7 @@ class CoalitionMissionPlanner:
# delayed until their takeoff time by AirConflictGenerator. # delayed until their takeoff time by AirConflictGenerator.
package.time_over_target = next(start_time) + tot package.time_over_target = next(start_time) + tot
def message(self, title: str, text: str) -> None: def message(self, title, text) -> None:
"""Emits a planning message to the player. """Emits a planning message to the player.
If the mission planner belongs to the players coalition, this emits a If the mission planner belongs to the players coalition, this emits a

View File

@@ -1,6 +1,5 @@
import logging import logging
from collections import Sequence from typing import List, Type
from typing import Type
from dcs.helicopters import ( from dcs.helicopters import (
AH_1W, AH_1W,
@@ -9,7 +8,6 @@ from dcs.helicopters import (
CH_47D, CH_47D,
CH_53E, CH_53E,
Ka_50, Ka_50,
Mi_24P,
Mi_24V, Mi_24V,
Mi_26, Mi_26,
Mi_28N, Mi_28N,
@@ -53,13 +51,10 @@ from dcs.planes import (
F_5E_3, F_5E_3,
F_86F_Sabre, F_86F_Sabre,
IL_76MD, IL_76MD,
IL_78M, I_16,
JF_17, JF_17,
J_11A, J_11A,
Ju_88A4, Ju_88A4,
KC130,
KC135MPRS,
KC_135,
KJ_2000, KJ_2000,
L_39ZA, L_39ZA,
MQ_9_Reaper, MQ_9_Reaper,
@@ -82,7 +77,6 @@ from dcs.planes import (
P_51D_30_NA, P_51D_30_NA,
RQ_1A_Predator, RQ_1A_Predator,
S_3B, S_3B,
S_3B_Tanker,
SpitfireLFMkIX, SpitfireLFMkIX,
SpitfireLFMkIXCW, SpitfireLFMkIXCW,
Su_17M4, Su_17M4,
@@ -106,12 +100,12 @@ from dcs.planes import (
) )
from dcs.unittype import FlyingType from dcs.unittype import FlyingType
from game.dcs.aircrafttype import AircraftType
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
from pydcs_extensions.a4ec.a4ec import A_4E_C from pydcs_extensions.a4ec.a4ec import A_4E_C
from pydcs_extensions.f22a.f22a import F_22A from pydcs_extensions.f22a.f22a import F_22A
from pydcs_extensions.hercules.hercules import Hercules
from pydcs_extensions.jas39.jas39 import JAS39Gripen, JAS39Gripen_AG 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 from pydcs_extensions.su57.su57 import Su_57
# All aircraft lists are in priority order. Aircraft higher in the list will be # All aircraft lists are in priority order. Aircraft higher in the list will be
@@ -125,30 +119,29 @@ from pydcs_extensions.su57.su57 import Su_57
CAP_CAPABLE = [ CAP_CAPABLE = [
Su_57, Su_57,
F_22A, F_22A,
F_15C, MiG_31,
F_14B, F_14B,
F_14A_135_GR, F_14A_135_GR,
MiG_25PD,
Su_33, Su_33,
Su_34,
J_11A,
Su_30, Su_30,
Su_27, Su_27,
J_11A,
F_15C,
MiG_29S, MiG_29S,
F_16C_50,
FA_18C_hornet,
JF_17,
JAS39Gripen,
F_16A,
F_4E,
MiG_31,
MiG_25PD,
MiG_29G, MiG_29G,
MiG_29A, MiG_29A,
F_16C_50,
FA_18C_hornet,
F_16A,
F_4E,
JAS39Gripen,
JF_17,
MiG_23MLD, MiG_23MLD,
MiG_21Bis, MiG_21Bis,
Mirage_2000_5, Mirage_2000_5,
F_15E,
M_2000C, M_2000C,
F_15E,
F_5E_3, F_5E_3,
MiG_19P, MiG_19P,
A_4E_C, A_4E_C,
@@ -175,7 +168,6 @@ CAS_CAPABLE = [
A_10C_2, A_10C_2,
A_10C, A_10C,
Hercules, Hercules,
Su_34,
Su_25TM, Su_25TM,
Su_25T, Su_25T,
Su_25, Su_25,
@@ -193,16 +185,17 @@ CAS_CAPABLE = [
F_14B, F_14B,
F_14A_135_GR, F_14A_135_GR,
AJS37, AJS37,
Su_24MR,
Su_24M, Su_24M,
Su_17M4, Su_17M4,
Su_33,
F_4E, F_4E,
S_3B, S_3B,
Su_34,
Su_30, Su_30,
MiG_19P,
MiG_29S, MiG_29S,
MiG_27K, MiG_27K,
MiG_29A, MiG_29A,
MiG_21Bis,
AH_64D, AH_64D,
AH_64A, AH_64A,
AH_1W, AH_1W,
@@ -211,17 +204,16 @@ CAS_CAPABLE = [
SA342L, SA342L,
Ka_50, Ka_50,
Mi_28N, Mi_28N,
Mi_24P,
Mi_24V, Mi_24V,
Mi_8MT, Mi_8MT,
MiG_19P, UH_1H,
MiG_15bis, MiG_15bis,
M_2000C, M_2000C,
F_5E_3, F_5E_3,
F_86F_Sabre, F_86F_Sabre,
C_101CC, C_101CC,
MB_339PAN,
L_39ZA, L_39ZA,
UH_1H,
A_20G, A_20G,
Ju_88A4, Ju_88A4,
P_47D_40, P_47D_40,
@@ -302,14 +294,13 @@ STRIKE_CAPABLE = [
Tornado_GR4, Tornado_GR4,
F_16C_50, F_16C_50,
FA_18C_hornet, FA_18C_hornet,
AV8BNA,
JF_17,
F_16A, F_16A,
F_14B, F_14B,
F_14A_135_GR, F_14A_135_GR,
JAS39Gripen_AG, JAS39Gripen_AG,
Tornado_IDS, Tornado_IDS,
Su_17M4, Su_17M4,
Su_24MR,
Su_24M, Su_24M,
Su_25TM, Su_25TM,
Su_25T, Su_25T,
@@ -321,9 +312,11 @@ STRIKE_CAPABLE = [
MiG_29S, MiG_29S,
MiG_29G, MiG_29G,
MiG_29A, MiG_29A,
JF_17,
F_4E, F_4E,
A_10C_2, A_10C_2,
A_10C, A_10C,
AV8BNA,
S_3B, S_3B,
A_4E_C, A_4E_C,
M_2000C, M_2000C,
@@ -332,6 +325,7 @@ STRIKE_CAPABLE = [
MiG_15bis, MiG_15bis,
F_5E_3, F_5E_3,
F_86F_Sabre, F_86F_Sabre,
MB_339PAN,
C_101CC, C_101CC,
L_39ZA, L_39ZA,
B_17G, B_17G,
@@ -377,7 +371,6 @@ RUNWAY_ATTACK_CAPABLE = [
Su_34, Su_34,
Su_30, Su_30,
Tornado_IDS, Tornado_IDS,
M_2000C,
] + STRIKE_CAPABLE ] + STRIKE_CAPABLE
# For any aircraft that isn't necessarily directly involved in strike # For any aircraft that isn't necessarily directly involved in strike
@@ -408,17 +401,8 @@ AEWC_CAPABLE = [
KJ_2000, 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 aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
def dcs_types_for_task(task: FlightType) -> Sequence[Type[FlyingType]]:
cap_missions = (FlightType.BARCAP, FlightType.TARCAP, FlightType.SWEEP) cap_missions = (FlightType.BARCAP, FlightType.TARCAP, FlightType.SWEEP)
if task in cap_missions: if task in cap_missions:
return CAP_CAPABLE return CAP_CAPABLE
@@ -444,8 +428,6 @@ def dcs_types_for_task(task: FlightType) -> Sequence[Type[FlyingType]]:
return CAP_CAPABLE return CAP_CAPABLE
elif task == FlightType.AEWC: elif task == FlightType.AEWC:
return AEWC_CAPABLE return AEWC_CAPABLE
elif task == FlightType.REFUELING:
return REFUELING_CAPABALE
elif task == FlightType.TRANSPORT: elif task == FlightType.TRANSPORT:
return TRANSPORT_CAPABLE return TRANSPORT_CAPABLE
else: else:
@@ -453,15 +435,7 @@ def dcs_types_for_task(task: FlightType) -> Sequence[Type[FlyingType]]:
return [] return []
def aircraft_for_task(task: FlightType) -> list[AircraftType]: def tasks_for_aircraft(aircraft: Type[FlyingType]) -> list[FlightType]:
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]:
tasks = [] tasks = []
for task in FlightType: for task in FlightType:
if aircraft in aircraft_for_task(task): if aircraft in aircraft_for_task(task):

View File

@@ -1,14 +1,16 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from enum import Enum from enum import Enum
from typing import List, Optional, TYPE_CHECKING, Union, Sequence from typing import List, Optional, TYPE_CHECKING, Type, Union
from dcs.mapping import Point from dcs.mapping import Point
from dcs.point import MovingPoint, PointAction from dcs.point import MovingPoint, PointAction
from dcs.unit import Unit from dcs.unit import Unit
from dcs.unittype import FlyingType
from game.dcs.aircrafttype import AircraftType from game import db
from game.squadrons import Pilot, Squadron from game.squadrons import Pilot, Squadron
from game.theater.controlpoint import ControlPoint, MissionTarget from game.theater.controlpoint import ControlPoint, MissionTarget
from game.utils import Distance, meters from game.utils import Distance, meters
@@ -67,7 +69,6 @@ class FlightType(Enum):
AEWC = "AEW&C" AEWC = "AEW&C"
TRANSPORT = "Transport" TRANSPORT = "Transport"
SEAD_ESCORT = "SEAD Escort" SEAD_ESCORT = "SEAD Escort"
REFUELING = "Refueling"
def __str__(self) -> str: def __str__(self) -> str:
return self.value return self.value
@@ -140,8 +141,8 @@ class FlightWaypoint:
waypoint_type: The waypoint type. waypoint_type: The waypoint type.
x: X cooidinate of the waypoint. x: X cooidinate of the waypoint.
y: Y coordinate of the waypoint. y: Y coordinate of the waypoint.
alt: Altitude of the waypoint. By default this is MSL, but it can be alt: Altitude of the waypoint. By default this is AGL, but it can be
changed to AGL by setting alt_type to "RADIO" changed to MSL by setting alt_type to "RADIO".
""" """
self.waypoint_type = waypoint_type self.waypoint_type = waypoint_type
self.x = x self.x = x
@@ -153,7 +154,7 @@ class FlightWaypoint:
# Only used in the waypoint list in the flight edit page. No sense # Only used in the waypoint list in the flight edit page. No sense
# having three names. A short and long form is enough. # having three names. A short and long form is enough.
self.description = "" self.description = ""
self.targets: Sequence[Union[MissionTarget, Unit]] = [] self.targets: List[Union[MissionTarget, Unit]] = []
self.obj_name = "" self.obj_name = ""
self.pretty_name = "" self.pretty_name = ""
self.only_for_player = False self.only_for_player = False
@@ -298,7 +299,7 @@ class Flight:
return self.roster.player_count return self.roster.player_count
@property @property
def unit_type(self) -> AircraftType: def unit_type(self) -> Type[FlyingType]:
return self.squadron.aircraft return self.squadron.aircraft
@property @property
@@ -322,12 +323,14 @@ class Flight:
def clear_roster(self) -> None: def clear_roster(self) -> None:
self.roster.clear() self.roster.clear()
def __repr__(self) -> str: def __repr__(self):
name = db.unit_type_name(self.unit_type)
if self.custom_name: if self.custom_name:
return f"{self.custom_name} {self.count} x {self.unit_type}" return f"{self.custom_name} {self.count} x {name}"
return f"[{self.flight_type}] {self.count} x {self.unit_type}" return f"[{self.flight_type}] {self.count} x {name}"
def __str__(self) -> str: def __str__(self):
name = db.unit_get_expanded_info(self.country, self.unit_type, "name")
if self.custom_name: if self.custom_name:
return f"{self.custom_name} {self.count} x {self.unit_type}" return f"{self.custom_name} {self.count} x {name}"
return f"[{self.flight_type}] {self.count} x {self.unit_type}" return f"[{self.flight_type}] {self.count} x {name}"

View File

@@ -16,10 +16,12 @@ from functools import cached_property
from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple
from dcs.mapping import Point from dcs.mapping import Point
from dcs.planes import E_3A, E_2C, A_50, KJ_2000
from dcs.unit import Unit from dcs.unit import Unit
from shapely.geometry import Point as ShapelyPoint from shapely.geometry import Point as ShapelyPoint
from game.data.doctrine import Doctrine from game.data.doctrine import Doctrine
from game.squadrons import Pilot
from game.theater import ( from game.theater import (
Airfield, Airfield,
ControlPoint, ControlPoint,
@@ -27,10 +29,9 @@ from game.theater import (
MissionTarget, MissionTarget,
SamGroundObject, SamGroundObject,
TheaterGroundObject, TheaterGroundObject,
NavalControlPoint,
) )
from game.theater.theatergroundobject import EwrGroundObject, NavalGroundObject from game.theater.theatergroundobject import EwrGroundObject
from game.utils import Distance, Speed, feet, meters, nautical_miles, knots from game.utils import Distance, Speed, feet, meters, nautical_miles
from .closestairfields import ObjectiveDistanceCache from .closestairfields import ObjectiveDistanceCache
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
from .traveltime import GroundSpeed, TravelTime from .traveltime import GroundSpeed, TravelTime
@@ -219,7 +220,11 @@ class FlightPlan:
tot_waypoint = self.tot_waypoint tot_waypoint = self.tot_waypoint
if tot_waypoint is None: if tot_waypoint is None:
return None return None
return self.tot - self._travel_time_to_waypoint(tot_waypoint)
time = self.tot
if time is None:
return None
return time - self._travel_time_to_waypoint(tot_waypoint)
def startup_time(self) -> Optional[timedelta]: def startup_time(self) -> Optional[timedelta]:
takeoff_time = self.takeoff_time() takeoff_time = self.takeoff_time()
@@ -390,9 +395,6 @@ class PatrollingFlightPlan(FlightPlan):
#: Maximum time to remain on station. #: Maximum time to remain on station.
patrol_duration: timedelta patrol_duration: timedelta
#: Racetrack speed TAS.
patrol_speed: Speed
#: The engagement range of any Search Then Engage task, or the radius of a #: The engagement range of any Search Then Engage task, or the radius of a
#: Search Then Engage in Zone task. Any enemies of the appropriate type for #: Search Then Engage in Zone task. Any enemies of the appropriate type for
#: this mission within this range of the flight's current position (or the #: this mission within this range of the flight's current position (or the
@@ -767,25 +769,6 @@ class AwacsFlightPlan(LoiterFlightPlan):
return self.push_time return self.push_time
@dataclass(frozen=True)
class RefuelingFlightPlan(PatrollingFlightPlan):
takeoff: FlightWaypoint
land: FlightWaypoint
divert: Optional[FlightWaypoint]
bullseye: FlightWaypoint
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) @dataclass(frozen=True)
class AirliftFlightPlan(FlightPlan): class AirliftFlightPlan(FlightPlan):
takeoff: FlightWaypoint takeoff: FlightWaypoint
@@ -936,8 +919,6 @@ class FlightPlanBuilder:
return self.generate_aewc(flight) return self.generate_aewc(flight)
elif task == FlightType.TRANSPORT: elif task == FlightType.TRANSPORT:
return self.generate_transport(flight) return self.generate_transport(flight)
elif task == FlightType.REFUELING:
return self.generate_refueling_racetrack(flight)
raise PlanningError(f"{task} flight plan generation not implemented") raise PlanningError(f"{task} flight plan generation not implemented")
def regenerate_package_waypoints(self) -> None: def regenerate_package_waypoints(self) -> None:
@@ -1078,28 +1059,35 @@ class FlightPlanBuilder:
orbit_location = self.aewc_orbit(location) orbit_location = self.aewc_orbit(location)
if flight.unit_type.patrol_altitude is not None: # As high as possible to maximize detection and on-station time.
patrol_alt = flight.unit_type.patrol_altitude 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: else:
patrol_alt = feet(25000) patrol_alt = feet(25000)
builder = WaypointBuilder(flight, self.game, self.is_player) builder = WaypointBuilder(flight, self.game, self.is_player)
orbit = builder.orbit(orbit_location, patrol_alt) orbit_location = builder.orbit(orbit_location, patrol_alt)
return AwacsFlightPlan( return AwacsFlightPlan(
package=self.package, package=self.package,
flight=flight, flight=flight,
takeoff=builder.takeoff(flight.departure), takeoff=builder.takeoff(flight.departure),
nav_to=builder.nav_path( nav_to=builder.nav_path(
flight.departure.position, orbit.position, patrol_alt flight.departure.position, orbit_location.position, patrol_alt
), ),
nav_from=builder.nav_path( nav_from=builder.nav_path(
orbit.position, flight.arrival.position, patrol_alt orbit_location.position, flight.arrival.position, patrol_alt
), ),
land=builder.land(flight.arrival), land=builder.land(flight.arrival),
divert=builder.divert(flight.divert), divert=builder.divert(flight.divert),
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
hold=orbit, hold=orbit_location,
hold_duration=timedelta(hours=4), hold_duration=timedelta(hours=4),
) )
@@ -1130,7 +1118,7 @@ class FlightPlanBuilder:
) )
@staticmethod @staticmethod
def anti_ship_targets_for_tgo(tgo: NavalGroundObject) -> List[StrikeTarget]: def anti_ship_targets_for_tgo(tgo: TheaterGroundObject) -> List[StrikeTarget]:
return [StrikeTarget(f"{g.name} at {tgo.name}", g) for g in tgo.groups] return [StrikeTarget(f"{g.name} at {tgo.name}", g) for g in tgo.groups]
def generate_anti_ship(self, flight: Flight) -> StrikeFlightPlan: def generate_anti_ship(self, flight: Flight) -> StrikeFlightPlan:
@@ -1143,9 +1131,12 @@ class FlightPlanBuilder:
from game.transfers import CargoShip from game.transfers import CargoShip
if isinstance(location, NavalControlPoint): if isinstance(location, ControlPoint):
targets = self.anti_ship_targets_for_tgo(location.find_main_tgo()) if not location.is_fleet:
elif isinstance(location, NavalGroundObject): 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) targets = self.anti_ship_targets_for_tgo(location)
elif isinstance(location, CargoShip): elif isinstance(location, CargoShip):
targets = [StrikeTarget(location.name, location)] targets = [StrikeTarget(location.name, location)]
@@ -1167,28 +1158,21 @@ class FlightPlanBuilder:
if isinstance(location, FrontLine): if isinstance(location, FrontLine):
raise InvalidObjectiveLocation(flight.flight_type, location) raise InvalidObjectiveLocation(flight.flight_type, location)
start_pos, end_pos = self.racetrack_for_objective(location, barcap=True) start, end = self.racetrack_for_objective(location, barcap=True)
patrol_alt = meters(
preferred_alt = flight.unit_type.preferred_patrol_altitude random.randint(
randomized_alt = preferred_alt + feet(random.randint(-2, 1) * 1000) int(self.doctrine.min_patrol_altitude.meters),
patrol_alt = max( int(self.doctrine.max_patrol_altitude.meters),
self.doctrine.min_patrol_altitude, )
min(self.doctrine.max_patrol_altitude, randomized_alt),
)
patrol_speed = flight.unit_type.preferred_patrol_speed(patrol_alt)
logging.debug(
f"BARCAP patrol speed for {flight.unit_type.name} at {patrol_alt.feet}ft: {patrol_speed.knots} KTAS"
) )
builder = WaypointBuilder(flight, self.game, self.is_player) builder = WaypointBuilder(flight, self.game, self.is_player)
start, end = builder.race_track(start_pos, end_pos, patrol_alt) start, end = builder.race_track(start, end, patrol_alt)
return BarCapFlightPlan( return BarCapFlightPlan(
package=self.package, package=self.package,
flight=flight, flight=flight,
patrol_duration=self.doctrine.cap_duration, patrol_duration=self.doctrine.cap_duration,
patrol_speed=patrol_speed,
engagement_distance=self.doctrine.cap_engagement_range, engagement_distance=self.doctrine.cap_engagement_range,
takeoff=builder.takeoff(flight.departure), takeoff=builder.takeoff(flight.departure),
nav_to=builder.nav_path( nav_to=builder.nav_path(
@@ -1214,12 +1198,10 @@ class FlightPlanBuilder:
target = self.package.target.position target = self.package.target.position
heading = self.package.waypoints.join.heading_between_point(target) heading = self.package.waypoints.join.heading_between_point(target)
start_pos = target.point_from_heading( start = target.point_from_heading(heading, -self.doctrine.sweep_distance.meters)
heading, -self.doctrine.sweep_distance.meters
)
builder = WaypointBuilder(flight, self.game, self.is_player) builder = WaypointBuilder(flight, self.game, self.is_player)
start, end = builder.sweep(start_pos, target, self.doctrine.ingress_altitude) start, end = builder.sweep(start, target, self.doctrine.ingress_altitude)
hold = builder.hold(self._hold_point(flight)) hold = builder.hold(self._hold_point(flight))
@@ -1413,15 +1395,11 @@ class FlightPlanBuilder:
""" """
location = self.package.target location = self.package.target
preferred_alt = flight.unit_type.preferred_patrol_altitude patrol_alt = meters(
randomized_alt = preferred_alt + feet(random.randint(-2, 1) * 1000) random.randint(
patrol_alt = max( int(self.doctrine.min_patrol_altitude.meters),
self.doctrine.min_patrol_altitude, int(self.doctrine.max_patrol_altitude.meters),
min(self.doctrine.max_patrol_altitude, randomized_alt), )
)
patrol_speed = flight.unit_type.preferred_patrol_speed(patrol_alt)
logging.debug(
f"TARCAP patrol speed for {flight.unit_type.name} at {patrol_alt.feet}ft: {patrol_speed.knots} KTAS"
) )
# Create points # Create points
@@ -1444,7 +1422,6 @@ class FlightPlanBuilder:
# requests an escort the CAP flight will remain on station for the # requests an escort the CAP flight will remain on station for the
# duration of the escorted mission, or until it is winchester/bingo. # duration of the escorted mission, or until it is winchester/bingo.
patrol_duration=self.doctrine.cap_duration, patrol_duration=self.doctrine.cap_duration,
patrol_speed=patrol_speed,
engagement_distance=self.doctrine.cap_engagement_range, engagement_distance=self.doctrine.cap_engagement_range,
takeoff=builder.takeoff(flight.departure), takeoff=builder.takeoff(flight.departure),
nav_to=builder.nav_path(flight.departure.position, orbit0p, patrol_alt), nav_to=builder.nav_path(flight.departure.position, orbit0p, patrol_alt),
@@ -1613,33 +1590,16 @@ class FlightPlanBuilder:
builder = WaypointBuilder(flight, self.game, self.is_player) builder = WaypointBuilder(flight, self.game, self.is_player)
# 2021-08-02: patrol_speed will currently have no effect because
# CAS doesn't use OrbitAction. But all PatrollingFlightPlan are expected
# to have patrol_speed
is_helo = flight.unit_type.dcs_unit_type.helicopter
ingress_egress_altitude = (
self.doctrine.ingress_altitude if not is_helo else meters(50)
)
patrol_speed = flight.unit_type.preferred_patrol_speed(ingress_egress_altitude)
use_agl_ingress_egress = is_helo
return CasFlightPlan( return CasFlightPlan(
package=self.package, package=self.package,
flight=flight, flight=flight,
patrol_duration=self.doctrine.cas_duration, patrol_duration=self.doctrine.cas_duration,
patrol_speed=patrol_speed,
takeoff=builder.takeoff(flight.departure), takeoff=builder.takeoff(flight.departure),
nav_to=builder.nav_path( nav_to=builder.nav_path(
flight.departure.position, flight.departure.position, ingress, self.doctrine.ingress_altitude
ingress,
ingress_egress_altitude,
use_agl_ingress_egress,
), ),
nav_from=builder.nav_path( nav_from=builder.nav_path(
egress, egress, flight.arrival.position, self.doctrine.ingress_altitude
flight.arrival.position,
ingress_egress_altitude,
use_agl_ingress_egress,
), ),
patrol_start=builder.ingress( patrol_start=builder.ingress(
FlightWaypointType.INGRESS_CAS, ingress, location FlightWaypointType.INGRESS_CAS, ingress, location
@@ -1652,75 +1612,6 @@ class FlightPlanBuilder:
bullseye=builder.bullseye(), 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)
# TODO: Could use flight.unit_type.preferred_patrol_speed(altitude) instead.
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 @staticmethod
def target_waypoint( def target_waypoint(
flight: Flight, builder: WaypointBuilder, target: StrikeTarget flight: Flight, builder: WaypointBuilder, target: StrikeTarget
@@ -1897,23 +1788,23 @@ class FlightPlanBuilder:
return self._retreating_rendezvous_point(attack_transition) return self._retreating_rendezvous_point(attack_transition)
return self._advancing_rendezvous_point(attack_transition) return self._advancing_rendezvous_point(attack_transition)
def _ingress_point(self, heading: float) -> Point: def _ingress_point(self, heading: int) -> Point:
return self.package.target.position.point_from_heading( return self.package.target.position.point_from_heading(
heading - 180 + 15, self.doctrine.ingress_egress_distance.meters heading - 180 + 15, self.doctrine.ingress_egress_distance.meters
) )
def _egress_point(self, heading: float) -> Point: def _egress_point(self, heading: int) -> Point:
return self.package.target.position.point_from_heading( return self.package.target.position.point_from_heading(
heading - 180 - 15, self.doctrine.ingress_egress_distance.meters heading - 180 - 15, self.doctrine.ingress_egress_distance.meters
) )
def _target_heading_to_package_airfield(self) -> float: def _target_heading_to_package_airfield(self) -> int:
return self._heading_to_package_airfield(self.package.target.position) return self._heading_to_package_airfield(self.package.target.position)
def _heading_to_package_airfield(self, point: Point) -> float: def _heading_to_package_airfield(self, point: Point) -> int:
return self.package_airfield().position.heading_between_point(point) return self.package_airfield().position.heading_between_point(point)
def _distance_to_package_airfield(self, point: Point) -> float: def _distance_to_package_airfield(self, point: Point) -> int:
return self.package_airfield().position.distance_to_point(point) return self.package_airfield().position.distance_to_point(point)
def package_airfield(self) -> ControlPoint: def package_airfield(self) -> ControlPoint:

View File

@@ -1,10 +1,11 @@
from __future__ import annotations from __future__ import annotations
import datetime 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.data.weapons import Weapon, Pylon
from game.dcs.aircrafttype import AircraftType
if TYPE_CHECKING: if TYPE_CHECKING:
from gen.flights.flight import Flight from gen.flights.flight import Flight
@@ -19,18 +20,16 @@ class Loadout:
is_custom: bool = False, is_custom: bool = False,
) -> None: ) -> None:
self.name = name self.name = name
# We clear unused pylon entries on initialization, but UI actions can still self.pylons = {k: v for k, v in pylons.items() if v is not None}
# cause a pylon to be emptied, so make the optional type explicit.
self.pylons: Mapping[int, Optional[Weapon]] = {
k: v for k, v in pylons.items() if v is not None
}
self.date = date self.date = date
self.is_custom = is_custom self.is_custom = is_custom
def derive_custom(self, name: str) -> Loadout: def derive_custom(self, name: str) -> Loadout:
return Loadout(name, self.pylons, self.date, is_custom=True) 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: if self.date is not None and self.date <= date:
return Loadout(self.name, self.pylons, self.date) return Loadout(self.name, self.pylons, self.date)
@@ -62,7 +61,7 @@ class Loadout:
# {"CLSID": class ID, "num": pylon number} # {"CLSID": class ID, "num": pylon number}
# "tasks": List (as a dict) of task IDs the payload is used by. # "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(): for payload in payloads.values():
name = payload["name"] name = payload["name"]
pylons = payload["pylons"] pylons = payload["pylons"]
@@ -96,7 +95,6 @@ class Loadout:
FlightType.CAS: ("CAS MAVERICK F", "CAS"), FlightType.CAS: ("CAS MAVERICK F", "CAS"),
FlightType.STRIKE: ("STRIKE",), FlightType.STRIKE: ("STRIKE",),
FlightType.ANTISHIP: ("ANTISHIP",), FlightType.ANTISHIP: ("ANTISHIP",),
FlightType.DEAD: ("DEAD",),
FlightType.SEAD: ("SEAD",), FlightType.SEAD: ("SEAD",),
FlightType.BAI: ("BAI",), FlightType.BAI: ("BAI",),
FlightType.OCA_RUNWAY: ("RUNWAY_ATTACK", "RUNWAY_STRIKE"), FlightType.OCA_RUNWAY: ("RUNWAY_ATTACK", "RUNWAY_STRIKE"),
@@ -128,8 +126,8 @@ class Loadout:
for name in cls.default_loadout_names_for(flight): for name in cls.default_loadout_names_for(flight):
# This operation is cached, but must be called before load_by_name will # This operation is cached, but must be called before load_by_name will
# work. # work.
flight.unit_type.dcs_unit_type.load_payloads() flight.unit_type.load_payloads()
payload = flight.unit_type.dcs_unit_type.loadout_by_name(name) payload = flight.unit_type.loadout_by_name(name)
if payload is not None: if payload is not None:
return Loadout( return Loadout(
name, name,
@@ -138,8 +136,4 @@ class Loadout:
) )
# TODO: Try group.load_task_default_loadout(loadout_for_task) # TODO: Try group.load_task_default_loadout(loadout_for_task)
return cls.empty_loadout()
@classmethod
def empty_loadout(cls) -> Loadout:
return Loadout("Empty", {}, date=None) return Loadout("Empty", {}, date=None)

View File

@@ -25,13 +25,16 @@ if TYPE_CHECKING:
class GroundSpeed: class GroundSpeed:
@classmethod @classmethod
def for_flight(cls, flight: Flight, altitude: Distance) -> Speed: def for_flight(cls, flight: Flight, altitude: Distance) -> Speed:
if not issubclass(flight.unit_type, FlyingType):
raise TypeError("Flight has non-flying unit")
# TODO: Expose both a cruise speed and target speed. # TODO: Expose both a cruise speed and target speed.
# The cruise speed can be used for ascent, hold, join, and RTB to save # 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 # on fuel, but mission speed will be fast enough to keep the flight
# safer. # safer.
# DCS's max speed is in kph at 0 MSL. # 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: if max_speed > SPEED_OF_SOUND_AT_SEA_LEVEL:
# Aircraft is supersonic. Limit to mach 0.85 to conserve fuel and # Aircraft is supersonic. Limit to mach 0.85 to conserve fuel and
# account for heavily loaded jets. # account for heavily loaded jets.

View File

@@ -1,5 +1,4 @@
from __future__ import annotations from __future__ import annotations
import logging
import random import random
from dataclasses import dataclass from dataclasses import dataclass
@@ -11,12 +10,11 @@ from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Tuple, Tuple,
Union, Union,
Any,
) )
from dcs.mapping import Point from dcs.mapping import Point
from dcs.unit import Unit from dcs.unit import Unit
from dcs.unitgroup import Group, VehicleGroup, ShipGroup from dcs.unitgroup import Group, VehicleGroup
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
@@ -35,9 +33,7 @@ from .flight import Flight, FlightWaypoint, FlightWaypointType
@dataclass(frozen=True) @dataclass(frozen=True)
class StrikeTarget: class StrikeTarget:
name: str name: str
target: Union[ target: Union[VehicleGroup, TheaterGroundObject, Unit, Group, MultiGroupTransport]
VehicleGroup, TheaterGroundObject[Any], Unit, ShipGroup, MultiGroupTransport
]
class WaypointBuilder: class WaypointBuilder:
@@ -58,7 +54,7 @@ class WaypointBuilder:
@property @property
def is_helo(self) -> bool: def is_helo(self) -> bool:
return self.flight.unit_type.dcs_unit_type.helicopter return getattr(self.flight.unit_type, "helicopter", False)
def takeoff(self, departure: ControlPoint) -> FlightWaypoint: def takeoff(self, departure: ControlPoint) -> FlightWaypoint:
"""Create takeoff waypoint for the given arrival airfield or carrier. """Create takeoff waypoint for the given arrival airfield or carrier.
@@ -445,7 +441,7 @@ class WaypointBuilder:
# description in gen.aircraft.JoinPointBuilder), so instead we give # description in gen.aircraft.JoinPointBuilder), so instead we give
# the escort flights a flight plan including the ingress point, target # the escort flights a flight plan including the ingress point, target
# area, and egress point. # area, and egress point.
ingress_wp = self.ingress(FlightWaypointType.INGRESS_ESCORT, ingress, target) ingress = self.ingress(FlightWaypointType.INGRESS_ESCORT, ingress, target)
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
FlightWaypointType.TARGET_GROUP_LOC, FlightWaypointType.TARGET_GROUP_LOC,
@@ -459,8 +455,8 @@ class WaypointBuilder:
waypoint.description = "Escort the package" waypoint.description = "Escort the package"
waypoint.pretty_name = "Target area" waypoint.pretty_name = "Target area"
egress_wp = self.egress(egress, target) egress = self.egress(egress, target)
return ingress_wp, waypoint, egress_wp return ingress, waypoint, egress
@staticmethod @staticmethod
def pickup(control_point: ControlPoint) -> FlightWaypoint: def pickup(control_point: ControlPoint) -> FlightWaypoint:

View File

@@ -32,8 +32,6 @@ class ForcedOptionsGenerator:
self.mission.forced_options.labels = ForcedOptions.Labels.Abbreviate self.mission.forced_options.labels = ForcedOptions.Labels.Abbreviate
elif self.game.settings.labels == "Dot Only": elif self.game.settings.labels == "Dot Only":
self.mission.forced_options.labels = ForcedOptions.Labels.DotOnly 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": elif self.game.settings.labels == "Off":
self.mission.forced_options.labels = ForcedOptions.Labels.None_ self.mission.forced_options.labels = ForcedOptions.Labels.None_
@@ -43,7 +41,7 @@ class ForcedOptionsGenerator:
if blue.unrestricted_satnav or red.unrestricted_satnav: if blue.unrestricted_satnav or red.unrestricted_satnav:
self.mission.forced_options.unrestricted_satnav = True self.mission.forced_options.unrestricted_satnav = True
def generate(self) -> None: def generate(self):
self._set_options_view() self._set_options_view()
self._set_external_views() self._set_external_views()
self._set_labels() self._set_labels()

View File

@@ -1,18 +1,15 @@
from __future__ import annotations
import logging import logging
import random import random
from enum import Enum from enum import Enum
from typing import Dict, List, TYPE_CHECKING from typing import Dict, List
from dcs.unittype import VehicleType
from game.theater import ControlPoint
from game.data.groundunitclass import GroundUnitClass from game.data.groundunitclass import GroundUnitClass
from game.dcs.groundunittype import GroundUnitType
from game.theater import ControlPoint
from gen.ground_forces.combat_stance import CombatStance from gen.ground_forces.combat_stance import CombatStance
if TYPE_CHECKING:
from game import Game
MAX_COMBAT_GROUP_PER_CP = 10 MAX_COMBAT_GROUP_PER_CP = 10
@@ -51,23 +48,22 @@ GROUP_SIZES_BY_COMBAT_STANCE = {
class CombatGroup: class CombatGroup:
def __init__( def __init__(self, role: CombatGroupRole):
self, role: CombatGroupRole, unit_type: GroundUnitType, size: int self.units: List[VehicleType] = []
) -> None:
self.unit_type = unit_type
self.size = size
self.role = role self.role = role
self.assigned_enemy_cp = None
self.start_position = None self.start_position = None
def __str__(self) -> str: def __str__(self):
s = f"ROLE : {self.role}\n" s = ""
if self.size: s += "ROLE : " + str(self.role) + "\n"
s += f"UNITS {self.unit_type} * {self.size}" if len(self.units) > 0:
s += "UNITS " + self.units[0].name + " * " + str(len(self.units))
return s return s
class GroundPlanner: class GroundPlanner:
def __init__(self, cp: ControlPoint, game: Game) -> None: def __init__(self, cp: ControlPoint, game):
self.cp = cp self.cp = cp
self.game = game self.game = game
self.connected_enemy_cp = [ self.connected_enemy_cp = [
@@ -87,41 +83,42 @@ class GroundPlanner:
self.units_per_cp[cp.id] = [] self.units_per_cp[cp.id] = []
self.reserve: List[CombatGroup] = [] self.reserve: List[CombatGroup] = []
def plan_groundwar(self) -> None: def plan_groundwar(self):
ground_unit_limit = self.cp.frontline_unit_count_limit ground_unit_limit = self.cp.frontline_unit_count_limit
remaining_available_frontline_units = ground_unit_limit remaining_available_frontline_units = ground_unit_limit
# TODO: Fix to handle the per-front stances. if hasattr(self.cp, "stance"):
# https://github.com/dcs-liberation/dcs_liberation/issues/1417 group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[self.cp.stance]
group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[CombatStance.DEFENSIVE] else:
self.cp.stance = CombatStance.DEFENSIVE
group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[CombatStance.DEFENSIVE]
# Create combat groups and assign them randomly to each enemy CP # Create combat groups and assign them randomly to each enemy CP
for unit_type in self.cp.base.armor: for unit_type in self.cp.base.armor:
unit_class = unit_type.unit_class if unit_type in GroundUnitClass.Tank:
if unit_class is GroundUnitClass.Tank:
collection = self.tank_groups collection = self.tank_groups
role = CombatGroupRole.TANK role = CombatGroupRole.TANK
elif unit_class is GroundUnitClass.Apc: elif unit_type in GroundUnitClass.Apc:
collection = self.apc_group collection = self.apc_group
role = CombatGroupRole.APC role = CombatGroupRole.APC
elif unit_class is GroundUnitClass.Artillery: elif unit_type in GroundUnitClass.Artillery:
collection = self.art_group collection = self.art_group
role = CombatGroupRole.ARTILLERY role = CombatGroupRole.ARTILLERY
elif unit_class is GroundUnitClass.Ifv: elif unit_type in GroundUnitClass.Ifv:
collection = self.ifv_group collection = self.ifv_group
role = CombatGroupRole.IFV role = CombatGroupRole.IFV
elif unit_class is GroundUnitClass.Logistics: elif unit_type in GroundUnitClass.Logistics:
collection = self.logi_groups collection = self.logi_groups
role = CombatGroupRole.LOGI role = CombatGroupRole.LOGI
elif unit_class is GroundUnitClass.Atgm: elif unit_type in GroundUnitClass.Atgm:
collection = self.atgm_group collection = self.atgm_group
role = CombatGroupRole.ATGM role = CombatGroupRole.ATGM
elif unit_class is GroundUnitClass.Shorads: elif unit_type in GroundUnitClass.Shorads:
collection = self.shorad_groups collection = self.shorad_groups
role = CombatGroupRole.SHORAD role = CombatGroupRole.SHORAD
elif unit_class is GroundUnitClass.Recon: elif unit_type in GroundUnitClass.Recon:
collection = self.recon_groups collection = self.recon_groups
role = CombatGroupRole.RECON role = CombatGroupRole.RECON
else: else:
@@ -140,23 +137,37 @@ class GroundPlanner:
while available > 0: while available > 0:
if role == CombatGroupRole.SHORAD: if role == CombatGroupRole.SHORAD:
count = 1 n = 1
else: else:
count = random.choice(group_size_choice) n = random.choice(group_size_choice)
if count > available: if n > available:
if available >= 2: if available >= 2:
count = 2 n = 2
else: else:
count = 1 n = 1
available -= count available -= n
group = CombatGroup(role, unit_type, count) group = CombatGroup(role)
if len(self.connected_enemy_cp) > 0: if len(self.connected_enemy_cp) > 0:
enemy_cp = random.choice(self.connected_enemy_cp).id enemy_cp = random.choice(self.connected_enemy_cp).id
self.units_per_cp[enemy_cp].append(group) self.units_per_cp[enemy_cp].append(group)
group.assigned_enemy_cp = enemy_cp
else: else:
self.reserve.append(group) self.reserve.append(group)
group.assigned_enemy_cp = "__reserve__"
for i in range(n):
group.units.append(unit_type)
collection.append(group) collection.append(group)
if remaining_available_frontline_units == 0: if remaining_available_frontline_units == 0:
break break
print("------------------")
print("Ground Planner : ")
print(self.cp.name)
print("------------------")
for unit_type in self.units_per_cp.keys():
print("For : #" + str(unit_type))
for group in self.units_per_cp[unit_type]:
print(str(group))

View File

@@ -9,23 +9,12 @@ from __future__ import annotations
import logging import logging
import random import random
from typing import ( from typing import Dict, Iterator, Optional, TYPE_CHECKING, Type, List
Dict,
Iterator,
Optional,
TYPE_CHECKING,
Type,
List,
TypeVar,
Any,
Generic,
Union,
)
from dcs import Mission, Point, unitgroup from dcs import Mission, Point, unitgroup
from dcs.action import SceneryDestructionZone from dcs.action import SceneryDestructionZone
from dcs.country import Country from dcs.country import Country
from dcs.point import StaticPoint, MovingPoint from dcs.point import StaticPoint
from dcs.statics import Fortification, fortification_map, warehouse_map from dcs.statics import Fortification, fortification_map, warehouse_map
from dcs.task import ( from dcs.task import (
ActivateBeaconCommand, ActivateBeaconCommand,
@@ -37,12 +26,12 @@ from dcs.task import (
from dcs.triggers import TriggerStart, TriggerZone from dcs.triggers import TriggerStart, TriggerZone
from dcs.unit import Ship, Unit, Vehicle, SingleHeliPad from dcs.unit import Ship, Unit, Vehicle, SingleHeliPad
from dcs.unitgroup import Group, ShipGroup, StaticGroup, VehicleGroup from dcs.unitgroup import Group, ShipGroup, StaticGroup, VehicleGroup
from dcs.unittype import StaticType, UnitType, ShipType, VehicleType from dcs.unittype import StaticType, UnitType
from dcs.vehicles import vehicle_map from dcs.vehicles import vehicle_map
from game import db from game import db
from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID
from game.db import unit_type_from_name, ship_type_from_name, vehicle_type_from_name from game.db import unit_type_from_name
from game.theater import ControlPoint, TheaterGroundObject from game.theater import ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import ( from game.theater.theatergroundobject import (
BuildingGroundObject, BuildingGroundObject,
@@ -67,10 +56,7 @@ FARP_FRONTLINE_DISTANCE = 10000
AA_CP_MIN_DISTANCE = 40000 AA_CP_MIN_DISTANCE = 40000
TgoT = TypeVar("TgoT", bound=TheaterGroundObject[Any]) class GenericGroundObjectGenerator:
class GenericGroundObjectGenerator(Generic[TgoT]):
"""An unspecialized ground object generator. """An unspecialized ground object generator.
Currently used only for SAM Currently used only for SAM
@@ -78,7 +64,7 @@ class GenericGroundObjectGenerator(Generic[TgoT]):
def __init__( def __init__(
self, self,
ground_object: TgoT, ground_object: TheaterGroundObject,
country: Country, country: Country,
game: Game, game: Game,
mission: Mission, mission: Mission,
@@ -103,7 +89,10 @@ class GenericGroundObjectGenerator(Generic[TgoT]):
logging.warning(f"Found empty group in {self.ground_object}") logging.warning(f"Found empty group in {self.ground_object}")
continue continue
unit_type = vehicle_type_from_name(group.units[0].type) unit_type = unit_type_from_name(group.units[0].type)
if unit_type is None:
raise RuntimeError(f"Unrecognized unit type: {group.units[0].type}")
vg = self.m.vehicle_group( vg = self.m.vehicle_group(
self.country, self.country,
group.name, group.name,
@@ -127,27 +116,24 @@ class GenericGroundObjectGenerator(Generic[TgoT]):
self._register_unit_group(group, vg) self._register_unit_group(group, vg)
@staticmethod @staticmethod
def enable_eplrs(group: VehicleGroup, unit_type: Type[VehicleType]) -> None: def enable_eplrs(group: Group, unit_type: Type[UnitType]) -> None:
if unit_type.eplrs: if hasattr(unit_type, "eplrs"):
group.points[0].tasks.append(EPLRS(group.id)) if unit_type.eplrs:
group.points[0].tasks.append(EPLRS(group.id))
def set_alarm_state(self, group: Union[ShipGroup, VehicleGroup]) -> None: def set_alarm_state(self, group: Group) -> None:
if self.game.settings.perf_red_alert_state: if self.game.settings.perf_red_alert_state:
group.points[0].tasks.append(OptAlarmState(2)) group.points[0].tasks.append(OptAlarmState(2))
else: else:
group.points[0].tasks.append(OptAlarmState(1)) group.points[0].tasks.append(OptAlarmState(1))
def _register_unit_group( def _register_unit_group(self, persistence_group: Group, miz_group: Group) -> None:
self,
persistence_group: Union[ShipGroup, VehicleGroup],
miz_group: Union[ShipGroup, VehicleGroup],
) -> None:
self.unit_map.add_ground_object_units( self.unit_map.add_ground_object_units(
self.ground_object, persistence_group, miz_group self.ground_object, persistence_group, miz_group
) )
class MissileSiteGenerator(GenericGroundObjectGenerator[MissileSiteGroundObject]): class MissileSiteGenerator(GenericGroundObjectGenerator):
@property @property
def culled(self) -> bool: def culled(self) -> bool:
# Don't cull missile sites - their range is long enough to make them easily # Don't cull missile sites - their range is long enough to make them easily
@@ -162,7 +148,7 @@ class MissileSiteGenerator(GenericGroundObjectGenerator[MissileSiteGroundObject]
for group in self.ground_object.groups: for group in self.ground_object.groups:
vg = self.m.find_group(group.name) vg = self.m.find_group(group.name)
if vg is not None: if vg is not None:
targets = self.possible_missile_targets() targets = self.possible_missile_targets(vg)
if targets: if targets:
target = random.choice(targets) target = random.choice(targets)
real_target = target.point_from_heading( real_target = target.point_from_heading(
@@ -179,7 +165,7 @@ class MissileSiteGenerator(GenericGroundObjectGenerator[MissileSiteGroundObject]
"Couldn't setup missile site to fire, group was not generated." "Couldn't setup missile site to fire, group was not generated."
) )
def possible_missile_targets(self) -> List[Point]: def possible_missile_targets(self, vg: Group) -> List[Point]:
""" """
Find enemy control points in range Find enemy control points in range
:param vg: Vehicle group we are searching a target for (There is always only oe group right now) :param vg: Vehicle group we are searching a target for (There is always only oe group right now)
@@ -188,7 +174,7 @@ class MissileSiteGenerator(GenericGroundObjectGenerator[MissileSiteGroundObject]
targets: List[Point] = [] targets: List[Point] = []
for cp in self.game.theater.controlpoints: for cp in self.game.theater.controlpoints:
if cp.captured != self.ground_object.control_point.captured: if cp.captured != self.ground_object.control_point.captured:
distance = cp.position.distance_to_point(self.ground_object.position) distance = cp.position.distance_to_point(vg.position)
if distance < self.missile_site_range: if distance < self.missile_site_range:
targets.append(cp.position) targets.append(cp.position)
return targets return targets
@@ -210,7 +196,7 @@ class MissileSiteGenerator(GenericGroundObjectGenerator[MissileSiteGroundObject]
return site_range return site_range
class BuildingSiteGenerator(GenericGroundObjectGenerator[BuildingGroundObject]): class BuildingSiteGenerator(GenericGroundObjectGenerator):
"""Generator for building sites. """Generator for building sites.
Building sites are the primary type of non-airbase objective locations that Building sites are the primary type of non-airbase objective locations that
@@ -239,7 +225,7 @@ class BuildingSiteGenerator(GenericGroundObjectGenerator[BuildingGroundObject]):
f"{self.ground_object.dcs_identifier} not found in static maps" f"{self.ground_object.dcs_identifier} not found in static maps"
) )
def generate_vehicle_group(self, unit_type: Type[VehicleType]) -> None: def generate_vehicle_group(self, unit_type: Type[UnitType]) -> None:
if not self.ground_object.is_dead: if not self.ground_object.is_dead:
group = self.m.vehicle_group( group = self.m.vehicle_group(
country=self.country, country=self.country,
@@ -338,7 +324,7 @@ class SceneryGenerator(BuildingSiteGenerator):
self.unit_map.add_scenery(scenery) self.unit_map.add_scenery(scenery)
class GenericCarrierGenerator(GenericGroundObjectGenerator[GenericCarrierGroundObject]): class GenericCarrierGenerator(GenericGroundObjectGenerator):
"""Base type for carrier group generation. """Base type for carrier group generation.
Used by both CV(N) groups and LHA groups. Used by both CV(N) groups and LHA groups.
@@ -390,12 +376,13 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator[GenericCarrierGroundO
self.add_runway_data(brc or 0, atc, tacan, tacan_callsign, icls) self.add_runway_data(brc or 0, atc, tacan, tacan_callsign, icls)
self._register_unit_group(group, ship_group) self._register_unit_group(group, ship_group)
def get_carrier_type(self, group: ShipGroup) -> Type[ShipType]: def get_carrier_type(self, group: Group) -> Type[UnitType]:
return ship_type_from_name(group.units[0].type) unit_type = unit_type_from_name(group.units[0].type)
if unit_type is None:
raise RuntimeError(f"Unrecognized carrier name: {group.units[0].type}")
return unit_type
def configure_carrier( def configure_carrier(self, group: Group, atc_channel: RadioFrequency) -> ShipGroup:
self, group: ShipGroup, atc_channel: RadioFrequency
) -> ShipGroup:
unit_type = self.get_carrier_type(group) unit_type = self.get_carrier_type(group)
ship_group = self.m.ship_group( ship_group = self.m.ship_group(
@@ -487,7 +474,7 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator[GenericCarrierGroundO
class CarrierGenerator(GenericCarrierGenerator): class CarrierGenerator(GenericCarrierGenerator):
"""Generator for CV(N) groups.""" """Generator for CV(N) groups."""
def get_carrier_type(self, group: ShipGroup) -> Type[ShipType]: def get_carrier_type(self, group: Group) -> UnitType:
unit_type = super().get_carrier_type(group) unit_type = super().get_carrier_type(group)
if self.game.settings.supercarrier: if self.game.settings.supercarrier:
unit_type = db.upgrade_to_supercarrier(unit_type, self.control_point.name) unit_type = db.upgrade_to_supercarrier(unit_type, self.control_point.name)
@@ -531,7 +518,7 @@ class LhaGenerator(GenericCarrierGenerator):
) )
class ShipObjectGenerator(GenericGroundObjectGenerator[ShipGroundObject]): class ShipObjectGenerator(GenericGroundObjectGenerator):
"""Generator for non-carrier naval groups.""" """Generator for non-carrier naval groups."""
def generate(self) -> None: def generate(self) -> None:
@@ -542,11 +529,14 @@ class ShipObjectGenerator(GenericGroundObjectGenerator[ShipGroundObject]):
if not group.units: if not group.units:
logging.warning(f"Found empty group in {self.ground_object}") logging.warning(f"Found empty group in {self.ground_object}")
continue continue
self.generate_group(group, ship_type_from_name(group.units[0].type))
def generate_group( unit_type = unit_type_from_name(group.units[0].type)
self, group_def: ShipGroup, first_unit_type: Type[ShipType] if unit_type is None:
) -> None: raise RuntimeError(f"Unrecognized unit type: {group.units[0].type}")
self.generate_group(group, unit_type)
def generate_group(self, group_def: Group, first_unit_type: Type[UnitType]) -> None:
group = self.m.ship_group( group = self.m.ship_group(
self.country, self.country,
group_def.name, group_def.name,
@@ -634,7 +624,7 @@ class GroundObjectsGenerator:
self.icls_alloc = iter(range(1, 21)) self.icls_alloc = iter(range(1, 21))
self.runways: Dict[str, RunwayData] = {} self.runways: Dict[str, RunwayData] = {}
def generate(self) -> None: def generate(self):
for cp in self.game.theater.controlpoints: for cp in self.game.theater.controlpoints:
if cp.captured: if cp.captured:
country_name = self.game.player_country country_name = self.game.player_country
@@ -647,7 +637,6 @@ class GroundObjectsGenerator:
).generate() ).generate()
for ground_object in cp.ground_objects: for ground_object in cp.ground_objects:
generator: GenericGroundObjectGenerator[Any]
if isinstance(ground_object, FactoryGroundObject): if isinstance(ground_object, FactoryGroundObject):
generator = FactoryGenerator( generator = FactoryGenerator(
ground_object, country, self.game, self.m, self.unit_map ground_object, country, self.game, self.m, self.unit_map

View File

@@ -32,16 +32,15 @@ from typing import Dict, List, Optional, TYPE_CHECKING, Tuple, Iterator
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
from dcs.mission import Mission from dcs.mission import Mission
from dcs.unit import Unit from dcs.unit import Unit
from dcs.unittype import FlyingType
from tabulate import tabulate from tabulate import tabulate
from game.data.alic import AlicCodes from game.data.alic import AlicCodes
from game.db import unit_type_from_name from game.db import unit_type_from_name
from game.dcs.aircrafttype import AircraftType
from game.savecompat import has_save_compat_for
from game.theater import ConflictTheater, TheaterGroundObject, LatLon from game.theater import ConflictTheater, TheaterGroundObject, LatLon
from game.theater.bullseye import Bullseye from game.theater.bullseye import Bullseye
from game.utils import meters from game.utils import meters
from .aircraft import FlightData from .aircraft import AIRCRAFT_DATA, FlightData
from .airsupportgen import AwacsInfo, TankerInfo from .airsupportgen import AwacsInfo, TankerInfo
from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator
from .flights.flight import FlightWaypoint, FlightWaypointType, FlightType from .flights.flight import FlightWaypoint, FlightWaypointType, FlightType
@@ -64,8 +63,7 @@ class KneeboardPageWriter:
else: else:
self.foreground_fill = (15, 15, 15) self.foreground_fill = (15, 15, 15)
self.background_fill = (255, 252, 252) self.background_fill = (255, 252, 252)
self.image_size = (768, 1024) self.image = Image.new("RGB", (768, 1024), self.background_fill)
self.image = Image.new("RGB", self.image_size, self.background_fill)
# These font sizes create a relatively full page for current sorties. If # These font sizes create a relatively full page for current sorties. If
# we start generating more complicated flight plans, or start including # we start generating more complicated flight plans, or start including
# more information in the comm ladder (the latter of which we should # more information in the comm ladder (the latter of which we should
@@ -84,7 +82,6 @@ class KneeboardPageWriter:
"resources/fonts/Inconsolata.otf", 20, layout_engine=ImageFont.LAYOUT_BASIC "resources/fonts/Inconsolata.otf", 20, layout_engine=ImageFont.LAYOUT_BASIC
) )
self.draw = ImageDraw.Draw(self.image) self.draw = ImageDraw.Draw(self.image)
self.page_margin = page_margin
self.x = page_margin self.x = page_margin
self.y = page_margin self.y = page_margin
self.line_spacing = line_spacing self.line_spacing = line_spacing
@@ -94,24 +91,10 @@ class KneeboardPageWriter:
return self.x, self.y return self.x, self.y
def text( def text(
self, self, text: str, font=None, fill: Tuple[int, int, int] = (0, 0, 0)
text: str,
font: Optional[ImageFont.FreeTypeFont] = None,
fill: Optional[Tuple[int, int, int]] = None,
wrap: bool = False,
) -> None: ) -> None:
if font is None: if font is None:
font = self.content_font font = self.content_font
if fill is None:
fill = self.foreground_fill
if wrap:
text = "\n".join(
self.wrap_line_with_font(
line, self.image_size[0] - self.page_margin - self.x, font
)
for line in text.splitlines()
)
self.draw.text(self.position, text, font=font, fill=fill) self.draw.text(self.position, text, font=font, fill=fill)
width, height = self.draw.textsize(text, font=font) width, height = self.draw.textsize(text, font=font)
@@ -151,24 +134,6 @@ class KneeboardPageWriter:
output = combo output = combo
return "".join(segments + [output]).strip() return "".join(segments + [output]).strip()
@staticmethod
def wrap_line_with_font(
inputstr: str, max_width: int, font: ImageFont.FreeTypeFont
) -> str:
if font.getsize(inputstr)[0] <= max_width:
return inputstr
tokens = inputstr.split(" ")
output = ""
segments = []
for token in tokens:
combo = output + " " + token
if font.getsize(combo)[0] > max_width:
segments.append(output + "\n")
output = token
else:
output = combo
return "".join(segments + [output]).strip()
class KneeboardPage: class KneeboardPage:
"""Base class for all kneeboard pages.""" """Base class for all kneeboard pages."""
@@ -177,8 +142,7 @@ class KneeboardPage:
"""Writes the kneeboard page to the given path.""" """Writes the kneeboard page to the given path."""
raise NotImplementedError raise NotImplementedError
@staticmethod def format_ll(self, ll: LatLon) -> str:
def format_ll(ll: LatLon) -> str:
ns = "N" if ll.latitude >= 0 else "S" ns = "N" if ll.latitude >= 0 else "S"
ew = "E" if ll.longitude >= 0 else "W" ew = "E" if ll.longitude >= 0 else "W"
return f"{ll.latitude:.4}°{ns} {ll.longitude:.4}°{ew}" return f"{ll.latitude:.4}°{ns} {ll.longitude:.4}°{ew}"
@@ -391,9 +355,8 @@ class BriefingPage(KneeboardPage):
if channel is None: if channel is None:
return str(frequency) return str(frequency)
channel_name = self.flight.aircraft_type.channel_name( namer = AIRCRAFT_DATA[self.flight.aircraft_type.id].channel_namer
channel.radio_id, channel.channel channel_name = namer.channel_name(channel.radio_id, channel.channel)
)
return f"{channel_name}\n{frequency}" return f"{channel_name}\n{frequency}"
@@ -489,10 +452,9 @@ class SupportPage(KneeboardPage):
if channel is None: if channel is None:
return str(frequency) return str(frequency)
channel_name = self.flight.aircraft_type.channel_name( namer = AIRCRAFT_DATA[self.flight.aircraft_type.id].channel_namer
channel.radio_id, channel.channel channel_name = namer.channel_name(channel.radio_id, channel.channel)
) return f"{channel_name} {frequency}"
return f"{channel_name}\n{frequency}"
def _format_time(self, time: Optional[datetime.timedelta]) -> str: def _format_time(self, time: Optional[datetime.timedelta]) -> str:
if time is None: if time is None:
@@ -589,24 +551,6 @@ class StrikeTaskPage(KneeboardPage):
] ]
class NotesPage(KneeboardPage):
"""A kneeboard page containing the campaign owner's notes."""
def __init__(
self,
notes: str,
dark_kneeboard: bool,
) -> None:
self.notes = notes
self.dark_kneeboard = dark_kneeboard
def write(self, path: Path) -> None:
writer = KneeboardPageWriter(dark_theme=self.dark_kneeboard)
writer.title(f"Notes")
writer.text(self.notes, wrap=True)
writer.write(path)
class KneeboardGenerator(MissionInfoGenerator): class KneeboardGenerator(MissionInfoGenerator):
"""Creates kneeboard pages for each client flight in the mission.""" """Creates kneeboard pages for each client flight in the mission."""
@@ -621,14 +565,14 @@ class KneeboardGenerator(MissionInfoGenerator):
temp_dir = Path("kneeboards") temp_dir = Path("kneeboards")
temp_dir.mkdir(exist_ok=True) temp_dir.mkdir(exist_ok=True)
for aircraft, pages in self.pages_by_airframe().items(): 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) aircraft_dir.mkdir(exist_ok=True)
for idx, page in enumerate(pages): for idx, page in enumerate(pages):
page_path = aircraft_dir / f"page{idx:02}.png" page_path = aircraft_dir / f"page{idx:02}.png"
page.write(page_path) 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. """Returns a list of kneeboard pages per airframe in the mission.
Only client flights will be included, but because DCS does not support Only client flights will be included, but because DCS does not support
@@ -639,7 +583,7 @@ class KneeboardGenerator(MissionInfoGenerator):
A dict mapping aircraft types to the list of kneeboard pages for A dict mapping aircraft types to the list of kneeboard pages for
that aircraft. that aircraft.
""" """
all_flights: Dict[AircraftType, List[KneeboardPage]] = defaultdict(list) all_flights: Dict[FlyingType, List[KneeboardPage]] = defaultdict(list)
for flight in self.flights: for flight in self.flights:
if not flight.client_units: if not flight.client_units:
continue continue
@@ -655,7 +599,6 @@ class KneeboardGenerator(MissionInfoGenerator):
return StrikeTaskPage(flight, self.dark_kneeboard, self.game.theater) return StrikeTaskPage(flight, self.dark_kneeboard, self.game.theater)
return None return None
@has_save_compat_for(4)
def generate_flight_kneeboard(self, flight: FlightData) -> List[KneeboardPage]: def generate_flight_kneeboard(self, flight: FlightData) -> List[KneeboardPage]:
"""Returns a list of kneeboard pages for the given flight.""" """Returns a list of kneeboard pages for the given flight."""
pages: List[KneeboardPage] = [ pages: List[KneeboardPage] = [
@@ -677,10 +620,6 @@ class KneeboardGenerator(MissionInfoGenerator):
), ),
] ]
# Only create the notes page if there are notes to show.
if notes := getattr(self.game, "notes", ""):
pages.append(NotesPage(notes, self.dark_kneeboard))
if (target_page := self.generate_task_page(flight)) is not None: if (target_page := self.generate_task_page(flight)) is not None:
pages.append(target_page) pages.append(target_page)

View File

@@ -0,0 +1,22 @@
from dataclasses import dataclass, field
from typing import List
from gen.locations.preset_locations import PresetLocation
@dataclass
class PresetControlPointLocations:
"""A repository of preset locations for a given control point"""
# List of possible ashore locations to generate objects (Represented in miz file by an APC_AAV_7_Amphibious)
ashore_locations: List[PresetLocation] = field(default_factory=list)
# List of possible offshore locations to generate ship groups (Represented in miz file by an Oliver Hazard Perry)
offshore_locations: List[PresetLocation] = field(default_factory=list)
# Possible antiship missiles sites locations (Represented in miz file by Iranian Silkworm missiles)
antiship_locations: List[PresetLocation] = field(default_factory=list)
# List of possible powerplants locations (Represented in miz file by static Workshop A object, USA)
powerplant_locations: List[PresetLocation] = field(default_factory=list)

View File

@@ -0,0 +1,21 @@
from dataclasses import dataclass
from dcs import Point
@dataclass
class PresetLocation:
"""A preset location"""
position: Point
heading: int
id: str
def __str__(self):
return (
"-" * 10
+ "X: {}\n Y: {}\nHdg: {}°\nId: {}".format(
self.position.x, self.position.y, self.heading, self.id
)
+ "-" * 10
)

View File

@@ -1,20 +1,13 @@
import logging import logging
import random import random
from typing import Optional from game import db
from dcs.unitgroup import VehicleGroup
from game import db, Game
from game.theater.theatergroundobject import MissileSiteGroundObject
from gen.missiles.scud_site import ScudGenerator from gen.missiles.scud_site import ScudGenerator
from gen.missiles.v1_group import V1GroupGenerator from gen.missiles.v1_group import V1GroupGenerator
MISSILES_MAP = {"V1GroupGenerator": V1GroupGenerator, "ScudGenerator": ScudGenerator} MISSILES_MAP = {"V1GroupGenerator": V1GroupGenerator, "ScudGenerator": ScudGenerator}
def generate_missile_group( def generate_missile_group(game, ground_object, faction_name: str):
game: Game, ground_object: MissileSiteGroundObject, faction_name: str
) -> Optional[VehicleGroup]:
""" """
This generate a missiles group This generate a missiles group
:return: Nothing, but put the group reference inside the ground object :return: Nothing, but put the group reference inside the ground object

View File

@@ -2,38 +2,33 @@ import random
from dcs.vehicles import Unarmed, MissilesSS, AirDefence from dcs.vehicles import Unarmed, MissilesSS, AirDefence
from game import Game from gen.sam.group_generator import GroupGenerator
from game.factions.faction import Faction
from game.theater.theatergroundobject import MissileSiteGroundObject
from gen.sam.group_generator import VehicleGroupGenerator
class ScudGenerator(VehicleGroupGenerator[MissileSiteGroundObject]): class ScudGenerator(GroupGenerator):
def __init__( def __init__(self, game, ground_object, faction):
self, game: Game, ground_object: MissileSiteGroundObject, faction: Faction
) -> None:
super(ScudGenerator, self).__init__(game, ground_object) super(ScudGenerator, self).__init__(game, ground_object)
self.faction = faction self.faction = faction
def generate(self) -> None: def generate(self):
# Scuds # Scuds
self.add_unit( self.add_unit(
MissilesSS.Scud_B, MissilesSS.SSM_SS_1C_Scud_B,
"V1#0", "V1#0",
self.position.x, self.position.x,
self.position.y + random.randint(1, 8), self.position.y + random.randint(1, 8),
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
MissilesSS.Scud_B, MissilesSS.SSM_SS_1C_Scud_B,
"V1#1", "V1#1",
self.position.x + 50, self.position.x + 50,
self.position.y + random.randint(1, 8), self.position.y + random.randint(1, 8),
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
MissilesSS.Scud_B, MissilesSS.SSM_SS_1C_Scud_B,
"V1#2", "V1#2",
self.position.x + 100, self.position.x + 100,
self.position.y + random.randint(1, 8), self.position.y + random.randint(1, 8),
@@ -42,7 +37,7 @@ class ScudGenerator(VehicleGroupGenerator[MissileSiteGroundObject]):
# Commander # Commander
self.add_unit( self.add_unit(
Unarmed.UAZ_469, Unarmed.LUV_UAZ_469_Jeep,
"Kubel#0", "Kubel#0",
self.position.x - 35, self.position.x - 35,
self.position.y - 20, self.position.y - 20,
@@ -51,7 +46,7 @@ class ScudGenerator(VehicleGroupGenerator[MissileSiteGroundObject]):
# Shorad # Shorad
self.add_unit( self.add_unit(
AirDefence.ZSU_23_4_Shilka, AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
"SHILKA#0", "SHILKA#0",
self.position.x - 55, self.position.x - 55,
self.position.y - 38, self.position.y - 38,
@@ -59,7 +54,7 @@ class ScudGenerator(VehicleGroupGenerator[MissileSiteGroundObject]):
) )
self.add_unit( self.add_unit(
AirDefence.Strela_1_9P31, AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL,
"STRELA#0", "STRELA#0",
self.position.x + 200, self.position.x + 200,
self.position.y + 15, self.position.y + 15,

View File

@@ -2,38 +2,33 @@ import random
from dcs.vehicles import Unarmed, MissilesSS, AirDefence from dcs.vehicles import Unarmed, MissilesSS, AirDefence
from game import Game from gen.sam.group_generator import GroupGenerator
from game.factions.faction import Faction
from game.theater.theatergroundobject import MissileSiteGroundObject
from gen.sam.group_generator import VehicleGroupGenerator
class V1GroupGenerator(VehicleGroupGenerator[MissileSiteGroundObject]): class V1GroupGenerator(GroupGenerator):
def __init__( def __init__(self, game, ground_object, faction):
self, game: Game, ground_object: MissileSiteGroundObject, faction: Faction
) -> None:
super(V1GroupGenerator, self).__init__(game, ground_object) super(V1GroupGenerator, self).__init__(game, ground_object)
self.faction = faction self.faction = faction
def generate(self) -> None: def generate(self):
# Ramps # Ramps
self.add_unit( self.add_unit(
MissilesSS.V1_launcher, MissilesSS.SSM_V_1_Launcher,
"V1#0", "V1#0",
self.position.x, self.position.x,
self.position.y + random.randint(1, 8), self.position.y + random.randint(1, 8),
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
MissilesSS.V1_launcher, MissilesSS.SSM_V_1_Launcher,
"V1#1", "V1#1",
self.position.x + 50, self.position.x + 50,
self.position.y + random.randint(1, 8), self.position.y + random.randint(1, 8),
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
MissilesSS.V1_launcher, MissilesSS.SSM_V_1_Launcher,
"V1#2", "V1#2",
self.position.x + 100, self.position.x + 100,
self.position.y + random.randint(1, 8), self.position.y + random.randint(1, 8),
@@ -42,7 +37,7 @@ class V1GroupGenerator(VehicleGroupGenerator[MissileSiteGroundObject]):
# Commander # Commander
self.add_unit( self.add_unit(
Unarmed.Kubelwagen_82, Unarmed.LUV_Kubelwagen_82,
"Kubel#0", "Kubel#0",
self.position.x - 35, self.position.x - 35,
self.position.y - 20, self.position.y - 20,
@@ -50,7 +45,9 @@ class V1GroupGenerator(VehicleGroupGenerator[MissileSiteGroundObject]):
) )
# Self defense flak # 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( self.add_unit(
flak_unit, flak_unit,
@@ -61,7 +58,7 @@ class V1GroupGenerator(VehicleGroupGenerator[MissileSiteGroundObject]):
) )
self.add_unit( self.add_unit(
Unarmed.Blitz_36_6700A, Unarmed.Truck_Opel_Blitz,
"Blitz#0", "Blitz#0",
self.position.x + 200, self.position.x + 200,
self.position.y + 15, self.position.y + 15,

View File

@@ -1,11 +1,12 @@
import random import random
import time import time
from typing import List, Any from typing import List
from dcs.country import Country 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 from gen.flights.flight import Flight
ALPHA_MILITARY = [ ALPHA_MILITARY = [
@@ -256,7 +257,7 @@ class NameGenerator:
existing_alphas: List[str] = [] existing_alphas: List[str] = []
@classmethod @classmethod
def reset(cls) -> None: def reset(cls):
cls.number = 0 cls.number = 0
cls.infantry_number = 0 cls.infantry_number = 0
cls.convoy_number = 0 cls.convoy_number = 0
@@ -265,7 +266,7 @@ class NameGenerator:
cls.existing_alphas = [] cls.existing_alphas = []
@classmethod @classmethod
def reset_numbers(cls) -> None: def reset_numbers(cls):
cls.number = 0 cls.number = 0
cls.infantry_number = 0 cls.infantry_number = 0
cls.aircraft_number = 0 cls.aircraft_number = 0
@@ -273,9 +274,7 @@ class NameGenerator:
cls.cargo_ship_number = 0 cls.cargo_ship_number = 0
@classmethod @classmethod
def next_aircraft_name( def next_aircraft_name(cls, country: Country, parent_base_id: int, flight: Flight):
cls, country: Country, parent_base_id: int, flight: Flight
) -> str:
cls.aircraft_number += 1 cls.aircraft_number += 1
try: try:
if flight.custom_name: if flight.custom_name:
@@ -291,42 +290,42 @@ class NameGenerator:
country.id, country.id,
cls.aircraft_number, cls.aircraft_number,
parent_base_id, parent_base_id,
flight.unit_type.name, db.unit_type_name(flight.unit_type),
) )
@classmethod @classmethod
def next_unit_name( def next_unit_name(cls, country: Country, parent_base_id: int, unit_type: UnitType):
cls, country: Country, parent_base_id: int, unit_type: UnitType[Any]
) -> str:
cls.number += 1 cls.number += 1
return "unit|{}|{}|{}|{}|".format( 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 @classmethod
def next_infantry_name( def next_infantry_name(
cls, country: Country, parent_base_id: int, unit_type: UnitType[Any] cls, country: Country, parent_base_id: int, unit_type: UnitType
) -> str: ):
cls.infantry_number += 1 cls.infantry_number += 1
return "infantry|{}|{}|{}|{}|".format( return "infantry|{}|{}|{}|{}|".format(
country.id, country.id,
cls.infantry_number, cls.infantry_number,
parent_base_id, parent_base_id,
unit_type.name, db.unit_type_name(unit_type),
) )
@classmethod @classmethod
def next_awacs_name(cls, country: Country) -> str: def next_awacs_name(cls, country: Country):
cls.number += 1 cls.number += 1
return "awacs|{}|{}|0|".format(country.id, cls.number) return "awacs|{}|{}|0|".format(country.id, cls.number)
@classmethod @classmethod
def next_tanker_name(cls, country: Country, unit_type: AircraftType) -> str: def next_tanker_name(cls, country: Country, unit_type: UnitType):
cls.number += 1 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 @classmethod
def next_carrier_name(cls, country: Country) -> str: def next_carrier_name(cls, country: Country):
cls.number += 1 cls.number += 1
return "carrier|{}|{}|0|".format(country.id, cls.number) return "carrier|{}|{}|0|".format(country.id, cls.number)
@@ -341,7 +340,7 @@ class NameGenerator:
return f"Cargo Ship {cls.cargo_ship_number:03}" return f"Cargo Ship {cls.cargo_ship_number:03}"
@classmethod @classmethod
def random_objective_name(cls) -> str: def random_objective_name(cls):
if cls.animals: if cls.animals:
animal = random.choice(cls.animals) animal = random.choice(cls.animals)
cls.animals.remove(animal) cls.animals.remove(animal)

View File

@@ -15,7 +15,7 @@ class RadioFrequency:
#: The frequency in kilohertz. #: The frequency in kilohertz.
hertz: int hertz: int
def __str__(self) -> str: def __str__(self):
if self.hertz >= 1000000: if self.hertz >= 1000000:
return self.format("MHz", 1000000) return self.format("MHz", 1000000)
return self.format("kHz", 1000) return self.format("kHz", 1000)
@@ -153,7 +153,7 @@ def get_radio(name: str) -> Radio:
for radio in RADIOS: for radio in RADIOS:
if radio.name == name: if radio.name == name:
return radio return radio
raise KeyError(f"Unknown radio: {name}") raise KeyError
class RadioRegistry: class RadioRegistry:

View File

@@ -14,21 +14,25 @@ class BoforsGenerator(AirDefenseGroupGenerator):
""" """
name = "Bofors AAA" name = "Bofors AAA"
price = 75
def generate(self) -> None: def generate(self):
grid_x = random.randint(2, 3)
grid_y = random.randint(2, 3)
spacing = random.randint(10, 40)
index = 0 index = 0
for i in range(4): for i in range(grid_x):
spacing_x = random.randint(10, 40) for j in range(grid_y):
spacing_y = random.randint(10, 40) index = index + 1
index = index + 1 self.add_unit(
self.add_unit( AirDefence.AAA_Bofors_40mm,
AirDefence.Bofors40, "AAA#" + str(index),
"AAA#" + str(index), self.position.x + spacing * i,
self.position.x + spacing_x * i, self.position.y + spacing * j,
self.position.y + spacing_y * i, self.heading,
self.heading, )
)
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:

View File

@@ -8,12 +8,12 @@ from gen.sam.airdefensegroupgenerator import (
) )
GFLAK = [ GFLAK = [
AirDefence.Flak38, AirDefence.AAA_Flak_Vierling_38_Quad_20mm,
AirDefence.Flak18, AirDefence.AAA_8_8cm_Flak_18,
AirDefence.Flak36, AirDefence.AAA_8_8cm_Flak_36,
AirDefence.Flak37, AirDefence.AAA_8_8cm_Flak_37,
AirDefence.Flak41, AirDefence.AAA_8_8cm_Flak_41,
AirDefence.Flak30, AirDefence.AAA_Flak_38_20mm,
] ]
@@ -23,32 +23,37 @@ class FlakGenerator(AirDefenseGroupGenerator):
""" """
name = "Flak Site" name = "Flak Site"
price = 135
def generate(self):
grid_x = random.randint(2, 3)
grid_y = random.randint(2, 3)
spacing = random.randint(20, 35)
def generate(self) -> None:
index = 0 index = 0
mixed = random.choice([True, False]) mixed = random.choice([True, False])
unit_type = random.choice(GFLAK) unit_type = random.choice(GFLAK)
for i in range(4): for i in range(grid_x):
index = index + 1 for j in range(grid_y):
spacing_x = random.randint(10, 40) index = index + 1
spacing_y = random.randint(10, 40) self.add_unit(
self.add_unit( unit_type,
unit_type, "AAA#" + str(index),
"AAA#" + str(index), self.position.x + spacing * i + random.randint(1, 5),
self.position.x + spacing_x * i + random.randint(1, 5), self.position.y + spacing * j + random.randint(1, 5),
self.position.y + spacing_y * i + random.randint(1, 5), self.heading,
self.heading, )
)
if mixed: if mixed:
unit_type = random.choice(GFLAK) unit_type = random.choice(GFLAK)
# Search lights # Search lights
search_pos = self.get_circular_position(random.randint(2, 3), 80) search_pos = self.get_circular_position(random.randint(2, 3), 80)
for index, pos in enumerate(search_pos): for index, pos in enumerate(search_pos):
self.add_unit( self.add_unit(
AirDefence.Flakscheinwerfer_37, AirDefence.SL_Flakscheinwerfer_37,
"SearchLight#" + str(index), "SearchLight#" + str(index),
pos[0], pos[0],
pos[1], pos[1],
@@ -57,14 +62,14 @@ class FlakGenerator(AirDefenseGroupGenerator):
# Support # Support
self.add_unit( self.add_unit(
AirDefence.Maschinensatz_33, AirDefence.PU_Maschinensatz_33,
"MC33#", "MC33#",
self.position.x - 20, self.position.x - 20,
self.position.y - 20, self.position.y - 20,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
AirDefence.KDO_Mod40, AirDefence.AAA_SP_Kdo_G_40,
"KDO#", "KDO#",
self.position.x - 25, self.position.x - 25,
self.position.y - 20, self.position.y - 20,
@@ -73,7 +78,7 @@ class FlakGenerator(AirDefenseGroupGenerator):
# Commander # Commander
self.add_unit( self.add_unit(
Unarmed.Kubelwagen_82, Unarmed.LUV_Kubelwagen_82,
"Kubel#", "Kubel#",
self.position.x - 35, self.position.x - 35,
self.position.y - 20, self.position.y - 20,
@@ -81,12 +86,10 @@ class FlakGenerator(AirDefenseGroupGenerator):
) )
# Some Opel Blitz trucks # Some Opel Blitz trucks
index = 0 for i in range(int(max(1, grid_x / 2))):
for i in range(int(max(1, 2))): for j in range(int(max(1, grid_x / 2))):
for j in range(int(max(1, 2))):
index += 1
self.add_unit( self.add_unit(
Unarmed.Blitz_36_6700A, Unarmed.Truck_Opel_Blitz,
"BLITZ#" + str(index), "BLITZ#" + str(index),
self.position.x + 125 + 15 * i + random.randint(1, 5), self.position.x + 125 + 15 * i + random.randint(1, 5),
self.position.y + 15 * j + random.randint(1, 5), self.position.y + 15 * j + random.randint(1, 5),

View File

@@ -14,8 +14,9 @@ class Flak18Generator(AirDefenseGroupGenerator):
""" """
name = "WW2 Flak Site" name = "WW2 Flak Site"
price = 40
def generate(self) -> None: def generate(self):
spacing = random.randint(30, 60) spacing = random.randint(30, 60)
index = 0 index = 0
@@ -24,7 +25,7 @@ class Flak18Generator(AirDefenseGroupGenerator):
for j in range(2): for j in range(2):
index = index + 1 index = index + 1
self.add_unit( self.add_unit(
AirDefence.Flak18, AirDefence.AAA_8_8cm_Flak_18,
"AAA#" + str(index), "AAA#" + str(index),
self.position.x + spacing * i + random.randint(1, 5), self.position.x + spacing * i + random.randint(1, 5),
self.position.y + spacing * j + random.randint(1, 5), self.position.y + spacing * j + random.randint(1, 5),
@@ -33,7 +34,7 @@ class Flak18Generator(AirDefenseGroupGenerator):
# Add a commander truck # Add a commander truck
self.add_unit( self.add_unit(
Unarmed.Blitz_36_6700A, Unarmed.Truck_Opel_Blitz,
"Blitz#", "Blitz#",
self.position.x - 35, self.position.x - 35,
self.position.y - 20, self.position.y - 20,

View File

@@ -13,8 +13,12 @@ class KS19Generator(AirDefenseGroupGenerator):
""" """
name = "KS-19 AAA Site" name = "KS-19 AAA Site"
price = 98
def generate(self):
spacing = random.randint(10, 40)
def generate(self) -> None:
self.add_unit( self.add_unit(
highdigitsams.AAA_SON_9_Fire_Can, highdigitsams.AAA_SON_9_Fire_Can,
"TR", "TR",
@@ -24,17 +28,16 @@ class KS19Generator(AirDefenseGroupGenerator):
) )
index = 0 index = 0
for i in range(4): for i in range(3):
spacing_x = random.randint(10, 40) for j in range(3):
spacing_y = random.randint(10, 40) index = index + 1
index = index + 1 self.add_unit(
self.add_unit( highdigitsams.AAA_100mm_KS_19,
highdigitsams.AAA_100mm_KS_19, "AAA#" + str(index),
"AAA#" + str(index), self.position.x + spacing * i,
self.position.x + spacing_x * i, self.position.y + spacing * j,
self.position.y + spacing_y * i, self.heading,
self.heading, )
)
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:

View File

@@ -14,13 +14,14 @@ class AllyWW2FlakGenerator(AirDefenseGroupGenerator):
""" """
name = "WW2 Ally Flak Site" name = "WW2 Ally Flak Site"
price = 140
def generate(self) -> None: def generate(self):
positions = self.get_circular_position(4, launcher_distance=30, coverage=360) positions = self.get_circular_position(4, launcher_distance=30, coverage=360)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit( self.add_unit(
AirDefence.QF_37_AA, AirDefence.AAA_QF_3_7,
"AA#" + str(i), "AA#" + str(i),
position[0], position[0],
position[1], position[1],
@@ -30,7 +31,7 @@ class AllyWW2FlakGenerator(AirDefenseGroupGenerator):
positions = self.get_circular_position(8, launcher_distance=60, coverage=360) positions = self.get_circular_position(8, launcher_distance=60, coverage=360)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit( self.add_unit(
AirDefence.M1_37mm, AirDefence.AAA_M1_37mm,
"AA#" + str(4 + i), "AA#" + str(4 + i),
position[0], position[0],
position[1], position[1],
@@ -40,7 +41,7 @@ class AllyWW2FlakGenerator(AirDefenseGroupGenerator):
positions = self.get_circular_position(8, launcher_distance=90, coverage=360) positions = self.get_circular_position(8, launcher_distance=90, coverage=360)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit( self.add_unit(
AirDefence.M45_Quadmount, AirDefence.AAA_M45_Quadmount_HB_12_7mm,
"AA#" + str(12 + i), "AA#" + str(12 + i),
position[0], position[0],
position[1], position[1],
@@ -49,28 +50,28 @@ class AllyWW2FlakGenerator(AirDefenseGroupGenerator):
# Add a commander truck # Add a commander truck
self.add_unit( self.add_unit(
Unarmed.Willys_MB, Unarmed.Car_Willys_Jeep,
"CMD#1", "CMD#1",
self.position.x, self.position.x,
self.position.y - 20, self.position.y - 20,
random.randint(0, 360), random.randint(0, 360),
) )
self.add_unit( self.add_unit(
Unarmed.M30_CC, Unarmed.Carrier_M30_Cargo,
"LOG#1", "LOG#1",
self.position.x, self.position.x,
self.position.y + 20, self.position.y + 20,
random.randint(0, 360), random.randint(0, 360),
) )
self.add_unit( self.add_unit(
Unarmed.M4_Tractor, Unarmed.Tractor_M4_Hi_Speed,
"LOG#2", "LOG#2",
self.position.x + 20, self.position.x + 20,
self.position.y, self.position.y,
random.randint(0, 360), random.randint(0, 360),
) )
self.add_unit( self.add_unit(
Unarmed.Bedford_MWD, Unarmed.Truck_Bedford,
"LOG#3", "LOG#3",
self.position.x - 20, self.position.x - 20,
self.position.y, self.position.y,

View File

@@ -12,15 +12,16 @@ class ZSU57Generator(AirDefenseGroupGenerator):
""" """
name = "ZSU-57-2 Group" name = "ZSU-57-2 Group"
price = 60
def generate(self) -> None: def generate(self):
num_launchers = 4 num_launchers = 5
positions = self.get_circular_position( positions = self.get_circular_position(
num_launchers, launcher_distance=110, coverage=360 num_launchers, launcher_distance=110, coverage=360
) )
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit( self.add_unit(
AirDefence.ZSU_57_2, AirDefence.SPAAA_ZSU_57_2,
"SPAA#" + str(i), "SPAA#" + str(i),
position[0], position[0],
position[1], position[1],

View File

@@ -14,20 +14,25 @@ class ZU23InsurgentGenerator(AirDefenseGroupGenerator):
""" """
name = "Zu-23 Site" name = "Zu-23 Site"
price = 56
def generate(self):
grid_x = random.randint(2, 3)
grid_y = random.randint(2, 3)
spacing = random.randint(10, 40)
def generate(self) -> None:
index = 0 index = 0
for i in range(4): for i in range(grid_x):
index = index + 1 for j in range(grid_y):
spacing_x = random.randint(10, 40) index = index + 1
spacing_y = random.randint(10, 40) self.add_unit(
self.add_unit( AirDefence.AAA_ZU_23_Insurgent_Closed_Emplacement,
AirDefence.ZU_23_Closed_Insurgent, "AAA#" + str(index),
"AAA#" + str(index), self.position.x + spacing * i,
self.position.x + spacing_x * i, self.position.y + spacing * j,
self.position.y + spacing_y * i, self.heading,
self.heading, )
)
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:

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