diff --git a/README.md b/README.md index 7c487395..52433bab 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ DCS Liberation is a [DCS World](https://www.digitalcombatsimulator.com/en/products/world/) turn based single-player or co-op dynamic campaign. It is an external program that generates full and complex DCS missions and manage a persistent combat environment. - + ## Downloads diff --git a/game/db.py b/game/db.py index d4142bcc..9d5c4edf 100644 --- a/game/db.py +++ b/game/db.py @@ -2,7 +2,7 @@ import json from datetime import datetime from enum import Enum from pathlib import Path -from typing import Dict, List, Optional, Tuple, Type, Union +from typing import List, Optional, Type, Union from dcs.countries import country_dict from dcs.helicopters import ( @@ -11,7 +11,6 @@ from dcs.helicopters import ( AH_64D, CH_47D, CH_53E, - HelicopterType, Ka_50, Mi_24V, Mi_26, @@ -132,29 +131,21 @@ from dcs.ships import ( ) from dcs.task import ( AWACS, - AntishipStrike, CAP, CAS, CargoTransportation, Embarking, - Escort, - FighterSweep, - GroundAttack, - Intercept, MainTask, Nothing, PinpointStrike, Reconnaissance, Refueling, - SEAD, - Task, Transport, - RunwayAttack, ) from dcs.terrain.terrain import Airport from dcs.unit import Ship, Unit, Vehicle from dcs.unitgroup import ShipGroup, StaticGroup -from dcs.unittype import FlyingType, ShipType, UnitType, VehicleType +from dcs.unittype import FlyingType, UnitType, VehicleType from dcs.vehicles import ( AirDefence, Armor, @@ -724,468 +715,12 @@ PRICES = { highdigitsams.SAM_SA_17_Buk_M1_2_LN_9A310M1_2: 40, } -""" -Units separated by tasks. This will include units for both countries. Be advised that unit could only belong to single task! - -Following tasks are present: -* CAP - figther aircraft for CAP/Escort/Intercept -* CAS - CAS aircraft -* Transport - transport aircraft (used as targets in intercept operations) -* AWACS - awacs -* AntishipStrike - units that will engage shipping -* PinpointStrike - armor that will engage in ground war -* AirDefense - AA units -* Reconnaissance - units that will be used as targets in destroy insurgents operations -* Nothing - troops that will be used for helicopter transport operations -* Embarking - helicopters that will be used for helicopter transport operations -* Carriage - aircraft carriers -* CargoTransportation - ships that will be used as targets for ship intercept operations -""" -UNIT_BY_TASK = { - CAP: [ - A_4E_C, - Bf_109K_4, - C_101CC, - FA_18C_hornet, - FW_190A8, - FW_190D9, - F_14A_135_GR, - F_14B, - F_15C, - F_16A, - F_16C_50, - F_22A, - F_4E, - F_5E_3, - I_16, - JF_17, - J_11A, - M_2000C, - MiG_19P, - MiG_21Bis, - MiG_23MLD, - MiG_25PD, - MiG_29A, - MiG_29G, - MiG_29S, - MiG_31, - Mirage_2000_5, - P_51D, - P_51D_30_NA, - SA342Mistral, - SpitfireLFMkIX, - SpitfireLFMkIXCW, - Su_27, - Su_30, - Su_33, - Su_57, - ], - CAS: [ - AH_1W, - AH_64A, - AH_64D, - AJS37, - AV8BNA, - A_10A, - A_10C, - A_10C_2, - A_20G, - B_17G, - B_1B, - B_52H, - F_117A, - F_15E, - F_86F_Sabre, - Hercules, - Ju_88A4, - Ka_50, - L_39ZA, - MB_339PAN, - MQ_9_Reaper, - MiG_15bis, - MiG_27K, - Mi_24V, - Mi_28N, - Mi_8MT, - OH_58D, - P_47D_30, - P_47D_30bl1, - P_47D_40, - RQ_1A_Predator, - SA342L, - SA342M, - SA342Minigun, - SH_60B, - S_3B, - Su_17M4, - Su_24M, - Su_24MR, - Su_25, - Su_25T, - Su_34, - Tornado_GR4, - Tornado_IDS, - Tu_160, - Tu_22M3, - Tu_95MS, - UH_1H, - WingLoong_I, - ], - Transport: [ - An_26B, - An_30M, - CH_47D, - CH_53E, - C_130, - C_17A, - IL_76MD, - Mi_26, - UH_60A, - Yak_40, - ], - Refueling: [ - IL_78M, - KC130, - KC135MPRS, - KC_135, - S_3B_Tanker, - ], - AWACS: [ - A_50, - E_2C, - E_3A, - KJ_2000, - ], - PinpointStrike: [ - Armor.APC_MTLB, - Armor.APC_MTLB, - Armor.APC_MTLB, - Armor.APC_MTLB, - Armor.APC_MTLB, - Artillery.Grad_MRL_FDDM__FC, - Artillery.Grad_MRL_FDDM__FC, - Artillery.Grad_MRL_FDDM__FC, - Artillery.Grad_MRL_FDDM__FC, - Artillery.Grad_MRL_FDDM__FC, - Armor.Scout_BRDM_2, - Armor.Scout_BRDM_2, - Armor.Scout_BRDM_2, - Armor.APC_BTR_RD, - Armor.APC_BTR_RD, - Armor.APC_BTR_RD, - Armor.APC_BTR_RD, - Armor.APC_BTR_80, - Armor.APC_BTR_80, - Armor.APC_BTR_80, - Armor.APC_BTR_80, - Armor.APC_BTR_80, - Armor.IFV_BTR_82A, - Armor.IFV_BTR_82A, - Armor.IFV_BMP_1, - Armor.IFV_BMP_1, - Armor.IFV_BMP_1, - Armor.IFV_BMP_2, - Armor.IFV_BMP_2, - Armor.IFV_BMP_3, - Armor.IFV_BMP_3, - Armor.IFV_BMD_1, - Armor.LT_PT_76, - Armor.ZBD_04A, - Armor.ZBD_04A, - Armor.ZBD_04A, - Armor.MBT_T_55, - Armor.MBT_T_55, - Armor.MBT_T_55, - Armor.MBT_T_72B, - Armor.MBT_T_72B, - Armor.MBT_T_72B3, - Armor.MBT_T_72B3, - Armor.MBT_T_80U, - Armor.MBT_T_80U, - Armor.MBT_T_90, - Armor.ZTZ_96B, - Armor.Scout_Cobra, - Armor.Scout_Cobra, - Armor.Scout_Cobra, - Armor.Scout_Cobra, - Armor.APC_M113, - Armor.APC_M113, - Armor.APC_M113, - Armor.APC_M113, - Armor.APC_TPz_Fuchs, - Armor.APC_TPz_Fuchs, - Armor.APC_TPz_Fuchs, - Armor.APC_TPz_Fuchs, - Armor.ATGM_HMMWV, - Armor.ATGM_HMMWV, - Armor.ATGM_VAB_Mephisto, - Armor.ATGM_VAB_Mephisto, - Armor.Scout_HMMWV, - Armor.Scout_HMMWV, - Armor.IFV_M2A2_Bradley, - Armor.IFV_M2A2_Bradley, - Armor.ATGM_Stryker, - Armor.ATGM_Stryker, - Armor.IFV_M1126_Stryker_ICV, - Armor.IFV_M1126_Stryker_ICV, - Armor.IFV_M1126_Stryker_ICV, - Armor.SPG_Stryker_MGS, - Armor.IFV_Warrior, - Armor.IFV_Warrior, - Armor.IFV_Warrior, - Armor.IFV_LAV_25, - Armor.IFV_LAV_25, - Armor.IFV_Marder, - Armor.IFV_Marder, - Armor.IFV_Marder, - Armor.IFV_Marder, - Armor.MBT_M60A3_Patton, - Armor.MBT_M60A3_Patton, - Armor.MBT_M60A3_Patton, - Armor.MBT_Leopard_1A3, - Armor.MBT_Leopard_1A3, - Armor.MBT_M1A2_Abrams, - Armor.MBT_Leclerc, - Armor.MBT_Leopard_2A6M, - Armor.MBT_Challenger_II, - Armor.MBT_Chieftain_Mk_3, - Armor.MBT_Merkava_IV, - Armor.MT_Pz_Kpfw_V_Panther_Ausf_G, - Armor.Tk_PzIV_H, - Armor.HT_Pz_Kpfw_VI_Tiger_I, - Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II, - Armor.APC_Sd_Kfz_251_Halftrack, - Armor.APC_Sd_Kfz_251_Halftrack, - Armor.APC_Sd_Kfz_251_Halftrack, - Armor.APC_Sd_Kfz_251_Halftrack, - Armor.IFV_Sd_Kfz_234_2_Puma, - Armor.IFV_Sd_Kfz_234_2_Puma, - Armor.Tk_M4_Sherman, - Armor.MT_M4A4_Sherman_Firefly, - Armor.CT_Cromwell_IV, - Unarmed.Carrier_M30_Cargo, - Unarmed.Carrier_M30_Cargo, - Armor.APC_M2A1_Halftrack, - Armor.APC_M2A1_Halftrack, - Armor.APC_M2A1_Halftrack, - Armor.APC_M2A1_Halftrack, - Armor.MT_Pz_Kpfw_V_Panther_Ausf_G, - Armor.Tk_PzIV_H, - Armor.HT_Pz_Kpfw_VI_Tiger_I, - Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II, - Armor.SPG_Jagdpanther_G1, - Armor.SPG_Jagdpanzer_IV, - Armor.SPG_Sd_Kfz_184_Elefant, - Armor.APC_Sd_Kfz_251_Halftrack, - Armor.IFV_Sd_Kfz_234_2_Puma, - Armor.Tk_M4_Sherman, - Armor.MT_M4A4_Sherman_Firefly, - Armor.CT_Cromwell_IV, - Unarmed.Carrier_M30_Cargo, - Unarmed.Carrier_M30_Cargo, - Unarmed.Carrier_M30_Cargo, - Armor.APC_M2A1_Halftrack, - Armor.APC_M2A1_Halftrack, - Armor.CT_Centaur_IV, - Armor.CT_Centaur_IV, - Armor.HIT_Churchill_VII, - Armor.Car_M8_Greyhound_Armored, - Armor.Car_M8_Greyhound_Armored, - Armor.SPG_M10_GMC, - Armor.SPG_M10_GMC, - Armor.SPG_StuG_III_Ausf__G, - Armor.SPG_StuG_IV, - Artillery.SPG_M12_GMC_155mm, - Armor.SPG_Sturmpanzer_IV_Brummbar, - Armor.Car_Daimler_Armored, - Armor.LT_Mk_VII_Tetrarch, - Artillery.MLRS_M270_227mm, - Artillery.SPH_M109_Paladin_155mm, - Artillery.SPM_2S9_Nona_120mm_M, - Artillery.SPH_2S1_Gvozdika_122mm, - Artillery.SPH_2S3_Akatsia_152mm, - Artillery.SPH_2S19_Msta_152mm, - Artillery.MLRS_BM_21_Grad_122mm, - Artillery.MLRS_BM_21_Grad_122mm, - Artillery.MLRS_9K57_Uragan_BM_27_220mm, - Artillery.MLRS_9A52_Smerch_HE_300mm, - Artillery.SPH_Dana_vz77_152mm, - Artillery.SPH_T155_Firtina_155mm, - Artillery.PLZ_05, - Artillery.SPG_M12_GMC_155mm, - Armor.SPG_Sturmpanzer_IV_Brummbar, - 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, - frenchpack.DIM__TOYOTA_BLUE, - frenchpack.DIM__TOYOTA_DESERT, - frenchpack.DIM__TOYOTA_GREEN, - frenchpack.DIM__KAMIKAZE, - frenchpack.AMX_10RCR, - frenchpack.AMX_10RCR_SEPAR, - frenchpack.ERC_90, - frenchpack.TRM_2000_PAMELA, - frenchpack.VAB__50, - frenchpack.VAB_MEPHISTO, - frenchpack.VAB_T20_13, - frenchpack.VBL__50, - frenchpack.VBL_AANF1, - frenchpack.VBAE_CRAB, - frenchpack.VBAE_CRAB_MMP, - frenchpack.AMX_30B2, - frenchpack.Leclerc_Serie_XXI, - frenchpack.DIM__TOYOTA_BLUE, - frenchpack.DIM__TOYOTA_GREEN, - frenchpack.DIM__TOYOTA_DESERT, - frenchpack.DIM__KAMIKAZE, - ], - AirDefence: [], - Reconnaissance: [ - Unarmed.Truck_M818_6x6, - Unarmed.Truck_Ural_375, - Unarmed.LUV_UAZ_469_Jeep, - ], - Nothing: [ - Infantry.Infantry_M4, - Infantry.Infantry_AK_74, - ], - Embarking: [], - Carriage: [ - CVN_74_John_C__Stennis, - LHA_1_Tarawa, - CV_1143_5_Admiral_Kuznetsov, - ], - CargoTransportation: [ - Cargo_Ivanov, - Bulker_Yakushev, - Tanker_Elnya_160, - Boat_Armed_Hi_speed, - ], -} - -""" -Units from AirDefense category of UNIT_BY_TASK that will be removed from use if "No SAM" option is checked at the start of the game -""" -SAM_BAN = [ - AirDefence.SAM_Linebacker___Bradley_M6, - AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL, - AirDefence.SAM_SA_8_Osa_Gecko_TEL, - AirDefence.SAM_SA_19_Tunguska_Grison, - AirDefence.SAM_SA_6_Kub_Gainful_TEL, - AirDefence.SAM_SA_8_Osa_Gecko_TEL, - AirDefence.SAM_SA_3_S_125_Goa_LN, - AirDefence.SAM_Hawk_Platoon_Command_Post__PCP, - AirDefence.SAM_SA_2_S_75_Guideline_LN, - AirDefence.SAM_SA_11_Buk_Gadfly_Fire_Dome_TEL, -] - -""" -Used to convert SAM site parts to the corresponding site -""" -SAM_CONVERT = { - AirDefence.SAM_P19_Flat_Face_SR__SA_2_3: AirDefence.SAM_SA_3_S_125_Goa_LN, - AirDefence.SAM_SA_3_S_125_Low_Blow_TR: AirDefence.SAM_SA_3_S_125_Goa_LN, - AirDefence.SAM_SA_3_S_125_Goa_LN: AirDefence.SAM_SA_3_S_125_Goa_LN, - AirDefence.SAM_SA_6_Kub_Gainful_TEL: AirDefence.SAM_SA_6_Kub_Gainful_TEL, - AirDefence.SAM_SA_6_Kub_Straight_Flush_STR: AirDefence.SAM_SA_6_Kub_Gainful_TEL, - AirDefence.SAM_SA_10_S_300_Grumble_TEL_C: AirDefence.SAM_SA_10_S_300_Grumble_TEL_C, - AirDefence.SAM_SA_10_S_300_Grumble_Clam_Shell_SR: AirDefence.SAM_SA_10_S_300_Grumble_TEL_C, - AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR: AirDefence.SAM_SA_10_S_300_Grumble_TEL_C, - AirDefence.SAM_SA_10_S_300_Grumble_C2: AirDefence.SAM_SA_10_S_300_Grumble_TEL_C, - AirDefence.SAM_SA_10_S_300_Grumble_Big_Bird_SR: AirDefence.SAM_SA_10_S_300_Grumble_C2, - AirDefence.SAM_Hawk_TR__AN_MPQ_46: AirDefence.SAM_Hawk_Platoon_Command_Post__PCP, - AirDefence.SAM_Hawk_SR__AN_MPQ_50: AirDefence.SAM_Hawk_Platoon_Command_Post__PCP, - AirDefence.SAM_Hawk_LN_M192: AirDefence.SAM_Hawk_Platoon_Command_Post__PCP, - "except": { - # this radar is shared between the two S300's. if we attempt to find a SAM site at a base and can't find one - # model, we can safely assume the other was deployed - # well, perhaps not safely, but we'll make the assumption anyway :p - AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR: AirDefence.SAM_SA_10_S_300_Grumble_C2, - AirDefence.SAM_P19_Flat_Face_SR__SA_2_3: AirDefence.SAM_SA_2_S_75_Guideline_LN, - }, -} - -""" -Units that will always be spawned in the air -""" -TAKEOFF_BAN: List[Type[FlyingType]] = [] - -""" -Units that will be always spawned in the air if launched from the carrier -""" -CARRIER_TAKEOFF_BAN: List[Type[FlyingType]] = [ - Su_33, # Kuznecow is bugged in a way that only 2 aircraft could be spawned -] - """ Units separated by country. country : DCS Country name """ FACTIONS = FactionLoader() -CARRIER_TYPE_BY_PLANE = { - FA_18C_hornet: CVN_74_John_C__Stennis, - F_14A_135_GR: CVN_74_John_C__Stennis, - F_14B: CVN_74_John_C__Stennis, - Ka_50: LHA_1_Tarawa, - SA342M: LHA_1_Tarawa, - UH_1H: LHA_1_Tarawa, - Mi_8MT: LHA_1_Tarawa, - AV8BNA: LHA_1_Tarawa, -} - -""" -Aircraft payload overrides. Usually default loadout for the task is loaded during the mission generation. -Syntax goes as follows: - - `AircraftIdentifier`: { - "Category": "PayloadName", - }, - -where: - * `AircraftIdentifier`: identifier of aircraft (the same that is used troughout the file) - * "Category": (in double quotes) is one of the tasks: CAS, CAP, Intercept, Escort or "*" - * "PayloadName": payload as found in resources/payloads/UNIT_TYPE.lua file. Sometimes this will match payload names - in the mission editor, sometimes it doesn't - -Payload will be used for operation of following type, "*" category will be used always, no matter the operation. -""" - -COMMON_OVERRIDE = { - CAP: "CAP", - Intercept: "CAP", - CAS: "CAS", - PinpointStrike: "STRIKE", - SEAD: "SEAD", - AntishipStrike: "ANTISHIP", - GroundAttack: "STRIKE", - Escort: "CAP", - RunwayAttack: "RUNWAY_ATTACK", - FighterSweep: "CAP", - AWACS: "AEW&C", -} - """ Aircraft livery overrides. Syntax as follows: @@ -1299,16 +834,6 @@ LHA_CAPABLE = [ ---------- END OF CONFIGURATION SECTION """ -UnitsDict = Dict[UnitType, int] -PlaneDict = Dict[FlyingType, int] -HeliDict = Dict[HelicopterType, int] -ArmorDict = Dict[VehicleType, int] -ShipDict = Dict[ShipType, int] -AirDefenseDict = Dict[AirDefence, int] - -AssignedUnitsDict = Dict[Type[UnitType], Tuple[int, int]] -TaskForceDict = Dict[Type[MainTask], AssignedUnitsDict] - StartingPosition = Union[ShipGroup, StaticGroup, Airport, Point] @@ -1332,22 +857,6 @@ def upgrade_to_supercarrier(unit, name: str): return unit -def unit_task(unit: UnitType) -> Optional[Task]: - for task, units in UNIT_BY_TASK.items(): - if unit in units: - return task - - if unit in SAM_CONVERT: - return unit_task(SAM_CONVERT[unit]) - - print(unit.name + " cause issue") - return None - - -def find_unittype(for_task: Type[MainTask], country_name: str) -> List[Type[UnitType]]: - return [x for x in UNIT_BY_TASK[for_task] if x in FACTIONS[country_name].units] - - MANPADS: List[Type[VehicleType]] = [ AirDefence.MANPADS_SA_18_Igla_Grouse, AirDefence.MANPADS_SA_18_Igla_S_Grouse, @@ -1476,106 +985,6 @@ def unit_type_of(unit: Unit) -> UnitType: return unit.type -def task_name(task) -> str: - if task == AirDefence: - return "AirDefence" - elif task == Embarking: - return "Transportation" - elif task == PinpointStrike: - return "Frontline units" - else: - return task.name - - -def choose_units( - for_task: Task, factor: float, count: int, country: str -) -> List[UnitType]: - suitable_unittypes = find_unittype(for_task, country) - suitable_unittypes = [ - x for x in suitable_unittypes if x not in helicopter_map.values() - ] - suitable_unittypes.sort(key=lambda x: PRICES[x]) - - idx = int(len(suitable_unittypes) * factor) - variety = int(count + count * factor / 2) - - index_start = min(idx, len(suitable_unittypes) - variety) - index_end = min(idx + variety, len(suitable_unittypes)) - return list(set(suitable_unittypes[index_start:index_end])) - - -def unitdict_append(unit_dict: UnitsDict, unit_type: UnitType, count: int): - unit_dict[unit_type] = unit_dict.get(unit_type, 0) + 1 - - -def unitdict_merge(a: UnitsDict, b: UnitsDict) -> UnitsDict: - b = b.copy() - for k, v in a.items(): - b[k] = b.get(k, 0) + v - - return b - - -def unitdict_split(unit_dict: UnitsDict, count: int): - buffer_dict: Dict[UnitType, int] = {} - for unit_type, unit_count in unit_dict.items(): - for _ in range(unit_count): - unitdict_append(buffer_dict, unit_type, 1) - if sum(buffer_dict.values()) >= count: - yield buffer_dict - buffer_dict = {} - - if len(buffer_dict): - yield buffer_dict - - -def unitdict_restrict_count(unit_dict: UnitsDict, total_count: int) -> UnitsDict: - if total_count == 0: - return {} - - groups = list(unitdict_split(unit_dict, total_count)) - if len(groups) > 0: - return groups[0] - else: - return {} - - -def assigned_units_split(fd: AssignedUnitsDict) -> Tuple[PlaneDict, PlaneDict]: - return ( - {k: v1 for k, (v1, v2) in fd.items()}, - {k: v2 for k, (v1, v2) in fd.items()}, - ) - - -def assigned_units_from(d: PlaneDict) -> AssignedUnitsDict: - return {k: (v, 0) for k, v in d.items()} - - -def assignedunits_split_to_count(dict: AssignedUnitsDict, count: int): - buffer_dict: Dict[Type[UnitType], Tuple[int, int]] = {} - for unit_type, (unit_count, client_count) in dict.items(): - for _ in range(unit_count): - new_count, new_client_count = buffer_dict.get(unit_type, (0, 0)) - - new_count += 1 - - if client_count > 0: - new_client_count += 1 - client_count -= 1 - - buffer_dict[unit_type] = new_count, new_client_count - if new_count >= count: - yield buffer_dict - buffer_dict = {} - - if len(buffer_dict): - yield buffer_dict - - -def unitdict_from(fd: AssignedUnitsDict) -> Dict: - return {k: v1 for k, (v1, v2) in fd.items()} - - def country_id_from_name(name): for k, v in country_dict.items(): if v.name == name: @@ -1583,24 +992,6 @@ def country_id_from_name(name): return -1 -def _validate_db(): - # check unit by task uniquity - total_set = set() - for t, unit_collection in UNIT_BY_TASK.items(): - for unit_type in set(unit_collection): - assert unit_type not in total_set, "{} is duplicate for task {}".format( - unit_type, t - ) - total_set.add(unit_type) - - # check prices - for unit_type in total_set: - assert unit_type in PRICES, "{} not in prices".format(unit_type) - - -_validate_db() - - class DefaultLiveries: class Default(Enum): af_standard = "" diff --git a/game/event/event.py b/game/event/event.py index ae1c3a4a..df63d34b 100644 --- a/game/event/event.py +++ b/game/event/event.py @@ -434,7 +434,7 @@ class Event: moved_units[frontline_unit] = int(count * move_factor) total_units_redeployed = total_units_redeployed + int(count * move_factor) - destination.base.commision_units(moved_units) + destination.base.commission_units(moved_units) source.base.commit_losses(moved_units) # Also transfer pending deliveries. diff --git a/game/factions/faction.py b/game/factions/faction.py index ef06ef0d..d17da280 100644 --- a/game/factions/faction.py +++ b/game/factions/faction.py @@ -3,7 +3,7 @@ from game.data.groundunitclass import GroundUnitClass import logging from dataclasses import dataclass, field -from typing import Optional, Dict, Type, List, Any, cast +from typing import Optional, Dict, Type, List, Any, cast, Iterator import dcs from dcs.countries import country_dict @@ -240,7 +240,7 @@ class Faction: return faction @property - def units(self) -> List[Type[UnitType]]: + def all_units(self) -> List[Type[UnitType]]: return ( self.infantry_units + self.aircrafts @@ -251,6 +251,12 @@ class Faction: + self.logistics_units ) + @property + def ground_units(self) -> Iterator[Type[VehicleType]]: + yield from self.artillery_units + yield from self.frontline_units + yield from self.logistics_units + def unit_loader(unit: str, class_repository: List[Any]) -> Optional[Type[UnitType]]: """ diff --git a/game/theater/base.py b/game/theater/base.py index 27390b2d..15d14f85 100644 --- a/game/theater/base.py +++ b/game/theater/base.py @@ -1,21 +1,12 @@ import itertools import logging -import math import typing from typing import Dict, Type -from dcs.task import AWACS, CAP, CAS, Embarking, PinpointStrike, Task, Transport -from dcs.unittype import FlyingType, UnitType, VehicleType -from dcs.vehicles import AirDefence, Armor +from dcs.unittype import FlyingType, VehicleType, UnitType -from game import db from game.db import PRICES -STRENGTH_AA_ASSEMBLE_MIN = 0.2 -PLANES_SCRAMBLE_MIN_BASE = 2 -PLANES_SCRAMBLE_MAX_BASE = 8 -PLANES_SCRAMBLE_FACTOR = 0.3 - BASE_MAX_STRENGTH = 1 BASE_MIN_STRENGTH = 0 @@ -24,9 +15,6 @@ class Base: def __init__(self): self.aircraft: Dict[Type[FlyingType], int] = {} self.armor: Dict[Type[VehicleType], int] = {} - # TODO: Appears unused. - self.aa: Dict[AirDefence, int] = {} - self.commision_points: Dict[Type, float] = {} self.strength = 1 @property @@ -47,119 +35,32 @@ class Base: logging.exception(f"No price found for {unit_type.id}") return total - @property - def total_aa(self) -> int: - return sum(self.aa.values()) - - def total_units(self, task: Task) -> int: - return sum( - [ - c - for t, c in itertools.chain( - self.aircraft.items(), self.armor.items(), 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() - ) + for t, c in itertools.chain(self.aircraft.items(), self.armor.items()) if t == unit_type ] ) - @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]): - + def commission_units(self, units: typing.Dict[typing.Type[UnitType], int]): for unit_type, unit_count in units.items(): if unit_count <= 0: continue - for_task = db.unit_task(unit_type) - - target_dict = None - if ( - for_task == AWACS - or for_task == CAS - or for_task == CAP - or for_task == Embarking - or for_task == Transport - ): - target_dict = self.aircraft - elif for_task == PinpointStrike: + if issubclass(unit_type, VehicleType): 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 + elif issubclass(unit_type, FlyingType): + target_dict = self.aircraft else: - logging.error("Unable to determine target dict for " + str(unit_type)) + logging.error( + f"Unexpected unit type of {unit_type}: " + f"{unit_type.__module__}.{unit_type.__name__}" + ) + return + + target_dict[unit_type] = target_dict.get(unit_type, 0) + unit_count def commit_losses(self, units_lost: typing.Dict[typing.Any, int]): @@ -190,55 +91,3 @@ class Base: def set_strength_to_minimum(self) -> None: self.strength = BASE_MIN_STRENGTH - - def scramble_count(self, multiplier: float, task: Task = None) -> int: - if task: - count = sum( - [v for k, v in self.aircraft.items() if db.unit_task(k) == task] - ) - else: - count = self.total_aircraft - - count = int(math.ceil(count * PLANES_SCRAMBLE_FACTOR * self.strength)) - return min( - min( - max(count, PLANES_SCRAMBLE_MIN_BASE), - int(PLANES_SCRAMBLE_MAX_BASE * multiplier), - ), - count, - ) - - def assemble_count(self): - return int(self.total_armor * 0.5) - - def assemble_aa_count(self) -> int: - # previous logic removed because we always want the full air defense capabilities. - return self.total_aa - - def scramble_sweep(self, multiplier: float) -> typing.Dict[FlyingType, int]: - return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP)) - - def scramble_last_defense(self): - # return as many CAP-capable aircraft as we can since this is the last defense of the base - # (but not more than 20 - that's just nuts) - return self._find_best_planes(CAP, min(self.total_aircraft, 20)) - - def scramble_cas(self, multiplier: float) -> typing.Dict[FlyingType, int]: - return self._find_best_planes(CAS, self.scramble_count(multiplier, CAS)) - - def scramble_interceptors(self, multiplier: float) -> typing.Dict[FlyingType, int]: - return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP)) - - def assemble_attack(self) -> typing.Dict[Armor, int]: - return self._find_best_armor(PinpointStrike, self.assemble_count()) - - def assemble_defense(self) -> typing.Dict[Armor, int]: - count = int(self.total_armor * min(self.strength + 0.5, 1)) - return self._find_best_armor(PinpointStrike, count) - - def assemble_aa(self, count=None) -> typing.Dict[AirDefence, int]: - return self._find_best_unit( - self.aa, - AirDefence, - count and min(count, self.total_aa) or self.assemble_aa_count(), - ) diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index 1b46e300..a94c7bd3 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -565,7 +565,7 @@ class ControlPoint(MissionTarget, ABC): while self.base.armor: unit_type, count = self.base.armor.popitem() for _ in range(count): - destination.control_point.base.commision_units({unit_type: 1}) + destination.control_point.base.commission_units({unit_type: 1}) destination = heapq.heappushpop(destinations, destination) def capture_aircraft( @@ -613,7 +613,7 @@ class ControlPoint(MissionTarget, ABC): return parking = destination.unclaimed_parking(game) transfer_amount = min([parking, count]) - destination.base.commision_units({airframe: transfer_amount}) + destination.base.commission_units({airframe: transfer_amount}) count -= transfer_amount def retreat_air_units(self, game: Game) -> None: diff --git a/game/transfers.py b/game/transfers.py index 988c1e84..8d26113a 100644 --- a/game/transfers.py +++ b/game/transfers.py @@ -109,7 +109,7 @@ class TransferOrder: def disband_at(self, location: ControlPoint) -> None: logging.info(f"Units halting at {location}.") - location.base.commision_units(self.units) + location.base.commission_units(self.units) self.units.clear() @property @@ -562,7 +562,7 @@ class PendingTransfers: if transfer.transport is not None: self.cancel_transport(transfer.transport, transfer) self.pending_transfers.remove(transfer) - transfer.origin.base.commision_units(transfer.units) + transfer.origin.base.commission_units(transfer.units) def perform_transfers(self) -> None: incomplete = [] diff --git a/game/unitdelivery.py b/game/unitdelivery.py index 752d4719..fd250825 100644 --- a/game/unitdelivery.py +++ b/game/unitdelivery.py @@ -104,11 +104,11 @@ class PendingUnitDeliveries: game.message(f"{coalition} sold: {name} x {-count} at {source}") self.units = defaultdict(int) - self.destination.base.commision_units(bought_units) + self.destination.base.commission_units(bought_units) self.destination.base.commit_losses(sold_units) if units_needing_transfer: - ground_unit_source.base.commision_units(units_needing_transfer) + ground_unit_source.base.commission_units(units_needing_transfer) self.create_transfer(game, ground_unit_source, units_needing_transfer) def create_transfer( diff --git a/gen/airsupportgen.py b/gen/airsupportgen.py index 3b39a37b..4943d0cf 100644 --- a/gen/airsupportgen.py +++ b/gen/airsupportgen.py @@ -16,6 +16,7 @@ from dcs.task import ( ) from game import db +from .flights.ai_flight_planner_db import AEWC_CAPABLE from .naming import namegen from .callsigns import callsign_for_support_unit from .conflictgen import Conflict @@ -102,7 +103,7 @@ class AirSupportConflictGenerator: fallback_tanker_number = 0 for i, tanker_unit_type in enumerate( - db.find_unittype(Refueling, self.conflict.attackers_side) + self.game.faction_for(player=True).tankers ): alt, airspeed = self._get_tanker_params(tanker_unit_type) variant = db.unit_type_name(tanker_unit_type) @@ -178,41 +179,46 @@ class AirSupportConflictGenerator: ) if not self.game.settings.disable_legacy_aewc: - possible_awacs = db.find_unittype(AWACS, self.conflict.attackers_side) + possible_awacs = [ + a + for a in self.game.faction_for(player=True).aircrafts + if a in AEWC_CAPABLE + ] - if len(possible_awacs) > 0: - awacs_unit = possible_awacs[0] - freq = self.radio_registry.alloc_uhf() - - awacs_flight = self.mission.awacs_flight( - country=self.mission.country(self.game.player_country), - name=namegen.next_awacs_name( - self.mission.country(self.game.player_country) - ), - plane_type=awacs_unit, - altitude=AWACS_ALT, - airport=None, - position=self.conflict.position.random_point_within( - AWACS_DISTANCE, AWACS_DISTANCE - ), - frequency=freq.mhz, - start_type=StartType.Warm, - ) - awacs_flight.set_frequency(freq.mhz) - - awacs_flight.points[0].tasks.append(SetInvisibleCommand(True)) - awacs_flight.points[0].tasks.append(SetImmortalCommand(True)) - - self.air_support.awacs.append( - AwacsInfo( - group_name=str(awacs_flight.name), - callsign=callsign_for_support_unit(awacs_flight), - freq=freq, - depature_location=None, - start_time=None, - end_time=None, - blue=True, - ) - ) - else: + if not possible_awacs: logging.warning("No AWACS for faction") + return + + awacs_unit = possible_awacs[0] + freq = self.radio_registry.alloc_uhf() + + awacs_flight = self.mission.awacs_flight( + country=self.mission.country(self.game.player_country), + name=namegen.next_awacs_name( + self.mission.country(self.game.player_country) + ), + plane_type=awacs_unit, + altitude=AWACS_ALT, + airport=None, + position=self.conflict.position.random_point_within( + AWACS_DISTANCE, AWACS_DISTANCE + ), + frequency=freq.mhz, + start_type=StartType.Warm, + ) + awacs_flight.set_frequency(freq.mhz) + + awacs_flight.points[0].tasks.append(SetInvisibleCommand(True)) + awacs_flight.points[0].tasks.append(SetImmortalCommand(True)) + + self.air_support.awacs.append( + AwacsInfo( + group_name=str(awacs_flight.name), + callsign=callsign_for_support_unit(awacs_flight), + freq=freq, + depature_location=None, + start_time=None, + end_time=None, + blue=True, + ) + ) diff --git a/qt_ui/windows/basemenu/NewUnitTransferDialog.py b/qt_ui/windows/basemenu/NewUnitTransferDialog.py index 5b58cc4f..57288d5f 100644 --- a/qt_ui/windows/basemenu/NewUnitTransferDialog.py +++ b/qt_ui/windows/basemenu/NewUnitTransferDialog.py @@ -166,9 +166,7 @@ class ScrollingUnitTransferGrid(QFrame): scroll_content = QWidget() task_box_layout = QGridLayout() - unit_types = set( - db.find_unittype(PinpointStrike, self.game_model.game.player_name) - ) + unit_types = set(self.game_model.game.faction_for(player=True).ground_units) sorted_units = sorted( {u for u in unit_types if self.cp.base.total_units_of_type(u)}, key=lambda u: db.unit_get_expanded_info( diff --git a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py index ce0b9e2a..74afe47e 100644 --- a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py +++ b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py @@ -167,7 +167,7 @@ class QHangarStatus(QHBoxLayout): next_turn = self.control_point.allocated_aircraft(self.game_model.game) max_amount = self.control_point.total_aircraft_parking - components = [f"{next_turn.present} present"] + components = [f"{next_turn.total_present} present"] if next_turn.total_ordered > 0: components.append(f"{next_turn.total_ordered} purchased") elif next_turn.total_ordered < 0: diff --git a/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py b/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py index 0a421a6a..3f104e7d 100644 --- a/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py +++ b/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py @@ -7,10 +7,8 @@ from PySide2.QtWidgets import ( QScrollArea, QVBoxLayout, QWidget, - QMessageBox, ) -from dcs.task import PinpointStrike -from dcs.unittype import FlyingType, UnitType +from dcs.unittype import UnitType from game import db from game.theater import ControlPoint @@ -32,31 +30,24 @@ class QArmorRecruitmentMenu(QFrame, QRecruitBehaviour): def init_ui(self): main_layout = QVBoxLayout() - units = { - PinpointStrike: db.find_unittype( - PinpointStrike, self.game_model.game.player_name - ), - } - scroll_content = QWidget() task_box_layout = QGridLayout() scroll_content.setLayout(task_box_layout) row = 0 - for task_type in units.keys(): - units_column = list(set(units[task_type])) - if len(units_column) == 0: - continue - units_column.sort( - key=lambda u: db.unit_get_expanded_info( - self.game_model.game.player_country, u, "name" - ) + unit_types = list( + set(self.game_model.game.faction_for(player=True).ground_units) + ) + unit_types.sort( + key=lambda u: db.unit_get_expanded_info( + self.game_model.game.player_country, u, "name" ) - for unit_type in units_column: - row = self.add_purchase_row(unit_type, task_box_layout, row) - stretch = QVBoxLayout() - stretch.addStretch() - task_box_layout.addLayout(stretch, row, 0) + ) + for unit_type in unit_types: + row = self.add_purchase_row(unit_type, task_box_layout, row) + stretch = QVBoxLayout() + stretch.addStretch() + task_box_layout.addLayout(stretch, row, 0) scroll_content.setLayout(task_box_layout) scroll = QScrollArea() diff --git a/qt_ui/windows/basemenu/intel/QIntelInfo.py b/qt_ui/windows/basemenu/intel/QIntelInfo.py index ea5dfa4c..cc9c3ca6 100644 --- a/qt_ui/windows/basemenu/intel/QIntelInfo.py +++ b/qt_ui/windows/basemenu/intel/QIntelInfo.py @@ -1,3 +1,6 @@ +from collections import defaultdict + +from PySide2.QtCore import Qt from PySide2.QtWidgets import ( QFrame, QGridLayout, @@ -7,8 +10,6 @@ from PySide2.QtWidgets import ( QScrollArea, QWidget, ) -from PySide2.QtCore import Qt -from dcs.task import CAP, CAS, Embarking, PinpointStrike from game import Game, db from game.theater import ControlPoint @@ -19,51 +20,44 @@ class QIntelInfo(QFrame): super(QIntelInfo, self).__init__() self.cp = cp self.game = game - self.init_ui() - def init_ui(self): layout = QVBoxLayout() scroll_content = QWidget() - intelLayout = QVBoxLayout() + intel_layout = QVBoxLayout() - units = { - CAP: db.find_unittype(CAP, self.game.enemy_name), - Embarking: db.find_unittype(Embarking, self.game.enemy_name), - CAS: db.find_unittype(CAS, self.game.enemy_name), - PinpointStrike: db.find_unittype(PinpointStrike, self.game.enemy_name), + units_by_task: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int)) + for unit_type, count in self.cp.base.aircraft.items(): + if count: + name = db.unit_get_expanded_info( + self.game.enemy_country, unit_type, "name" + ) + units_by_task[unit_type.task_default.name][name] += count + + units_by_task = { + task: units_by_task[task] for task in sorted(units_by_task.keys()) } - for task_type in units.keys(): - units_column = list(set(units[task_type])) + front_line_units = defaultdict(int) + for unit_type, count in self.cp.base.armor.items(): + if count: + name = db.unit_get_expanded_info( + self.game.enemy_country, unit_type, "name" + ) + front_line_units[name] += count - if sum([self.cp.base.total_units_of_type(u) for u in units_column]) > 0: + units_by_task["Front line units"] = front_line_units + for task, unit_types in units_by_task.items(): + task_group = QGroupBox(task) + task_layout = QGridLayout() + task_group.setLayout(task_layout) - group = QGroupBox(db.task_name(task_type)) - groupLayout = QGridLayout() - group.setLayout(groupLayout) + for row, (name, count) in enumerate(unit_types.items()): + task_layout.addWidget(QLabel(f"{name}"), row, 0) + task_layout.addWidget(QLabel(str(count)), row, 1) - row = 0 - for unit_type in units_column: - existing_units = self.cp.base.total_units_of_type(unit_type) - if existing_units == 0: - continue - groupLayout.addWidget( - QLabel( - "" - + db.unit_get_expanded_info( - self.game.enemy_country, unit_type, "name" - ) - + "" - ), - row, - 0, - ) - groupLayout.addWidget(QLabel(str(existing_units)), row, 1) - row += 1 + intel_layout.addWidget(task_group) - intelLayout.addWidget(group) - - scroll_content.setLayout(intelLayout) + scroll_content.setLayout(intel_layout) scroll = QScrollArea() scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) diff --git a/qt_ui/windows/groundobject/QGroundObjectMenu.py b/qt_ui/windows/groundobject/QGroundObjectMenu.py index 9df4b454..23c6e3ba 100644 --- a/qt_ui/windows/groundobject/QGroundObjectMenu.py +++ b/qt_ui/windows/groundobject/QGroundObjectMenu.py @@ -340,11 +340,7 @@ class QBuyGroupForGroundObjectDialog(QDialog): buy_ewr_layout.addLayout(stretch, 2, 0) # Armored units - - armored_units = db.find_unittype( - PinpointStrike, faction.name - ) # Todo : refactor this legacy nonsense - for unit in set(armored_units): + for unit in set(faction.ground_units): self.buyArmorCombo.addItem( db.unit_type_name_2(unit) + " [$" + str(db.PRICES[unit]) + "M]", userData=unit, diff --git a/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py b/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py index ec4530c2..ab4899d2 100644 --- a/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py +++ b/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py @@ -90,7 +90,7 @@ class PilotControls(QHBoxLayout): self.selector.currentIndexChanged.connect(self.on_pilot_changed) self.addWidget(self.selector) - self.player_checkbox = QCheckBox() + self.player_checkbox = QCheckBox(text="Player") self.player_checkbox.setToolTip("Checked if this pilot is a player.") self.on_pilot_changed(self.selector.currentIndex()) self.addWidget(self.player_checkbox) diff --git a/resources/campaigns/caen_to_evreux.json b/resources/campaigns/caen_to_evreux.json new file mode 100644 index 00000000..5776979e --- /dev/null +++ b/resources/campaigns/caen_to_evreux.json @@ -0,0 +1,11 @@ +{ + "name": "Normandy - From Caen to Evreux", + "theater": "Normandy", + "authors": "Khopa", + "recommended_player_faction": "Allies 1944", + "recommended_enemy_faction": "Germany 1944", + "description": "
This is a light scenario on the Normandy map.
August 1944, allied forces are pushing from Caen/Carpiquet to the cities of Lisieux and Evreux.
Lisieux is an important logistic hub for the Werhmacht, and Evreux airbase is hosting most of the Luftwaffe forces in the region.
", + "miz": "caen_to_evreux.miz", + "performance": 1, + "version": "6.0" +} diff --git a/resources/campaigns/caen_to_evreux.miz b/resources/campaigns/caen_to_evreux.miz new file mode 100644 index 00000000..a0f321ab Binary files /dev/null and b/resources/campaigns/caen_to_evreux.miz differ diff --git a/resources/campaigns/normandy_small.json b/resources/campaigns/normandy_small.json deleted file mode 100644 index d09e9a77..00000000 --- a/resources/campaigns/normandy_small.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "Normandy - Normandy Small", - "theater": "Normandy", - "authors": "Khopa", - "recommended_player_faction": "Allies 1944", - "recommended_enemy_faction": "Germany 1944", - "description": "A lighter version of the Normandy 1944 D-Day scenario.
", - "miz": "normandy_small.miz", - "performance": 2 -} \ No newline at end of file